Rust逆向工程实战:从闭包到宏的底层原理分析
在今年的Atr2con会议上,我将演示如何破解一个用Rust编写的CrackMe二进制文件。由于会议是在线举行的,我选择将其主要录制为一个大演示,只有非常少的幻灯片。然而,你们中的一些人可能想阅读一些细节/理论。Rust编译器所做的事情非常巧妙且有趣。
字符串是“胖指针”
它不像在C语言中那样,你的字符串实际上只是一个指向字符的简单指针。在Rust中,你的内联字符串将指向一个包含以下内容的结构:
- 长度。你的字符串的长度。你的字符串不需要像在C语言中那样以
\0结尾。 - 指向字符串字符的指针。
Radare2将这些字符串显示为“reloc.fixup.xxxx”。例如,在下面的例子中,字符串显然位于地址0x5e790。如果我们查看该地址的字节,我们可以清楚地看到指针(6bc0 0400 -> 0x04c06b),然后是长度(0x25)。我们确认字符位于0x04c06b。
;0x5e790;"k\xc0\x04"
0x00008b6a 488d051f5c.. lea rax,reloc.fixup.Space_Station_Airlock_Control_S
[0x00008b40]> px10 @0x5e790
-offset- 909192939495969798999A9B9C9D9E9F0123456789ABCDEF
0x0005e790 6bc0 0400 0000 0000 2500 k.......%.
[0x00008b40]> px 0x25 @0x04c06b
-offset- 6B6C6D6E6F707172737475767778797ABCDEF0123456789A
0x0004c06b 5370 6163 6520 5374 6174 696f 6e20 4169 SpaceStationAi
0x0004c07b 726c 6f63 6b20 436f 6e74 726f 6c20 5379 rlockControlSy
0x0004c08b 7374 656d 0a stem.
单态化
你可能听说过多态性。这是指一个给定的函数能够操作不同的类型。
单态化则“相反”:我们确保每种类型严格对应一个函数。
Rust编译器会自动使用单态化——出于优化原因,例如不需要虚函数表。当它遇到泛型函数时,在底层,它实际上会为每种类型生成一个函数的汇编代码。开发者看不到这一点,这是编译器的工作。
// generic
fn square<T: std::ops::Mul<Output = T> + Copy>(x: T) -> T {x * x
}fn main() {let a = square(3i32); // generates square::<i32>let b = square(2.5f64); // generates square::<f64>
}
编译器也经常为闭包做同样的事情。
闭包
闭包是一种捕获其环境的函数。在下面的例子中,add_x 是一个闭包。它捕获了环境,其中x = 5。调用 add_x(3) 返回 8。
fn main() {let x = 5;let add_x = |y| x + y;println!("{}", add_x(3));
}
Rust编译器的行为很大程度上取决于优化级别。假设你使用 -C opt-level=0 编译这个程序。
[0x00007960]> pdi @sym.main::main::h8f1c9e3495794b54
0x00007b90 sym.main::main::h8f1c9e3495794b54:
0x00007b90 4883ec68 sub rsp, 0x68
0x00007b94 c744240405000000 mov dword [rsp + 4], 5
0x00007b9c 488d442404 lea rax, [rsp + 4]
0x00007ba1 4889442408 mov qword [rsp + 8], rax
0x00007ba6 488d7c2408 lea rdi, [rsp + 8]
0x00007bab be03000000 mov esi, 3
0x00007bb0 e85b000000 call sym.main::main::__u7b__u7b_closure_u7d__u7d_::h0ea7a13e6c14ebb2
0x00007bb5 89442464 mov dword [rsp + 0x64], eax
编译器为我们的主函数生成了一个特定的闭包。它被命名为:sym.main::main::__u7b__u7b_closure_u7d__u7d_::h0ea7a13e6c14ebb2。该闭包的参数是:
- 第一个参数(在
rdi中):rsp + 8。这是闭包环境。它包含一个指向rsp + 4的指针,该位置存储着值 5。 - 第二个参数(在
esi中):3。这是提供给闭包的参数。
在闭包的汇编代码中,我们有用于整数的指令。
0x00007c11 488b07 mov rax, qword [rdi]
0x00007c14 0330 add esi, dword [rax]
...
0x00007c1f 8b442404 mov eax, dword [rsp + 4]
如果闭包被用于浮点数,则会生成另一个闭包,带有不同的指令。这就是单态化。我们可以通过创建一个既适用于浮点数又适用于整数的闭包来更好地看到这一点:
use std::ops::Add;fn get_adder<T>(x: T) -> impl Fn(T) -> T
whereT: Add<Output = T> + Copy,
{move |y| x + y
}fn main() {let add_int = get_adder(5);let add_float = get_adder(5.0);println!("{}", add_int(3));println!("{}", add_float(4.5));
}
现在,如果我们检查汇编代码,请注意编译器已经生成了 2 个 get_adder 函数。
[0x00007960]> afl~main
0x00007c00 11 sym.main::get_adder::hba4b3b420a6e866b
0x00007c10 13 sym.main::get_adder::hbe8e2ff9c2a8357d
0x00007c20 122 sym.main::get_adder::__u7b__u7b_closure_u7d__u7d_::h6ff0b01bf90189e9
0x00007c40 117 sym.main::get_adder::__u7b__u7b_closure_u7d__u7d_::h72c4b1cd2fd53136
0x00007c60 1251 sym.main::main::h8f1c9e3495794b54
如果我们检查第一个 get_adder 的汇编代码,我们会看到它适用于浮点数:
0x00007c20 sym.main::get_adder::__u7b__u7b_closure_u7d__u7d_::h6ff0b01bf90189e9:
0x00007c20 50 push rax
0x00007c21 0f28c8 movaps xmm1, xmm0
0x00007c24 f20f1007 movsd xmm0, qword [rdi]
0x00007c28 488d3d19ee0400 lea rdi, [rip + 0x4ee19]
0x00007c2f e87cfeffff call sym.__f64_as_core::ops::arith::Add_::add::hf4bd57df382c73d6
0x00007c34 58 pop rax
0x00007c35 c3 ret
而第二个 get_adder 适用于整数:
0x00007c40 sym.main::get_adder::__u7b__u7b_closure_u7d__u7d_::h72c4b1cd2fd53136:
0x00007c40 50 push rax
0x00007c41 8b3f mov edi, dword [rdi]
0x00007c43 488d15feed0400 lea rdx, [rip + 0x4edfe]
0x00007c4a e871feffff call sym.__i32_as_core::ops::arith::Add_::add::h471fa892cd8f10a4
0x00007c4f 59 pop rcx
0x00007c50 c3 ret
去糖化
在Rust中,开发者通常调用诸如 obj.blah() 这样的方法。Rust编译器在内部将其转换为更明确(但没那么“甜”)的形式:class::blah(&obj)。
let obj = MyObject { value: 10 };
obj.blah(); // 带语法糖的语法
查看下面生成的汇编代码:
; set value = 10
0x00007bc4 c744240c0a000000 mov dword [rsp + 0xc], 0xa
; put obj in RDI (1st argument of blah())
0x00007bcc 488d7c240c lea rdi, [rsp + 0xc]
; call blah() with address of obj as argument
0x00007bd1 e8baffffff call sym.sugar::MyObject::blah::h059bdb1e25fa70c4
宏
在Rust中,用于显示内容的函数 println! 是一个宏,而不是一个“真正的”函数。因此,在汇编代码中,你不会看到对 println! 的调用(这个“函数”并不存在),而是:
- 设置格式化字符串和参数。
- 构建 Arguments 结构。
- 调用
dbg._print,它实际上调用dbg.write_fmt,dbg.write...
我建议你阅读这篇文章以了解更多细节。
下面的汇编代码是由一个 Hello World 程序生成的。
0x00007b20 sym.print::main::h13b5f0e40e4e7865:
0x00007b20 4883ec38 sub rsp, 0x38
; allocate place for Arguments object on the stack
0x00007b24 488d7c2408 lea rdi, [rsp + 8]
; fat pointer to the string Hello World
0x00007b29 488d35d0a30400 lea rsi, [rip + 0x4a3d0]
; instantiate the Arguments object
0x00007b30 e83bffffff call sym.core::fmt::rt::__impl_core::fmt::Arguments_::new_const::ha519b55ee7e59acf
0x00007b35 488d7c2408 lea rdi, [rsp + 8]
; call to the dbg._print routine via GOT
0x00007b3a ff1550cf0400 call qword [rip + 0x4cf50]
0x00007b40 4883c438 add rsp, 0x38
0x00007b44 c3 ret
请注意,对 dbg._print 的调用是间接的:dbg._print 的 GOT(全局偏移表)条目位于 rip + 0x4cf50。Radare2 将其显示为一个 reloc.fixup:
[0x00007b20]> px10 @ reloc.fixup.UHSHxHH_
-offset- 909192939495969798999A9B9C9D9E9F0123456789ABCDEF
0x00054a90 c0430200 0000 0000 0000 .C........
[0x00007b20]> pd 10 @0x0243c0
;-- std::io::stdio::_print::h915f3273edec6464:
;DATA XREF from reloc.fixup.UHSHxHH_ @
┌ 206: dbg._print (int64_t arg1);
│ ; arg int64_t arg1 @ rdi
│ ; var int64_t var_18h @ sp+0x18
│ ; var int64_t var_80h @ sp+0x80
│ 0x000243c0 55 push rbp ; sync.rs:0:13 ; void _print();
— Cryptax
CSD0tFqvECLokhw9aBeRqtFmyXmRsBISmPE7edMOI2OK8XHtAZcBhDmHp75CbukkcV4OE5l5gwOQpJ1KPqBQB2mY1bHR5IDRIauIsx0/nRk=
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码
