RE CTFd CMU x86 Phase7 writeup
はじめに
以下の常設CTFに掲載されているCMU x86は,Phase1~6まで解けばクリアになりますが,秘密のPhase7があります.
- https://reversing.ctfd.io
- Cutterを使用.
- 勉強のためにアセンブラを読むため,デコンパイラ機能は不使用.
Phase 7への入り方
Phase1~6までの回答をそのまま入力すると,Congratulations!
が出力され,プログラムが終了する.そのため,何らかの方法でsecret_phase
を実行する方法を考える.
各Phaseはmain
で呼び出され,以下の順序で実行される.
-
read_line
で文字列を読み取る -
phase_X
が実行される -
phase_defused
が実行される
phase_defused
に,secret_phase
をcall
している箇所があるため,どのように条件分岐しているかを調べる.
初めに,read_line
で文字列を入力した回数num_input_strings
が6かを確認している.つまりPhase6まで解かないと実行されない.
0x08049533 cmp dword [num_input_strings], 6 ; 0x804b480
0x0804953a jne 0x804959f ; 関数終了
次に,sscanf
を用いてアドレス0x0804b770
に格納された文字列を,整数と文字列に分けてそれぞれ[var_58h]
と[var_54h]
に格納している.
0x0804953c lea ebx, [var_54h] ; %sの先頭アドレス
0x0804953f push ebx
0x08049540 lea eax, [var_58h] ; %dの値(Phase4の回答)
0x08049543 push eax
0x08049544 push str.d__s ; 0x8049d03 ; const char *format
0x08049549 push data.0804b770 ; 0x804b770 ; const char *s
0x0804954e call sscanf ; sym.imp.sscanf ; int sscanf(const char *s, const char *format, va_list args)
0x08049553 add esp, 0x10
0x08049556 cmp eax, 2 ; 2 ; sscanfで変換できた数
ここで,アドレス0x0804b770
に格納される値を考える.
read_line
は呼び出される毎にinput_strings
のアドレスから呼び出し回数×80文字分加えたアドレスに文字を書き込む.以下の表を参照.
Phase | 文字列書き込み位置 | アドレス |
---|---|---|
1 |
input_strings + 80x0 |
0x0804b680 |
2 |
input_strings + 80x1 |
0x0804b6D0 |
3 |
input_strings + 80x2 |
0x0804b720 |
4 | input_strings + 80x3 |
0x0804b770 |
5 |
input_strings + 80x4 |
0x0804b7C0 |
6 |
input_strings + 80x5 |
0x0804b680 |
従って,アドレス0x0804b770
に書き込まれた文字列はPhase4の回答である.
そのためPhase4の回答時にaustinpowers
も追加で入力すればよい.
0x08049559 jne 0x8049592
0x0804955b add esp, 0xfffffff8
0x0804955e push str.austinpowers ; 0x8049d09 ;
0x08049563 push ebx ; int32_t arg_4h
0x08049564 call strings_not_equal ; sym.strings_not_equal
回答
9 austinpowers
secret_phase
secret_phase
では,またread_line
で値を読み取っている.さらにstrtol
で,整数に変換している.
secret_phase();
; var int32_t var_1ch @ stack - 0x1c
0x08048ee8 push ebp
0x08048ee9 mov ebp, esp
0x08048eeb sub esp, 0x14
0x08048eee push ebx ; 読み取った1行の文字列の先頭アドレス
0x08048eef call read_line ; sym.read_line
0x08048ef4 push 0 ;
0x08048ef6 push 0xa ; 基数は10進数
0x08048ef8 push 0 ; endptrはNULL
0x08048efa push eax ; 文字列の先頭ポインタ
0x08048efb call __strtol_internal ; sym.imp.__strtol_internal ; strtol(String to Long)でLong型に変換
0x08048f00 add esp, 0x10
次に,入力した整数から1減じた値が1000以下であるかチェックし,1000以下であれば爆発しないことが分かる.
0x08048f03 mov ebx, eax ; ユーザの入力した整数
0x08048f05 lea eax, [ebx - 1] ; ユーザの入力した整数 <= 1001なら爆発しない
0x08048f08 cmp eax, 0x3e8 ; 1000
0x08048f0d jbe 0x8048f14
0x08048f0f call explode_bomb ; sym.explode_bomb
0x08048f14 add esp, 0xfffffff8
さらに,fun7
にアドレス0x804b320
とユーザが入力した整数を渡している.
fun7
の戻り値が7であれば,本Phaseがクリアとなる.
0x08048f17 push ebx ; int32_t arg_8h ; ユーザの入力した整数
0x08048f18 push n1 ; 0x804b320 ; int32_t arg_4h ; obj.n1
0x08048f1d call fun7 ; sym.fun7
0x08048f22 add esp, 0x10
0x08048f25 cmp eax, 7 ; 7
0x08048f28 je 0x8048f2f
0x08048f2a call explode_bomb ; sym.explode_bomb
0x08048f2f add esp, 0xfffffff4
0x08048f32 push str.Wow__You_ve_defused_the_secret_stage ; 0x8049820 ; const char *format
0x08048f37 call printf ; sym.imp.printf ; int printf(const char *format)
0x08048f3c call phase_defused ; sym.phase_defused
0x08048f41 mov ebx, dword [var_1ch]
0x08048f44 mov esp, ebp
0x08048f46 pop ebp
fun7
fun7
は引数にアドレスと整数を受け取る.
fun7(int32_t arg_4h, int32_t arg_8h);
; arg int32_t arg_4h @ stack + 0x4
; arg int32_t arg_8h @ stack + 0x8
0x08048e94 push ebp
0x08048e95 mov ebp, esp
0x08048e97 sub esp, 8
0x08048e9a mov edx, dword [arg_4h] ; obj.nX(アドレス)
0x08048e9d mov eax, dword [arg_8h] ; 整数
はじめに,[arg_4h]
に格納されたアドレスが0の場合,eax=-1
となり,jmp
で関数の末尾に移動し,戻り値-1を返す.
0でない場合はjne
で,上記の処理をジャンプする.
; arg_4が0の場合,関数終了
0x08048ea0 test edx, edx
0x08048ea2 jne 0x8048eb0
0x08048ea4 mov eax, 0xffffffff ; -1
0x08048ea9 jmp 0x8048ee2 ; 関数の末尾へ
次に,[arg_4]
に格納されたアドレスが[arg_8]
に格納された整数以上であるかを確認している.
そうであれば,2*fun7(*[arg_4 + 4], arg_8h)
を計算し,この値を戻り値として返す.
; 整数 < [arg_4]か?
0x08048eb0 cmp eax, dword [edx]
0x08048eb2 jge 0x8048ec5
0x08048eb4 add esp, 0xfffffff8
; arg_8h = 2*fun7(*[arg_4 + 4], arg_8h)
0x08048eb7 push eax ; int32_t arg_8h
0x08048eb8 mov eax, dword [edx + 4]
0x08048ebb push eax ; int32_t arg_4h
0x08048ebc call fun7
0x08048ec1 add eax, eax
0x08048ec3 jmp 0x8048ee2 ; 関数の末尾へ
ただし,[arg_4]
に格納されたアドレスが[arg_8]
に格納された整数と等しい場合は戻り値として0を返す.
; 整数 == [arg_4]か?
0x08048ec5 cmp eax, dword [edx]
0x08048ec7 je 0x8048ee0
(省略)
0x08048ee0 xor eax, eax ; 0
0x08048ee2 mov esp, ebp ; 関数の末尾
0x08048ee4 pop ebp
0x08048ee5 ret
0x08048ee6 mov esi, esi
最後に,[arg_4]
に格納されたアドレスが[arg_8]
に格納された整数未満であれば,
2*fun7(*(arg_4 + 8), arg_8h) + 1
を計算し,この値を戻り値として返す.
; 整数 == [arg_4]か?
0x08048ec5 cmp eax, dword [edx]
0x08048ec7 je 0x8048ee0
; 整数 > [arg_4]か?
0x08048ec9 add esp, 0xfffffff8
; 2*fun7(*(arg_4 + 8), arg_8h) + 1
0x08048ecc push eax ; int32_t arg_8h
0x08048ecd mov eax, dword [edx + 8]
0x08048ed0 push eax ; int32_t arg_4h
0x08048ed1 call fun7
0x08048ed6 add eax, eax
0x08048ed8 inc eax
0x08048ed9 jmp 0x8048ee2
まとめると,fun7
は以下の処理を行う.
(arg_4
はアドレス,*arg_4
はarg_4
に格納された値,arg_8
は整数.)
secret_phase
でfun7
が7を返せばクリアになる.
この式で7を得るためには,(2)式と(4)式とを利用する.
- (2)式で7を返すには
fun7(*(arg_4+8), arg_8) = 3
であればよい. - (2)式で3を返すには
fun7(*(arg_4+8), arg_8) = 1
であればよい. - (2)式で1を返すには
fun7(*(arg_4+8), arg_8) = 0
であればよい. - (4)式で0を返すには条件式
*(arg_4+8) = arg_8
であればよい.
secret_phase
でfun7
に与えた初期値はarg_4 = 0x804b320
,arg_8 = ユーザの入力した整数
である.
0x804b320
の指す値は次の通り.
obj.nX
;-- n48:
0x0804b278 .dword 0x000003e9
0x0804b27c .dword 0x00000000
0x0804b280 .dword 0x00000000
;-- n46:
0x0804b284 .dword 0x0000002f
0x0804b288 .dword 0x00000000
0x0804b28c .dword 0x00000000
;-- n43:
0x0804b290 .dword 0x00000014
0x0804b294 .dword 0x00000000
0x0804b298 .dword 0x00000000
;-- n42:
0x0804b29c .dword 0x00000007
0x0804b2a0 .dword 0x00000000
0x0804b2a4 .dword 0x00000000
;-- n44:
0x0804b2a8 .dword 0x00000023
0x0804b2ac .dword 0x00000000
0x0804b2b0 .dword 0x00000000
;-- n47:
0x0804b2b4 .dword 0x00000063
0x0804b2b8 .dword 0x00000000
0x0804b2bc .dword 0x00000000
;-- n41:
0x0804b2c0 .dword 0x00000001
0x0804b2c4 .dword 0x00000000
0x0804b2c8 .dword 0x00000000
;-- n45:
0x0804b2cc .dword 0x00000028
0x0804b2d0 .dword 0x00000000
0x0804b2d4 .dword 0x00000000
;-- n34:
0x0804b2d8 .dword 0x0000006b
0x0804b2dc .dword 0x0804b2b4 ; obj.n47
0x0804b2e0 .dword 0x0804b278 ; obj.n48
;-- n31:
0x0804b2e4 .dword 0x00000006
0x0804b2e8 .dword 0x0804b2c0 ; obj.n41
0x0804b2ec .dword 0x0804b29c ; obj.n42
;-- n33:
0x0804b2f0 .dword 0x0000002d
0x0804b2f4 .dword 0x0804b2cc ; obj.n45
0x0804b2f8 .dword 0x0804b284 ; obj.n46
;-- n32:
0x0804b2fc .dword 0x00000016
0x0804b300 .dword 0x0804b290 ; obj.n43
0x0804b304 .dword 0x0804b2a8 ; obj.n44
;-- n22:
0x0804b308 .dword 0x00000032
0x0804b30c .dword 0x0804b2f0 ; obj.n33
0x0804b310 .dword 0x0804b2d8 ; obj.n34
;-- n21:
0x0804b314 .dword 0x00000008
0x0804b318 .dword 0x0804b2e4 ; obj.n31
0x0804b31c .dword 0x0804b2fc ; obj.n32
;-- n1:
0x0804b320 .dword 0x00000024 ; <- start
0x0804b324 .dword 0x0804b314 ; obj.n21
0x0804b328 .dword 0x0804b308 ; obj.n22
- (2)式で7を返すには
fun7(*(0x804b320 + 8), arg_8) = fun7(*0x804b3288, arg_8) = fun7(0x0804b308, arg_8) = 3
であればよい. - (2)式で3を返すには
fun7(*(0x0804b308 + 8), arg_8) = fun7(*0x0804b310, arg_8) = fun7(0x0804b2d8, arg_8) = 1
であればよい. - (2)式で1を返すには
fun7(*(0x0804b2d8 + 8), arg_8) = fun7(*0x0804b2e0, arg_8) = fun7(0x0804b278, arg_8) = 0
であればよい. - (4)式で0を返すには条件式
*0x0804b278 = 0x000003e9 = arg_8
であればよい.
従って,0x000003e9
を10進数で表現した1001が回答である.
おわりに
入力のしきい値が1001以下であることが分かっているため,回答が負でないと予想してスクリプトを組む方が早かったりします.
for i in {1..1001} do
./bomb <<< EOS
(Phase1~6の回答,省略)
$i
EOS > /dev/null 2>&1
if [[ $? -eq 0 ]] then
echo "answer => ${i}"
break
fi
done
Discussion