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_phasecallしている箇所があるため,どのように条件分岐しているかを調べる.

初めに,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_4arg_4に格納された値,arg_8は整数.)

\mathrm{fun7}(\mathrm{arg\_4}, \mathrm{arg\_8}) = \begin{cases} -1 & (\mathrm{*arg\_4} = 0) & (1)\\ 2\times \mathrm{fun7}(*(\mathrm{arg\_4} + 8), \mathrm{arg\_8}) + 1 & (\mathrm{arg\_8} \gt \mathrm{*arg\_4}) & (2) \\ 2\times \mathrm{fun7}(*(\mathrm{arg\_4} + 4), \mathrm{arg\_8}) & (\mathrm{arg\_8} \lt \mathrm{*arg\_4}) & (3) \\ 0 & (\mathrm{arg\_8} = \mathrm{*arg\_4}) & (4) \end{cases}

secret_phasefun7が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_phasefun7に与えた初期値はarg_4 = 0x804b320arg_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