对于逆向工程师来说,能够直接从分析的二进制代码中调用函数是一条捷径,可以省去很多麻烦。虽然在某些情况下可以理解函数的逻辑并用高级语言重新实现它,但这并不总是可能的,原始函数的逻辑越脆弱和复杂,这种方法就越不可行.在处理自定义散列和加密时,这是一个特别棘手的问题,计算中某处的错误可能导致完全不同的最终输出,并且调试起来很麻烦。在本文中,我们将介绍3种不同的方式来实现这种“快捷方式”并直接从程序集调用函数。我们首先介绍IDAPro原生支持的IDAAppcall功能,可以直接通过idpython使用。然后我们展示如何使用Dumpulator实现相同的行为,最后,我们展示如何使用UnicornEngine模拟此结果。本文中使用的实际示例基于MiniDuke恶意软件样本实施的“调整后的”SHA1哈希算法。MiniDuke实施的修改后的SHA1散列算法MiniDuke示例中修改后的SHA1算法用于为恶意软件配置创建每个系统的加密密钥。要散列的缓冲区包含连接到所有接口描述的DWORD的当前计算机名称,例如'DESKTOP-ROAC4IJ\x00MicrWANWANMicrWANMicrWANInteWANInteWANInte'。此函数(SHA1Hash)在初始摘要和中间阶段使用与原始SHA1相同的常量,但产生不同的输出。MiniDukeSHA1Hash函数常量由于原始和修改后的SHA1使用相同的常量,因此函数的1241条汇编指令中一定会出现差异。我们不能说这样的调整是否是故意引入的,但事实是恶意软件开发人员越来越喜欢插入这样的“惊喜”,而处理它们的责任落在了分析师身上。为此,我们必须首先了解函数期望其输入并产生其输出的形式。事实证明,Duke-SHA1程序集使用自定义调用约定,其中要散列的缓冲区的长度在ecx寄存器中传递,而缓冲区本身的地址在edi中传递。从技术上讲,在eax中也传递了一个值,但是每当可执行文件调用该函数时,该值都是相同的0xffffffff,因此我们可以将其视为常量。有趣的是,恶意软件还会在每次调用该函数时将缓冲区长度(ecx)设置为0x40,从而有效地仅对缓冲区的前0x40个字节进行哈希处理。SHA1Hash函数参数得到的160位SHA1哈希值以5个双字(从高到低:eax、edx、ebx、ecx、esi)的形式返回到寄存器中。例如,冲冲DESKTOP-ROAC4IJ\x00MicrWANMicrWANMicrWANInteWANInteWANInte的Duke-SHA1值为1851fff77f0957d1d690a32f31df2c32a1a84af7,返回为EAX:0x1851fff7EDX:0x7f0957d1EBX:0xd690a32fECX:0x31df2c32ESI:0xa1a84af7。GeneratedSHA1bufferhashexampleAsabove,findtheexactplacewherethelogicofSHA1andDuke-SHA1diverge,thenre-在Python中实现Duke-SHA1,但是这种方法非常耗时。接下来,我们将使用一些方法来“插入”一个函数的调用约定并直接调用它。IDA–AppcallAppcall是IDAPro的一项功能,它允许IDAPython脚本在调试器中调用函数,就好像它们是内置函数一样。这非常方便,但也不是通用的,即当用例变得有些不寻常或复杂时,应用它的难度就会飙升。虽然在ecx中传递缓冲区长度和在edi中传递缓冲区是正常的,但将160位返回值拆分为5个寄存器并不是典型的函数输出形式,Appcall需要一些创造力来解决这个问题。接下来,我们创建一个自定义结构体struc_SHA1HASH,它保存5个寄存器的值,作为函数原型的返回类型:IDA结构体窗口-“struc_SHA1HASH”现在有了结构体定义,Appcall可以和这个函数原型交互,如下面的PROTO值所示。由于IDAAppcall依赖于调试器,为了调用这个逻辑,我们首先需要编写一个脚本来启动调试器,对堆栈进行必要的调整,并进行其他必要的管理工作。IDAView-StackAdjustmentUsingAppcall是最后一步,有几种方法可以使用它来调用函数。我们可以在不指定原型的情况下直接调用函数,但这高度依赖于IDA的IDB中具有正确类型的函数。第二种方式是根据函数名和定义的原型创建一个可调用对象。这样,无论在IDB中设置什么类型,我们都可以调用具有特定原型的函数,如下所示:使用Appcall调用Duke-SHA1的完整脚本如下所示。还有一些示例输出:ScriptExecution-"IDAAppcall"producesthesameSHA1hashastheMiniDukesample在特定的执行状态下,像上面那样指定原型是一件乏味的事情。令人高兴的是,这两个缺点都可以得到优化。由于IDAAppcall依赖于调试器并且可以直接从IDAPython调用,我们可以从调试器调用Appcall并更好地控制它的执行。例如,我们可以通过为Appcall设置一个特殊选项-APPCALL_MANUAL来让Appcall在执行期间将控制权返回给调试器。这样我们就可以使用Appcall来准备参数,分配缓冲区,然后恢复之前的执行上下文。我们还可以避免为返回值指定结构类型(将其键入void),因为这将由调试器处理。获取函数返回值的方法有多种,因此在控制调试器时,可以使用条件断点在特定执行状态(如返回时)打印所需的值。我们可以通过调用cleanup_appcall()在任何需要的执行时刻恢复之前的状态(在Appcall之前)。在我们的例子中,在遇到条件断点之后。完整的脚本如下:DumpulatorDumpulator是一个python库,有助于在小型转储文件中进行代码模拟。dumator的核心模拟引擎基于Unicorn引擎,但在同类工具中有一个独到之处,就是可以获得整个过程的内存。这导致性能提升(在不离开Unicorn的情况下模拟大部分分析的二进制文件),如果我们可以在调用函数所需的程序上下文(堆栈等)已经到位时对内存转储进行计时,那么会更方便.此外,只有模拟的系统调用才能提供真实的Windows环境(因为实际上一切都是合法的进程环境)。可以使用许多工具(x64dbg-MiniDumpPlugin、processExplorer、processHacker、TaskManager)或WindowsAPI(MiniDumpWriteDump)捕获所需进程的小型转储。我们可以使用x64dbg-MiniDumpPlugin在SHA1Hash函数调用之前,在几乎所有进程都已为SHA1哈希创建设置的状态下创建一个小型转储。请注意,没有必要以这种方式对转储进行计时,因为在进行转储后可以在转储程序中手动设置环境,这只是为了方便。使用“x64dbg-MiniDumpPlugin”创建一个minidumpDumpulator,不仅可以访问整个转储的进程内存,还可以分配额外的内存,读取内存,写入内存,读取注册表值,写入注册表值。换句话说,模拟器可以做的任何事情。也可以实现系统调用,因此可以模拟使用它们的代码。要通过Dumpulator调用Duke-SHA1,我们需要指定将在minidump中调用的函数的地址及其参数。在这个例子中,SHA1Hash的地址是0x407108。在IDA中打开生成的minidump由于我们不希望minidump的当前状态使用已经设置好的值,所以我们为函数定义了自己的参数值。我们甚至可以分配一个新缓冲区用作哈希缓冲区。完成此任务的代码如下所示。执行此脚本将生成正确的Duke-SHA1值脚本执行-“Dumpulator”生成与MiniDuke示例相同的SHA1哈希值仿真–独角兽引擎对于仿真方法,我们可以使用任何类型的CPU仿真器(例如Qiling、Speakeasy等),它模拟x86程序集并具有Python语言的绑定。由于我们不需要任何更高级别的抽象(系统调用、API函数),我们可以使用大多数其他引擎的基础设施——独角兽引擎。Unicorn是一个基于QEMU的轻量级、多平台、多架构的CPU仿真器框架,使用纯C语言实现,并绑定了许多其他语言。我们将使用Python绑定。我们的目标是创建一个独立的函数SHA1Hash,它可以像Python中的任何其他普通函数一样被调用,生成与MiniDuke中的原始函数相同的SHA1哈希。我们使用的实现背后的想法非常简单——我们只是提取函数的操作码字节并将它们与CPU仿真一起使用。提取原始函数操作码的所有字节可以简单地通过idpython或使用IDA→编辑→导出数据来完成。使用IDA“导出数据”对话框导出SHA1Hash函数的操作码字节与前面的方法相同,我们需要设置执行上下文。在此示例中,这意味着为函数准备参数并为提取的操作码和输入缓冲区设置地址。注意最后一条retn指令应该从提取的操作码列表中移除,以免将执行转移回堆栈上的返回地址,并且堆栈帧应该通过指定ebp和esp的值来手动设置。所有这些都显示在下面的最终Python脚本中。脚本输出如下所示:ScriptExecution--"UnicornEngine"产生与MiniDuke示例相同的SHA1散列总结以上所有直接调用程序集的方法都各有利弊。EasyDumpulator给我们留下了特别深刻的印象,它免费、执行速度快且非常有效。它非常适合编写通用字符串解密器、配置提取器和其他必须按顺序调用许多不同逻辑片段的上下文,同时保留难以设置的上下文。当我们想直接用调用特定函数的结果来丰富IDA数据库时,IDAAppcall功能是最好的解决方案之一。系统调用可以是Appcall在实际执行环境中使用的函数的一部分-使用调试器。Appcall的最大优势之一是快速简便的上下文恢复。由于Appcall依赖于调试器,它可以与idpython脚本一起使用,理论上它甚至可以作为模糊器的基础,将随机输入提供给函数以发现意外行为(即错误),但这种方法太昂贵了。使用纯仿真与UnicornEngine是独立实现特定功能的通用解决方案。使用这种方法,您可以按原样获取部分代码并在不连接到原始示例的情况下使用它。这种方法不依赖于工作示例,仅适用于部分代码的重新实现。对于不连续、易于转储的代码块的函数,这种方法可能更难实现。对于API或系统调用的部分代码,或者难以设置执行上下文的部分,上述方法通常是更好的选择。本文翻译自:https://research.checkpoint.com/2022/native-function-and-assembly-code-invocation/如有转载请注明出处
