导语:在本文中,我们将为读者介绍如何劫持WebAssembly程序的控制流。

本文的上篇中,我们对WebAssembly语言及其安全设计进行了简要的概述,同时给出了一个含有类型混淆漏洞的示例代码,然后,介绍了相关工具的配置方法,最后,介绍了使用WebAssembly类型检查阻止漏洞利用的例子。在本文中,我们将通过具体示例为读者介绍如何劫持WebAssembly程序的控制流。

实验#2:防止WebAssembly中的类型混淆漏洞被利用

我们可以使用emscripten编译易受攻击/被利用的程序,以便观察WebAssembly进行类型检查时会发生什么情况:

[tmux] [email protected]:/src/clang-cfi-showcase# emcc cfi_vcall_diff.cpp -Werror -s WASM=1 -o cfi_vcall_diff.html

上面的命令会生成一个WebAssembly模块(.wasm)、要调用的Javascript代码的包装器(.js)和一个将它们绑定在一起的HTML文件(.html)。我们可以使用浏览器来浏览生成的HTML页面,并打开开发人员控制台来观察其结果。为此,可以在主机系统上运行Python SimpleHTTPServer:

mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081

浏览生成的页面:

1.png

WebAssembly代码捕获到了错误,并通过陷阱返回主程序。当解释器执行 [call_indirect](https://github.com/WebAssembly/design/blob/master/Semantics.md#calls){:target="_blank" rel="noopener"}时,类型检查失败,并触发陷阱。为了更好地理解这里到底发生了什么,我们可以将WebAssembly代码转换为其文本表示形式,具体命令如下所示:

wasm2wat cfi_vcall_diff.wasm > cfi_vcall_diff.wat

查看该文件,我们可以看到call_indirect调用:

    [...]
    f64.const 0x1.bcp+5 (;=55.5;) // Push arg (55.5) onto the stack
    get_local 4                   // Calculate function ptr (cont’d)
    i32.const 15                  // (an index into the func table)
    i32.and                       // ..
    i32.const 5376                // ..
    i32.add                       // ..
    call_indirect (type 0)        // call func ptr: printMe/makeAdmin
    [..]

call_indirect检查的类型定义定义了一个浮点函数:

  (type (;0;) (func (param i32 f64)))

因此,这里的类型检查将失败,因为WebAssembly中的两个函数具有不同的签名:“victim”函数需要的是浮点数(f64),而“evil”函数需要的是整数(i32)。

请注意,虽然此时WebAssembly程序将暂停并触发陷阱,但根据其安全设计,该故障将隔离到WebAssembly的客户程序实例。这意味着托管WebAssembly的浏览器进程可以继续安全执行,而不必担心内存损坏之类的安全问题。

利用WebAssembly类型检查漏洞

虽然WebAssembly提供了许多优雅的安全设计,包括声音类型系统等;但是,该设计仍然有一些不足之处,即WebAssembly提供的值类型较少,只有:i32、i64、f32和f64。这意味着源语言(例如C或C ++)中的所有值类型都需要映射到这些类型,并且,WebAssembly对间接调用的类型检查,也仅限于这四种类型。相应地,这意味着在我们的示例中,如果攻击者能够设法提供WebAssembly类型签名匹配的函数(详情请参阅WebAssembly内存安全文档),则攻击者可能能够劫持被攻击的WebAssembly代码的控制流。

为了观察这种行为,我们将对Bit of Bits的cfi_vcall.cpp进行修改,并将使用的修改版命名为cfi_vcall_same.cpp。在cfi_vcall_same.cpp中,受害者函数(程序本意要调用的函数)的参数为整型,而恶意函数(攻击者以某种方式提供的函数)的参数为void型。尽管两者在C++中是不同的类型,但它们都被映射到相同的WebAssembly类型。这意味着“受害者”和“邪恶”函数之间的函数签名是匹配的,因此,攻击者就能劫持受害者WebAssembly代码的控制流。回顾一下,在cfi_vcall_same.cpp中,攻击者试图让受害者执行下列代码:

    virtual void makeAdmin(void * i) {
        std::cout << "CFI Prevents this control flow " << i << "\n";
        std::cout << "Evil::makeAdmin\n";
    }

而非执行下列操作:

    virtual void printMe(int i) {
        std::cout << "Derived::printMe " << i << "\n";
    }

同样的,在下一节中,我们将展示针对这些程序的利用方法。

实验#3:利用本机可执行文件中的类型混淆漏洞

我们可以先用clang编译易受攻击/被利用的程序,观察没有WebAssembly类型检查时会发生什么情况:

[email protected]:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_same cfi_vcall_same.cpp

然后运行下列命令:

[email protected]:/src/clang-cfi-showcase# ./cfi_vcall_same
Derived::printMe 55
CFI Prevents this control flow 66
Evil::makeAdmin

从上面的输出中可以看出,包含漏洞的程序被利用了——攻击者的有效载荷makeAdmin被执行了。这是完全可能的,因为本机机器代码运行时,还是没有对函数的参数类型进行任何检查——因此,“恶意”函数仍会畅行无阻(尽管结果有些奇怪,因为该函数把提供给它的整数误认为是浮点数)。

实验#4:防止WebAssembly中的类型混淆漏洞被利用

我们可以使用emscripten编译易受攻击/被利用的程序,以便观察WebAssembly进行类型检查时会发生什么情况:

[tmux] [email protected]:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -Werror -s WASM=1 -o cfi_vcall_same.html

与之前的WebAssembly实验一样,上面的命令将生成cfi_vcall_same.wasm(WebAssembly模块)、cfi_vcall_same.js(定义浏览器和WebAssembly模块之间接口的Javascript文件)和cfi_vcall_same.html(用于运行JavaScript)文件。

这里,我们可以再次在主机上运行Python SimpleHTTPServer:

mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081

浏览生成的页面:

1.png

从上图可以看出,这里不仅存在类型混淆漏洞,并且“漏洞利用代码”也得到了执行,即makeAdmin函数得到了运行。这是因为,printMe和makeAdmin现在具有了相匹配的类型签名:

  (type (;0;) (func (param i32 i32)))

这是因为C语言的void *和int类型都被映射为WebAssembly中的i32类型(请注意,WebAssembly程序使用32位寻址——有关该主题的更多信息,请参考这里的文献)。因此,当WebAssembly检查将要调用的函数的函数签名(即函数参数和结果的WebAssembly类型)时,发现这两个函数具有相同的签名,因此,漏洞利用代码就得以成功运行。

使用Clang CFI缓解WebAssembly攻击

正如我们在上面所观察到的,虽然WebAssembly的简单类型系统带来了巨大的好处,但是仍然难以避免缺陷的存在,例如类型混淆漏洞。幸运的是,就像在本机可执行文件中一样,我们可以在编译WebAssembly代码时进行Clang CFI检查。正如WebAssembly内存安全文档中所讨论的,这有助于抵御上面考察的代码重用攻击,并使用更精细的C/C++类型对函数签名进行检查。

总之,这意味着可以将clang CFI检查编译到WebAssembly程序中,并在嵌入器(即浏览器)中强制执行。真是太酷了!

实验#5:在本机可执行文件中强制执行clang CFI检查

我们可以通过使用-fsanitize = cfi-vcall(将二进制文件输出到cfi_vcall_same_cfi)选项来编译cfi_vcall_same.cpp示例代码,以此考察本机二进制文件中的clang CFI强制执行情况:

[email protected]:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -fvisibility=hidden -flto -fsanitize=cfi-vcall -fno-sanitize-trap=all -o cfi_vcall_same_cfi cfi_vcall_same.cpp

然后运行下列命令:

[email protected]:/src/clang-cfi-showcase# ./cfi_vcall_same_cfi
Derived::printMe 55
cfi_vcall_same.cpp:45:5: runtime error: control flow integrity check for type 'Derived' failed during virtual call (vtable address 0x0000004300a0)
0x0000004300a0: note: vtable is of type 'Evil'
 00 00 00 00  20 84 42 00 00 00 00 00  30 84 42 00 00 00 00 00  60 84 42 00 00 00 00 00  00 00 00 00

如上所示,clang CFI可以按预期工作——漏洞被堵住了。

实验#6:在WebAssembly程序中执行clang CFI检查

现在,我们开始进入真正有趣的部分——通过使用-fsanitize = cfi-vcall之类的标志,调用emscripten,观察clang CFI在前面被利用的WebAssembly程序中的实施情况:

[tmux] [email protected]:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -fvisibility=hidden -flto -fsanitize=cfi -s WASM=1 -o cfi_vcall_same_cfi.html

然后,在浏览器中查看生成的HTML文件:

1.png

我们可以在上面看到,clang CFI检查的执行发生在浏览器中,从而阻止了漏洞利用。

结束语

在这篇文章中,我们通过示例为读者介绍了如何利用类型混淆漏洞劫持WebAssembly程序的控制流程,同时,还展示了WebAssembly提供的一些内存安全措施。此外,我们还讨论了使用clang CFI检查加固WebAssembly程序以防御这类攻击的方法。

总的来说,WebAssembly是一种精心设计的技术,为安全开发打下了良好的基础;希望本教程对大家的工作能够提供有益的帮助。

请大家继续关注本系列的第2部分,届时将为读者详细介绍WebAssembly嵌入器(提供WebAssembly客户程序运行环境的软件,即浏览器)有关的各种安全主题。

源链接

Hacking more

...