逆向工程技术-检测开源库及其功能
○ [逆向技巧总结] 对开源库函数的手动识别
○ 思路
○ 自动化分析 – BinDiff
◇ 环境安装
◇ 使用方法
◇ 示例
○ 手动分析 – 对照源码
○ 总结
○ 声明
关注 天虞实验室 官方微信公众号,发送“逆向技巧”获得本文完整解题脚本、额外附录及附件内容~
[逆向技巧总结] 对开源库函数的手动识别
NO.1
在各种逆向工程项目以及CTF竞赛题目中,很多都会用到开源库,例如LuaJIT中集成的Lua解释器,和OpenSSL、zlib等常用的库。
很多库都有十分复杂的逻辑,在逆向过程中如果详细的去分析便会加大很多工作量,而且很多的逻辑会应用到数学、密码方面的知识,如果缺乏这些理论知识基本没有办法去逆向这些程序。
那么这时候对于各种库函数的识别便会变得十分重要,这篇文章主要介绍两种方式对开源库函数进行识别。
思路
NO.2
首先说一下大体的思路,我们既然找到了开源库,那么在程序中用到的很多函数的代码逻辑基本上和库里是相同的。
也就是说,我们拿到了程序的部分源代码,但是并不知道这些源代码分别对应程序中的哪一个部分。
如果我们可以找到对应的函数,就可以通过查阅开源库的源代码、函数名(符号)、开发文档去了解函数的功能。本篇文章要介绍的两种方式均是以这种思路去展开。
自动化分析 – BinDiff
NO.3
BinDiff是一款用于比对IDA数据库中的函数代码相似度的自动化工具,识别出的代码相似度越高,实现相同逻辑的可能性就越大。换句话说,就是将函数名与程序中的函数体进行匹配的工具。
环境安装
Java SE Development Kit
首先需要安装JDK,笔者这里用的是JDK 11,在Oracle的官网即可下载安装。
可根据需要选择相应的版本:
https://www.oracle.com/java/technologies/downloads/
IDA Pro
BinDiff是通过IDA生成的数据库进行的匹配,IDA Pro产品需要购买才可使用,是一款十分强大的逆向工具,笔者这里使用的是IDA 7.6版本。
官方网站地址:https://hex-rays.com/ida-pro/
BinDiff
官方下载地址在此,根据系统和环境的版本选择即可:
https://www.zynamics.com/software.html
注意:安装过程中会让你选择IDA的路径,此时要选择你的IDA工具(ida.exe/ida64.exe)的所在目录
使用方法
若要使用BinDiff工具,需要提供另一个idb数据库作为比对。
在此我们首先使用IDA打开被分析程序的数据库,然后File->BinDiff中选择要比对的idb数据库即可
等待一段时间后便可以在Matched Functions中得到匹配结果。
在左侧的sub_XXXX这些没有被命名的函数便可以通过这种方式找出对应的函数名。
示例
在这里使用Defcon Final 2021 – Barb Metal举例。
这道题使用了mrubyc库实现了一个虚拟机,并对虚拟机的代码进行了加密,题目要求在虚拟机代码中寻找漏洞得到flag。
在此我们只进行相关库的匹配。
第一步是寻找库的特征,例如OpenSSL中的字符串,编译时添加的Assertion信息等,通过这些信息定位到具体使用了哪个开源库。
直接在IDA中进行字符串的查找,可以得到以下结果,有很多的代码路径信息。
直接进入Google搜索LibTomMath、mrubyc,以及代码的路径信息。
得到以下三个开源库,均托管在Github:
LibTomMath(https://github.com/libtom/libtommath)
LibTomCrypt(https://github.com/libtom/libtomcrypt)
mrubyc(https://github.com/mrubyc/mrubyc)
第二步,将开源库编译成相同架构的二进制库文件,这里是Linux下的x86架构程序
这里要注意的有两点,第一点是一定要在32位环境下编译(如果在64位环境下需要在CFLAG上加-m32参数),因为我们要逆向的文件是32位的。第二点是,如果要在比较方便的情况下使用BinDiff,就需要将静态链接库(ar)转化为动态链接库(so),因为静态链接库实质是一个压缩包,里面有很多编译后的库文件,那么每个库文件就需要一个idb去存储它的代码信息。而动态链接库只需要一个idb即可,因此我们尽量使用动态链接库的方式生成二进制文件。
修改Makefile文件,以mrubyc为例
这样make后的结果便是libmrubyc.so这一个动态链接库文件
以此类推,继续编译LibTomCrypt和LibTomMath库,最终我们得到了三个so文件
分别导入IDA生成对应的idb,使用BinDiff选中进行分析
但是对于libTomCrypt和libTomMath,BinDiff便有些鸡肋了。
通过BinDiff,我们的分析便会变得十分简单,例如如下函数
同时mrubyc的opcode也一清二楚
对于BinDiff就介绍这么多,这款自动化工具十分好用,对于各种开源库的识别十分有帮助,当然,选择开源库的版本也十分重要,最好是要让所有库的版本都和待逆向程序的版本相同,这样才能提高匹配度。
手动分析 – 对照源码
NO.4
如果BinDiff这种自动化工具不是很有效的匹配函数,那么我们只能通过手动的对照源代码来分析每个函数的作用了。
拿刚刚的LibTom举例,我们可以得到的信息有源码所在目录和文件,甚至是本行的代码或是Assertion信息,有的函数说不定直接把函数名信息写在了里面。通过这些信息去定位源代码的位置函数,可以用来匹配函数符号。
不过这种情况在CTF的逆向中比较少见(除非题目侧重点不在逆向上面),这时候程序本身的逻辑便会显得十分重要,不论怎么改变函数名、源代码,函数的逻辑不会产生变化。而通过人工直接分析函数逻辑,便可以在信息不足的情况下定位函数。接下来要演示通过函数本身的特性手动分析的方法。
示例
本题基本没有任何的信息可供查找(出题人甚至将函数本身可以追溯到源代码的某些特性删除了),那么我们首先从main函数入手。
可以大概浏览伪代码,程序首先打印出key,然后将data.txt加密存入enc.dat中,再用字符串FLAG分割,写入经过sub_E9F和sub_C06E变换的key。
但是进入任何一个函数,逻辑都十分复杂,根本没有办法看懂。
因此我们从可以看到的常量值下手,首先从上往下看,首先进入sub_B91F,里面有两块数据,但很遗憾经过搜索里面的数据都没有信息表明是哪个算法。
那么进入sub_B7BC,可以看到简单的逻辑,再进入sub_B714看下它进行了什么变换
将a1类型修改为_DWORD*,因为根据前文分析a1是一个整型数组。
0x312 0x212 0x12 0x112这些值看起来很特殊,那么我们搜索这些值尝试一下。
找到了加密算法来源,是任天堂DS的算法。
根据对照源代码,可以得到以下函数映射
sub_B91F -> apply_keycode
sub_B7BC -> crypt_64bit_up
而上面那两个数据集,如果仔细分析源代码和该程序的逻辑后发现,无非是将自身的key写入了一个sbox中,后面的一大堆数据甚至根本都没有用到,这两个数据集实际上是对我们逆向的一个干扰。
退回到main函数,找到加密逻辑sub_CB0
sub_B86E是什么实际上已经没有任何意义了,因为v15是我们加密后的数据,而sub_B86E对其一点影响都没有,不过根据逻辑对照后这个函数其实是将刚刚加密的数据解密回去。。。不知道出题人为什么要加入这个函数。
下图两个函数都是将unsigned int四个字节的大端序和小端序进行交换,实际上就是上面源代码的bswap_32bit函数逻辑。
接下来思路就十分明显了,首先想办法解密key,再将生成的sbox Dump下来,再进行解密即可。
解密逻辑如下。
def crypt_64bit_down(x, y):
for i in range(0x11, 1, -1):
z = sbox[i] ^ x
x = sbox[0x012 + ((z>>24)&0xff)];
x = sbox[0x112 + ((z>>16)&0xff)] + x;
x = sbox[0x212 + ((z>> 8)&0xff)] ^ x;
x = sbox[0x312 + ((z>> 0)&0xff)] + x;
x = y ^ x
y = z
x = x ^ sbox[1]
y = y ^ sbox[0]
return (x, y)
def byteswap32(a):
return (a >> 8) & 0xFF00 | (a << 8) & 0xFF0000 | (a << 24) & 0xFF000000 | (a >> 24) & 0xFF
for i in range(0, len(encbuf), 2):
b = encbuf[i], encbuf[i+1]
b = crypt_64bit_down(byteswap32(a), byteswap32(b))
a = byteswap32(a)
b = byteswap32(b)
i+2] = [a, b] :
接下来回到main函数,跟踪sub_2200->sub_1CB0,这个函数很大,不过有一些关键变量可以让我们进行搜索
放入Google搜索得到MIRACL库的结果
我还找到一篇文章,专门讲了如何识别MIRACL的库函数,只可惜这道题把文章中说到的函数的特征去掉了,所以没办法通过这篇文章的思路进行识别了。
那么我们直接编译好拖进BinDiff,不过很遗憾,识别率低的出奇,根本没办法参考
不过我们编译好的so文件也不是没有用处的,用IDA打开,第一件事搜索关键值,得到我们函数的名称为mirsys_basic,接下来对照符号表,将so文件对应的函数填入我们逆向的程序中
接下来跟进每个函数中,根据判断条件、调用函数等函数逻辑继续给函数重命名,例如这个函数用到了0x80000000这个值,直接搜索,跟进每一个函数自己看代码相似度
猜测其为convert函数,接下来以此类推,用X交叉引用,查到这两个函数很相似,尤其是那个调用参数1
继续向下看,基本可以断定这两个函数完全一样,命名为mr_jsf,然后将copy add sub subdiv mr_addbit premult等函数也识别了出来,以此类推继续识别到cinstr函数
这里有一个小技巧,我们既然识别到了mr_berror函数,这个函数在很多函数中进行了调用,但是调用次数很少,都在1-5次之间,那么我们可以用交叉引用判断它在程序中调用的次数和位置,再看它的参数(例如此处的3)即可判断这个函数到底是什么名字。
例如很复杂的sub_6370函数,调用了两次,值分别为10和7
找调用两次并且参数均为10和7的函数
通过手动分析基本可以断定其为powmod函数。
以此类推,分别恢复符号。
那么加密的逻辑就是以下结果
很简单的逻辑,接下来继续回到main函数的sub_C06E函数
经过搜索和识别很明显是base58算法,字典被改为
9876432*Flag{n0T-EA5y=to+f1Nd}BCDGHJKLMPQRSUVWXYZbcehijkmp
那么一下代码即可得到key的值,开三次整数平方根即可
(在此使用gmpy2工具,在原文的附录中对此有展开说明)
STANDARD_ALPHABET = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
CUSTOM_ALPHABET = b'9876432*Flag{n0T-EA5y=to+f1Nd}BCDGHJKLMPQRSUVWXYZbcehijkmp'
with open("./enc.dat.bak", "rb") as f:
buf = f.read()
encrypted, key = buf.split(b"FLAG")
key = base58.b58decode(key.translate(bytes.maketrans(CUSTOM_ALPHABET, STANDARD_ALPHABET)))
print("Key: " + int(gmpy2.iroot(int.from_bytes(key, "big"))[0]).to_bytes(8, "big").decode())
通过调试,将key的值更改掉,然后待程序生成好sbox之后去将其dump下来,得到sbox.bin
总结
NO.5
本篇文章主要介绍了两种对开源库进行识别的方法,这种方法在逆向工程中较为重要,希望能给大家带来帮助。
声明
NO.6
本文作者:Sakana
本文编辑:Yusa
感谢 Sakana 师傅 (๑•̀ㅂ•́)و✧
往期回顾
扫码关注我们
天虞实验室为赛宁网安旗下专业技术团队,重点攻关公司业务相关信息安全前沿技术。
原文始发于微信公众号(天虞实验室):逆向工程技术-检测开源库及其功能