ACProtect 1.09 加密壳脱壳

前言:ACProtect 1.09 加密壳脱壳分析笔记

关于Code Splicing

一个新的知识点,它是一种anti-dump的方式,在这个脱壳过程中就有碰到

脱壳过程

查壳结果如下所示,可以看到特征码是ACProtect 1.x系列

载入到调试器中,观察界面是如下所示

看到是有pushad,那么这里可以尝试先用ESP定律设置硬件断点尝试到OEP的地方,但是会发现这样子不行,F9之后程序就直接运行起来了

那么说明在硬件断点的部分存在反调试的部分,那么这里可以尝试下内存断点看看,同样也存在反调试

内核异常处理反调试部分没有学过,所以这里这边也解释不了,对于硬件断点的反调试的话可以通过KiUserExceptionDispatcher来进行追踪

穿插KiUserExceptionDispatcher知识点

OID KiUserExceptionDispatcher(__in PEXCEPTION_RECORD ExceptionRecord,__in PCONTEXT ContextRecord)

KiUserExceptionDispatcher是SEH分发器的用户模式的负责函数。

当一个异常发生的时候,该异常将生成一个异常事件,内核检查该异常是否是由于执行用户模式代码导致的。

如果是这样的话,内核修改栈上的trap frame,因此当内核从中断或者异常返回的时候,线程将从KiUserExceptionDispatcher 函数执行而不是导致异常的指令。

内核将另外安排几个参数(一个 PCONTEXT 和一个 PEXCEPTION_RECORD),它们描述了异常发生时机器的状态,而且在线程返回到用户模式之前被传递给KiUserExceptionDispatcher函数。

那么这里打个断点bp KiUserExceptionDispatcher,然后F9运行程序,我们这里看下[esp+4]的位置,里面存储的地址是指向PCONTEXT的结构体,跟过去如下

32位下的CONTEXT的结构体定义如下,其中DR0,DR1,DR2,DR3,此时当前线程的上下文的这四个寄存器中存储的就是硬件断点的值

所以如果程序设置了硬件断点,则 dr0 - dr7 寄存器的值就会改变,其中 dr0 - dr3,存放硬件断点的地址

kd> dt _CONTEXT
ntdll!_CONTEXT
+0x000 ContextFlags : Uint4B
+0x004 Dr0 : Uint4B
+0x008 Dr1 : Uint4B
+0x00c Dr2 : Uint4B
+0x010 Dr3 : Uint4B
+0x014 Dr6 : Uint4B
+0x018 Dr7 : Uint4B
+0x01c FloatSave : _FLOATING_SAVE_AREA
+0x08c SegGs : Uint4B
+0x090 SegFs : Uint4B
+0x094 SegEs : Uint4B
+0x098 SegDs : Uint4B
+0x09c Edi : Uint4B
+0x0a0 Esi : Uint4B
+0x0a4 Ebx : Uint4B
+0x0a8 Edx : Uint4B
+0x0ac Ecx : Uint4B
+0x0b0 Eax : Uint4B
+0x0b4 Ebp : Uint4B
+0x0b8 Eip : Uint4B
+0x0bc SegCs : Uint4B
+0x0c0 EFlags : Uint4B
+0x0c4 Esp : Uint4B
+0x0c8 SegSs : Uint4B
+0x0cc ExtendedRegisters : [512] UChar

那么此时断点到了如下,当步过这个CALL的时候你就会发现此时的硬件断点被清空了,我认为是抛给了三环的异常处理函数,但是实际上我不太懂内部的细节,这样的操作就会导致硬件断点的失效

那么这里无法通过正常的断点,但是可以在异常触发之后来进行设置断点,这样就不会触发异常了,这里看下日志记录中的信息

记录数据, 条目 4 地址=0047108E 消息=访问违规: 读取 [FFFFFFFF]

那么这里就使用最后一次异常0x0047108E 来定位OEP即可

到了0x0047108E这里的时候再给代码段下一个内存断点,然后接着一个shift+F9即可到OEP

到了OEP之后,其实你会看到OEP明显被抽取了一部分

这个看着很想VC6的入口OEP,我们来进行对比下,可以看到前三条指令被抽取掉了

那么这里补上被抽取的三条指令即可,如下图所示

004271B0 55 PUSH EBP
004271B1 8BEC MOV EBP,ESP
004271B3 6A FF PUSH -1

但是IAT表也需要自己修复,可以看到如下图,IAT表的函数地址部分都被加密了

所以这里还需要定位IAT加密的过程来进行恢复IAT表,这里如果按照正常来定位IAT加密的方式话那其实很简单,直接在开头对对应的IAT表的460818-460F28全部设置一个写入断点就好了

但是这边不同,虽然可以写入断点可以断到,但是会发现此时写入的值并不是函数的地址,而是已经被准备好重定向的地址了

到这里还可以学习到关于ACProtect 1.09的IAT加密思路通过IAT重定位跳转,之后进行异或运算来进行跳转到真正要调用的函数的地址来进行执行

那我自己想了下,就是这种的话是不是在对程序进行加壳的时候 是不是就把函数的地址提前做好了运算,然后放置在一个表里面,程序解压的时候就直接进行了替换?

我自己觉得不太可能,因为要兼容其他的系统,所以不可能会这样做,要不然在其他系统运行的时候替换了原本系统上计算的地址那不就导致程序无法运行了了吗。。。

但是此时的情况对IAT覆盖的时候,跳转的地址就已经计算好了,就有另外一种可能,那就是跳转的内容早就已经在加壳的时候就写好了,然后解压的时候就通过获取函数名称,然后通过算法计算好异或的过程,然后将数值进行替换,这样就可以实现这样的功能

填充IAT表的过程分析,通过分析发现是这样子的

首先需要知道的是 -> 壳程序在表中进行了加密

壳程序运行的时候对导入表中的各个IMAGE_IMPORT_DESCRIPTOR中指向的表(不确定是IAT还是INT)进行解密,然后获得对应的函数名称

接着就是通过GetProcAddress来获取函数名称函数地址,然后再把加密出来的名称再次进行抹除

接着通过一个算法构造出来一个push指令和xor指令,使得运算的结果刚好是对应的要跳转的函数地址,将这些指令覆盖到重定向的地址

接着再把要重定向的地址覆盖到IAT表中

上面的操作循环执行

004742B7 8B95 28404000 MOV EDX,DWORD PTR SS:[EBP+404028] ; 当前进程的imagebase
004742BD 8B06 MOV EAX,DWORD PTR DS:[ESI] ; EAX = 要填充的基址的偏移地址
004742BF 0BC0 OR EAX,EAX
004742C1 75 07 JNZ SHORT UnPackMe.004742CA
004742C3 90 NOP
004742C4 90 NOP
004742C5 90 NOP
004742C6 90 NOP
004742C7 8B46 10 MOV EAX,DWORD PTR DS:[ESI+10] ; EAX = IMAGE_IMPORT_DESCRIPTOR的地址
004742CA 03C2 ADD EAX,EDX ; imagebase + _IMAGE_IMPORT_DESCRIPTOR的地址
004742CC 0385 24404000 ADD EAX,DWORD PTR SS:[EBP+404024] ; imagebase + _IMAGE_IMPORT_DESCRIPTOR的地址 + 要填充的IAT地址的偏移
004742D2 8B18 MOV EBX,DWORD PTR DS:[EAX] ; 获取对应IAT偏移地址中的值(原本是函数地址)
004742D4 8B7E 10 MOV EDI,DWORD PTR DS:[ESI+10] ; EDI = IMAGE_IMPORT_DESCRIPTOR的地址
004742D7 03FA ADD EDI,EDX ; imagebase + _IMAGE_IMPORT_DESCRIPTOR的地址
004742D9 03BD 24404000 ADD EDI,DWORD PTR SS:[EBP+404024] ; imagebase + _IMAGE_IMPORT_DESCRIPTOR的地址 + 要填充的IAT地址的偏移
004742DF 85DB TEST EBX,EBX ; 判断有没有获取成功
004742E1 0F84 FA000000 JE UnPackMe.004743E1 ; 没有成功就跳转
004742E7 F7C3 00000080 TEST EBX,80000000
004742ED 75 1D JNZ SHORT UnPackMe.0047430C
004742EF 90 NOP
004742F0 90 NOP
004742F1 90 NOP
004742F2 90 NOP
004742F3 03DA ADD EBX,EDX ; EBX = base + 函数名称的偏移地址
004742F5 83C3 02 ADD EBX,2
004742F8 56 PUSH ESI
004742F9 57 PUSH EDI
004742FA 50 PUSH EAX
004742FB 8BF3 MOV ESI,EBX ; ESI = base + 函数名称的偏移地址
004742FD 8BFB MOV EDI,EBX ; EDI = base + 函数名称的偏移地址
004742FF AC LODS BYTE PTR DS:[ESI]
00474300 C0C0 03 ROL AL,3
00474303 AA STOS BYTE PTR ES:[EDI]
00474304 803F 00 CMP BYTE PTR DS:[EDI],0
00474307 ^ 75 F6 JNZ SHORT UnPackMe.004742FF ; 算法:将EBX当前的IAT表中的干扰数据转化为函数名称
00474309 58 POP EAX
0047430A 5F POP EDI
0047430B 5E POP ESI
0047430C 81E3 FFFFFF0F AND EBX,0FFFFFFF
00474312 53 PUSH EBX ; 函数名称的地址
00474313 FFB5 20404000 PUSH DWORD PTR SS:[EBP+404020] ; DLL基址
00474319 FF95 68C24100 CALL DWORD PTR SS:[EBP+41C268] ; GetProcAddress获取地址
0047431F 3B9D 28404000 CMP EBX,DWORD PTR SS:[EBP+404028] ; 比较是否是合法的函数名称的地址
00474325 7C 0F JL SHORT UnPackMe.00474336
00474327 90 NOP
00474328 90 NOP
00474329 90 NOP
0047432A 90 NOP
0047432B 60 PUSHAD
0047432C 2BC0 SUB EAX,EAX ; 抹除痕迹,抹除函数名称
0047432E 8803 MOV BYTE PTR DS:[EBX],AL ; 抹除痕迹,抹除函数名称
00474330 43 INC EBX ; 抹除痕迹,抹除函数名称
00474331 3803 CMP BYTE PTR DS:[EBX],AL ; 抹除痕迹,抹除函数名称
00474333 ^ 75 F9 JNZ SHORT UnPackMe.0047432E ; 抹除痕迹,抹除函数名称
00474335 61 POPAD ; 恢复现场
00474336 0BC0 OR EAX,EAX ; 判断是否获取函数地址成功
00474338 ^ 0F84 2EFFFFFF JE UnPackMe.0047426C ; 获取失败的话就返回
0047433E 3B85 78C24100 CMP EAX,DWORD PTR SS:[EBP+41C278] ; 判断函数地址是否是MessageBox
00474344 75 0A JNZ SHORT UnPackMe.00474350 ; 不是的话就继续
00474346 90 NOP
00474347 90 NOP
00474348 90 NOP
00474349 90 NOP
0047434A 8D85 CB454000 LEA EAX,DWORD PTR SS:[EBP+4045CB]
00474350 56 PUSH ESI ; ESI = 要准备覆盖的IAT的地址
00474351 FFB5 20404000 PUSH DWORD PTR SS:[EBP+404020] ; 压入模块地址
00474357 5E POP ESI ; ESI = 模块地址
00474358 39B5 E9204000 CMP DWORD PTR SS:[EBP+4020E9],ESI ; 判断是否是kernel32.dll
0047435E 74 15 JE SHORT UnPackMe.00474375
00474360 90 NOP
00474361 90 NOP
00474362 90 NOP
00474363 90 NOP
00474364 39B5 ED204000 CMP DWORD PTR SS:[EBP+4020ED],ESI
0047436A 74 09 JE SHORT UnPackMe.00474375
0047436C 90 NOP
0047436D 90 NOP
0047436E 90 NOP
0047436F 90 NOP
00474370 EB 60 JMP SHORT UnPackMe.004743D2
00474372 90 NOP
00474373 90 NOP
00474374 90 NOP
00474375 80BD 87A34000 0>CMP BYTE PTR SS:[EBP+40A387],0
0047437C 74 54 JE SHORT UnPackMe.004743D2
0047437E 90 NOP
0047437F 90 NOP
00474380 90 NOP
00474381 90 NOP
00474382 EB 07 JMP SHORT UnPackMe.0047438B
00474384 90 NOP
00474385 90 NOP
00474386 90 NOP
00474387 0100 ADD DWORD PTR DS:[EAX],EAX
00474389 0000 ADD BYTE PTR DS:[EAX],AL
0047438B 8BB5 ED404000 MOV ESI,DWORD PTR SS:[EBP+4040ED]
00474391 83C6 0D ADD ESI,0D
00474394 81EE C71F4000 SUB ESI,UnPackMe.00401FC7
0047439A 2BF5 SUB ESI,EBP
0047439C 83FE 00 CMP ESI,0
0047439F EB 31 JMP SHORT UnPackMe.004743D2 ; 关键跳
004743A1 90 NOP
004743A2 90 NOP
004743A3 90 NOP
004743A4 90 NOP
004743A5 8BB5 ED404000 MOV ESI,DWORD PTR SS:[EBP+4040ED] ; ESI = 准备重定向的地址
004743AB 53 PUSH EBX ; 重定向的地址
004743AC 50 PUSH EAX ; 函数的地址
004743AD 0F31 RDTSC ; 时间戳计数器
004743AF 8BD8 MOV EBX,EAX ; EBX = 时间戳
004743B1 58 POP EAX ; EAX = 函数地址
004743B2 33C3 XOR EAX,EBX ; EAX = 函数名称 ^ 时间戳
004743B4 C606 68 MOV BYTE PTR DS:[ESI],68 ; 开始重定向,push指令
004743B7 8946 01 MOV DWORD PTR DS:[ESI+1],EAX
004743BA C746 05 8134240>MOV DWORD PTR DS:[ESI+5],243481 ; 开始重定向,xor指令
004743C1 895E 08 MOV DWORD PTR DS:[ESI+8],EBX
004743C4 C646 0C C3 MOV BYTE PTR DS:[ESI+C],0C3
004743C8 5B POP EBX
004743C9 8BC6 MOV EAX,ESI ; EAX = 准备重定向的地址
004743CB 8385 ED404000 0>ADD DWORD PTR SS:[EBP+4040ED],0D
004743D2 5E POP ESI
004743D3 8907 MOV DWORD PTR DS:[EDI],EAX ; 填充IAT数据
004743D5 8385 24404000 0>ADD DWORD PTR SS:[EBP+404024],4 ; 要填充地址的偏移,每次+4
004743DC ^ E9 D6FEFFFF JMP UnPackMe.004742B7 ; 循环填充跳转
004743E1 83C6 14 ADD ESI,14
004743E4 8B95 28404000 MOV EDX,DWORD PTR SS:[EBP+404028]
004743EA ^ E9 38FEFFFF JMP UnPackMe.00474227

经过分析这个是关键跳转,当这个跳转为JMP的时候,那么就不会对IAT地址进行重定向,而是直接将获取到的函数地址进行填充到IAT表中

0047439F EB 31 JMP SHORT UnPackMe.004743D2 ; 关键跳

接着再到最后一次异常,然后代码段下断,shift+F9即可到OEP,重新观察IAT表,如下图所示,可以看到已经没有被重定向了

这里用importREC进行修复,可以看到还是有一个地址00460DE4是不对的,如下所示

00460DE0 77D2A78F USER32.GetAsyncKeyState
00460DE4 77D1A8AD USER32.wsprintfA
00460DE8 0046E5CB UnPackMe.0046E5CB
00460DEC 77D2E8F6 USER32.LoadIconA

按道理来说0046E5CB应该是一个函数地址,但是跟过去看了下实际上这个填充的不是函数地址

0046E5CB 60 PUSHAD

那么这里就对00460DE4下一个写入断点,因为下一个填充的就是00460DE8,调试的过程中是可以看到获取到的名称为MessageBoxA的,但是为什么最后填充的地址却是0046E5CB?

这里继续跟,接着获取到MessageBoxA的地址77D507EA

继续跟下去你就会发现,有一个判断语句,这里的判断如果是MessageBox的话,那么就会赋值给EAX为0046E5CB,而不是对应的函数地址

到这里就已经分析好了,那么最后修复的就是importREC修复0046E5CB地址的时候手动改成MessageBoxA的地址即可,重新打开修复好的地址如下图所示

但是运行发现还是报错,报错地址是在0x46C5F3

这里跟到0x46C5F3进行观察会发现修复好的程序在这个地址中跳转的0x167C9C,是无法跳转的

0046C5F3 FF25 9C7C1600 JMP DWORD PTR DS:[167C9C]

这里的原因就是涉及到开头说的Code Splicing,因为这块区段是在壳程序执行的过程中进行申请的内存,所以相关的转储工具无法进行DUMP

我自己原本修复的想法是想DUMP区段然后补到脱壳后的程序中,但是有一个问题就是0x167C9C这个地址是在原程序的基址之上的,所以也就无法实现了

看了其他的文章,感觉修复的方法比较巧妙,他们是发现一个FF25间接跳转,机器码占6个字节,然而跳转所指向的内容也正好占6个字节

所以这里的话就可以将这些JMP全部都替换为0x1678BC起始地址到0x168854结束地址中指向的所有机器码,替换的结果如下图所示,然后进行保存即可

0046C0FB - FF25 DC781600 JMP DWORD PTR DS:[1678DC] //这是第一个跳转的地址 -> 0016887E 8BF1 MOV ESI,ECX
0046D85F - FF25 74881600 JMP DWORD PTR DS:[168874] //这是最后一个跳转的地址 -> 00169FE2 B6 B6 MOV DH,0B6

运行结果如下图所示,那么就是修复成功了

posted @   zpchcbd  阅读(498)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示