ペネトレーションしのべくん

さようなら、すべてのセキュリティエンジニア

本当のReversing初心者がGDBでpicoCTF 2022「GDB Test Drive」をガチャガチャやる

はじめに

Reversing、というかGDBの練習がてら、picoCTF2022の「GDB Test Drive」をいじくり回しました。

https://play.picoctf.org/practice/challenge/273?category=3&originalEvent=70&page=1

よく分かる用語解説

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感の湧く種目なので、精進していきます。

*1:PythonのREPLを開いて、「int(0x186a0)」する

*2:x64アーキテクチャなので、64ビットあるrdiレジスタの32ビットぶんだけを使っている状態になる