当前位置: 首页 > 科技观察

针对常见混淆技术的对策

时间:2023-03-22 16:36:13 科技观察

现代软件通常使用混淆技术作为其防篡改策略的一部分,以防止黑客对软件的关键组件进行逆向工程。他们经常使用多种混淆技术来抵御黑客的攻击,这有点像滚雪球:随着雪层的增加,软件规模也随之增大,逆向分析难度加大。在这篇文章中,我们将仔细研究两种常见的混淆技术,以了解它们的工作原理并弄清楚如何对它们进行反混淆。概述在这里,我们将研究以下混淆技术:IAT导入表混淆技术基于控制流的混淆技术IAT导入表混淆技术在深入IAT导入表混淆方法之前,让我解释一下导入表都是什么关于它是什么。什么是导入函数?进行逆向工程时,首先要弄清楚它是如何从操作系统中调用函数的。在我们的例子中,我们将专注于Windows10系统,因为大多数视频游戏只能在Windows系统上运行。不管怎样,对于那些还不知道的人,Windows提供了一系列重要的动态链接库(DLL)文件,几乎每个Windows可执行文件都使用这些文件。这些DLL文件包含许多可以由Windows可执行文件“导入”的函数,允许它加载和执行给定DLL中的函数。为什么它们如此重要?例如Ntdll.dll库负责几乎所有与内存相关的功能,如打开进程句柄(NtOpenProcess)、分配内存页(NtVirtualAlloc、NtVirtualAllocEx)、查询内存页(NtVirtualQuery、NtVirtualQueryEx)等。等待。另一个重要的DLL库是ws2_32.dll,它通过以下函数处理各种网络活动:SocketConnect/WSAConnectSend/WSASendSendTo/WSASendToRecv/WSARecvRecvFrom/WSARecvFrom现在读者可能会问,知道这些有什么意义呢?好吧,如果你将二进制文件扔进像IDA这样的反汇编程序中(我通常做的第一件事)就是检查所有导入的函数,以大致了解二进制文件的作用。例如,当ws2_32.dll出现在导入表中时,表示二进制文件可以连接到Internet。现在,我们可能想更深入地挖掘一下,看看使用了哪些ws2_32.dll函数。如果我们使用Socket函数,找到调用的地方,就可以查看它的参数,那么通过搜索引擎查找对应的函数名,就可以很容易地知道它使用的是什么协议和类型。注意:IDA已自动为反汇编代码添加注释。混淆的导入表不管怎样,这些Windows函数都非常有用,因为它们是有据可查的函数。因此,攻击者希望能够隐藏这些功能以隐藏正在发生的事情。我们在反汇编器中看到的所有这些导入函数都是从导入地址表(IAT)加载的,该表在可执行文件的PE头文件中的某处被引用。一些恶意软件/游戏通常试图通过不直接指向DLL函数来隐藏这些导入地址。相反,他们可能会使用蹦床或环形交叉路口功能。检查我们的例子在这个例子中,我们使用了如下的蹦床混淆技术:下面的地址0x7FF7D7F9B000引用了我们的函数0x19AA1040FE1,尽管看起来根本不是这样。您可能认为这是垃圾代码,但仔细观察就会发现它不是。请仔细检查前两条指令:前一条指令是movrax,FFFF8000056C10A1,后一条指令是jmp19AA1040738,后面都是垃圾指令。无论如何,让我们跟随跳转指令,看看它跳转到哪里:看,4条更有效的指令,这次是一条异或指令和两条加法指令,然后是另一条跳转指令。让我们再重复这个过程几次……终于,我们来到了jmprax指令!请注意,所有XOR、SUB和ADD指令都在Rax寄存器上执行,这意味着它可能包含imports函数的实际指针。接下来,让我们算一下。事实上,经过计算,我们得到了一个指向advapi32.regopenkeyexa的指针!现在,我们只需要重复计算几百次,就可以彻底消除IAT导入表的混淆。基于IAT的自动去混淆处理我想,没有人喜欢用计算器手动重复上面的过程,做一次就已经很烦了。从现在开始,我们将使用C#来实现自动计算。您可能已经看到,我们只需要处理对同一寄存器执行的ADD、SUB和XOR操作。原因是Rax被用作返回地址,而Rcx、Rdx、R8、R9等寄存器对被调用者来说是不安全的,可能与调用约定冲突。这意味着我们甚至不需要使用反汇编程序,因为涉及的寄存器和操作码很少,我们可以轻松区分这些指令。到目前为止,我们已经详细解释了混淆技术。接下来不妨以Unsnowman项目中的importfix.cs为例,了解去混淆处理相关的代码。在反转二进制文件时,基于控制流的混淆技术的另一个有价值的信息来源是汇编指令本身。对于人类来说,它们可能是无法理解的,但是对于IDA这样的反编译器,我们只要按下F5键,IDA就会生成我们人类可以理解的伪代码。混淆实际指令的一种简单方法是使用垃圾代码和不透明分支的组合(即分支条件始终为假,即分支被使用但从未被采用)。这意味着:将垃圾代码放在分支指令之后。诀窍是我们可以使用条件分支,但要确保条件始终为真,以便始终采用分支。反汇编程序不知道的是条件跳转在运行时始终为真,这使得它相信条件跳转的两个分支都可以在运行时到达。好吧,如果你还不明白,你可以通过下面的截图来加深你的理解。第一个屏幕截图显示了jbe落入另一条指令。注意:标记为红色的字节是垃圾代码。现在仔细看看下面的第二张图,我在这里所做的只是对最后一条指令的两个字节进行NOP,以便让IDA显示隐藏在and[rdx+24448B48h],bh指令后面的指令。我们还可以用无条件跳转修补条件跳转,以确保IDA不会再次被愚弄。在我们继续之前,我想展示最后一个例子,因为前面的例子太简单了。当我们链接这些混淆的跳转时,事情变得复杂,如下图所示。然而,这张图只显示了它在控制流方面造成的混乱,但想象一下当IDA不遗余力地根据垃圾指令创建这张图时我的CPU所处的痛苦。现在,您可能想知道去混淆函数是什么样子的,但是等等,看看下面的图片!看到我在左边画的蓝色小箭头了吗?右边显示的是这个的放大版本。现在看右边,一小部分函数有七个去混淆跳转。想象一下手动或半自动去混淆需要多少时间。事实上,即使使用IDA脚本手动执行此操作也花了我40分钟……而且这只是处理一个函数。想象一下,为了找到您真正想要的东西,还需要处理多少其他功能?!基于控制流的自动反混淆技术好了,既然我们已经了解了基于控制流的反混淆原理,那么接下来,我们将这个过程自动化。正如我之前提到的,我们曾经使用IDA脚本来修补无条件跳转指令,并将垃圾指令替换为NOP指令。但是,这个反混淆过程还是花了我40分钟,因为识别不透明的分支非常费力。那么,我们如何解决这个问题?你可能认为你应该检查每个条件跳转指令并检查它是否不透明,如果是,则用NOP替换它并重复这个过程,对吧?错误的!让我告诉你一个秘密,我们不关心什么是不透明的,或类似的东西。我真正担心的是当我按下F5时IDA是否可以返回反编译代码——只要这些混淆的跳转指令导致垃圾指令与实际的汇编指令发生冲突,这种情况就不会发生。但这是否意味着我们需要弄清楚条件跳转是否不透明?不,我们只是检查跳转操作是否与现有指令冲突,如果是,则相应地修改指令,正如我们在第一个示例中看到的那样。DeFlow去混淆算法现在我们知道如何解决这个问题,让我们深入研究我想出的算法来对使用这种混淆技术处理过的内容进行去混淆处理。vardisasm=newDisassembler(buffer,address-base);//注意:base=BaseAddress+.textoffsetforeach(varinsnindisasm.Disassemble())ulongtarget=0;ulonglastAddrStartboolisJmp=true;switch(insn.Mnemonic)//当我们没有lastTarget时遇到无效或返回指令时停止分析caseud_mnemonic_code.Invalid:caseud_mnemonic_code.Ret:if(lastTarget==0)returnnewChunks;//只有在没有lastTarget时才接受,因为我们可能正在查看垃圾代码中断;caseud_mnemonic_code.ConditionalJump://所有条件跳转if(lastTarget==0)target=calcTargetJump(insn);//帮助程序从指令中提取跳转位置if(!isInRange(target))//帮助程序查看目标地址是否位于我们的缓冲区中isJmp=false;休息;//检查指令是否大于2,如果是则不会混淆ted但我们//确实想分析目标位置if(insn.Length>2)isJmp=false;newChunks.Add(目标);休息;否则isJmp=false;//当我们已经有一个目标(可能正在查看垃圾代码)时,不要接受这个条件跳转break;caseud_mnemonic_code.UnconditionalJump:caseud_mnemonic_code.Call:if(lastTarget==0)ulongnewAddress=calcTargetJump(insn);//从指令中提取跳转位置的助手if(!isInRange(newAddress))isJmp=false;休息;//添加目标和下一条指令IFnotJMP(CALL确实返回,JMP没有)if(insn.Mnemonic==ud_mnemonic_code.Call)newChunks.Add(address+insn.PC);//添加指令目标以供进一步分析newChunks.Add(newAddress);返回新C帅哥;休息;//quickmafsulonglocation=(address+insn.Offset);stepsLeft=(int)(lastTarget-位置);//只有当我们有一个lastTarget时才有效!//如果当前指令是条件跳转而没有lastTarget则设置一个新目标if(lastTarget==0&&isJmp)lastBranch=loction;lastBranchSize=insn.Length;最后一个目标=目标;elseif(stepsLeft<=0&&lastTarget!=0)//如果stepsLeft不为零,那么我们的lastTarget位于我们上方,//这意味着我们部分位于前一条指令内,因此我们被隐藏(混淆)如果(stepsLeft!=0)intcount=lastTarget=lastBranch;//计算我们在下一条指令中有多少字节if(count>0)//确保我们是正跳转intbufferOffset=lastBranch-base;//从输出地址中减去基址,这样我们就可以写入本地缓冲区r//NOP滑动除我们自己的指令之外的所有内容if(inti=0;i