反调试技巧收集楼
年末似乎大家都很忙,某人问我最近怎么了,怎么都不更新了,我说都没人来做,怎么更新?看雪老大没有把HIDEOD加壳,也没使用任何反调试手段。很适合新人学习如何分析程序里面的插件,不过却没有一个人来做。今天考完试,看了一下,把HIDEOD最后两个未知的(反)反调试代码分析出来了。
给出几个相关链接,本地留一份,以后肯定用得着的:
http://bbs.pediy.com/showthread.php?s=&threadid=28929
查TEB和PEB成员会用到
http://www.pediy.com/bbshtml/bbs4/kanxue310.htm
查SEH成员
http://www.metasploit.com/users/opcode/syscalls.html
查系统调用号
关于IsDebuggerPresent,随便载入一个程序,用BP下断,从断点窗口就可以转到这个API的区域了,然后看到下面这段代码:
77E6191E > 64:A1 18000000 mov eax, dword ptr fs:[18]
77E61924 8B40 30 mov eax, dword ptr [eax+30]
77E61927 0FB640 02 movzx eax, byte ptr [eax+2]
77E6192B C3 retn
查找TEB和PEB的相关资料,可以知道这里其实就是检查PEB +2h处的BYTE。程序被调式时,这个BYTE为1,否则返回0。对付的方法很简单,把PEB +2h 的BYTE改成0就可以了。要注意到该反调试技巧其实可以不使用API,而是把代码移到自己的程序中。
关于SetUnhandledExceptionFilter,NtQueryInformationProcess,ZwQueryInformationProcess,第一个API是设置异常处理例程的,假如程序被调试,异常发生时该例程则不会被激活。
除了直接使用该API之外,还有可以这么用:
00455216 8B46 38 mov eax,dword ptr ds:[esi+38] ;[esi+38]=SetUnhandledExceptionFilter
00455219 8B40 0B mov eax,dword ptr ds:[eax+B]
0045521C 8908 mov dword ptr ds:[eax],ecx ;ECX是程序自定义的SEH
现在用前面说过的方法看看SetUnhandledExceptionFilter
77E678A7 >/$ 8B4C24 04 mov ecx,dword ptr ss:[esp+4]
77E678AB |. A1 4C04EC77 mov eax,dword ptr ds:[77EC044C]
77E678B0 |. 890D 4C04EC77 mov dword ptr ds:[77EC044C],ecx
77E678B6 \. C2 0400 retn 4
API入口点+B的偏移便是需要设置的系统内部变量。该API实际就是设置这个变量。
当异常出现时,UnhandledExceptionFilter就会被激活。跟进这个函数,你会看到NtQueryInformationProcess,再跟进,是ZwQueryInformationProcess的调用。该函数其实就是查看EPROCESS中的DebugPort是不是一个有效的值。
将ZwQueryInformationProcess返回值(即EAX)改为0x80000000。就可以正常调试了。
反硬件断点(调试寄存器),异常发生时,进入SEH例程。这里有4个入口参数:
pExcept: --- EXCEPTION_RECORD结构的指针
pErr: --- 前面ERR结构的指针
pContext: --- CONTEXT结构的指针
pDispatch:---没有发现有啥意义
注意到CONTEXT结构:
iDr0 DWORD ? ; | +4
iDr1 DWORD ? ; | +8
iDr2 DWORD ? ; >调试寄存器 +C
iDr3 DWORD ? ; | +10
iDr6 DWORD ? ; | +14
iDr7 DWORD ? ; _| +18
显然可以人为制造异常,进入SEH,然后检查调试寄存器的值,甚至改变该调试寄存器的值让硬件断点失效。
当异常发生时,系统便会进入KiUserExceptionDispatcher,跟进里面可以看到RtlDispatchException,RtlExecuteHandlerForException,最后看到CALL ECX,ECX即SEH例程的入口地址。
也就是说HOOK RtlExecuteHandlerForException,在保存调式寄存器中的值之后清0。在SEH结束时,重新赋值。
这些都是怎么发现的?
希望你还记得第一期第三题,这会是其中一个方法。
CheckRemoteDebuggerPresent
最初由 叶飘萍 发布
OD载入一个程序,Ctrl+G:CheckRemoteDebuggerPresent
然后看到下面这段代码:
7C859B1E > 8BFF mov edi, edi
7C859B20 55 push ebp
7C859B21 8BEC mov ebp, esp
7C859B23 837D 08 00 cmp dword ptr [ebp+8], 0
7C859B27 56 push esi
7C859B28 74 35 je short 7C859B5F
7C859B2A 8B75 0C mov esi, dword ptr [ebp+C]
7C859B2D 85F6 test esi, esi
7C859B2F 74 2E je short 7C859B5F
7C859B31 6A 00 push 0
7C859B33 6A 04 push 4
7C859B35 8D45 08 lea eax, dword ptr [ebp+8]
7C859B38 50 push eax
7C859B39 6A 07 push 7
7C859B3B FF75 08 push dword ptr [ebp+8]
7C859B3E FF15 AC10807C call dword ptr [<&ntdll.NtQueryInform>; ntdll.ZwQueryInformationProcess
7C859B44 85C0 test eax, eax
7C859B46 7D 08 jge short 7C859B50
7C859B48 50 push eax
7C859B49 E8 1DF8FAFF call 7C80936B
7C859B4E EB 16 jmp short 7C859B66
7C859B50 33C0 xor eax, eax
7C859B52 3945 08 cmp dword ptr [ebp+8], eax
7C859B55 0F95C0 setne al
7C859B58 8906 mov dword ptr [esi], eax
7C859B5A 33C0 xor eax, eax
7C859B5C 40 inc eax
7C859B5D EB 09 jmp short 7C859B68
7C859B5F 6A 57 push 57
7C859B61 E8 4AF7FAFF call 7C8092B0
7C859B66 33C0 xor eax, eax
7C859B68 5E pop esi
7C859B69 5D pop ebp
7C859B6A C2 0800 retn 8
程序被调试时,eax为1,否则返回0。对付的方法,把7C859B5C 40 inc eax //修改为: nop
就可以了
FindWindow,CreateToolhelp32Snapshot,Process32First/Next
详见该贴,另外看看该贴28楼,作为检查父进程的扩展,检查父进程是否在系统目录下,这样就不能通过简单修改程序名逃过检查了。
http://bbs.pediy.com/showthread.php?s=&threadid=4013&perpage=15&highlight=&pagenumber=1
对于FindWindow,可以修改程序的类。
对于父进程检查,我补充一下吧。跟踪CreateProcess,可以找到NtCreateProcess。它的定义如下
NtCreateProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ParentProcess,
IN BOOLEAN InheritObjectTable,
IN HANDLE SectionHandle OPTIONAL,
IN HANDLE DebugPort OPTIONAL,
IN HANDLE ExceptionPort OPTIONAL );
注意到ParentProcess,只要把EXPLORER的HANDLE传给它就OK了。
单步陷阱,API断点检测,时间检查
,SMC反单步跟踪,多线程anti-debug,CreateFile
详见这里:
http://www.pediy.com/practise/Paper.htm
文章比较旧了,对一些进行扩充。
API断点检测:不在入口点下断,而在别的什么地方,例如在该API里面的NATIVE API,当然你可以全程检查API的代码是否有断点,要注意的是,只要引发异常,调试器就能取得控制权,所以实际上也不一定用INT3(机械码CC),除0异常,非法访问内存等也是可以的。
更好的做法是全程模拟该API,放在自己的程序代码中。这样就无法在该API下断了,除非你找到它的位置,如果一个程序全部代码都看,也没必要在API下断了。假如你碰到模拟API的情况,如果它需要到某个NATIVE API,并且通过系统调用号来使用,在RING 3下最终是要访问SSDT的,可以在SSDT下访问断点,不过那是RING 0调试器才能做的事。
时间检查
这东西缺乏稳定性,试想你的程序在一台老机器上运行,或者你的用户正在打开很多程序,或者其他程序出现死循环等错误,然后你不得不放宽它的限制。太宽了就失去反调试的意思。当然你可以在时间检查超出预期范围时提示用户资源不足。
对付方法见:
http://bbs.pediy.com/showthread.php?s=&threadid=16913
CreateFile有两个,打开自己,由于很多DUMP工具都需要从原文件中读取PE头,所以打不开需要DUMP的程序文件就报错。另外就是确定某一个位置是否存在某个文件或者打开某个设备对象,一般用于检查驱动。
多线程anti-debug
这个可以用ICESWORD关线程,一次搞定,然后就可以ATTACH了。反正被加壳的程序不会校验其他线程是否存在。
ZwSetInformationThread
最初由 Ptero 发布
invoke ZwSetInformationThread,-2,17,0,0
这个native API将设置当前线程的调试端口为0,使调试器无法调试。
不知道除了hook这个API以外还有没有别的解决方法。
改PE信息,VM
http://www.pediy.com/bbshtml/bbs7/pediy7-707.htm
对于PE信息:
程序可能会使用PE信息解码。较方便的对付方法是,将EP修改为CC,EB FE(死循环)。然后ATTACH
特征码
一些壳侦察工具,都是使用特征码搜索,判断是什么壳的,人为加入这些特征码,便能让侦察工具误以为是别的壳了。同样地也可以用这个方法侦察一些逆向工具。从psapi.dll导出EnumProcesses,OpenProcess,ReadProcessMemory,然后搜索特征码。
ANTI DUMP
使用VirtualProtect改页面保护,破坏PE头和修改PEB中的imagesize,或者使用完代码就立刻清除。
对付前者,可从硬盘读取PE信息,对于后者,只有在清除前DUMP下来了。
检查一些调试器的驱动
通过注册表如“Software\Systems Internals\Filemon”或者使用EnumDeviceDrivers
OLLYDBG的字符处理BUG
无法处理%s%s和某些浮点数
前者除了常见的outputdebugstring之外,还可以把文件名改成包含%s%s的程序,同样可以让OLLYDBG崩溃。outputdebugstring的问题被PATCH了,但是随便找个程序,改成加上%s%s,仍然可以让主页上的OLLYICE崩溃。
后者在OLLYICE中已被修复
详见
http://bbs.pediy.com/showthread.php?s=&threadid=33621
另类的查HOOK方法
在PEB+2H写入某个BYTE,然后调用IsDebuggerPresent,不等于你写入的那个BYTE。代表IsDebuggerPresent被HOOK了。
LDR_MODULE
PEB中的LDR_MODULE成员,指向一块内存。在NT系统中,程序被R3调试器调试,便会包含大量0xFEEEFEEE。我看到的例子是超过30h个0xFEEEFEEE就提示程序被调试,详见
http://bbs.pediy.com/showthread.php?s=&threadid=36374
当然你也可以自己分析CREATEPROCESS的过程。
NtGlobalFlag
PEB中的NtGlobalFlag成员,它控制一些开关,R3调试器运行时,它便等于70h。
int3,int1
某些调试器(OLLYICE没问题)对这些处理得不够好,详见:
http://bbs.pediy.com/showthread.php?s=&threadid=36369
PAGE_GUARD
OD使用这个作为内存断点,因此可以申请一块内存,在里面写入代码,设置该内存区域不可运行,然后运行那段代码。如果代码成功运行了,代表OD存在,如果进入我们的SEH例程,则OD不存在,该方法的缺陷是使用时,OD会莫名奇妙的中断下来。貌似使用VirtualProtect需要知道该内存页面原来的保护方式,或者我们可以通过该API设置自身页面属性来达到侦察内存断点的目的。
prefixes
OD对指令前缀支持得不太好,如果F9运行下面代码(EAX=0,会产生内存违规访问异常),能够正常进入SEH,但是单步的时候,便会跳过该异常,使用其他异常也是如此(正常指令不会被跳过)。
0040101A |. F3:64: prefix rep:
0040101C FF00 inc dword ptr [eax]
hideod中的HeapFlags和ForceFlags
PEB中的/*018*/ HANDLE DefaultHeap
[DefaultHeap+10h]不为0则被调试
[DefaultHeap+0ch]不为2则被调式
注册表
检查SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug下的Debugger的内容
检查HKEY_CLASSES_ROOT下是否存在exefile\shell\Open with Olly&Dbg\command或者dllfile\shell\Open with Olly&Dbg\command
SetDebugPrivilege
这里主要是调试程序时,调试器提升了被调试程序的权限,因此可以通过打开系统进程(被调试前没有这个权限)来确定是否被调试。HIDEOD中的解决方法是先LookupPrivilegeValueA检查进程权限,权限不正常则OpenProcessToken打开进程令牌,然后用AdjustTokenPrivileges降低进程权限