コース目次 / 第2章
逆アセンブルを読んで、正解を計算する
objdumpでcheck関数を逆アセンブルし、比較対象のバイト列とXOR鍵が即値オペランドとして埋め込まれていることを読み取ります。実行せずにPythonで正解を計算し、最後に実行で答え合わせをします。
check関数を、逆アセンブルする
objdump -d -M intel crackme
<check>: から始まる部分に注目します。長い出力ですが、必要なのは最初の数命令です。
0000000000001209 <check>:
1209: f3 0f 1e fa endbr64
120d: 55 push rbp
120e: 48 89 e5 mov rbp,rsp
1211: 48 83 ec 30 sub rsp,0x30
1215: 48 89 7d d8 mov QWORD PTR [rbp-0x28],rdi
1219: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
1220: 00 00
1222: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
1226: 31 c0 xor eax,eax
1228: c7 45 f2 1a 0e 02 0c mov DWORD PTR [rbp-0xe],0xc020e1a
122f: 66 c7 45 f6 08 0f mov WORD PTR [rbp-0xa],0xf08
1235: c6 45 e7 55 mov BYTE PTR [rbp-0x19],0x55
1239: 48 8b 45 d8 mov rax,QWORD PTR [rbp-0x28]
関数プロローグ(push rbp / mov rbp,rsp / sub rsp,…)は、sec-asmで見た「スタックフレームを作る」定型パターンです。読み飛ばしてよい部分と、注目すべき部分を見分けられることが、revの第一歩です。
即値オペランドに、答えが埋まっている
0x1228 の mov DWORD PTR [rbp-0xe],0xc020e1a に注目します。これは 「4バイトの即値 0x0c020e1a を、スタック上の変数に書き込む」命令です。x86はリトルエンディアンなので、メモリ上のバイト列は下位バイトから並びます——つまりこの4バイトは 1a 0e 02 0c です。
続く 0x122f の mov WORD PTR [rbp-0xa],0xf08 は2バイト 08 0f。合わせると 6バイトの配列 1a 0e 02 0c 08 0f が、target 配列の初期化だと分かります(crackme.c の target[] と同じ値です——ソースを知らなくても、この並びだけで読み取れます)。
さらに 0x1235 の mov BYTE PTR [rbp-0x19],0x55 が、1バイトの XOR鍵 0x55 です。
これで実行せずに答えが分かります。 ソースが手元にある今回は答え合わせがすぐできますが、ソースなしバイナリだけが与えられるrev問題でも、同じ手順——mov命令の即値オペランドを拾って、比較ロジック(今回はXOR)を読み取る——で解けます。
Pythonで、正解を計算する
solve.py として保存します。
target = [0x1a, 0x0e, 0x02, 0x0c, 0x08, 0x0f]
key = 0x55
password = ''.join(chr(t ^ key) for t in target)
print(password)
python3 solve.py
O[WY]Z
まだ一度も ./crackme に正解を入力していません。逆アセンブルを読んで、計算だけで答えを出しました。
実行で、答え合わせをする
printf 'O[WY]Z\n' | ./crackme
合言葉を入力してください: 正解! flag{O[WY]Z_verified}
計算した通りの結果になりました。これがrevの基本の型です——「動かして総当たり」ではなく、「読んで論理を理解し、計算で答えを出し、最後に実行で確認する」。今回のXORのように単純な比較でなくても、暗号処理や複雑な条件分岐であっても、この順序自体は変わりません。
こうなっていればOK
卒業まであと1章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。