Macで試すBinary Hacks Rebooted その1:イントロダクション

去る8月28日に、私が執筆に参加した「Binary Hacks Rebooted」が無事に発売されました。

「Binary Hacks Rebooted」はLinuxを前提に書かれたHackが多いです(特に前半)。しかし、読者の中にはMacを使っているという方も一定数いるのではないかと思います。というか、Macを使っている人にも読んでもらいたいです。

もちろん、Mac上でもDocker, multipass, QEMU等を使ってLinuxを動かせば、本の内容は再現できます。しかし、バイナリアンの気持ちとしては、マシンでネイティブに動いているOSを使ってHackしたいものではないでしょうか。少なくとも私はそうです。(まあMac上でネイティブにLinuxを入れるという手もありますが、それだと機材がMacである必要がないのでは?と私は思ってしまいます。)

また、x86-64 Linuxであれば本の内容がそのまま動くのは当たり前(※)なので、違う環境でどうなるか試行錯誤することによって圧倒的成長💪💪💪が見込まれます。(※と言いつつ、Linuxでもディストロによって、あるいはUbuntuであってもバージョンが違うと無変更で動くかはわからないわけですが。本に載っている実行例はUbuntu 22.04のものが多いと思います。)

というわけで、MacでmacOSを使ってBinary Hacks Rebootedの内容を試したいです。

「はじめに」の「Macユーザーのための読み方ガイド」に書いたことをもう一度繰り返すと、Macで本の内容を試す際は

  • OSの違い(Linux vs macOS)
  • 実行ファイルフォーマットの違い(ELF vs Mach-O)
  • 命令セットアーキテクチャの違い(x86-64 vs AArch64)
    • 2020年暮れにAArch64への移行が始まったので、買い替えサイクルを考えるとIntel Macはそろそろ無視できる数になってきたかもしれません。
  • ツールチェインの違い(GNU vs LLVM)

について意識する必要があります。と言っても、意識しろと言われて対処できたら苦労はしない!という読者もいるかもしれないので、著者陣の中の数少ないMacユーザーとして、私が「Binary Hacks Rebootedの内容をMacで試すとどうなるか」を実践してみたいと思います。

(執筆裏話:執筆中、原稿を管理するgitリポジトリに「ファイル名の大文字と小文字が違う2つのファイル」が紛れ込んで対処が面倒だったことがありました。foo.txtFoo.txt みたいなファイルが独立して存在するわけですね。Linuxでデフォルトで使われるファイルシステムだとそういうファイルを区別できますが、Macのデフォルトはファイル名の大文字と小文字を区別しないので実体が同一となり、git的には「foo.txt の中身が Foo.txt で上書きされた状態」か「Foo.txt の中身が foo.txt で上書きされた状態」のどちらかになります。何も変更していないのにgit的には「変更がある」状態になってしまう。そうするとrebase等の操作が非常に面倒になります。もちろん私もLinuxの環境を持っているので対処はできますけどね。)

今回は「1章 イントロダクション」の内容を扱いますが、今後の記事で取り上げる順番は必ずしも本の通りとは限りません。

本のサンプルコードを写経するのが面倒な場合はサポートリポジトリから取ってきてください。

Hack #1 未知のバイナリの読み方

Macにも file コマンドはあるので、概ね本文の通りに実行できるのではないかと思います。

ただし、本文はLinuxで実行しているので ls コマンドの実体がELFフォーマットとなっていますが、MacはMach-Oフォーマットを使うのでその辺の出力が違います。readelf コマンドもありません。

また、Macのtarコマンドは --old-archive オプションを受け付けず、デフォルトで「古い形式」を出力するようです。

ちなみに、最近のMacのデフォルトのシェルはzshですが、zshだとダブルクォートを使った

$ echo "Hello, World!" > hello.txt

は期待通りに動作しません。なので本文ではシングルクォートを使いました(執筆裏話)。

Hack #2 アセンブリ入門

まず、本文では gcc コマンドを使っており、Macでも gcc コマンドは使えますが、Macの gcc コマンドは実体が clang なので、ここでは混乱を避けるために明示的に clang コマンドを使います(これは今後も同様です)。

また、Macの objdump はLLVMベースで、GNUのものと受け付けるオプションが若干異なります。特に、 --disassemble オプションの代わりに --disassemble-symbols オプションを使うようです。MacではCのシンボルにはアセンブリレベルでは先頭にアンダースコアがつくので、アンダースコアをつけた名前を指定します。

アーキテクチャについてですが、最近はApple Silicon Macの人が多いと思われるので、AArch64のアセンブリを解説します。Intel Macを使っている人は本も参考にしてください。

$ clang -o add add.c
$ objdump --disassemble-symbols=_add --no-addresses add

add:	file format mach-o arm64

Disassembly of section __TEXT,__text:

<_add>:
 d10043ff              	sub	sp, sp, #16
 f90007e0              	str	x0, [sp, #8]
 f90003e1              	str	x1, [sp]
 f94007e8              	ldr	x8, [sp, #8]
 f94003e9              	ldr	x9, [sp]
 8b090100              	add	x0, x8, x9
 910043ff              	add	sp, sp, #16
 d65f03c0              	ret

機械語とアセンブリ言語が表示されました。これを読み解いていきます。

左側に出ている16進法8桁が機械語です。x86-64は可変長ですが、AArch64のA64命令セットは4バイト(32ビット)の固定長です。人間が読むのは右側の逆アセンブル結果です。

本にあるx86-64の例はROP/JOP/COPなどの攻撃への対策となる endbr64 という命令で始まっていました。Armにも類似の命令として BTI というものがあるようですが、ここでは使われていません。ArmではFEAT_BTIという拡張機能でBranch Target Identificationをやるようですが、私が使っているApple M1はこれに対応していません。

関数の最初の sub sp, sp, #16 というのはスタックポインター sp の値を16減らすという意味です。AArch64ではスタックはアドレスが小さくなる方に伸びるので、これは使えるスタック領域を16バイト分増やすという意味です。ややこしいですね。

スタックに確保した16バイトで何をするのかというと、ローカル変数 ab を保存します。最初の引数は x0 レジスターで渡され、2番目の引数は x1 レジスターで渡されるので、str 命令(ストア命令)でそれらをスタックに保存しています。[sp, #8] はアドレス sp + 8 の内容物を意味します。

変数をスタックに保存したのはいいのですが、AArch64では演算を行う際には演算対象をレジスターに持ってこないといけません。そういうわけで、その次の ldr 命令(ロード命令)でローカル変数の内容を再びレジスター x8x9 に配置しています。

レジスターで受け取った引数をスタックに保存して、それを演算のためにまた読み出すなんて、無駄なことをしていると思いませんか?実際この簡単な関数ではこれは無駄で、コンパイル時に最適化オプション -O を有効にするとスタックの操作は全部消えます。

実際の加算は add x0, x8, x9 命令で行っています。最初のオペランドである x0 は結果を保存するレジスターで、残りの x8x9 が入力です。ちなみにx86-64では add 命令は2オペランドで、オペランドの一つが入力と出力を兼ねています。

add sp, sp, #16 では確保したスタックを元に戻しています。

ret は関数の呼び出し元に戻る命令です。x86-64と同じですね。

main 関数も見ておきます。

$ objdump --disassemble-symbols=_main --no-addresses add

add:	file format mach-o arm64

Disassembly of section __TEXT,__text:

<_main>:
 d10083ff              	sub	sp, sp, #32
 a9017bfd              	stp	x29, x30, [sp, #16]
 910043fd              	add	x29, sp, #16
 b81fc3bf              	stur	wzr, [x29, #-4]
 d2800020              	mov	x0, #1
 d2800041              	mov	x1, #2
 97fffff2              	bl	0x100003f58 <_add>
 a9417bfd              	ldp	x29, x30, [sp, #16]
 910083ff              	add	sp, sp, #32
 d65f03c0              	ret

この中で最も重要なのが bl ... <_add> で、この命令が add 関数を呼び出しています。x86-64では同等の命令は call ですが、AArch64では bl と言います。これはBranch with Linkの略で、Branchはジャンプを意味するようです(分岐に限らない)。Linkは呼び出し元の情報を保存する、みたいな意味だと思います。

Armの機械語やアセンブリの命令については、Arm Architecture Reference Manualというのがオフィシャルな情報源です。Arm Architecture Reference Manual for A-profile architecture から入手できますが、執筆時点の最新版(K.a)で1万4千ページあります。ちなみにIntelのマニュアル(Intel SDM)は5000ページ程度です。ArmのやつにはAArch64に加えてAArch32の情報も載っているからでかいのでしょうか。

Hack #3 Hello, World!再訪

これも本文には「x86-64上で動くLinuxを対象とします」とあります。ですが、途中まで(C言語だけで書かれた分)ならmacOSでもそのまま動きます。Unixですからね。

標準CのHello worldは当然macOS/Clangでも同様にコンパイル・実行できます。

$ clang -o hello_c hello.c
$ ./hello_c
Hello, World!

write システムコールを使ったやつも同様にコンパイル・実行できます(Unixなので)。

$ clang -o hello_c_syscall hello_syscall.c
$ ./hello_c_syscall
Hello, World!

syscall 関数はどうでしょうか。

$ clang -o hello_c_syscall_2 hello_syscall_2.c
hello_syscall_2.c:3:18: warning: 'syscall' is deprecated: first deprecated in macOS 10.12 - syscall(2) is unsupported; please switch to a supported interface. For SYS_kdebug_trace use kdebug_signpost(). [-Wdeprecated-declarations]
int main(void) { syscall(SYS_write, 1, "Hello, World!\n", 14); }
                 ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/unistd.h:748:6: note: 'syscall' has been explicitly marked deprecated here
int      syscall(int, ...);
         ^
1 warning generated.
$ ./hello_c_syscall_2
Hello, World!

動きはするものの、syscall 関数は非推奨というようなメッセージが出てきました(私はmacOS 13で試しています)。

アセンブリ言語でのHello worldですが、macOS(というかDarwin)のシステムコールのABIは一般ユーザーが使うことを想定していないようです。なので、ここでは詳しい解説はしません。どうしても試したい場合は below/HelloSilicon: An introduction to ARM64 assembly on Apple Silicon Macs などのページを参照してください。

【追記】昔のGo言語はmacOS上で直接システムコールを呼び出していたようですが、ABIの不安定性によりlibSystem経由に変更しています:

【/追記】


というわけで、Binary Hacks Rebootedの第1章をMacで実践してみました。私はmacOS (Darwin) に関する深い知識を持ち合わせているわけではないので調べつつにはなりますが、今後も不定期的に記事を書いていきたいと思います。