ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
/ セキュリティブログ / セキュリティエンジニア /
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:
ペネトレーションテスト
セキュリティ診断

👈GNU pokeの紹介👉


デジタルペンテスト部のkjです。
普段の私の業務ではバイナリ解析なんてやってないんですが、気になっているバイナリエディタがあります。「ぜひ広まってほしい」という思いのものと、紹介記事を書いてみました。
👉GNU poke (https://www.jemarch.net/poke)

(1) GNU pokeとは?

GNU pokeは、バイナリデータを対話的に読み書きできるCUIベースのバイナリエディタです。
GNUプロジェクトの一部として開発されており、中心的な開発者はJose E. Marchesi氏*1です。pokeが開発されたもともとの動機は、ELFファイルに意図的な欠陥を注入し、ツールチェーンのバグを再現することでした。既存のツールではバイナリデータの構造を意識した編集が煩雑だったため、「データの構造を宣言すれば、デコードとエンコードを自動で行ってくれるエディタ」として設計されたのがGNU pokeです。

しかし、その実態は単なるバイナリエディタではありません。バイナリエディタの皮を被った「バイナリデータ処理専用のプログラミング環境」と呼べる代物です。
pokeの実態は、DSLである「Poke言語」と、それを実行する仮想マシン「The Poke Virtual Machine」から構成されています。
Poke言語は、通常のプログラミング言語と同じように変数・関数・制御構文など持ちますが、それに加えて、バイナリデータの読み書きや構造解釈に関する概念・機能がファーストクラスとして組み込まれている点が大きな特徴です。
pokeコマンドで起動するCUIエディタ画面は、Poke言語のREPLそのものであり、ユーザーはPoke言語を用いて型の定義や値の操作をインタラクティブに行うことができます。

GNU pokeのその根底にある設計思想は、単なるビューアとしてのバイナリエディタとは大きく異なります。

百聞は一見に如かず

何はともあれ、まずはpokeを使って簡単にバイナリデータを眺めてみましょう。まず適当なサンプル(hoge.bin)を用意します。

$ echo 'int main(){return 0;}' | gcc -xc -o hoge.bin -

作成したサンプルをpokeに読み込ませます。

$ poke hoge.bin
     _____
 ---'   __\_______
            ______)  GNU poke 4.3
            __)
           __)
 ---._______)

Copyright (C) 2024 The poke authors.
License GPLv3+: GNU GPL version 3 or later.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Powered by Jitter 0.7.312.
Perpetrated by Jose E. Marchesi.

hserver listening in port 32779.

For help, type: ".help".
Type ".exit" to leave the program.
(poke) 

ファイルを読み込ませたら、dumpコマンドでファイルの中身を眺めてみます。

(poke) dump
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000000: 7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  .ELF.... ........
00000010: 01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  ..>..... ........
00000020: 00 00 00 00 00 00 00 00  c0 01 00 00 00 00 00 00  ........ ........
00000030: 00 00 00 00 40 00 00 00  00 00 40 00 0c 00 0b 00  ....@... ..@.....
00000040: 55 48 89 e5 b8 00 00 00  00 5d c3 00 47 43 43 3a  UH...... .]..GCC:
00000050: 20 28 47 4e 55 29 20 31  35 2e 32 2e 31 20 32 30   (GNU) 1 5.2.1 20
00000060: 32 36 30 31 30 33 00 00  04 00 00 00 20 00 00 00  260103.. .... ...
00000070: 05 00 00 00 47 4e 55 00  02 00 01 c0 04 00 00 00  ....GNU. ........

1行目はルーラーです。
- 左列(76543210)がファイルオフセット
- 中央列(00 11 〜 ee ff)が各バイトの16進数表現
- 右列(012〜DEF)がそのASCII表現
です。
2行目以降から実際のファイルの内容が表示されており、デフォルトでは128バイトが表示されます。右列のASCII部分に「.ELF」という文字列が見えますが、これはELFファイルのマジックナンバーです。非印字文字は「.」で表示されます。

表示範囲を変えるには、dumpコマンドに「:size」と「:from」を指定します。

(poke) dump :size 16#B :from 0x50#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000050: 20 28 47 4e 55 29 20 31  35 2e 32 2e 31 20 32 30   (GNU) 1 5.2.1 20

pokeでは、コマンドのオプションを「:<option name>」という形で渡します。
- :size ダンプするデータのサイズ
- :from ファイルの先頭からのオフセット
また、オプションの値であるサイズ「16#B」やオフセット「0x50#B」は、poke独自の表現形式です。
- 16#B(16バイト)
- 0x50#B(0x50バイト)

ダンプサイズを指定する際、直接バイト数で指定するのではなく構造体のサイズを指定することも可能です。

(poke) type GNU = struct {byte[3] gnu;}
(poke) dump :size 1#GNU :from 0x52#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000052: 47 4e 55                                          GNU

もちろん、単純に表示するだけでなく手続き的な操作も可能です。

(poke) var x = GNU@0x52#B
(poke) x.gnu
[0x47UB,0x4eUB,0x55UB]
(poke) for(c in x.gnu){printf "%c\n", (c + 0x20);}
g
n
u

結構柔軟だと思いませんか?

(2) GNU pokeに入門しよう

柔軟であるということは、つまり複雑であるということです。
pokeには、バイナリデータを操作するための独自の概念や機能が数多く存在します。すべてを紹介するのは難しいので、入門するうえで絶対に理解が必要なものを紹介します。

数量と単位

前章でdumpコマンドに引数(:fromと:size)を渡しました。
ここで、:sizeや:fromに指定された値は「United values」(またはオフセット)と呼ばれ、以下の形式で表現されます。

<magnitude(数量)>#<unit(単位)>

United valuesは「単位を持つ数値」であり、バイナリデータ解析で頻出する「量」と「単位」を一緒くたに表現可能です。
一般的な単位(SI系など)はデフォルトで定義されています。
- 1#b 1ビット
- 3#N 3ニブル
- 10#B 10バイト
- 1#KB 1キロバイト
- 2#Mib 2メビビット
などなど。

自分で単位を定義することも可能です。

// 3ビットを表す単位"tri"を定義する。
(poke) unit tri = 3
(poke) 1#tri
0x00000001#3

少しややこしいのが、united valuesには算術演算が定義されており、異なる単位間でも演算できることです。

// 1ビットが1つ分 + 3ビットが1つ分 == 1ビット4つ分
(poke) 1#b + 1#tri
4#b
// 3ビットが3つ分 - 1ビットが1つ分 == 1ビット8つ分
(poke) 3#tri - 1#b 
8#b
// 1バイト1つ分に1024(これは単なる数)を掛けると1キビバイト1つ分
// "1"はtrueを意味する
(poke) 1#B * 1024 == 1#KiB 
1
// 1キロバイト1つ分を1バイト1つ分で割ると、1000(これは単なる数)
(poke) 1#KB / 1#B
1000

United valuesの最も実用的な用途は「オフセット」の表現です。
例えば、ディスクダンプを解析する場合はオフセットの単位をセクター単位で扱うと便利ですし、メモリダンプを解析している場合はページ単位で扱うと便利でしょう。
C言語などでは、オフセットを表現する際の型としてuint64_tやoff_tが使用されますが、これは「量」のみが表現されており、単位は表現できません。オフセットを表す変数に値が入っていても、その単位がわからないのです。そのため、型で表現しきれない「単位」という情報は変数名やコメント、ドキュメントで補足するのが常でした。「数量と単位」という表現方法やビットレベルでの定義が可能な柔軟さが言語仕様に組み込まれてるあたり、バイナリ専用DSL感があります。

型システムとマッピング

バイナリデータ解析の目的は、そのデータ構造を解き明かすことにあります。それはデータを構成するビット列に対し、本来の意味(型)を見つけ出し、解釈を割り当てる作業です。
pokeでは、ビット列に対し型を割り当てる操作を「マッピング」と呼び、以下のシンタックスで行います。

<type(型)>@<offset(オフセット)>

typeには割り当てたい型を、offsetには割り当てたい場所を指定します。

プリミティブ型

まず、最も基本的な整数型のマッピングを説明します。

(poke) byte @ 0#B
0x7fUB

これは、「ファイルの0バイト目を、1バイト(8bit)として読み出す」という意味です。上記例では、読み出した値が「0x7fUB」で末尾のUBはUnsigned Byteを示す接尾辞です。

byteはuint<8>という型のエイリアスです。pokeでは、uint<N>(符号無し整数)またはint<N>(符号付き整数)という構文で任意のビット幅の整数型を表現できます。C言語などでよく目にする代表的なプリミティブ型はエイリアスとして事前に定義されています。
- uint<8>: char、byte、uint8
- uint<16>: ushort、uint16
- int<32>: int、int32
- int<64>: long、int64
などなど。

また、typeディレクティブを使用することで任意のビット幅の整数型を独自に定義できます。

(poke) type int3 = int<3>
(poke) int3 @ 0#B
(int<3>) 0b011
配列と文字列

同じ型の並びは、配列としてマッピングできます。

(poke) char[4] @ 0#B
[0x7fUB,0x45UB,0x4cUB,0x46UB]

上記例は、先頭から4バイトをchar型の配列として読み込んでいます。

文字列に対しては文字列型(string)が用意されています。ただし、文字列型はASCII文字列を前提としており、またC言語の文字列(char配列)と同じくNULL文字(\x00)で終端されている必要があります。

(poke) string @ 0x4c#B
"GCC: (GNU) 15.2.1 20260103"

上記例は、0x4Cバイト目に存在する文字列を読み込んでいます。

構造体

最後に構造体のマッピングを見ていきます。
構造体をマッピングするためには、事前に目的とする構造体の型を定義します。pokeでは struct を使って構造体の型を定義します。

(poke) type Elf64_Ehdr = struct {
  byte[16] e_ident;
  uint<16> e_type;
  uint<16> e_machine;
  uint<32> e_version;
  uint<64> e_entry;
  offset<uint<64>,B> e_phoff;
  offset<uint<64>,B> e_shoff;
  uint<32> e_flags;
  uint<16> e_ehsize;
  uint<16> e_phentsize;
  uint<16> e_phnum;
  uint<16> e_shentsize;
  uint<16> e_shnum;
  uint<16> e_shstrndx;
}

あとは他と同じく@演算子を使ってマッピングします。

(poke) Elf64_Ehdr @ 0#B
Elf64_Ehdr {
  e_ident=[0x7fUB,0x45UB,0x4cUB,0x46UB,0x02UB,...],
  e_type=0x0001UH,
  e_machine=0x003eUH,
  e_version=0x00000001U,
  e_entry=0x0000000000000000UL,
  e_phoff=0x0000000000000000UL#B,
  e_shoff=0x00000000000001c0UL#B,
  e_flags=0x00000000U,
  e_ehsize=0x0040UH,
  e_phentsize=0x0000UH,
  e_phnum=0x0000UH,
  e_shentsize=0x0040UH,
  e_shnum=0x000cUH,
  e_shstrndx=0x000bUH
}

マッピングの概念自体はシンプルで理解しやすいと思いますが、みなさんが気になるのは「どれくらい複雑な構造を定義できるのか」ですよね?
pokeは(バイト指向でなく)ビット指向なので、圧縮データフォーマット等でよくあるようなビット列単位での分解ももちろん可能ですし、可変長配列や動的なフィールドの追加、TLV形式における動的な型の割り当てなどの複雑なマッピング方法ももちろん表現可能です。

IOスペース

pokeには「IOスペース」という、データアクセスの抽象化レイヤーが存在します。
バイナリを読み書きする際、pokeはファイルに対し直にアクセスのではなく、この抽象化レイヤーを通じてアクセスしています。ただ、pokeのユーザーから見えるIOスペースは、「エディタが開いているファイル」を示すイメージに近いものです。
これまでの例では、IOスペースの裏側にあるのはファイル(hoge.o)でしたが、ファイル以外にも、メモリバッファ、実行中のプロセスメモリ、NBD*2などをIOスペースとして扱えます。マッピング操作はIOスペースに対して行われるため、裏側がファイルだろうとメモリだろうとマッピングのルールは同じです。

.info iosコマンドで現在開いているIOスペースを確認できます。

(poke) .info ios
   Id   Type   Mode   Bias           Size           Name
 * 0    FILE   rw     0x00000000#B   0x000004c0#B   /tmp/hoge.o   [close]

左端の「 * 」が現在操作対象としているIOスペースを示しています。
Id例はIOスペースの識別子で、IOスペースの切り替えなどに使用します。

.fileコマンドで別ファイルのファイル(/bin/ls)を開いてみます。

(poke) .file /bin/ls
(poke) .info ios
    Id   Type   Mode   Bias           Size           Name
  * 1    FILE   r      0x00000000#B   0x00026b10#B   /bin/ls       [close]
    0    FILE   rw     0x00000000#B   0x000004c0#B   /tmp/hoge.o   [close]

新しく開いたIOスペースが自動的に現在のIOスペースになります。

hoge.oは開いたままなので、Id 0で参照できます。
.iosコマンドで、hoge.oのIOスペースに戻ってみます。

(poke) .ios 0
The current IOS is now `/tmp/hoge.o'.
(poke) .info ios
   Id   Type   Mode   Bias           Size           Name          
   1    FILE   r      0x00000000#B   0x00026b10#B   /bin/ls       [close]
 * 0    FILE   rw     0x00000000#B   0x000004c0#B   /tmp/hoge.o   [close]

IOスペースを閉じるには、.closeコマンドで閉じます。

(poke) .close 1
(poke) .info ios
   Id   Type   Mode   Bias           Size           Name
 * 0    FILE   rw     0x00000000#B   0x000004c0#B   /tmp/hoge.o   [close]

/bin/lsのIOスペースが消えました。

前述の通り、ファイルだけでなくメモリバッファもIOスペースとして扱えます。
.memコマンドでメモリ上のみに存在するIOスペースを作成します。

(poke) .mem scratch
The current IOS is now `*scratch*'.
(poke) .info ios
   Id   Type     Mode   Bias           Size           Name          
 * 1    MEMORY   rw     0x00000000#B   0x00001000#B   *scratch*     [close]
   0    FILE     rw     0x00000000#B   0x000004c0#B   /tmp/hoge.o   [close]

メモリバッファの場合、ファイル名の前後に*が付きます。

IOスペース間のデータのコピーは、copyコマンドを使用します。

copy :from_ios 0 :from 0#B :size iosize(0) :to_ios 1

- :from_ios コピー元のIOスペースID
- :from コピー元のオフセット
- :size コピーサイズ※1
- :to_ios コピー先のIOスペースID
※1 iosize(<IOSのID>)でIDが示すIOスペースのサイズが返ります。

scratchにコピーしたデータを実ファイルとして書き出すには、saveコマンドを使用します。

save :ios 1 :from 0#B :size iosize(0) :file "<filepath>"

- :ios 保存するIOスペース
- :from 保存を開始するオフセット
- :size 保存するサイズ※2
- :file 出力パス
※2 IOスペースのサイズはiosize関数で得られますが、メモリバッファがバックエンドとなるIOスペースはページサイズが返ってきます。なので、元ファイルと同じサイズを出力するためにコピー元のIOスペースのiosizeを引っ張ってくる必要があります。

書き込み

今まで書き込み(変更)操作について触れてきませんでしたが、ここが一番重要です。
pokeは、データの読み込みだけでなく書き込みももちろん可能ですが、その変更はそのままファイルに即時反映されます。そしてundoはありません。
ですので、必ず元データのバックアップを取ってください。
(前述のメモリバッファは、ファイルを壊すリスクなく実験するためのスクラッチ領域として便利ですよ)

データの変更方法は二種類あります。
一つは、マッピング式に対して直接代入する方法です。

(poke) dump :from 0#B :size 4#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000000: 7f 45 4c 46                                      .ELF
(poke) ushort@0#B = 0x4f47
(poke) dump :from 0#B :size 4#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000000: 47 4f 4c 46                                      GOLF

".ELF"が"GOLF"に変更されました。

もう一つは、変数に対して行う方法です。ただし、変数を介してIOスペースの値を変更する場合は型によってセマンティクスが異なります。
プリミティブ型: コピー
整数などのプリミティブ型の場合、@演算子は常に「値のコピー」を返します。そのため、変数への変更はIOスペースに反映されません。前述した手順で明示的に書き戻す必要があります。

(poke) var n = int@0#B
(poke) n = n + 1   // 変数の値が変更されるだけ、IOスペースは変化無し
(poke) int@0#B = n  // 書き戻して初めて反映される

複合型: マップド
複合型の場合、@演算子から返される複合型の値はマップド値となり、要素の変更が直接IOスペースに反映されます。

(poke) dump :size 1#GNU :from 0x52#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000052: 47 4e 55                                         GNU
(poke) var x = GNU @ 0x52#B
(poke) x.gnu = ['A', 'B', 'C']
(poke) dump :size 1#GNU :from 0x52#B
76543210  00 11 22 33 44 55 66 77  88 99 aa bb cc dd ee ff  01234567 89ABCDEF
00000052: 41 42 43                                         ABC

コワッ

まだまだいろんな機能があります

pokeにはバイナリ操作に関して便利な機能が沢山あります。ぜひ公式ドキュメントを読んでみてください。
↓はおすすめ便利機能です。

- Pinned struct
https://jemarch.net/poke-4.3-manual/poke.html#Pinned-Structs
C言語のunionに似たものです。
- Union
https://jemarch.net/poke-4.3-manual/poke.html#Unions
名前はunionですがC言語のunionやpinned structとは若干異なり、条件に応じて一つだけのフィールドがデコードされます。
- Field-Constraints
https://jemarch.net/poke-4.3-manual/poke.html#Field-Constraints
フィールドに対する制約(条件)を指定できます。これを利用し、データのバリデーションを行ったり、フィールドを動的に定義(Optinal Fields)することもできます。
- 関数/メソッド
https://jemarch.net/poke-4.3-manual/poke.html#Functions-in-Structs
https://jemarch.net/poke-4.3-manual/poke.html#Struct-Methods
structには関数とメソッドを定義できます。
関数は構築時のロジックとして動作し、パース時の複雑な調整や動的な値の決定に使用できます。
メソッドは構築後のロジックとして動作し、通常のオブジェクト指向言語のメソッドと同じ感じです。

(3) スクリプティングしてみよう

最後に、より実戦的な例としてスクリプティングを使用してみます。
GNU pokeはプログラミング環境ですから、Poke言語を使って好きなように解析処理を記述できます。Poke言語のスクリプトは「pickles」と呼ばれ、拡張子が.pkのファイルに保存します。
今回はPEファイル(64ビット版)をパースして、セクション情報を表示するpickleを書いてみます。

パース方針の決定

まず、PEファイルのパース方針を決めます。
PEファイルの構造は、ファイルの先頭から、

 |DOSヘッダー| -> |NTヘッダー| -> |セクションテーブル(複数のセクションヘッダーの配列)| -> ...(他データ)

と続いています。
セクションテーブルの位置は(厳密には)固定ではなく、DOSヘッダーとNTヘッダーからの情報を元に算出します。
ただし、各ヘッダーを細々解析する必要はありません。
時として、バイナリデータ解析ではよくわからん部分はよくわからんまま残しそのまま進まざるを得ない場合がままあるように、今回もセクションの情報を表示するための最短距離を目指します。

1) DOSヘッダー(_IMAGE_DOS_HEADER)
DOSヘッダーのメンバー変数e_lfanewが、続くNTヘッダーのファイルオフセットを示しています。
DOSヘッダーの先頭からのe_lfanewのオフセットは0x3cバイトです。

LONG e_lfanew; // offset 0x3c


2) NTヘッダー(_IMAGE_NT_HEADERS64)
NTヘッダーには、順に
- シグネチャ("PE\x00\x00")
- COFFファイルヘッダー(_IMAGE_FILE_HEADER)
- オプショナルヘッダー(_IMAGE_OPTIONAL_HEADER64)
が含まれています。
このうち必要なものは、COFFファイルヘッダーのメンバー変数
- NumberOfSections (セクションテーブルの要素数)
- SizeOfOptionalHeader (オプショナルヘッダーのサイズ)
の2つです。

USHORT NumberOfSections; // offset 0x2
USHORT SizeOfOptionalHeader; // offset 0x10

COFFファイルヘッダー自体は固定長の0x14バイトで、NumberOfSectionsのオフセットは0x2バイト、SizeOfOptionalHeaderのオフセットは0x10バイトです。

オプショナルヘッダーにはセクションテーブルの位置特定に必要な情報は含まれませんので読み飛ばします。

3) セクションテーブル位置の計算
必要な情報は出揃ったので、セクションテーブルの位置を計算します。計算式は↓になります。

e_lfanew
+ 0x4バイト(NTヘッダーのシグネチャサイズ)
+ 0x14バイト(COFFファイルヘッダーのサイズ)
+ SizeOfOptionalHeader(オプショナルヘッダーのサイズ)


4) セクションヘッダー(_IMAGE_SECTION_HEADER)
セクションテーブルはセクションヘッダーの配列なので、NumberOfSectionsの数だけ各セクションヘッダーの内容をダンプします。
今回はセクションヘッダーに含まれる、
- Name(セクション名)
- VirtualAddress(そのセクションのRVA、イメージベースからの相対アドレス)
- VirtualSize(そのセクションがメモリ上に展開された際のサイズ)
を表示してみます。

UCHAR Name[8]; // offset 0x0
ULONG VirtualSize; // offset 0x8
ULONG VirtualAddress; // offset 0xc

セクションヘッダーは固定長の0x28バイトです。

※ WindowsはLLP64のため、ULONGは32bitです。

スクリプト

決定した方針に従い、Pokeスクリプトを実装します。
以下に示す解析処理の内容を「dump_section.pk」に保存します。
ポイントごとにコメントを書いています。

// NTヘッダーの定義
type NT = struct {
  struct {
    // ★ポイント1: フィールドラベル(Fields label)
    // フィールドラベル(@<offset>)を指定することで、
    // 構造体のメンバーを特定のオフセット(バイト位置)に配置できます。  
    uint<16> NumberOfSections @ 0x2#B;
    uint<16> SizeOfOptionalHeader @ 0x10#B;
    
    // ★ポイント2: 構造体サイズを調整するハック
    // フィールドラベルを使用した際に構造体全体サイズを固定したい場合、
    // ダミーとなるメンバー変数を定義することで、構造体のサイズを調整します。
    byte _EOS @ (0x14 - 1)#B;
  } coff @ 0x4#B; // PEシグネチャを飛ばすため、0x4バイト目にCOFFファイルヘッダーを配置
};


// DOSヘッダーの定義
type DOS = struct {
  // ★ポイント3: offset型の第3引数
  // offset型の第3引数にマッピングする型を指定することで、
  // オフセットが示す先の型を指定できます。
  // 指定した型にマッピングした値は、ref属性から参照できます。
  // Poke言語には前方参照の仕組みがないため、最初にNTヘッダーを定義し、
  // あとにDOSヘッダーを定義してます。
  offset<uint<32>, B, NT> e_lfanew @ 0x3c#B;
};


// セクションヘッダーの定義
type SECTION = struct {
  byte[8] Name;
  uint<32> VirtualSize @ 0x8#B;
  uint<32> VirtualAddress @ 0xc#B;

  // ★ポイント4: コンピューテッドフィールド(Computed Fields)
  // メソッドを使用し、ゲッターとして機能するフィールドを表現できます。
  computed string NameStr;
  method get_NameStr = string:
  {
      // セクション名をバイト列から文字列へ変換する
      var s = "";
      for(c in Name){
        if(c == 0x00){
          break;
        }
        s += c as string;
      }
     return s;
  }

  byte _ @ (0x28 - 1)#B;
};


// 以降がメイン処理。このpickleが読み込まれた際に実行されます。

// ファイルの先頭からDOSヘッダーをマッピング
var dos = DOS@0#B;
printf "%v\n", dos;

// e_lfanewのref属性から、マッピング済のNTヘッダーを取得
var nt = dos.e_lfanew'ref;
printf "%v\n", nt;

// セクションテーブルのスタート位置を計算
var section_table =
	dos.e_lfanew
	+ 4#B
	+ (nt.coff'size / #B)#B
	+ nt.coff.SizeOfOptionalHeader#B;
printf "Section Table: %v\n", section_table;

// セクションテーブル内の各セクションをダンプ
for(var i = 0; i < nt.coff.NumberOfSections; ++i){
  var section = SECTION @ section_table + i#SECTION;
	printf("Name: %s, VirtualAddress: %v, VirtualSize %v\n", 
            section.NameStr, section.VirtualAddress, section.VirtualSize);
};

スクリプトが出来たら、あとはpokeから読み込むだけで実行されます。
スクリプトの読み込みは.loadコマンドを使用します。

(poke) .file /mnt/c/windows/system32/notepad.exe 
(poke) .load dump_section.pk 
DOS {e_lfanew=0x000000f8U#B}
NT {coff=struct {NumberOfSections=0x0008UH,SizeOfOptionalHeader=0x00f0UH,_EOS=0x00UB}}
Section Table: 0x0000000000000200UL#B
Name: .text, VirtualAddress: 0x00001000U, VirtualSize 0x000266e2U
Name: fothk, VirtualAddress: 0x00028000U, VirtualSize 0x00001000U
Name: .rdata, VirtualAddress: 0x00029000U, VirtualSize 0x0000a5d8U
Name: .data, VirtualAddress: 0x00034000U, VirtualSize 0x00002740U
Name: .pdata, VirtualAddress: 0x00037000U, VirtualSize 0x0000120cU
Name: .didat, VirtualAddress: 0x00039000U, VirtualSize 0x000000f8U
Name: .rsrc, VirtualAddress: 0x0003a000U, VirtualSize 0x0001e1d0U
Name: .reloc, VirtualAddress: 0x00059000U, VirtualSize 0x00000350U
(poke) 

まとめ

以上、GNU pokeの簡単な紹介でした。
今回はpokeのREPLを通していろいろ試しましたが、pythonなどのLLと同じように.pkスクリプトを単体でも動かすことが可能です。
pokeの本質は「バイナリ処理専用のプログラミング環境」であり、この強力な柔軟性の面白さ、伝わったでしょうか。

と良い面ばかりを紹介しましたが、pokeの弱点も紹介しておきます。

1) 知名度の低さ
誰も使ってません。ウェブ上に情報がほぼありません。
※ただし、公式ドキュメントの質が極めて高いので困ることはあまりありません。

2) pickleの少なさ
pokeにはELFやpcap、jpegなどのpickleがバンドルされていますが、まだまだ種類は少ないです。
いざ「使おう!」となった場合、構造が既知のバイナリデータだとしても、対象とするpickleがない場合は自分で実装する必要があります。

3) Windowsで動きません
Windowsで動きません。WSLから使ってください。

「poke一本で行くのは厳しい」というのが正直なところです。010やImHexのような華やかさはありません。しかし、ここで紹介したようにポテンシャルは極めて高いツールです。特に、普段ターミナル上で生活してる方にとっては、GUIのバイナリエディタと行き来するコストがなくなるのでお勧めです。
ぜひつついてみてください。👉0️⃣1️⃣

*1:オラクルのツールチェイン/コンパイラチームのテクニカルリード

*2:https://ja.wikipedia.org/wiki/Network_block_device