この本について
『Binary Hacks Rebooted ―低レイヤの世界を探検するテクニック89選』を読み終えたので、内容の紹介と所感をまとめます。
「低レイヤの本」と聞くと、業務で直接使う機会が少ないジャンルに思えるかもしれません。しかし本書を読み進めると、私たちが普段当たり前に使っている抽象化(コンテナ、動的リンク、例外処理、デバッガ)が、どのような部品の組み合わせで成立しているのかが具体的に見えてきます。設計やアーキテクチャに関心のある方にとって、「抽象化の下側を知る」という体験そのものが大きな収穫になる一冊でした。
書誌情報
| 項目 | 内容 |
|---|---|
| 書名 | Binary Hacks Rebooted ―低レイヤの世界を探検するテクニック89選 |
| 著者 | 河田 旺、小池 悠生、渡邉 慶一、佐伯 学哉、荒田 実樹 |
| 寄稿 | 鈴木 創、中村 孝史、竹腰 開、光成 滋生、hikalium、浜地 慎一郎 |
| 出版社 | オライリー・ジャパン |
| 刊行 | 2024年 |
| ISBN | 978-4-8144-0085-0 |
| サンプルコード | https://github.com/oreilly-japan/binary-hacks-rebooted |
本書の位置づけ
本書は2006年に刊行された『Binary Hacks ――ハッカー秘伝のテクニック100選』の再構築版として書かれています。「Rebooted」というタイトルの通り、旧版の改訂ではなく、現代に必要とされるHackを新規に集め直した一冊です。
著者らは旧版のHackの多くが今でも有用であるとしたうえで、この18年でセキュリティ意識が高まり、ソフトウェアの大規模化・多層化が進み、ハードウェアも進化したことで、必要とされる知識が増えたと説明しています。そのため本書は旧版との内容的な重複を避けつつ、単独で完結する構成になっています。旧版を持っていなくても問題なく読めますし、両方を読み比べると、時間の経過が低レイヤ技術に与えた影響を体感できる作りになっています。
なお、ここでいう「低レイヤ」とは抽象化の度合いが低く計算機に近いレイヤのこと、「Hack」とは巧妙な方法で問題を解決する手法のことを指します。本書はこの2つの定義に忠実で、89個のHackが「役に立つもの」「一見役に立たないもの」「易しいもの」「難しいもの」を取り混ぜて並んでいます。
対象環境と前提知識
- 対象は基本的に x86-64 上の GNU/Linux(特定アーキテクチャ限定のHackはごく一部)
- サンプルコードは C言語とPython が中心、一部 Rust
- UNIXライクなシステムでコマンドライン操作ができることが前提
- アセンブリが登場する箇所には都度解説あり
システムプログラミングの経験がある方を想定読者としていますが、著者自身が「技術的な興味さえあれば経験は必須ではない」と書いている通り、興味が牽引力になるタイプの本です。実行例はx86-64のDebian系ディストリビューションで確認されています。
Macユーザー向けには、5章・7章・8章にmacOSでも通用するHackが含まれるという案内があります。OS(Linux vs macOS)、実行ファイルフォーマット(ELF vs Mach-O)、命令セット(x86-64 vs AArch64)、ツールチェイン(GNU vs LLVM)の4つの違いを意識するとよい、という整理は実務でも使える視点です。
章ごとの見どころ
1章 イントロダクション(#1〜#3)
未知のバイナリをどう読み解くか、という導入から始まります。file コマンドでファイル形式を特定するところから入り、「では file コマンドはどうやって形式を判別しているのか」と問いを立てて、マジックナンバーを手作業で追う「人力fileコマンド」へ進む流れが秀逸です。
$ echo 'Hello, World!' > hello.txt
$ zip -r hello.zip hello.txt
$ file hello.zip
hello.zip: Zip archive data, at least v1.0 to extract, compression method=store
拡張子を取り除いても正しく判別できることを実演し、そこから「便利なツールもバイナリでしかないのだから理解できるはずだ」と読者を引き込みます。本書全体を貫く姿勢が、この最初の数ページに凝縮されています。
2章 ELF Hack(#4〜#21)
Linuxの実行ファイル・共有ライブラリのフォーマットであるELFを扱う章です。ELFヘッダ・セクションヘッダ・プログラムヘッダという3種類のヘッダの役割分担から始まり、readelf -l でプログラムヘッダを読む具体例へと進みます。
$ gcc -o hello hello.c
$ readelf -l hello
個人的に有用だと感じたのは、実行ファイルにELFヘッダとプログラムヘッダは必須だが、セクションヘッダは必ずしも必要ではないという指摘です。リンカ・デバッガが見るものと、実行時にカーネル・ダイナミックリンカが見るものが分かれている、という設計上の分離が明確になります。
章の後半では patchelf や LIEF によるバイナリ書き換え、PT_NOTE を利用したバイナリパッチなど、実践的なHackが続きます。そして圧巻が DWARF三部作(#19〜#21) です。
DWARFはデバッグ情報のフォーマットですが、C++の例外処理における大域脱出でcallee-savedレジスタを復元するために、DWARF Expressionという独自のバイトコードとVMを内蔵しています。著者らはこれを利用し、例外を投げることでバイトコードを実行させ、四則演算を行う計算機をDWARF Expressionだけで実装してしまいます。しかもDWARF Expressionはメモリ読み出しとレジスタ操作しかできず、書き込みも入出力も持たないという制約付きです。
「デバッグ情報フォーマットの中に汎用計算機が潜んでいた」という発見は、フォーマット設計の副作用として何が起こりうるかを考えさせられます。実用性という観点では確かに意味が薄いのですが、こういう探究こそが本書の醍醐味だと感じました。
3章 OS Hack(#22〜#36)
OSが提供する機能を深く使い、時にはOS自体を改造するHackを扱います。前半はプロセス・メモリ・ファイルシステム、後半はカーネル内部やファームウェア自作へと進みます。
導入となる #22 では、Linuxにおける「実行可能ファイル」を広義に捉え、ELF・shebang・binfmt_misc の3方式を並べて解説しています。
-
ELF:
PT_INTERPセグメントの有無で起動パスが2通りに分かれる。存在しなければカーネルが直接実行、存在すればダイナミックリンカ(多くはld-linux.so)を経由する -
shebang:
#!の解釈はカーネルが行う(fs/binfmt_script.c)。任意の実行可能ファイルを指定できるため、#!/bin/ls -alhのような遊びも成立する -
binfmt_misc:マジックナンバーに応じて起動プログラムを差し替える仕組み。
qemu-user-staticでAArch64のバイナリがそのまま起動できるのはこの機能によるもの
「なぜクロスアーキテクチャのバイナリが透過的に動くのか」という日常的な疑問に、カーネルの機能名で答えが返ってくるのは気持ちのよい体験でした。
このほか Huge Page、CRIU によるプロセスの保存と再開、FUSE によるファイルシステム自作、KVM を使ったハイパーバイザ作成、Unikernel、UEFI と Secure Boot、Chromebook 上での自作ファームウェア動作まで、射程がかなり広い章です。
4章 コンテナHack(#37〜#43)
個人的に最も実務に近いと感じた章です。コンテナを「箱」というイメージで捉えている人に向けて、その箱が何をどう分離しているのかを分解していきます。
出発点として、OCI Runtime Specification におけるコンテナの定義(隔離とリソース制限を設定可能な、プロセスを実行するための環境)を引き、そこから2つの帰結を導きます。
- コンテナは「プロセスの実行環境」であり、通常カーネルを含まない → 同一ホスト上の他コンテナとカーネルを共有する → VMより軽量
- コンテナの役割は「隔離」と「リソース制限」である
そして重要な指摘が続きます。Linuxカーネルはコンテナを作るための単一のAPIを提供していません。DockerなどのコンテナランタイムはLinuxネームスペース、cgroup、chroot/pivot_root、ケーパビリティといった機能を適切に組み合わせて実装されている、というのが実態です。
Linuxネームスペースは現在8種類(マウント、UTS、IPC、ネットワーク、PID、cgroup、ユーザー、Time)がサポートされており、操作には clone / unshare / setns の各システムコールを使います。実際に手を動かす例も用意されています。
$ sudo unshare --pid --mount --fork /bin/bash
# echo $$
1
# mount -t proc procfs /proc
PID名前空間を作るとbashがPID 1になり、procfsをマウントし直すと見えるプロセスが激変する。この一連の操作を自分の手で実行すると、コンテナが「魔法」から「既存機能の組み合わせ」に変わります。
「一般ユーザーがrootのように振る舞う方法3選」「rootlessコンテナの使い方としくみ」「/proc/PID/root からコンテナ内のファイルに直接アクセスする」など、運用寄りのHackも含まれており、Kubernetesを触っている方には投資対効果が高い章だと思います。
5章 デバッガ・トレーサHack(#44〜#52)
前半はデバッグのテクニック、後半はトレーシングによる挙動・パフォーマンス解析という構成です。
冒頭の gdb Tips だけでも読む価値があります。たとえば starti コマンドは、ユーザーランドのコードが実行され始める直前、つまり ld.so の最初の命令でブレークします。エントリポイントより前で止まるため、動的リンカ内でクラッシュするケースや、共有ライブラリがロードされる瞬間を観察したい場合に有効です。
$ gdb /usr/bin/echo -q
(gdb) starti
(gdb) info proc mappings
catch syscall write のように特定のシステムコールでブレークする方法も紹介されています。
後半では、サニタイザ(Address Sanitizerの仕組みまで踏み込む)、perf によるパフォーマンス解析、ftrace、eBPFトレーシング、DBIによる実行命令のトレースと改変、Intel PTによる高速トレース取得が扱われます。rr による Record and Replay デバッグ は、再現困難なバグに悩んでいる方には特に刺さる内容だと思います。
6章 セキュリティHack(#53〜#67)
旧版ではセキュリティ関連のHackはごく少数でしたが、本書では独立した章になっています。著者らはこれを時代の変化の反映だと説明しており、この20年で攻撃手法が体系化・高度化し、防御策も進化したことが背景にあります。
構成としては、防御機構と攻撃手法を対にして並べているのが特徴です。
- サンドボックス:seccomp(システムコールのフィルタリング)、Landlock(非特権プロセスのサンドボックス)
- メモリ破壊対策と攻撃:ASLR ⇔ ROP ⇔ Intel CET、Clang CFI による不正な制御フローの検知
- 脆弱性探索:ファジングの概要と分類、グレイボックスファジング、LibAFLでのファザー実装と改良、angrによるシンボリック実行
- ハードウェア起因の攻撃:BadUSB、Row Hammer、Meltdown と Spectre
seccompの解説は具体的です。SECCOMP_MODE_STRICT では read / write / exit / sigreturn の4つしか許可されず、SECCOMP_MODE_FILTER では許可リストをBPFプログラムとして記述できる(このため seccomp-bpf と呼ばれる)、引数によるフィルタリングも可能、という整理がなされています。
libseccompを使うと、次のような流れでサンドボックスを構成できます。
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_load(ctx);
untrusted_func();
「信頼できないサードパーティ関数を呼ぶが、想定外のシステムコールは実行させたくない」という具体的なシチュエーションから入るため、システムコールがユーザー空間とカーネル空間の境界であり、そこを絞ることが効果的な制限になるという原理が腑に落ちます。設計としての「境界の選び方」を考えるうえでも示唆的でした。
7章 数値表現とデータ処理Hack(#68〜#78)
整数と浮動小数点数、そしてSIMDを扱う章です。2の補数、算術シフトと論理シフト、符号拡張、エンディアンといった基礎から始まりますが、単なる復習では終わりません。
たとえば、x86-64とAArch64で同じCコードをコンパイルして命令列を比較する例があります。
void add_int8(const int8_t *a, const int8_t *b, int8_t *restrict c) {
*c = *a + *b;
}
x86-64では8ビット加算命令(add al, ...)が出るのに対し、AArch64では8ビット幅の加算命令が存在しないため32ビット加算命令(add w0, w0, w1)になります。結果を8ビットで書き戻すなら符号拡張の有無で結果が変わらないため、符号拡張は省略される。こうした「なぜこの命令列になるのか」の説明が、コンパイラの気持ちを理解する助けになります。
浮動小数点数まわりは特に濃く、ビット列表現、浮動小数点例外、丸めモードの変更、浮動小数点環境を触るコードに対するコンパイラの最適化と戦う、NaNの深掘り、アーキテクチャごとの差異と続きます。数値計算に関わる方は、この章だけでも元が取れる分量だと思います。
8章 言語処理系Hack(#79〜#86)
コンパイラの教科書には載らないが現実世界で使われている技術を集めた章です。処理系を実装する予定がなくても、設計テクニックのカタログとして読めます。
NaN boxing(#79) が白眉でした。動的型付き言語の典型的な実装では、タグと内容を組み合わせて値を表現します。
struct Value {
uint8_t tag;
union {
Object *obj;
double f64;
int32_t i32;
};
};
この素朴な定義では、64ビット環境でパディング込み16バイトを消費します。Value型は至るところに現れるため、1バイトでも削りたい。ここでIEEE 754のNaNが大量のビットパターンを持つことを利用し、上位ビットをタグ、下位48ビットをペイロードとして使うことで、8バイトに任意のタグ付き値を格納します。64ビット環境でもポインタが実質48ビット程度しか使われていない事実が、この技法を成立させています。
#define TAG_MASK UINT64_C(0x7FFF000000000000)
#define PAYLOAD_MASK UINT64_C(0x0000FFFFFFFFFFFF)
#define STRING_TAG UINT64_C(0x7FF9000000000000)
#define INT32_TAG UINT64_C(0x7FFA000000000000)
規格上の「未使用領域」を意図的に利用して表現を圧縮する発想は、データ構造設計を考えるうえで応用範囲が広いと感じました。
このほか、ucontext.h によるコルーチン実装、PGO(Profile Guided Optimization)、LD_PRELOAD によるメモリアロケータの差し替え、ABIと呼び出し規約、libffiによる実行時シグネチャ解決、実行時の機械語生成(JITの基礎)、GCC/Clangの組み込み関数が扱われます。
9章 そのほかのHack(#87〜#89)
用語集、必要なツール、文献案内の3本立てです。付録的な位置づけですが、実用性は高いです。
用語集はABI、ASLR、DWARF、ELF、PIC/PIE、seccomp、TLS、UEFI、unwind、x86-64 など、本文で登場する概念を簡潔にまとめています。定義がやや辛口なのも読みどころで、たとえばglibcは「ソースコードに興味深いコメントが多数残されている」、root権限は「使用には大いなる責任が伴う」といった調子です。
ツール紹介(#88)は現場で効きます。特に elfutils 系のツール の存在を知れたのが収穫でした。
-
eu-addr2line:-pでプロセスIDを指定でき、実行中のプロセスのアドレスを行番号に変換できる -
eu-readelf:壊れたELFファイルなど、通常のreadelfで読めない場合に有効
$ eu-addr2line -p $(pidof hello) 0x57cbc2caf171
/bh2/hello.c:6:9
GNU Binutils版と elfutils版の両方を入れておくとよい、というアドバイスは今後実践していこうと思います。objdump の --target オプションでメタデータのない生の機械語を逆アセンブルできる、xxd -include でバイナリをCのソースに埋め込める、といった小ネタも押さえられています。
設計・アーキテクチャの観点から得たもの
低レイヤの本ではありますが、読後に残ったのは個別のテクニックよりも、設計に関する次の3つの視点でした。
1. 抽象化は「隠す」と同時に「機能を封じる」
本書の「はじめに」では、抽象化レイヤを剥がしたくなる場面として、既存の枠組みでは不可能なことを実現したいとき、高性能なソフトウェアを作りたいとき、セキュリティ関連のプログラミングを行うときが挙げられています。抽象化のメカニズムそのものや、抽象化によって隠蔽された機能を使いたくなるからです。
これは日々の設計にも通じます。抽象化を1枚重ねるたびに、利便性と引き換えに下層の機能へのアクセスを失う。そのトレードオフを自覚しているかどうかで、レイヤの切り方は変わってきます。
2. 「組み合わせ」としてのシステム
コンテナ章の「Linuxカーネルはコンテナ作成の単一APIを提供していない」という指摘は象徴的です。私たちが1つの製品名で呼んでいるものは、多くの場合、既存の直交した機能を組み合わせた結果でしかありません。
これを知っていると、トラブルシューティングの解像度が上がります。「コンテナが動かない」ではなく「どのネームスペースの分離が意図通りでないのか」「cgroupの制限に引っかかっているのか」と切り分けられるようになります。
3. 制約の中で表現を圧縮する
NaN boxingやDWARF Expressionでの計算機実装は、いずれも「既存フォーマットの制約と余白」を読み切ることで成立しています。仕様を厳密に理解すると、そこに残された自由度が見えてくる。設計とは制約を課すことでもあり、同時に制約の中の余白を設計することでもある、と改めて感じました。
読み方のおすすめ
- 通読しなくてよい:各Hackは基本的に独立しており、目次から気になるものを選んで読めます。連続して読むことを想定したHackには、その旨が明記されています
- 難易度の目安がある:各Hackのタイトル横に温度計アイコンで初級・中級・上級が示されているため、体力に合わせて選べます
- 手を動かす前提で環境を用意する:Debian系のx86-64環境(Docker、multipass、QEMUなどでも可)があると体験が段違いです
- 興味の入口から入る:コンテナやKubernetesを触っている方は4章から、セキュリティに関心があれば6章から、処理系に興味があれば8章から入っても問題ありません
分量は多く、内容も軽くはありません。しかし1つ1つのHackが独立しているため、「今週はこのHackだけ試す」といった読み方が成立します。積読になりにくい構成だと思います。
まとめ
本書は「低レイヤの世界を探検するための入口」を89個並べた本です。すべてが実務に直結するわけではありませんし、著者ら自身も一見役に立たないHackが含まれることを認めています。それでもなお本書が魅力的なのは、実用性から離れて純粋に面白さを突き詰める姿勢が全編に貫かれているからです。
推薦文を寄せている高林 哲氏は、仕事のソフトウェア開発では現実的な解決策こそが大切という価値観がある一方、そこから離れてひたすら面白いことを突き詰める営みの尊さに触れています。この本を読んでいる間、まさにその感覚を追体験できました。
こんな方におすすめです。
- コンテナやデバッガの「中身」をブラックボックスのままにしておきたくない方
- 難解なバグやパフォーマンス問題に、より深いレイヤから切り込みたい方
- セキュリティの攻撃手法と防御機構を、実装レベルで対比して理解したい方
- 抽象化レイヤの下側を覗くことそのものを楽しめる方
逆に、明日の業務ですぐ使えるノウハウ集を期待すると、期待とはズレるかもしれません。とはいえ4章(コンテナ)、5章(デバッガ・トレーサ)、6章(セキュリティ)あたりは即戦力になる内容が多く、そこだけでも十分に価値があります。
読み終えて何より強く残ったのは、「所与のものとして扱っていた仕組みは、調べれば理解できる」という感覚でした。この感覚を得られること自体が、本書最大の効用かもしれません。