如何为现有反编译器构建stripped Rust binary所需要的签名文件?
不知道从什么时候开始,有些老b登构建Rust程序时会把符号strip掉(尤其是在恶意软件和CTF中),大幅度降低了逆向体验😅。如果能恢复符号,哪怕是std
库的那些函数符号的话,可读性都会提高很多。
恢复符号的话,一个方法是函数相似性匹配,往小了看有bindiff,往大了看有BinaryAI或者其它一些大数据AI,但前者你需要构建一个合适的程序进行diff,并且选择什么范围的similarity和confidence也是个比较玄学的问题;后者的话目前并没有什么大型的在线大数据匹配有存储大量的rust函数语料。
除此之外,另一个方法就是依靠像IDA和BN这样的反编译器为rust程序所用到的库做签名文件,例如FLIRT和sigkit。它们有着相似的思路:匹配函数硬编码字节与调用图等。其实这就有点像它自动化地做了bindiff的一些事情并顺便帮你做匹配了。这个方法最大的优势就是方便!
对于本地进行符号恢复,无论是bindiff还是为特定反编译器构造签名文件,最重要的当然还是正确构造程序使用的库以进行匹配了。但就像匹配C库的函数你需要考虑编译器版本和库版本等因素一样,在rust中你同样需要考虑rust工具链+构造配置+库及其版本。如何从二进制程序中提取这些信息,并自动化构建所需的库,就是我们进行本地符号恢复最需要解决的问题。
结果前段时间刚好有人写了相关文章捏,介绍了如何自动化地为反编译器构造指定rust程序的签名文件,现在就让我们一起来学习借鉴下他的思路吧!
https://nofix.re/posts/2024-11-02-rust-symbs/
https://nofix.re/posts/2024-23-05-rust-mingw/
https://github.com/N0fix/rustbininfo
https://github.com/N0fix/rustbinsign
工具链识别
首先我们需要识别rust工具链,例如1.79.0-x86_64-unknown-linux-gnu
。可以看出以及从官方文档上得到,我们要的rust工具链包含几个信息:rust版本、目标指令集架构、目标运行平台、所用编译器套件等。
rust版本可以通过提取程序字符串中匹配rustc/([a-z0-9]{40})
的部分获取rust版本的commit,然后通过请求提交该commit到GitHub存储库,从返回的页面的tag
中即可找到commit所对应的rust版本。
目标指令集架构和目标运行平台这个很容易从二进制程序本身获取,不多说。
获取所用编译器套件就比较复杂,目前只能依靠经验和启发式规则来获取。例如,一般目标运行平台是linux
的往往是gnu
套件,但也有部分是musl
;目标运行平台是Windows的,往往是MSVC
本地编译或者mingw
交叉编译。我们可以使用字符串或者特定函数的硬编码特征来启发式识别所用编译器套件。
依赖分析
rust为了方便快速锁定错误,使用unwarp等函数产生错误时会将该模块信息以字符串的形式打包进去,例如/rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/alloc/src/collections/btree/navigate.rs
和/src/index.crates.io-6f17d22bba15001f/addr2line-0.17.0/src/function.rs
等。可以通过正则匹配的方式将像addr2line-0.17.0
这样的包名提取出来。这样依赖就可以简单地进行提取了。
构造配置
Cargo.toml
中的一些配置信息会显著影响最终生成的二进制程序,例如opt-level
和lto
。尽管大多数项目都是采用默认配置,但难免像恶意软件总是会玩一些不一样的花里胡哨。
几乎没有什么简单方法可以通过最终的二进制程序推断配置信息,有的只是一些启发式方法,以及手动修改配置并编译一个test与之比较。
对比库的构造
即使成功找到了以上配置,也不能像C一样简单地编译库并进行比对。例如,大量的rust函数包括了泛型参数,传递的具体类型的不同会很大程度上影响这个函数最终编译结果;如果程序开启了lto
,那么库中单一的函数与实际项目编译得到的函数有可能出现很大的不同。
作者想出了一个比较简单粗暴的解决方案:一般的公开库都会包括 tests
, examples
和benches
,我们可以编译这些程序,并合并签名,以求最大程度地覆盖可能的用例。
下面是简单的流程:
- 通过
rustup
添加指定的工具链(如果没有的话); - 在工具链目录下查找所有动态库(
.dll
和.so
等),作为标准库; - 通过向 https://crates.io/ 发送请求获取依赖包并存放在临时目录;
- 通过
cargo build
和修改后的配置(如果有的话)构建依赖包本身及其tests
,examples
和benches
,然后搜索获取构建得到的二进制程序和库; - 将上面得到的所有程序和库交由反编译器自带的签名生成套件来生成、合并签名文件。
具体实现可以直接看源码,源码的可读性还是很不错的。
总结
多亏了大哥,我们至少有了一个比较方便的自动化恢复rust符号的方法!这个方法最主要的一点就是rust程序本身包含了自身及其依赖项的一些元信息,如果是像C++那样的话想要提取这些信息就很困难了。
这个方法还可以推广到其它语言的符号恢复,例如Zig和Nim,不过Nim似乎前几年已经有人用类似的方法构建过了。
但需要注意,这个方法的实用性相对来说还是有限的,我自己测试了下,有的时候还是有误报的。毕竟无论是IDA还是BN,它们的签名生成主要还是针对C/C++。对于像rust这样高度现代化的语言,单单泛型参数就够它们喝一壶的了。我个人觉得,目前的反编译器除非有更强大的类型系统及其配套的控制流、数据流分析,甚至是一些AI辅助,不然还是很难跟得上现在编译得到的现代化程序的复杂程度。