一个反调试程序的分析
前言:一个反调试程序的分析笔记
知识点
1、如果反调试存在的话并且DLL也同样存在,那么此时不仅要考虑程序自身函数执行的反调试,还需要注意下DLL里面的反调试
2、有些DLL会在到达入口点之前被加载,而这些DLL会检测主程序是否正在被调试,如果正在被调试的话就立即结束进程。有一点必须明确,就是系统动态库并不会对OD进行检测。
3、关于jmp self的利用 -> EB FE
逆向过程
发现一个程序附带的一个DLL,结果一看里面全是用于反调试的操作,自己就记录学习下
IDA拖进这个DLL分析重点如下,这边只能看到入口点是进行初始化环境的操作
然后后面的指令全部都加花了,所以只能动态调试来进行分析了
因为这里发现拖入调试器的时候程序就直接结束了,并且此时这里还有DLL的加载,上面静态分析中看到DLL中也有相关的反调试,所以这里就先考虑反调试在DLL中进行,所以把调试器设置的系统断点选上,然后在DLL的代码区段打一个断点,然后F9,先来观察DLL的入口加载是如何进行运行的
F9之后就断在DLL的加载入口处DLL_PROCESS_ATTACH的代码,如下图所示
通过单步分析,发现下面的指令是主要进行反调试函数的操作,所以这里主要分析这个函数0x10002580
1000651F E8 5CC0FFFF CALL AntiDebu.10002580 ; 反调试主函数
跟进去发现确实加了很多的花指令
调试过程
HOOK NTDLL
7C921187 |. FF55 08 CALL DWORD PTR SS:[EBP+8] -> 7C921187 EBFE
CreateProcess
100027AE FF15 84C00010 CALL DWORD PTR DS:[<&KERNEL32.CreateProc>; kernel32.CreateProcessA
7C921187 FF55 08
7C921187 EB FE
7C921187 |. FF55 08 CALL DWORD PTR SS:[EBP+8]
EB FE jmp self
7C921187 |. FF55 08 CALL DWORD PTR SS:[EBP+8]
分析过程
这边把两个进程的IAT表都进行访问断点设置然后来进行调试,因为过程中需要观察如何进行反调试操作和整体的流程
子进程首先创建MainInstance互斥体,如下图所示
此时子进程就已经有了两个互斥体
接着子进程又开始对两个函数进行相关的WriteProcessMemory操作,第一个函数是,它的地址是0x7C97211C,将其写入C3机器码
1000147D FF15 4CC00010 CALL DWORD PTR DS:[<&KERNEL32.WriteProce>; kernel32.WriteProcessMemory
0012F0C8 FFFFFFFF |hProcess = FFFFFFFF 0012F0CC 7C97211C |Address = 7C97211C 0012F0D0 0012F0E3 |Buffer = 0012F0E3 0012F0D4 00000001 |BytesToWrite = 1 0012F0D8 00000000 \pBytesWritten = NULL
写入的是C3的机器码,当这里F8单步过后可以重新来查看下0x7C97211C的地址,如下图所示
第二个函数是DbgBreakPoint,函数地址为7C92120E
0012F0C8 FFFFFFFF |hProcess = FFFFFFFF 0012F0CC 7C92120E |Address = 7C92120E 0012F0D0 0012F0E3 |Buffer = 0012F0E3 0012F0D4 00000001 |BytesToWrite = 1 0012F0D8 00000000 \pBytesWritten = NULL
DbgBreakPoint写入的也是同样是一个字节,也是C3的机器码
接着子进程继续调用OpenMutexA来获取"WAIT"的互斥体,这个互斥体是在主进程中进行创建的,此时子进程要获取这个"WAIT"的互斥体
0012F0FC 001F0001 |Access = 1F0001 0012F100 00000000 |Inheritable = FALSE 0012F104 1000C324 \MutexName = "WAIT"
当获取到了"WAIT"互斥体之后下面就开始WaitForSingleObject,如果这里不将参数默认5秒修改的话,那么默认过了5秒之后,子进程就会直接进行ExitProcess的结束,所以这里将参数修改为INFINTE -> FFFFFFFF
10002DAD FF15 78C00010 CALL DWORD PTR DS:[<&KERNEL32.WaitForSin>; kernel32.WaitForSingleObject
接着子进程就会进行堵塞,如下图所示,此时如果单步F8走的话都一直无法进行响应
这里继续切回主进程来,这里主进程会开始CreateFile来打开相关的文件,但是不知道具体为什么这样做,wp说是检测相关的int 3 -> 0xCC断点,然后我发现就算0xCC存在的话,那么也不会存在相关的反调试的操作
0012EF4C 0012EFA8 |FileName = "AntiDebugDll.dll" 0012EF50 80000000 |Access = GENERIC_READ 0012EF54 00000000 |ShareMode = 0 0012EF58 00000000 |pSecurity = NULL 0012EF5C 00000003 |Mode = OPEN_EXISTING 0012EF60 00000000 |Attributes = 0 0012EF64 00000000 \hTemplateFile = NULL
接着又继续读取Patrick.exe,这里的时候读取模式FILE_SHARE_READ
0012F0E0 00146488 |FileName = "C:\get\Patrick.exe" 0012F0E4 80000000 |Access = GENERIC_READ 0012F0E8 00000001 |ShareMode = FILE_SHARE_READ 0012F0EC 00000000 |pSecurity = NULL 0012F0F0 00000003 |Mode = OPEN_EXISTING 0012F0F4 00000080 |Attributes = NORMAL 0012F0F8 00000000 \hTemplateFile = NULL
接着会看到主进程开始对0x00401000地址开始进行操作,这里VirtualProtectEx数据修改相关的权限,那么后面就开始进行读写内存了
0012EE74 00000064 |hProcess = 00000064 (window) 0012EE78 00401000 |Address = Patrick.00401000 0012EE7C 00005000 |Size = 5000 (20480.) 0012EE80 00000040 |NewProtect = PAGE_EXECUTE_READWRITE 0012EE84 0012EE9C \pOldProtect = 0012EE9C
可以看到下面就开始ReadProcessMemory进行读写操作
100036E7 8B1D 9CC00010 MOV EBX,DWORD PTR DS:[<&KERNEL32.ReadPro>; kernel32.ReadProcessMemory
这个还需要注意下,这里的ReadProcessMemory是一个循环操作,对401000这个区段重复进行读写操作,写入的次数这里取决于EBP寄存器中存储的值
上面写入完成之后主进程这里就会调用ReleaseMutex释放锁,给互斥体"WAIT"发送一个信号,那么就知道下面就会来到子进程的WaitForSingleObject就开始接收到对互斥体的控制权,下面就来到子进程的处理过程
10002B7C FF15 80C00010 CALL DWORD PTR DS:[<&KERNEL32.ReleaseMut>; kernel32.ReleaseMutex
这里切回子进程可以看到此时已经来到了WaitForSingleObject的下一行,原因就是上面的主进程进行ReleaseMutex释放锁,接着子进程的WaitForSingleObject就会接收到相关的信息
接着这里主进程还会进程WaitForInputIdle等待输入进行堵塞的操作,接下来就是子进程的开始了
10002BB5 FF15 70C10010 CALL DWORD PTR DS:[<&USER32.WaitForInput>; USER32.WaitForInputIdle
如下图所示,接下来继续来跟子进程,这里可以看到子进程又执行了ReleaseMutex,但是这里没有人通过WaitForSingleObject来接收互斥体的信号,所以这里的话子进程还是继续往下进行
子进程这里也会跟主进程进行相同的操作,这里调用了CreateFileA
10003E9C FF15 98C00010 CALL DWORD PTR DS:[<&KERNEL32.CreateFile>; kernel32.CreateFileA
0012F0E0 001451C8 |FileName = "AntiDebugDll.dll" 0012F0E4 80000000 |Access = GENERIC_READ 0012F0E8 00000001 |ShareMode = FILE_SHARE_READ 0012F0EC 00000000 |pSecurity = NULL 0012F0F0 00000003 |Mode = OPEN_EXISTING 0012F0F4 00000080 |Attributes = NORMAL 0012F0F8 00000000 \hTemplateFile = NULL
接着第二次读取AntiDebugDll.dll,但是这里的参数ShareMode 此时为0,默认的话就读取失败,所以这里需要自己将其设置为1
0012EF54 00000000 |ShareMode = 0
上面读取了,这里就会继续读取程序Patrick.exe
0012F0E0 00145458 |FileName = "C:\get\Patrick.exe" 0012F0E4 80000000 |Access = GENERIC_READ 0012F0E8 00000001 |ShareMode = FILE_SHARE_READ 0012F0EC 00000000 |pSecurity = NULL 0012F0F0 00000003 |Mode = OPEN_EXISTING 0012F0F4 00000080 |Attributes = NORMAL 0012F0F8 00000000 \hTemplateFile = NULL
子进程这里就开始第二次进行读取,默认读取ShareMode参数为0,但是如果为0的话正常的程序会读取失败,所以这里还是将其参数ShareMode修改为1让其读取成功
0012EF4C 0012EFA8 |FileName = "C:\get\Patrick.exe" 0012EF50 80000000 |Access = GENERIC_READ 0012EF54 00000000 |ShareMode = 0 0012EF58 00000000 |pSecurity = NULL 0012EF5C 00000003 |Mode = OPEN_EXISTING 0012EF60 00000000 |Attributes = 0 0012EF64 00000000 \hTemplateFile = NULL
子进程这里也会进行VirtualProtectEx函数调用,让00401000代码段进行修改访问权限
100036D2 FF15 00C00010 CALL DWORD PTR DS:[<&KERNEL32.VirtualPro>; kernel32.VirtualProtectEx
0012EE74 FFFFFFFF |hProcess = FFFFFFFF 0012EE78 00401000 |Address = Patrick.00401000 0012EE7C 00005000 |Size = 5000 (20480.) 0012EE80 00000040 |NewProtect = PAGE_EXECUTE_READWRITE 0012EE84 0012EE9C \pOldProtect = 0012EE9C
子进程一样的进行WriteProcessMemory,读取次数为EBP寄存器中的值
10003720 FF15 4CC00010 CALL DWORD PTR DS:[<&KERNEL32.WriteProce>; kernel32.WriteProcessMemory
0012EE74 FFFFFFFF |hProcess = FFFFFFFF 0012EE78 00401070 |Address = 401070 0012EE7C 0012EEA4 |Buffer = 0012EEA4 0012EE80 00000010 |BytesToWrite = 10 (16.) 0012EE84 0012EEA0 \pBytesWritten = 0012EEA0
子进程中读完完了之后,这里就会通过CreateThread来创建线程,如下图所示,这个是第一个线程,下面还可以看到第二个线程,如下图所示
0012F0F0 00000000 |pSecurity = NULL 0012F0F4 00000000 |StackSize = 0 0012F0F8 10002140 |ThreadFunction = AntiDebu.10002140 0012F0FC 00000000 |pThreadParm = NULL 0012F100 00000000 |CreationFlags = 0 0012F104 0012F390 \pThreadId = 0012F390
这里看到线程地址为0x10002140
10002140 51 PUSH ECX 10002141 56 PUSH ESI 10002142 8B35 70C00010 MOV ESI,DWORD PTR DS:[<&KERNEL32.Sleep>] ; kernel32.Sleep 10002148 68 C8000000 PUSH 0C8 1000214D FFD6 CALL ESI 1000214F C74424 04 00000>MOV DWORD PTR SS:[ESP+4],0 10002157 50 PUSH EAX 10002158 64:A1 18000000 MOV EAX,DWORD PTR FS:[18] 1000215E 3E:8B40 30 MOV EAX,DWORD PTR DS:[EAX+30] 10002162 3E:0FB640 02 MOVZX EAX,BYTE PTR DS:[EAX+2] 10002167 894424 08 MOV DWORD PTR SS:[ESP+8],EAX 1000216B 58 POP EAX 1000216C 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+4] 10002170 85C0 TEST EAX,EAX 10002172 ^ 74 D4 JE SHORT AntiDebu.10002148 10002174 6A 00 PUSH 0 10002176 FF15 0CC00010 CALL DWORD PTR DS:[<&KERNEL32.ExitProces>; kernel32.ExitProcess 1000217C CC INT3 1000217D 90 NOP 1000217E 90 NOP 1000217F 90 NOP 10002180 68 0E000780 PUSH 8007000E 10002185 E8 F6EFFFFF CALL AntiDebu.10001180
这里可以到该线程执行的就是一直sleep,然后执行的就是ExitProcess,那么这里的话就不能让其执行,所以这里的话就是让线程进行挂起操作
到了这里就可以把之前设置的IAT内存断点都删除了,接着在代码段进行下个访问断点,最后进行F9来到如下,有问题
这时候将00401d55的地址为OEP进行DUMP,发现此时还是无法进行运行程序
接着这里重新打开,观察所有的函数相关调用,然后这里会发现还有一个DLL中调用的函数,这里不用该DLL,那么这里就对该DLL的函数进行NOP,直接不使用DLL即可
接着在DLL的启动口进行RETN处理,让其不使用DLL
然后这里F9运行,可以发现程序已经能运行了,而且是不依赖于DLL的情况下
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
2020-03-20 PHP POP 链