ret2csu csu 链利用
我们直接结合 BUUCTF 的一道题来了解 ret2csu 的利用方式 BUUCTF jarvisoj_level3_x64
什么是 ret2csu?
ret2csu 是 64 位 ROP 中针对缺少高寄存器 Gadget(如pop rdx/pop r12)的通用解决方案利用程序
.init_array段的**CSU 通用初始化代码**,间接给rdx/r12-r15等寄存器赋值,实现多参数函数调用。
ret2csu 的利用原理
首先再次介绍一下 64 位程序的传参方式:当参数少于 7 个时,参数从左到右依次放入寄存器:rdi,rsi,rdx,rcx,r8,r9;当参数为 7 个以上时,前面 6 个相同,后面的依次放入栈中,和 32 位压栈原理相同
__libc_csu_init 详细分析
在为程序当中,程序的前六个参数都是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。这时候我们可以利用 x64 下的__libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。
我们在 BUUCTF 下载 jarvisoj_level3_x64 附件,用 IDA 打开,查看__libc_csu_init 函数的汇编代码:
1 | .text:0000000000400650 ; =============== S U B R O U T I N E ======================================= |
其中从0x4006A6 一直到结尾的地址都是我们可以利用还在那溢出构造栈上数据来控制的,因为 rbx,rbp,r12,r13,r14,r15 都是寄存器的数据,也就是向寄存器进行 pop 指令操作
1 | .text:00000000004006A6 add rsp, 8 |
我们可以将 r13 赋值给 rdx,将 r14 赋值给 rsi,将 r15 赋值给 edi。(需要注意的是,虽然这里赋值给的是 edi,但其实此时 rdi 的高 32 位寄存器为 0,所以我们可以控制 rdi 的值,只不过只能控制低 32 位,而这三个寄存器也是 x64 函数调用中传递的前三个寄存器(rdx,rsi,edi))。此外,如果我们可以合理地控制 r12和rbx,那我们就可以调用我们想要调用的函数。
比如我们可以控制 rbx 为 0,r12 位存储我们想要调用的函数的地址。控制 rbp 对应的汇编如下:
1 | .text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j |
1 | .text:000000000040069D add rbx, 1 |
从 0x40069D 我们可以控制 rbx 与 rbp 的关系为 rbp = rbx+1,这样就不会再执行loc_400690
1 | cmp 是 x86_64 汇编的 “比较指令”,cmp rbx, rbp 的作用是: |
再来看跳转地址的指令:
1 | call ds:(__frame_dummy_init_array_entry - 600840h)[r12+rbx*8] |
间接寻址:call [r12+rbx8]。这意味着程序会去 r12 + rbx8 这个地址所指向的内存单元里,取出一个 64 位的值,然后跳转到这个值代表的地址。
我们前面控制了 rbx = 0。 此时指令简化为: call qword ptr [r12]
也就是跳转到内存地址 r12 处存储的那个 64 位数值所指向的地方。通过这个我们就可以随意篡改跳转地址。
ret2csu 的利用场景
当我们找不到pop rdi; ret, pop rsi; ret, pop rdx; ret但是有__libc_csu_init 时就需要使用 ret2csu 方法。
由于 32 位程序无需任何寄存器 Gadget,直接压栈,因此不使用 ret2csu
可以使用一条命令检查是否需要用 ret2csu
1 | # 检查64位程序的Gadget |
1 | ROPgadget --binary ./目标程序 --only "pop rdx; ret" |

jarvisoj_level3_x64(ret2csu)
继续跟着 jarvisoj_level3_x64 这道题理解 exp 的构造(也可以只使用 ret2libc 实现 getshell,这里不介绍了)
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
1 | ssize_t __fastcall vulnerable_function(__int64 argc, __int64 argv, __int64 envp) |
从 vulnerable_function 函数我们发现可以通过 read 函数实现栈溢出,偏移量为 0x80+0x8,之后可以拿一下 gadgets 的地址,为后面的 exp 做准备

1 | 0x4006b3: pop rdi; ret; |
从 IDA 中查看 csu 片段
1 | .text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j |
1 | .text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j |
gadgets 准备工作
1 | csu1=0x4006A6 # CSU POP段:pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret |
封装ret2csu参数构造函数
1 | def csu_args(extra, rbx, rbp, r12, r13, r14, r15): |
csu1 传参规则
1 | # 跳转到CSU POP段 |
1 | csu_args 参数 对应寄存器 write 函数参数 作用 |
1 | from pwn import * |

更新: 2026-04-05 10:49:13
原文: https://www.yuque.com/idcm/wnemg9/xtg5an6t747sylv0