gdbを用いてプログラムの各ステップを解析・操作することで、アクセス承認を目指します。
はじめに
おはよう。@bioerrorlogです。
前回は、アセンブラコードを解析してプログラムの流れを把握しました。
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.
今回は、gdbのデバッグ機能を利用してそれぞれのステップを解析し、パスワード認証過程の特定と迂回を目指します。
作業環境
Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work
レジスタ操作でアクセス承認を引き出す
プログラムの実行 | 第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, 赤矢頭)。
さて、どちらの分岐ルートに行くのでしょうか。
ni
コマンドで先へ進むと、
(gdb) ni
0x0000555555554748 in main ()
アドレス748
、つまり左の分岐ルートに行ったことがわかります。
これは、アドレス6e9
において、ある変数と0x2
を比較cmp
した結果が"not equal"だったため、アドレス748
にjne
(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. ~
709
のprintf
関数ではChecking Your Pass: wrong-password
が表示され、その後の分岐点72a
では左側のルートに行き、741
のputs
関数ではAccess Denied.
が表示されました(Fig. 3)。
つまり、この左ルートはハズレです。
プログラムの実行 | 第2分岐 - アクセス承認ルート
残るは、第2分岐の右ルートです。
おそらく、ここがアクセス承認のルートでしょう。
そこで、レジスタの値を操作することによって、このアクセス承認ルートへの突入を目指します。
分岐点0x72a jne
は、直前の判定を受けてルートの分岐を決定しています。
改めてアセンブラコードからこの辺りを読むと、
~ 0x0000555555554723 <+73>: call 0x5555555545b0 <strcmp@plt> 0x0000555555554728 <+78>: test eax,eax 0x000055555555472a <+80>: jne 0x55555555473a <main+96> ~
ここから読み取れる条件判定の流れは、次のようになります。
strcmp
で入力パスワードと正規パスワードを比較し、eax
に結果を格納
↓
test
でeax
の値をチェック
↓
test
の結果に応じてjne
下から順に詳細を考えていきます。
jne
は、"zero flag" ZF==0のときにジャンプするコードです5。
そしてtest
は、2つの変数のAND演算の結果を受けて、ZFを設定しています6。
今回の場合eax
とeax
の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.
を表示させることができました。
おわりに
今回は、gdbを用いてプログラムを実行させ、ルート分岐条件を解析することで、最終的にアクセス承認を引き出すことができました。
今回解析でわかったことを書き出すと、次のようになります。
細かくプログラムを解析してきたので、だいぶプログラミングの仕組みといったものもわかってきました。 なかなか複雑で面白いものです。
ただし、アクセス承認を引き出す方法は、他にもたくさんあります。 というよりも、もっとずっと簡単な方法が何個もあります。 次回は、それをまとめたいと思います。
なお、今回もLiveOverflow氏の動画9を参考にしました。
[関連記事]
www.bioerrorlog.work
-
ブレークポイントとは、その名の通り"一時停止"する場所です。
run
コマンドによってプログラムを実行した時、ブレークポイントを設置していればそこで立ち止まってくれます。逆に、ブレークポイントを設置せずに実行すると、最後まで一気に実行されてしまいます。↩ -
ni
とよく似たコマンドに、si
があります。ni
は関数内部に入らずに実行し、si
は関数内部に入り込んで実行します。今回はmain関数の流れのみに興味があるので、関数内部に入り込まないni
コマンドを使っています。詳しくは別途記事にまとめる予定です。↩ -
同じコマンドを繰り返すときは、そのままEnterキーを押します。↩
-
puts
関数とは、マニュアルによるとputs - output of characters and strings
、つまりprintf
関数と同じようなものでしょう。↩ -
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を返します。↩ -
Reversing and Cracking first simple Program - bin 0x05 - YouTube↩