识别二进制文件中的API函数
2023年8月2日 - Tim Blazytko
原文: https://www.synthesis.to/2023/08/02/api_functions.html
在我于REcon 2023的演讲中,我展示了如何在各种逆向工程环境中导航未知二进制文件的启发式方法,例如恶意软件分析、漏洞发现和嵌入式固件分析。在这次演讲中,我还介绍了一种简单但强大的技术,用于识别静态链接可执行文件和嵌入式固件中的常见API函数。这篇博客文章将深入探讨这一主题,探索这些API函数、启发式方法背后的直觉,以及其在恶意软件分析背景下的其他用例。
在逆向工程中,API调用为程序的行为提供了重要的见解。它们通常执行高级操作,如文件访问、网络通信、字符串操作或内存管理。它们帮助我们逆向工程师更好地理解程序的功能和工作原理。在漏洞研究的背景下,我们可以定位与漏洞常相关的代码区域,如与内存管理或用户输入处理相关的函数。对于恶意软件分析,我们可以了解恶意软件样本如何与操作系统、网络、文件等交互;这可以帮助我们理解其目的和功能。
然而,在某些情况下,API函数在二进制文件中难以识别:例如,在静态链接的可执行文件和嵌入式固件中。在静态链接的二进制文件中,标准库和第三方库的代码直接被合并到二进制文件中。由于用户代码和库之间没有明确的分隔,没有符号,很难识别哪些函数属于程序,哪些来自库。在嵌入式逆向工程的情况下,固件通常设计为直接与特定设备的硬件交互;因此,它可能包含与标准库中类似功能的独特API函数,但这些函数并不存在于标准库中。有时,制造商甚至使用自己的专有标准库实现;没有源代码或文档的访问权限,几乎不可能识别这些函数。
在这篇博客文章中,我们将首先讨论检测API函数的常用方法;然后,我们将评估启发式方法并展示其在恶意软件分析中的其他用例。因此,如果你对如何在各种逆向场景中检测API函数感到好奇,请继续关注。如果你想自己尝试这个启发式方法,可以使用我的插件。
检测API函数的方法
没有符号信息,识别静态链接二进制文件和嵌入式固件中的API函数仍然具有挑战性,原因如上所述。在实践中,两种常见的方法依赖于签名匹配和与已知库的交叉引用。
签名匹配的概念很简单:它涉及在二进制文件中搜索已知API函数的特定字节序列或签名。这些签名存储在数据库中,每个签名都作为相关API函数的独特指纹。在分析过程中,二进制文件被扫描,函数的字节序列与数据库中的签名进行比较。当匹配发生时,表示该特定API函数存在于二进制文件中。许多系统采用这种函数签名方法,例如Binary Ninja。
然而,签名匹配的效果很大程度上取决于签名数据库的准确性。这个数据库必须包含与二进制文件中使用的特定库版本相对应的签名;此外,它必须为相同的平台(操作系统和CPU架构)编译,并使用类似的编译器设置。变化可能导致显著差异并导致不匹配,限制了此类系统在实践中的可用性。
第二种方法,与已知库的交叉引用,采用类似的策略。在这种方法中,使用二进制差异技术将二进制文件与已知库的不同版本进行比较以找到相似之处。当找到匹配时,可以将已知库中的函数名称和类型信息转移到正在调查的二进制文件中。由于此方法考虑了控制流图结构和调用层次结构等附加特征,它允许库版本和编译器设置的轻微差异而不会导致完全不匹配;这样,它可能克服了签名匹配方法的一些限制。
作为缺点,由于依赖于特征匹配,这种方法通常带有显著的不确定性,这可能导致误报和漏报。此外,交叉引用仍然受到相应库可用性的限制。如果二进制文件是用一种不常见或专有的库编译的,而没有可用的版本进行比较,这些方法将无效。
接下来,我们将看看另一种简单但有效的启发式方法,该方法在许多逆向场景中被证明非常高效。
启发式方法:频繁调用的函数
一种具有显著潜力的启发式方法是调用频率分析:识别二进制文件中被其他函数频繁调用的函数。这种简单的技术可以产生大量的见解:具有高调用频率的函数通常在逆向工程过程中具有重要意义,因为它们通常代表软件依赖的核心功能。
通常,这些频繁调用的函数是重要的API函数,促进程序内广泛的基本操作。例子包括字符串操作(如strlen
、strcmp
)、内存管理函数(如malloc
、free
、memcpy
和memset
)、文件访问方法(如open
、close
)和网络通信(如send
、receive
)。这些API函数在二进制文件中扮演关键角色,使它们成为频繁调用的目标,并提供有关二进制文件操作的宝贵见解。因此,识别这些高频调用目标可以为理解二进制文件的内部工作原理提供有价值的立足点。
要在你选择的反汇编器中实现这种启发式方法,直观地,我们按独立调用者的数量降序排列所有函数。为了专注于最相关的发现,可以选择集中于这些函数的前10%。
在BinaryNinja中,这个操作可以通过几行Python代码实现:
def find_most_frequently_called_functions(bv):
print("Frequently Called Functions")
# print top 10% (iterate in descending order)
for f, num_callers in get_top_10_functions(bv.functions, lambda f: len(f.callers)):
print(f"Function {hex(f.start)} ({f.name}) is called from {num_callers} different functions.")
在上面的代码中,函数find_most_frequently_called_functions
接受一个二进制视图bv
(分析的二进制文件的表示)作为输入,并将get_top_10_functions
函数应用于二进制文件中的所有函数列表。这个函数根据独立调用者的数量对API函数进行排名;然后打印出前10%的函数。这个方法揭示了那些最频繁被调用的函数,使它们成为逆向工程过程中进一步调查的主要候选者。
在IDA或Ghidra中可以实现类似的实现:在IDA中,可以通过迭代对特定函数的交叉引用来找到相应的函数;在Ghidra中,可以利用函数对象的方法来识别调用者。
然而,虽然我们概述的启发式方法为识别二进制文件中的API函数提供了一种直观的方法,但现在让我们调查是否可以在各种真实世界的逆向工程场景中验证其有效性。
评估
为了评估我们启发式方法在真实世界场景中的有效性,我们将其应用于一组多样化的案例研究:
-
来自Linux包的 ls
命令, -
来自XOR DDos家族的静态链接恶意软件样本, -
一个嵌入式固件,以及 -
来自PlugX家族的另一个恶意软件样本。
为了保持一致性和可比性,我们将分析限制在每个案例中识别的前10个函数。我们预计这些函数将揭示常被调用的API调用。此外,我们的启发式方法可能还会揭示支撑二进制文件关键功能或逻辑的关键函数。在这些情况下,我们将深入了解它们的角色和操作。话不多说,让我们深入研究结果!
Coreutils
我们的第一个评估案例是来自Coreutils包的ls
命令,以生成命令行目录列表而闻名。由于这个二进制文件是动态链接的,API函数表现为使跳转到未包含在二进制文件中的函数的包装器;在加载时,操作系统在动态链接阶段解析确切的函数位置。
应用我们的启发式方法后,我们得到以下结果:
Function 0x100007528 (_putchar) is called from 28 different functions.
Function 0x100007522 (_printf) is called from 23 different functions.
Function 0x100007558 (_snprintf) is called from 12 different functions.
Function 0x1000074bc (_getenv) is called from 10 different functions.
Function 0x10000755e (_strcmp) is called from 9 different functions.
Function 0x1000069f0 (sub_1000069f0) is called from 9 different functions.
Function 0x100007582 (_strlen) is called from 8 different functions.
Function 0x100007564 (_strcoll) is called from 8 different functions.
Function 0x10000745c (_err) is called from 8 different functions.
Function 0x1000073fc (___error) is called from 8 different functions.
快速扫描结果显示,识别出的前10个函数中有9个直接与API调用相关。这些包括与字符串操作相关的操作(如_strcmp
、_strlen
、_strcoll
、_snprintf
)、输出到stdout
(_putchar
、_printf
)、环境变量解析(_getenv
)和错误处理(_err
、___error
)。
有趣的是,前10个列表中的一个函数sub_1000069f0
似乎不是典型的API函数。然而,仔细检查发现这个函数充当API函数_putchar
的包装器:
1000069f0 55 push rbp {__saved_rbp}
1000069f1 48 89 e5 mov rbp, rsp {__saved_rbp}
1000069f4 e8 2f 0b 00 00 call _putchar
1000069f9 31 c0 xor eax, eax {0x0}
1000069fb 5d pop rbp {__saved_rbp}
1000069fc c3 retn {__return_addr}
总之,识别出的前10个函数都是与API相关的。
XOR DDos
我们的第二个案例研究涉及来自XOR DDos家族的静态链接样本。顾名思义,这种针对Linux的恶意软件执行并使用基于XOR的加密(用于字符串和C&C服务器通信)。这个特定样本保留了所有符号;因此,它是一个很好的评估目标,因为我们可以使用这些函数名称轻松确认识别出的函数是否为API函数。我们在这个样本上进行的启发式测试的输出如下:
Function 0x8065320 (free) is called from 298 different functions.
Function 0x8066a70 (memcpy) is called from 191 different functions.
Function 0x80662b0 (strlen) is called from 184 different functions.
Function 0x80669b0 (memset) is called from 174 different functions.
Function 0x8063d30 (__libc_malloc) is called from 151 different functions.
Function 0x8053810 (__lll_unlock_wake_private) is called from 148 different functions.
Function 0x8053700 (__lll_lock_wait_private) is called from 122 different functions.
Function 0x8060080 (ptmalloc_init) is called from 114 different functions.
Function 0x80569b0 (__strtol_internal) is called from 99 different functions.
Function 0x80661f0 (strcmp) is called from 93 different functions.
最频繁调用的函数是free
,一个用于释放堆内存的API函数。其他频繁调用的API函数也与内存管理(__libc_malloc
)、数据移动(memcpy
)、内存初始化(memset
)和字符串操作(strlen
、__strtol_internal
、strcmp
)相关。此外,API函数如__lll_unlock_wake_private
、__lll_unlock_wake_private
和ptmalloc_init
出现在最常调用的函数中,表明恶意软件广泛使用多线程。这种行为与样本的性质非常吻合,该样本建立大量网络连接以执行DDoS攻击。再次,所有识别出的函数都是API函数。
嵌入式固件
在我们的下一个案例研究中,我们深入研究嵌入式固件逆向工程。我们检查一个为嵌入式微控制器操作的RTOS设计的未进一步指定的演示应用程序。这个应用程序实现了硬件初始化例程以及依赖于加密技术的基本网络通信逻辑。固件没有使用标准库,而是使用了一个自定义的静态链接库。我们以最高的优化级别编译了固件,同时保留了函数符号以便于分析。
这些是结果:
Function 0x8081cd8 (memcpy) is called from 379 different functions.
Function 0x808140c (vLoggingPrintfError) is called from 343 different functions.
Function 0x8081d26 (memset) is called from 338 different functions.
Function 0x80625a4 (sys_assert) is called from 307 different functions.
Function 0x80785ae (DbgConsole_Printf) is called from 250 different functions.
Function 0x8040738 (multiply_casper) is called from 247 different functions.
Function 0x807e2a0 (ulSetInterruptMask) is called from 160 different functions.
Function 0x807fd28 (CASPER_MEMCPY) is called from 139 different functions.
Function 0x80821e0 (__mbedtls_mpi_free_veneer) is called from 102 different functions.
Function 0x80511b8 (mbedtls_platform_zeroize) is called from 87 different functions.
启发式方法揭示了嵌入式固件中最频繁调用的函数的混合:在列表的顶部,我们发现memcpy
和memset
,反映了固件中大量的内存操作。接下来是与控制台输出函数相关的vLoggingPrintfError
和DbgConsole_Printf
,暗示固件中实现了一个日志或调试系统,而sys_assert
暗示了一个自定义的错误处理机制。
固件还包括自定义例程来处理与内存相关的任务,如CASPER_MEMCPY
、__mbedtls_mpi_free_veneer
和mbedtls_platform_zeroize
。这些函数属于加密库,提供了标准内存操作的优化或安全替代方案。值得注意的是,mbedtls_platform_zeroize
提供了一种安全的方法来从内存中擦除敏感数据,这是加密环境中的关键功能。
此外,multiply_casper
提供了加密乘法的优化版本。此外,ulSetInterruptMask
似乎与固件的中断处理系统有关,这是基于微控制器的应用程序中的常见元素。
总之,我们的启发式方法通过突出显示嵌入式固件中的标准库函数、应用程序特定的错误处理例程、控制台输出函数、自定义加密实现以及与固件中的中断管理相关的函数,提供了对固件核心功能的见解。
PlugX
在我们的最后一个实验中,我们研究了来自PlugX恶意软件家族的一个样本。与其他案例研究相比,由于缺乏函数符号,我们的指导显著减少。换句话说,我们完全依赖于我们的启发式方法和手动分析。
以下是从PlugX样本中获得的结果:
Function 0x10001620 (sub_10001620) is called from 1253 different functions.
Function 0x10001590 (sub_10001590) is called from 1253 different functions.
Function 0x1002cc5e (__seterrormode) is called from 320 different functions.
Function 0x1002cc4e (sub_1002cc4e) is called from 219 different functions.
Function 0x1002d530 (__free_base) is called from 141 different functions.
Function 0x100302f0 (sub_100302f0) is called from 90 different functions.
Function 0x10001530 (sub_10001530) is called from 73 different functions.
Function 0x10003af0 (sub_10003af0) is called from 63 different functions.
Function 0x10004360 (sub_10004360) is called from 61 different functions.
Function 0x10003cb0 (sub_10003cb0) is called from 31 different functions.
根据这些结果,有两个观察结果尤为突出:首先,唯一可识别的API函数是__seterrormode
和__free_base
;它们负责错误处理和内存管理。其次,两个函数——sub_10001620
和sub_10001590
——的调用频率显著高于二进制文件中的所有其他函数。
分析这些函数的调用模式揭示了一个有趣的相互作用:每当调用sub_10001620
时,sub_10001590
总是在几行之前被执行。更重要的是,sub_10001590
的输入参数是与Windows API对应的字符串值,如kernel32
。最后,我们还注意到这个函数的返回值被作为输入传递给sub_10001620
,这表明一个函数的输出作为另一个函数的输入;如果我们将所有内容放在一起,我们可以将这种调用结构的典型实例表示如下:
sub_10001620(0, sub_10001590("kernel32"), 0xffc97c1f)
我们现在明白为什么这两个函数被调用的次数相等:它们是相互依赖的。然而,经过更仔细的检查,我们发现了另一个值得注意的发现。外部函数的第三个参数——常量0xffc97c1f
——与字符串kernel32
结合,表明这个函数调用序列代表了一种API哈希例程,一种恶意软件常用的机制,用于从静态分析中混淆API调用。这个机制通过遍历一系列表示为字符串的API函数名称,计算它们的哈希值,并在运行时将计算的哈希值与预先计算的常量进行比较。如果找到匹配,则导入并执行相应的API函数。
不深入细节,进一步分析表明sub_10001590
充当LoadLibraryA
的混淆调用,该函数将指定的Windows库加载到调用进程的地址空间中。随后,sub_10001620
实现了crc32
算法的略微修改版本,将计算的CRC32哈希与常量输入(0xffc97c1f
)进行比较。这个工作流程完美地匹配了API函数哈希的操作模式。
利用这些知识,我们现在可以将函数调用表示如下:
crc32(0, LoadLibraryA("kernel32"), 0xffc97c1f) ; "GetProcAddressx00"
在这种情况下,常量0xffc97c1f
对应于从kernel32
加载的GetProcAddress
函数的哈希。
总结这个案例研究,我们的启发式方法再次识别了分析的恶意软件样本中的所有关键API函数。虽然它不像其他案例研究中那样明确指出特定的API函数,但启发式方法证实了API函数被频繁调用的基本假设。在这个特定案例中,API哈希例程——旨在导入隐藏的API函数——成为整个恶意软件样本中最频繁调用的函数结构。因此,启发式方法定位了提供对恶意软件行为深刻理解的代码位置;因此,它可以在恶意软件分析中作为一种强大的工具。
从更广泛的角度来看,我们可以说启发式方法在检测频繁使用的API函数方面非常有效。即使在突出显示的函数不是API调用的情况下,它们通常也会暴露二进制文件的其他重要功能——例如嵌入式固件样本中的加密操作。启发式方法不仅简单易于实现,而且极其高效。无论是恶意软件分析、嵌入式固件逆向工程还是静态链接可执行文件的分析,它都能提供对二进制文件的深入见解;它是我们逆向工程工具库中的一项宝贵工具。
更多文章
立即关注【二进制磨剑】公众号
原文始发于微信公众号(二进制磨剑):【翻译】识别二进制文件中的API函数
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论