昔、社内の勉強会用に作った資料のメモ。
以下の本が参考。

Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際
- 作者: Jon Erickson,村上雅章
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/10/22
- メディア: 単行本(ソフトカバー)
- 購入: 9人 クリック: 163回
- この商品を含むブログ (19件) を見る
まず、C言語を触ったことがある人はわかるかと思うが、 コンパイルという作業がある。 これは、ソースコードを、機械が読める形(バイナリ)で吐き出したもので、人間には読むことはできない。
$ gcc -o firstprog firstprog.c $ ./firstprog # Hello World # Hello World # Hello World # Hello World # Hello World # Hello World # Hello World # Hello World # Hello World # Hello World
#include <stdio.h> int main(){ int i; for( i = 0; i < 10; i++ ){ printf("Hello World\n"); } return 0; } // ↓ // こんな感じのものが、400行ほど続く // cffa edfe 0700 0001 0300 0080 0200 0000 // 0f00 0000 b004 0000 8500 2000 0000 0000 // 1900 0000 4800 0000 5f5f 5041 4745 5a45 // 524f 0000 0000 0000 0000 0000 0000 0000 // 0000 0000 0100 0000 0000 0000 0000 0000 // 0000 0000 0000 0000 0000 0000 0000 0000 // 0000 0000 0000 0000 1900 0000 d801 0000 // 5f5f 5445 5854 0000 0000 0000 0000 0000 // 0000 0000 0100 0000 0010 0000 0000 0000 // 0000 0000 0000 0000 0010 0000 0000 0000 // 0700 0000 0500 0000 0500 0000 0000 0000 // 5f5f 7465 7874 0000 0000 0000 0000 0000 // 5f5f 5445 5854 0000 0000 0000 0000 0000 // 400f 0000 0100 0000 4700 0000 0000 0000 // 400f 0000 0400 0000 0000 0000 0000 0000 // 0004 0080 0000 0000 0000 0000 0000 0000 // 5f5f 7374 7562 7300 0000 0000 0000 0000 // 5f5f 5445 5854 0000 0000 0000 0000 0000 // 880f 0000 0100 0000 0600 0000 0000 0000 // 880f 0000 0100 0000 0000 0000 0000 0000 // 0804 0080 0000 0000 0600 0000 0000 0000 // 5f5f 7374 7562 5f68 656c 7065 7200 0000 // 5f5f 5445 5854 0000 0000 0000 0000 0000 // 900f 0000 0100 0000 1a00 0000 0000 0000
これらをデバッグするツールとして、gdbというものがある。 なんか、動かなくなった。
アセンブリ言語
今の時点でのイメージは、人間がギリギリ理解できる超低級言語。
アセンブリ言語の命令(Intel方式)
アセンブリ命令は、一般的に、
命令語 <操作の対象>, <参照元>
という形式になっている。 操作の対象と参照元には、それぞれ、レジスタ、メモリアドレス、即値のいずれかを指定する。
コマンド | 意味 |
---|---|
mov | 移動 |
sub | 減算 |
inc | 加算 |
$ gdb -q ./firstprog # Reading symbols from ./firstprog...Reading symbols from /Users/myname/Hacking/0x2/firstprog.dSYM/Contents/Resources/DWARF/firstprog...done. # done. (gdb) list # 1 # 2 // これらのヘッダは、/usr/includeに格納されている # 3 #include <stdio.h> # 4 # 5 // mainという名前の関数から始まる # 6 int main(){ # 7 int i; # 8 for( i = 0; i < 10; i++ ){ # 9 printf("Hello World\n"); # 10 } # (gdb) list # 11 # 12 return 0; # 13 } (gdb) disassemble main # Dump of assembler code for function main: # 0x0000000100000f40 <+0>: push rbp # 0x0000000100000f41 <+1>: mov rbp,rsp # 0x0000000100000f44 <+4>: sub rsp,0x10 # 0x0000000100000f48 <+8>: mov DWORD PTR [rbp-0x4],0x0 # 0x0000000100000f4f <+15>: mov DWORD PTR [rbp-0x8],0x0 # 0x0000000100000f56 <+22>: cmp DWORD PTR [rbp-0x8],0xa # 0x0000000100000f5a <+26>: jge 0x100000f7f <main+63> # 0x0000000100000f60 <+32>: lea rdi,[rip+0x43] # 0x100000faa # 0x0000000100000f67 <+39>: mov al,0x0 # 0x0000000100000f69 <+41>: call 0x100000f88 # 0x0000000100000f6e <+46>: mov DWORD PTR [rbp-0xc],eax # 0x0000000100000f71 <+49>: mov eax,DWORD PTR [rbp-0x8] # 0x0000000100000f74 <+52>: add eax,0x1 # 0x0000000100000f77 <+55>: mov DWORD PTR [rbp-0x8],eax # 0x0000000100000f7a <+58>: jmp 0x100000f56 <main+22> # 0x0000000100000f7f <+63>: xor eax,eax # 0x0000000100000f81 <+65>: add rsp,0x10 # 0x0000000100000f85 <+69>: pop rbp # 0x0000000100000f86 <+70>: ret # End of assembler dump. (gdb) quit
この例では最初に、ソースコードを表示させた後、main()関数の逆アセンブル結果を表示している。
その後、ブレークポイントをmain()
の先頭に設定し、プログラムを実行している。
その他にも言えることは、プログラムの実行の大半は、プロセッサとメモリセグメントにおける状態の変化で構成される。
よって、起こっていることを見極めるには、まずメモリを調査することになる。
examineのオプション一覧
コマンド | 意味 |
---|---|
o | 8進表記 |
x | 16進表記 |
u | 符号なし10進表記 |
t | 2進表記 |
gdbというものを使って、でデバッグしていく。 これは、逆アセンブルをした結果が、これ。
この左端の、意味不明な文字列は、メモリアドレスで、ある。 これはCのポインタという概念が必要になる。
#include <stdio.h> #include <string.h> int main(){ char str_a[20];// 20個の要素を持つ文字の配列 char *pointer;// 文字の配列をさすポインタ char *pointer2; strcpy(str_a, "Hello,World!\n"); pointer = str_a;// ひとつ目のポインタが、配列の先頭を指すように設定する。 printf(pointer);// ひとつ目のポインタが指している文字列を表示する。 pointer2 = pointer + 2;// ポインタ1の二つ先を参照するようにする。 printf(pointer2);// それが指している文字列を表示する strcpy(pointer2, "y you guys!\n");// そのばしょに他の文字列をコピーする printf(pointer); }
結果
$ ./pointer # Hello,World! # llo,World! # Hey you guys!
では、実際に、ポインタをのぞいてみる。
#include <stdio.h> int main(){ int i; char char_array[5] = {'a','b','c','d','e'}; int int_array[5] = {1,2,3,4,5}; char *char_pointer; int *int_pointer; char_pointer = (char *) int_array; int_pointer = (int *) char_pointer; for ( i=0; i < 5; i++ ){ printf("[整数へのポインタ]は、%pをさしており、その内容は、'%c'です。\n",int_pointer,*int_pointer); int_pointer = (int *) ( (int *) char_pointer + 1 ); } }
これを実行すると、
$ gcc -o pointer_types3 pointer_types3.c $ ./pointer_types3 # [整数へのポインタ]は、0x7fff50107950をさしており、その内容は、''です。 # [整数へのポインタ]は、0x7fff50107954をさしており、その内容は、''です。 # [整数へのポインタ]は、0x7fff50107954をさしており、その内容は、''です。 # [整数へのポインタ]は、0x7fff50107954をさしており、その内容は、''です。 # [整数へのポインタ]は、0x7fff50107954をさしており、その内容は、''です。
次に、コマンドライン引数
#include <stdio.h> void usage(char *programname){ printf("使用方法: %s <メッセージ> <繰り返し回数>\n", programname); exit(1); } int main( int argc, char *argv[] ){ int i,count; if( argc < 3 ){ usage(argv[0]); } count = atoi( argv[2] ); printf( "%d回繰り返します。" ); for( i = 0; i < count; i++ ){ printf("%3d - %s\n", i, argv[1]); } }
これを実行すると、
$ ./convert # 使用方法: ./convert <メッセージ> <繰り返し回数> $ ./convert test 20 0回繰り返します。 0 - test # 1 - test # 2 - test # 3 - test # 4 - test # 5 - test # 6 - test # 7 - test # 8 - test # 9 - test # 10 - test # 11 - test # 12 - test # 13 - test # 14 - test # 15 - test # 16 - test # 17 - test # 18 - test # 19 - test
メモリのセグメント化
メモリ空間は、 コンパイルされたプログラムを実行する際に、テキスト、データ、bss、ヒープ、スタック、という5つのセグメントに分かれる。
テキストセグメントには、別名コードセグメントともいい、 プログラムのマシン語が格納される領域。
プログラム実行時には、テキストセグメントの先頭の命令を指すように、eipが設定される。
1:eipが指しているメモリから命令を読み込む 2:該当命令のバイト長をeipに加算する 3:手順1で読み込んだ命令を実行する 4:手順1に戻る
この繰り返し
テキストセグメント
テキストセグメントには、コードのみが格納され、変数の割り当ては行われない
よって、書き込みは禁止されている
データセグメントと、bssセグメント
このセグメントには、プログラムが使用する、大域変数や、静的変数を格納される。
データセグメントには、初期化されたものが入るのにたいして、 bssには、初期化されていないものが入る。 書き込みは可能だが、サイズは固定。永続的
ヒープセグメント
ここはプログラマが直接制御できる。どのような目的にも使用でき、その割り当ては動的。
メモリ空間を図解した場合、これは、下に向かって、つまり、上位のメモリアドレスに向かって伸びて行く。
スタックセグメント
関数の局所変数や、関数呼び出し中のコンテキストなど、一時的なメモ帳として利用される
関数呼び出し時に、eipとコンテキストが変更されるため、スタックセグメントを利用して、 引数の受け渡し、関数の実行が終了した際に復元するeipの値の保存、該当関数が使用するすべての局所変数の割り当てが行われる これらをまとめて、スタックフレームと呼ばれるまとまりにされ、スタック上に格納される。
スタック
スタックは、先入れ後出し(数珠みたいなもの) スタックに入れることを、プッシュするといい、 出すことを、ポップする、という。
void test_function(int a, int b, int c, int d){ int flag; char buffer[10]; flag = 31337; buffer[0] = 'A'; } int main(){ test_function(1,2,3,4); }
$ gdb -q ./stack_example # Reading symbols from ./stack_example...Reading symbols from /Users/myname/Hacking/0x2/stack_example.dSYM/Contents/Resources/DWARF/stack_example...done. # done. (gdb) list # warning: Source file is more recent than executable. # 1 # 2 void test_function(int a, int b, int c, int d){ # 3 int flag; # 4 char buffer[10]; # 5 # 6 flag = 31337; # 7 buffer[0] = 'A'; # 8 } # 9 # 10 int main(){ (gdb) list # 11 test_function(1,2,3,4); # 12 } (gdb) disass main # Dump of assembler code for function main: # 0x0000000100000f70 <+0>: push rbp # 0x0000000100000f71 <+1>: mov rbp,rsp # 0x0000000100000f74 <+4>: mov edi,0x1 # 0x0000000100000f79 <+9>: mov esi,0x2 # 0x0000000100000f7e <+14>: mov edx,0x3 # 0x0000000100000f83 <+19>: mov ecx,0x4 # 0x0000000100000f88 <+24>: call 0x100000f20 <test_function> # 0x0000000100000f8d <+29>: xor eax,eax # 0x0000000100000f8f <+31>: pop rbp # 0x0000000100000f90 <+32>: ret # End of assembler dump. (gdb) disass test_function # Dump of assembler code for function test_function: # 0x0000000100000f20 <+0>: push rbp # 0x0000000100000f21 <+1>: mov rbp,rsp # 0x0000000100000f24 <+4>: sub rsp,0x30 # 0x0000000100000f28 <+8>: mov rax,QWORD PTR [rip+0xe1] # 0x100001010 # 0x0000000100000f2f <+15>: mov r8,QWORD PTR [rax] # 0x0000000100000f32 <+18>: mov QWORD PTR [rbp-0x8],r8 # 0x0000000100000f36 <+22>: mov DWORD PTR [rbp-0x18],edi # 0x0000000100000f39 <+25>: mov DWORD PTR [rbp-0x1c],esi # 0x0000000100000f3c <+28>: mov DWORD PTR [rbp-0x20],edx # 0x0000000100000f3f <+31>: mov DWORD PTR [rbp-0x24],ecx # 0x0000000100000f42 <+34>: mov DWORD PTR [rbp-0x28],0x7a69 # 0x0000000100000f49 <+41>: mov BYTE PTR [rbp-0x12],0x41 # 0x0000000100000f4d <+45>: mov rax,QWORD PTR [rax] # 0x0000000100000f50 <+48>: cmp rax,QWORD PTR [rbp-0x8] # 0x0000000100000f54 <+52>: jne 0x100000f60 <test_function+64> # 0x0000000100000f5a <+58>: add rsp,0x30 # 0x0000000100000f5e <+62>: pop rbp # 0x0000000100000f5f <+63>: ret # 0x0000000100000f60 <+64>: call 0x100000f92 # End of assembler dump. (gdb) quit
本来、4321の順で引数を渡すはず。
0x100000f20
これは、test_functionの、アドレスである。
こんな感じで、C言語の基本的な部分をやった。
次に、プログラムの脆弱性について。
プログラムの脆弱性
バッファオーバーフロー
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]){ int value = 5; char buffer_one[8], buffer_two[8]; strcpy(buffer_one, "one"); strcpy(buffer_two, "two"); printf("[前] buffer_twoは、%pにあり、その値は、\'%s\'です\n", buffer_two, buffer_two); printf("[前] buffer_oneは、%pにあり、その値は、\'%s\'です\n", buffer_one, buffer_one); printf("[前] valueは、%pにあり、その値は、%d(0x%08x)です。\n", &value, value, value); printf("\n[strcpy] %dバイトを、buffer_twoにコピーします。\n\n", strlen(argv[1])); strcpy(buffer_two, argv[1]); printf("[後]buffer_twoは %p にあり、その値は、\'%s\'です。\n", buffer_two, buffer_two); printf("[後]buffer_oneは %p にあり、その値は、\'%s\'です。\n", buffer_one, buffer_one); printf("[後] valueは、%pにあり、その値は、%d(0x%08x)です。\n", &value, value, value); }
$ ./overflow_example 4 # [前] buffer_twoは、0x7fff51cc6948にあり、その値は、'two'です # [前] buffer_oneは、0x7fff51cc6950にあり、その値は、'one'です # [前] valueは、0x7fff51cc6934にあり、その値は、5(0x00000005)です。 # [strcpy] 1バイトを、buffer_twoにコピーします。 # [後]buffer_twoは 0x7fff51cc6948 にあり、その値は、'4'です。 # [後]buffer_oneは 0x7fff51cc6950 にあり、その値は、'one'です。 # [後] valueは、0x7fff51cc6934にあり、その値は、5(0x00000005)です。 $ ./overflow_example 4000000000000 # [前] buffer_twoは、0x7fff5f138938にあり、その値は、'two'です # [前] buffer_oneは、0x7fff5f138940にあり、その値は、'one'です # [前] valueは、0x7fff5f138924にあり、その値は、5(0x00000005)です。 # [strcpy] 13バイトを、buffer_twoにコピーします。 # Abort trap: 6
本来割り当てられていたメモリをオーバーすることで、制御を奪取する。
#include <stdio.h> #include <stdlib.h> #include <string.h> int check_authentication(char *password){ int auth_flag = 0; char password_buffer[16]; strcpy(password_buffer, password); if(strcmp(password_buffer, "brillig") == 0){ auth_flag = 1; } if(strcmp(password_buffer, "outgrabe") == 0){ auth_flag = 1; } return auth_flag; } int main(int argc, char *argv[]){ if(argc < 2){ printf("使用方法: %s <password>\n", argv[0]); } if(check_authentication(argv[1])){ printf("------------\n"); printf("-----ok-----\n"); printf("------------\n"); }else{ printf("\ndeny!\n"); } }
$ ./auth_overflow aaa # deny! $ ./auth_overflow brillig # ------------ # -----ok----- # ------------
もしここで、
$ ./auth_overflow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
このようにやったら、破られてしまう。
これは、メモリが勝手に、違うとこまで埋めてしまう。そこで、0以外を残す。これで、破られてしまう。