はじめに
Reversing、というかGDBの練習がてら、picoCTF2022の「GDB Test Drive」をいじくり回しました。
https://play.picoctf.org/practice/challenge/273?category=3&originalEvent=70&page=1
よく分かる用語解説
- Reversing
- バイナリをリバースエンジニアリングしてflagを得るCTFの種目
- アセンブリ言語の読解力が問われる
- レジスタ
- CPU内の記憶領域
- 変数みたいに使える
Writeup
問題文の指示に従うだけで、flagが取れるようになっています。
### gdbmeに実行権限を付ける $ chmod +x gdbme ### gdbmeをデバッグ対象としてGDBを起動する $ gdb gdbme ### アセンブリの実行状況を表示する (gdb) layout asm ### main+99(sleep処理)にブレークポイントを設定する (gdb) break *(main+99) ### 実行開始 ### ブレークポイントを設定したので、sleepが実行される直前でストップする (gdb) run ### sleep処理をスキップして次の処理へ ### もうブレークポイントがないので、最後まで実行されてflagが表示される (gdb) jump *(main+104)
勉強のための遠回りWriteup
静的解析
バイナリを実行せずに情報を取得します。まずはfile
コマンド。x64のELF実行バイナリということが分かりました。
$ file gdbme gdbme: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1452b4fe8090256c4ed6f7c4c7ed5b60d2124746, for GNU/Linux 3.2.0, not stripped
strings
コマンド。これでflagが出てくることもありますが、そこまで簡単ではありませんでした。
$ strings gdbme ...(特に手がかりになりそうな情報なし)
rabin2 -z
は、メモリアドレスまで教えてくれるstrings
というつもりで使っていますが、ちょっと違うかもしれない。
$ rabin2 -z gdbme [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――
checksec
。Pwnじゃないので必要ないと思いますが一応。
$ checksec gdbme [*] '/home/akari/work/rev-shugyo/gdbme' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
objdump
で逆アセンブルの結果を見ます。とりあえずmain関数だけ。途中でsleepを呼んでいますね。直前のmov edi,0x186a0
が引数なので、10進数に直すと100,000*1。バイナリを実行するとまず100,000秒待たされた後、rotate_encrypt関数、fputs関数、putchar関数という順番で実行されるようです。
00000000000012c7 <main>: 12c7: f3 0f 1e fa endbr64 12cb: 55 push rbp 12cc: 48 89 e5 mov rbp,rsp 12cf: 48 83 ec 50 sub rsp,0x50 12d3: 89 7d bc mov DWORD PTR [rbp-0x44],edi 12d6: 48 89 75 b0 mov QWORD PTR [rbp-0x50],rsi 12da: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 12e1: 00 00 12e3: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 12e7: 31 c0 xor eax,eax 12e9: 48 b8 41 3a 34 40 72 movabs rax,0x4c75257240343a41 12f0: 25 75 4c 12f3: 48 ba 35 62 33 46 38 movabs rdx,0x4362383846336235 12fa: 38 62 43 12fd: 48 89 45 d0 mov QWORD PTR [rbp-0x30],rax 1301: 48 89 55 d8 mov QWORD PTR [rbp-0x28],rdx 1305: 48 b8 30 35 43 60 47 movabs rax,0x6030624760433530 130c: 62 30 60 130f: 48 ba 68 66 34 62 66 movabs rdx,0x4e32676662346668 1316: 67 32 4e 1319: 48 89 45 e0 mov QWORD PTR [rbp-0x20],rax 131d: 48 89 55 e8 mov QWORD PTR [rbp-0x18],rdx 1321: c6 45 f0 00 mov BYTE PTR [rbp-0x10],0x0 1325: bf a0 86 01 00 mov edi,0x186a0 132a: e8 e1 fd ff ff call 1110 <sleep@plt> 132f: 48 8d 45 d0 lea rax,[rbp-0x30] 1333: 48 89 c6 mov rsi,rax 1336: bf 00 00 00 00 mov edi,0x0 133b: e8 c9 fe ff ff call 1209 <rotate_encrypt> 1340: 48 89 45 c8 mov QWORD PTR [rbp-0x38],rax 1344: 48 8b 15 c5 2c 00 00 mov rdx,QWORD PTR [rip+0x2cc5] # 4010 <stdout@GLIBC_2.2.5> 134b: 48 8b 45 c8 mov rax,QWORD PTR [rbp-0x38] 134f: 48 89 d6 mov rsi,rdx 1352: 48 89 c7 mov rdi,rax 1355: e8 96 fd ff ff call 10f0 <fputs@plt> 135a: bf 0a 00 00 00 mov edi,0xa 135f: e8 5c fd ff ff call 10c0 <putchar@plt> 1364: 48 8b 45 c8 mov rax,QWORD PTR [rbp-0x38] 1368: 48 89 c7 mov rdi,rax 136b: e8 40 fd ff ff call 10b0 <free@plt> 1370: b8 00 00 00 00 mov eax,0x0 1375: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8] 1379: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28 1380: 00 00 1382: 74 05 je 1389 <main+0xc2> 1384: e8 57 fd ff ff call 10e0 <__stack_chk_fail@plt> 1389: c9 leave 138a: c3 ret 138b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
TODO:readelf(これもPwnで見ればよさそう?)
動的解析
単純にバイナリを動かしてみます。案の定次の結果は表示されず。100,000秒は待っていられないので、次いきます。
$ ./gdbme
gdbmeに実行権限を付けた後、GDBを起動します。
$ chmod +x gdbme $ gdb gdbme
以下のコマンドを実行すると、画面に実行状況と現在のレジスタの状態を表示してくれます。レジスタの状態は、バイナリを実行したら表示されます。
(gdb) layout asm (gdb) layout r
このまま実行したら100,000秒待たされるので、実行を停止する箇所(ブレークポイント)を設定します。指定した位置の処理が実行される直前で止まります。ここではsleepが実行される直前で止まるように、sleep処理の位置を指定します。位置はメモリ番地でもいいし、以下のようにGDBが計算してくれた?関数内の番地でもいいです。
(gdb) b *(main+99)
すると、さっきlayout asm
で表示したフィールドの、ブレークポイントを設定した位置にb+
と表示されるようになりました。便利過ぎ。
ではいよいよ実行です。
(gdb) run
>
が指しているのが現在位置で、ブレークポイントで処理が止まっているのが分かります。
これをこのまま実行すると100,000秒待たされます。さくっと次に行きたいので、jump
を使います。
(gdb) jump *(main+104)
すると、sleepが実行されずにその次のmain+104を処理し、そのまま最後まで実行されます。標準出力にflagが表示されています。
flagはいつ表示されるのか?
この問題のキモは「いかにしてsleepを迂回するか」ということでした。jump
を実行した時点でバイナリが最後まで実行されてしまったので、flagがどの位置の処理によって表示されたのかがよく分かりません。それを確認するために、もう一度run
してsleep処理の直前まで来ました。ここで新たにmain+116にブレークポイントを設定してからjump *(main+104)
します。すると、main+116で処理が止まります。
ここから、nexti
で1行ずつ、かつcall
による関数呼び出しがあった場合は関数内へ処理を移さずに、1行の処理として進めます。以下はfputsの処理が終わったところです。
putcharの処理が終わったところで、flagが表示されました。
レジスタの値を操作してsleepの時間を短くする
sleepをjump
で迂回しましたが、もう1つの方法としてsleepの引数を変えてしまうというのがあります。sleepの直前を見るとmov $0x186a0,%edi
という処理があります。0x186a0が100,000であるということは前述の通りですが、これがrdiレジスタ*2を経由してsleepに引数として渡ります。どこに保存されている引数が何個目の引数として次の関数に渡されるかはアーキテクチャ毎の関数呼び出し規約によって決められていますが、あんまり詳しくないのでここでは割愛します。とりあえず画面上部のレジスタ状態を見ると、rdiに0x186a0が入っていることが分かります。
これを、以下のコマンドで0x1に書き換えてみます。
(gdb) set $rdi=0x1
書き換えたところです。rdiが0x1になっているのが分かります。
continue
で処理を実行したところ、1秒の停止の後にflagが表示されました。
おわりに
以下のコマンドの使い道を学びました。GDBめちゃくちゃ便利だなと思いました。
- run
- continue
- b
- nexti
- layout asm
- layout r
今日の今日までlayout
を知りませんでした。一応gdb-pedaもセットアップしているのですが、そこに頼るレベルにはまだなさそうです。RevやPwnは俺TSUEEE感の湧く種目なので、精進していきます。