レジスタ操作でアクセス承認を引き出す - gdb | はじめてのリバースエンジニアリング#3

gdbを用いてプログラムを実行させ、各ステップの処理を解析・操作することで、アクセス承認を目指しました。


Introduction

前回は、アセンブラコードを解析してプログラムの流れを把握しました。
www.bioerrorlog.work 具体的には、次のアセンブラコードから、全体の流れ図(Fig. 1)を書き出しました。

Dump of assembler code for function main:
   0x00000000000006da <+0>:  push   rbp
   0x00000000000006db <+1>:  mov    rbp,rsp
   0x00000000000006de <+4>:  sub    rsp,0x10
   0x00000000000006e2 <+8>:  mov    DWORD PTR [rbp-0x4],edi
   0x00000000000006e5 <+11>: mov    QWORD PTR [rbp-0x10],rsi
   0x00000000000006e9 <+15>: cmp    DWORD PTR [rbp-0x4],0x2
   0x00000000000006ed <+19>: jne    0x748 <main+110>
   0x00000000000006ef <+21>: mov    rax,QWORD PTR [rbp-0x10]
   0x00000000000006f3 <+25>: add    rax,0x8
   0x00000000000006f7 <+29>: mov    rax,QWORD PTR [rax]
   0x00000000000006fa <+32>: mov    rsi,rax
   0x00000000000006fd <+35>: lea    rdi,[rip+0xe4]        # 0x7e8
   0x0000000000000704 <+42>: mov    eax,0x0
   0x0000000000000709 <+47>: call   0x5a0 <printf@plt>
   0x000000000000070e <+52>: mov    rax,QWORD PTR [rbp-0x10]
   0x0000000000000712 <+56>: add    rax,0x8
   0x0000000000000716 <+60>: mov    rax,QWORD PTR [rax]
   0x0000000000000719 <+63>: lea    rsi,[rip+0xe0]        # 0x800
   0x0000000000000720 <+70>: mov    rdi,rax
   0x0000000000000723 <+73>: call   0x5b0 <strcmp@plt>
   0x0000000000000728 <+78>: test   eax,eax
   0x000000000000072a <+80>: jne    0x73a <main+96>
   0x000000000000072c <+82>: lea    rdi,[rip+0x107]        # 0x83a
   0x0000000000000733 <+89>: call   0x590 <puts@plt>
   0x0000000000000738 <+94>: jmp    0x754 <main+122>
   0x000000000000073a <+96>: lea    rdi,[rip+0x108]        # 0x849
   0x0000000000000741 <+103>:    call   0x590 <puts@plt>
   0x0000000000000746 <+108>:    jmp    0x754 <main+122>
   0x0000000000000748 <+110>:    lea    rdi,[rip+0xfa]        # 0x849
   0x000000000000074f <+117>:    call   0x590 <puts@plt>
   0x0000000000000754 <+122>:    mov    eax,0x0
   0x0000000000000759 <+127>:    leave  
   0x000000000000075a <+128>:    ret    
End of assembler dump.

f:id:BioErrorLog:20190202175344j:plain
Fig. 1 アセンブラコードから解析したプログラムの流れ
今回は、gdbのデバッグ機能を利用してそれぞれのステップを解析し、パスワード認証過程の特定と迂回を目指します。


Material

Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work


Results

プログラムの実行 | 第1分岐 - ハズレルート

プログラムを実行させ、ステップごとの挙動を解析することで、より詳細な情報を得ることができます。

main関数にブレークポイント1を設置し、まずは引数を与えずにプログラムを実行してみます。

(gdb) break *main
Breakpoint 1 at 0x6da
(gdb) run
Starting program: /home/user/your_pass 

Breakpoint 1, 0x00005555555546da in main ()

main関数の開始点0x6daにブレークポイントが設置されたことによって、プログラムを実行runした際にmain関数で処理が一時停止している状態です。

ここでよくわからないのは、ブレークポイントを設置したときのアドレスは0x6da、同様にアセンブラコードで表示されるmain関数の開始点は0x00000000000006daなのに、runしたときのブレークポイントは0x00005555555546daを指していることです。 この555555554という数字は何を意味しているのでしょうか。 "gdb 555555554"などで検索すると、やはりgdbのブレークポイント関連でこの"555555554"というアドレスが登場しています。 とても気になるところですが、ひとまずこの数字は置いておいて、下三桁6daのみに着目して進めていきたいと思います。

それでは、niコマンド2を用いて、プログラムを一行ずつ実行していきます。

(gdb) ni
0x00005555555546db in main ()
(gdb) 
0x00005555555546de in main ()
(gdb) 
0x00005555555546e2 in main ()
(gdb) 
0x00005555555546e5 in main ()
(gdb) 
0x00005555555546e9 in main ()
(gdb) 
0x00005555555546ed in main ()

アドレス6edまで来ました3。 ここは、プログラムの流れ図でみると、最初の分岐点であることがわかります(Fig. 2, 赤矢頭)。

f:id:BioErrorLog:20190202175653j:plain
Fig. 2 第1分岐点
さて、どちらの分岐ルートに行くのでしょうか。 niコマンドで先へ進むと、

(gdb) ni
0x0000555555554748 in main ()

アドレス748、つまり左の分岐ルートに行ったことがわかります。 これは、アドレス6e9において、ある変数と0x2を比較cmpした結果が"not equal"だったため、アドレス748jne(jump not equal)した、ということを意味しています。

このままniコマンドで先へ行くと、

(gdb) ni
0x000055555555474f in main ()
(gdb) 
Access Denied.
0x0000555555554754 in main ()
(gdb) 
0x0000555555554759 in main ()
(gdb) 
0x000055555555475a in main ()

アドレス74f<puts>関数4によってAccess Denied.の文字列が表示された後、main関数の終了点75aに至りました。 つまり、このルートはアクセスが拒否されてしまうハズレルートであるということです。

ここまで、引数を何も与えずにプログラムを実行してきました。 次は、引数を与えて実行してみます。

プログラムの実行 | 第1分岐 - 当たりルート

それでは、引数wrong-passwordを与えて、同様の解析を行います。

(gdb) run wrong-password
Starting program: /home/user/your_pass wrong-password

Breakpoint 1, 0x00005555555546da in main ()
(gdb) ni
0x00005555555546db in main ()
(gdb) 
0x00005555555546de in main ()
(gdb) 
0x00005555555546e2 in main ()
(gdb) 
0x00005555555546e5 in main ()
(gdb) 
0x00005555555546e9 in main ()
(gdb) 
0x00005555555546ed in main ()

分岐点6edにやってきました。 niコマンドで先へ進むと、

(gdb) 
0x00005555555546ef in main ()

先程と違い、アドレス748へ飛ばず、6efへ行きました。 つまり今回は、流れ図での右側ルート、おそらく当たりルートに行けたということです。 お見事。

プログラムの実行 | 第2分岐 - ハズレルート

それでは引き続き、niコマンドで最後まで進んでみます。

~
(gdb) 
0x0000555555554709 in main ()
(gdb) 
Checking Your Pass: wrong-password
~
(gdb) 
0x000055555555472a in main ()
(gdb) 
0x000055555555473a in main ()
(gdb) 
0x0000555555554741 in main ()
(gdb) 
Access Denied.
~

709printf関数ではChecking Your Pass: wrong-passwordが表示され、その後の分岐点72aでは左側のルートに行き、741puts関数ではAccess Denied.が表示されました(Fig. 3)。 つまり、この左ルートはハズレです。

f:id:BioErrorLog:20190202182709j:plain
Fig. 3 第2分岐 左ルートはハズレ

プログラムの実行 | 第2分岐 - アクセス承認ルート

残るは、第2分岐の右ルートです。 おそらく、ここがアクセス承認のルートでしょう。
そこで、レジスタの値を操作することによって、このアクセス承認ルートへの突入を目指します。

分岐点0x72a jneは、直前の判定を受けてルートの分岐を決定しています。 改めてアセンブラコードからこの辺りを読むと、

~
   0x0000555555554723 <+73>: call   0x5555555545b0 <strcmp@plt>
   0x0000555555554728 <+78>: test   eax,eax
   0x000055555555472a <+80>: jne    0x55555555473a <main+96>
~

ここから読み取れる条件判定の流れは、次のようになります。

strcmpで入力パスワードと正規パスワードを比較し、eaxに結果を格納

testeaxの値をチェック

testの結果に応じてjne

下から順に詳細を考えていきます。
jneは、"zero flag" ZF==0のときにジャンプするコードです5。 そしてtestは、2つの変数のAND演算の結果を受けて、ZFを設定しています6。 今回の場合eaxeaxのAND演算を行っているので、eax==0のときのみ、ZFに1が格納されます。 つまり、jneによってジャンプせず、右の分岐ルートに行くには、eaxに0が格納されている必要があるわけです。

しかし、eax==0となるのは、strcmpが入力パスワードと正規パスワードを同一であるとみなした時だけです7。 正規パスワードがわからない以上、正面からのやり方では突破できません。

そこで、直接eaxの値を操作することによって、アクセス承認を目指します。 eaxは、レジスタraxの下位 32 ビットを指しています8。 そこで、testが行われるアドレス728で、raxレジスタを確認してみました。 ブレークポイントを0x0000555555554728に設定してcontinueコマンドを実行することで、一つずつniせずともアドレス728に行くことができます。 そこでレジスタを見ると、

Breakpoint 2, 0x0000555555554728 in main ()
(gdb) info registers
rax            0xfffffff0  4294967280
rbx            0x0 0
rcx            0x0 0
rdx            0x41    65
rsi            0x555555554800  93824992233472
rdi            0x7fffffffe30e  140737488347918
rbp            0x7fffffffde80  0x7fffffffde80
rsp            0x7fffffffde70  0x7fffffffde70
r8             0x0 0
r9             0x3 3
r10            0xfffffffd  4294967293
r11            0x246   582
r12            0x5555555545d0  93824992232912
r13            0x7fffffffdf60  140737488346976
r14            0x0 0
r15            0x0 0
rip            0x555555554728  0x555555554728 <main+78>
eflags         0x287   [ CF PF SF IF ]
cs             0x33    51
ss             0x2b    43
ds             0x0 0
es             0x0 0
fs             0x0 0
gs             0x0 0

raxには0xfffffff0が格納されていました。 それではここでeaxレジスタを操作して、0を格納します。 目論見が正しければ、それでアクセス承認が引き出せるはずです。

(gdb) set $eax=0

これでeaxに0を格納しました。 レジスタを確認すると、

(gdb) info registers
rax            0x0 0
~

確かにraxの値が0になっています。
それでは、このままniで先へ進むと、

(gdb) ni
0x000055555555472a in main ()
(gdb) 
0x000055555555472c in main ()
(gdb) 
0x0000555555554733 in main ()
(gdb) 
Hello, Master.
0x0000555555554738 in main ()

すばらしい。
見事、右の分岐ルートに入り、アクセス承認Hello, Master.を表示させることができました。


Discussion

今回は、gdbを用いてプログラムを実行させ、ルート分岐条件を解析することで、最終的にアクセス承認を引き出すことができました。

今回解析でわかったことを書き出すと、次のようになります。

f:id:BioErrorLog:20190202213942j:plain
Fig. 4 プログラムの解析結果

細かくプログラムを解析してきたので、だいぶプログラミングの仕組みといったものもわかってきました。 なかなか複雑で面白いものです。

ただし、アクセス承認を引き出す方法は、他にもたくさんあります。 というよりも、もっとずっと簡単な方法が何個もあります。 次回は、それをまとめたいと思います。

なお、今回もLiveOverflow氏の動画9を参考にしました。

See also

次↓ www.bioerrorlog.work

前↓ www.bioerrorlog.work


  1. ブレークポイントとは、その名の通り"一時停止"する場所です。runコマンドによってプログラムを実行した時、ブレークポイントを設置していればそこで立ち止まってくれます。逆に、ブレークポイントを設置せずに実行すると、最後まで一気に実行されてしまいます。

  2. niとよく似たコマンドに、siがあります。niは関数内部に入らずに実行し、siは関数内部に入り込んで実行します。今回はmain関数の流れのみに興味があるので、関数内部に入り込まないniコマンドを使っています。詳しくは別途記事にまとめる予定です。

  3. 同じコマンドを繰り返すときは、そのままEnterキーを押します。

  4. puts関数とは、マニュアルによるとputs - output of characters and strings、つまりprintf関数と同じようなものでしょう。

  5. http://dukedog.webcrow.jp/CheatEngineHelp/asm-basics-3.htm

  6. TEST (x86 instruction) - Wikipedia

  7. strcmpのマニュアルによると、It returns an integer less than, equal to, or greater than zero if s1 is found, respectively, to be less than, to match, or be greater than s2.つまりstrcmpは、比較する2つの変数が等しい時、0を返します。

  8. x64 アセンブリーの概要 | iSUS

  9. Reversing and Cracking first simple Program - bin 0x05 - YouTube