啊冲 |
2016-02-03 10:46 |
发现OD的处理 一、如何获取OD窗口的句柄 1.已经获取了窗口类名或标题:FindWindow 2.没有获取窗口类名或标题:GetForeGroundWindow返回前台窗口,这里就是OD的窗口句柄了。注意这种方法更为重要,因为大多数情况下不会知道OD的窗口类名。 invoke IsDebuggerPresent .if eax invoke GetForegroundWindow ;获得的是OD的窗口句柄 invoke SendMessage,eax,WM_CLOSE,NULL,NULL .endif 二、获取OD窗口句柄后的处理 (1)向窗口发送WM_CLOSE消息 invoke FindWindow,addr szClassName,NULL ;通过类名进行检测 .if eax ;找到 mov hWinOD,eax invoke MessageBox,NULL,offset szFound,offset szCaption,MB_OK invoke SendMessage,hWinOD,WM_CLOSE,NULL,NULL .endif (2)终止相关进程,根据窗口句柄获取进程ID,根据进程ID获取进程句柄, _GetODProcID proc LOCAL @hWinOD ;窗口句柄 LOCAL @hProcessOD ;进程句柄 LOCAL @idProcessOD ;进程ID invoke FindWindow,addr szClassName,NULL ;通过类名进行检测 .if eax ;找到 mov @hWinOD,eax ;窗口句柄 invoke GetWindowThreadProcessId,@hWinOD,addr @idProcessOD ;获取进程ID在@idProcessOD里 invoke OpenProcess,PROCESS_TERMINATE,TRUE,@idProcessOD ;获取进程句柄在返回值里 .if eax ;获取句柄成功 mov @hProcessOD,eax invoke TerminateProcess,@hProcessOD,200 ;利用句柄终止进程 invoke CloseHandle,@hProcessOD ;关闭进程句柄 invoke MessageBox,NULL,addr szClose,addr szMerry,MB_OK .else ;获取句柄失败,多因权限问题 invoke MessageBox,NULL,addr szFail,addr szCaption,MB_OK .endif . .endif ret _GetODProcIDendp 1. 窗口类名、窗口名 (1) FindWindow (2) EnumWindow函数调用后,系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用GetWindowText得到窗口标题,用strstr等函数查找有无Ollydbg字符串。StrStr(大小写敏感,对应的StrStrI大小写不敏感)函数返回str2第一次出现在str1中的位置,如果没有找到,返回NULL。 (3) GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,这样就可以向其发送WM_CLOSE消息将其关闭了。 (1)FindWindow szClassName db 'ollydbg',0 invoke FindWindow,addr szClassName,NULL ;通过类名进行检测 .if eax ;找到 jmp debugger_found .endif (2)EnumWindow .386 .modelflat,stdcall optioncasemap:none includewindows.inc includeuser32.inc includelibuser32.lib includekernel32.inc includelibkernel32.lib include Shlwapi.inc includelib Shlwapi.lib ;strstr .const szTitle db 'ollydbg',0 szCaption db '结果',0 szFindOD db '发现目标窗口',0 szText db '枚举已结束,没提示发现目标,则没有找到目标窗口',0 .code ;定义回调函数 _CloseWnd procuses ebx edi esi,_hWnd,_lParam LOCAL @szBuffer[1024]:BYTE ;接收窗口标题 invoke IsWindowVisible,_hWnd .if eax ;是否是可见的窗口 invoke GetWindowText,_hWnd,addr@szBuffer,sizeof @szBuffer invoke StrStrI,addr@szBuffer,offset szTitle ;查找标题中有无字符串,不带I的大小写敏感 .if eax invoke MessageBox,NULL,addr szFindOD,addrszCaption,MB_OK invoke PostMessage,_hWnd,WM_CLOSE,0,0 ;关闭目标 .endif .endif mov eax,TRUE ;返回true 时,EnumWindows继续枚举下一个窗口,false退出枚举. ret _CloseWnd endp start: invoke EnumWindows,addr _CloseWnd,NULL ;EnumWindows调用,系统枚举所有顶级窗口,为每个窗口调用一次回调函数 invoke MessageBox,NULL,addr szText,addrszCaption,MB_OK invoke ExitProcess,NULL end start 1. 检测调试器进程 枚举进程列表,看是否有调试器进程(OLLYDBG.EXE,windbg.exe等)。 利用kernel32!ReadProcessMemory()读取进程内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。 .386 .model flat, stdcall option casemap :none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .const stSysProc db 'OLLYDBG.EXE',0 szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code _GetProcList proc LOCAL @stProcessEntry:PROCESSENTRY32 LOCAL @hSnapShot invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,NULL mov @hSnapShot,eax mov @stProcessEntry.dwSize,sizeof @stProcessEntry invoke Process32First,@hSnapShot,addr @stProcessEntry .while eax invokelstrcmp,addr @stProcessEntry.szExeFile,addr stSysProc .if eax == 0 ;为0,说明进程名相同 push 20 invoke MessageBox,NULL,addrszFound,addr szCaption,MB_OK .endif invokeProcess32Next,@hSnapShot,addr @stProcessEntry .endw pop eax .if eax != 20 invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK .endif ret _GetProcListendp start: invoke _GetProcList invoke ExitProcess,NULL end start 1. 父进程是否是Explorer 原理:通常进程的父进程是explorer.exe(双击执行的情况下),否则可能程序被调试。 下面是实现这种检查的一种方法: 1.通过TEB(TEB.ClientId)或者使用GetCurrentProcessId()来检索当前进程的PID 2.用Process32First/Next()得到所有进程的列表,注意explorer.exe的PID(通过PROCESSENTRY32.szExeFile)和通过PROCESSENTRY32.th32ParentProcessID获得的当前进程的父进程PID。Explorer进程ID也可以通过桌面窗口类和名称获得。 3.如果父进程的PID不是explorer.exe,cmd.exe,Services.exe的PID,则目标进程很可能被调试 对策:OllyAdvanced提供的方法是让Process32Next()总是返回fail,使进程枚举失效,PID检查将会被跳过。这些是通过补丁kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。 (1)通过桌面类和名称获得Explorer的PID 源码见附件 .data? szDesktopClass db 'Progman',0 ;桌面的窗口类 szDesktopWindow db 'ProgramManager',0 ;桌面的窗口名称 dwProcessID dd ? ;保存进程ID dwThreadID dd ? ;保存线程ID .code invoke FindWindow,addr szDesktopClass,addrszDesktopWindow ;获取桌面窗口句柄 invoke GetWindowThreadProcessId,eax,offsetdwProcessID ;获取EXPLORER进程ID mov dwThreadID,eax ;线程ID (2)通过进程列表快照获得Explorer的PID 源码见附件 szExplorer db 'EXPLORER.EXE',0 dwParentID dd ? dwExplorerID dd ? _ProcTest proc local @stProcess:PROCESSENTRY32 ;每一个进程的信息 local @hSnapShot ;快照句柄 pushad invoke GetCurrentProcessId mov ebx,eax ;当前进程ID invoke RtlZeroMemory,addr @stProcess,sizeof @stProcess ; 0初始化进程信息结构 mov @stProcess.dwSize,sizeof@stProcess ;手工填写结构大小 invoke CreateToolhelp32Snapshot,TH32CS_SNAPPROCESS,0;获取进程列表快照 mov @hSnapShot,eax ;快照句柄 invoke Process32First,@hSnapShot,addr @stProcess ;第一个进程 .while eax .if ebx [email protected] ;是当前进程吗? mov eax,@stProcess.th32ParentProcessID ;是,则保存父进程ID mov dwParentID,eax .endif invoke lstrcmp,addr @stProcess.szExeFile,addrszExplorer ;Explorer进程ID .if eax == 0 ;为0,说明进程名相同 mov eax,@stProcess.th32ProcessID mov dwExplorerID,eax .endif invoke Process32Next,@hSnapShot,addr @stProcess ;下一个进程 .endw invoke CloseHandle,@hSnapShot ;关闭快照 mov ebx,dwParentID .if ebx == dwExplorerID ;父进程ID与EXPLORER进程ID比较 invoke MessageBox,NULL,offset szNotFound,offset szCaption,MB_OK .else invoke MessageBox,NULL,offset szFound,offsetszCaption,MB_OK .endif popad ret _ProcTest endp 1. RDTSC/ GetTickCount时间敏感程序段 当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。 (1)RDTSC 将计算机启动以来的CPU运行周期数放到EDX:EAX里面,EDX是高位,EAX是低位。 如果CR4的TSD(timestamp disabled)置位,则rdtsc在ring3下运行会导致异常(特权指令),所以进入ring0,把这个标记置上,然后Hook OD的WaitForDebugEvent,拦截异常事件,当异常代码为特权指令时,把异常处的opcode读出检查,如果是rdtsc,把eip加2,SetThreadContext,edx:eax的返回由你了。 (2)GetTickCount 源码见附件 invoke GetTickCount ;第一次调用 mov ebx,eax ;结果保存在ebx里 mov ecx,10 ;延时开始 mov edx,6 ;单步走,放慢速度 mov ecx,10 ;延时结束 invoke GetTickCount ;第二次调用 sub eax,ebx ;计算差值 .if eax > 1000 ;假定大于1000ms,就说明有调试器 jmp debugger_found .endif 2. StartupInfo结构 原理:Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断OD是否在调试程序. if (Info.dwX<>0) or(Info.dwY<>0) or (Info.dwXCountChars<>0) or(Info.dwYCountChars<>0) or (Info.dwFillAttribute<>0) or (Info.dwXSize<>0) or(Info.dwYSize<>0) then “有调试器” ******************************************************************************* 结构体 typedef struct _STARTUPINFO { DWORD cb; 0000 PSTR lpReserved; 0004 PSTR lpDesktop; 0008 PSTR lpTitle; 000D DWORD dwX; 0010 DWORD dwY; 0014 DWORD dwXSize; 0018 DWORD dwYSize; 001D DWORD dwXCountChars; 0020 DWORDdwYCountChars; 0024 DWORDdwFillAttribute; 0028 DWORD dwFlags; 002D WORD wShowWindow; 0030 WORD cbReserved2; 0034 PBYTE lpReserved2; 0038 HANDLE hStdInput; 003D HANDLE hStdOutput; 0040 HANDLE hStdError; 0044 } STARTUPINFO, *LPSTARTUPINFO; _ProcTest proc LOCAL @stStartupInfo:STARTUPINFO pushad invoke GetStartupInfo,addr @stStartupInfo cmp @stStartupInfo.dwX,0 jnz foundDebugger cmp @stStartupInfo.dwY,0 jnz foundDebugger cmp @stStartupInfo.dwXCountChars,0 jnz foundDebugger cmp @stStartupInfo.dwYCountChars,0 jnz foundDebugger cmp @stStartupInfo.dwFillAttribute,0 jnz foundDebugger cmp @stStartupInfo.dwXSize,0 jnz foundDebugger cmp @stStartupInfo.dwYSize,0 jnz foundDebugger noDebugger: “无调试器” jmp TestOver foundDebugger: “有调试器” TestOver: popad ret _ProcTest endp 1. BeingDebugged kernel32!IsDebuggerPresent() API检测进程环境块(PEB)中的BeingDebugged标志检查这个标志以确定进程是否正在被用户模式的调试器调试。 每个进程都有PEB结构,一般通过TEB间接得到PEB地址 Fs:[0]指向当前线程的TEB结构,偏移为0处是线程信息块结构TIB TIB偏移18H处是self字段,是TIB的反身指针,指向TIB(也是PEB)首地址 TEB偏移30H处是指向PEB结构的指针 PEB偏移2H处,就是BeingDebugged字段,Uchar类型 (1) 调用IsDebuggerPresent函数,间接读BeingDebugged字段 (2) 利用地址直接读BeingDebugged字段 对策: (1) 数据窗口中Ctrl+G fs:[30] 查看PEB数据,将PEB.BeingDebugged标志置0 (2) Ollyscript命令"dbh"可以补丁这个标志 .386 .modelflat,stdcall optioncasemap:none include windows.inc include user32.inc include kernel32.inc includelibuser32.lib includelibkernel32.lib .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: ;调用函数IsDebuggerPresent invoke IsDebuggerPresent .if eax invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK .else invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK .endif ;直接去读字段 assume fs:nothing mov eax,fs:[30h] movzx eax,byte ptr [eax+2] .if eax invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK .else invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK .endif invoke ExitProcess,NULL end start 1. PEB.NtGlobalFlag, Heap.HeapFlags, Heap.ForceFlags (1)通常程序没有被调试时,PEB另一个成员NtGlobalFlag(偏移0x68)值为0,如果进程被调试通常值为0x70(代表下述标志被设置): FLG_HEAP_ENABLE_TAIL_CHECK(0X10) FLG_HEAP_ENABLE_FREE_CHECK(0X20) FLG_HEAP_VALIDATE_PARAMETERS(0X40) 这些标志是在ntdll!LdrpInitializeExecutionOptions()里设置的。请注意PEB.NtGlobalFlag的默认值可以通过gflags.exe工具或者在注册表以下位置创建条目来修改: HKLM\Software\Microsoft\WindowsNt\CurrentVersion\Image File Execution Options assume fs:nothing mov eax,fs:[30h] mov eax,[eax+68h] and eax,70h (2)由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。正常情况下系统为进程创建第一个堆时会将Flags和ForceFlags分别设为2(HEAP_GROWABLE)和0 。当进程被调试时,这两个标志通常被设为50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。 assume fs:nothing mov ebx,fs:[30h] ;ebx指向PEB mov eax,[ebx+18h] ;PEB.ProcessHeap cmp dword ptr [eax+0ch],2 ;PEB.ProcessHeap.Flags jne debugger_found cmp dword ptr [eax+10h],0 ;PEB.ProcessHeap.ForceFlags jne debugger_found 这些标志位都是因为BeingDebugged引起的。系统创建进程的时候设置BeingDebugged=TRUE,后来NtGlobalFlag根据这个标记设置FLG_VALIDATE_PARAMETERS等标记。在为进程创建堆时,又由于NtGlobalFlag的作用,堆的Flags被设置了一些标记,这个Flags随即被填充到ProcessHeap的Flags和ForceFlags中,同时堆中被填充了很多BAADF00D之类的东西(HeapMagic,也可用来检测调试)。 一次性解决这些状态见加密解密P413 .386 .model flat,stdcall optioncasemap:none include windows.inc include user32.inc include kernel32.inc includelibuser32.lib includelibkernel32.lib .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: assume fs:nothing mov ebx,fs:[30h] ;ebx指向PEB ;PEB.NtGlobalFlag mov eax,[ebx+68h] cmp eax,70h je debugger_found ;PEB.ProcessHeap mov eax,[ebx+18h] ;PEB.ProcessHeap.Flags cmp dwordptr [eax+0ch],2 jne debugger_found ;PEB.ProcessHeap.ForceFlags cmp dword ptr [eax+10h],0 jne debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start 1. DebugPort:CheckRemoteDebuggerPresent()/NtQueryInformationProcess() Kernel32!CheckRemoteDebuggerPresent()是用于确定是否有调试器被附加到进程。 BOOL CheckRemoteDebuggerPresent( HANDLE hProcess, PBOOL pbDebuggerPresent ) Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。 这个API内部调用了ntdll!NtQueryInformationProcess(),由它完成检测工作。 .386 .modelflat,stdcall optioncasemap:none include windows.inc include user32.inc include kernel32.inc includelibuser32.lib includelibkernel32.lib .data? dwResult dd ? .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: invoke GetCurrentProcessId invoke OpenProcess,PROCESS_ALL_ACCESS,NULL,eax invoke CheckRemoteDebuggerPresent,eax,addr dwResult cmp dword ptr dwResult,0 jne debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start ntdll!NtQueryInformationProcess()有5个参数。 为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7)。 NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员,这个成员是系统用来与调试器通信的端口句柄。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation将被置为0xFFFFFFFF,否则ProcessInformation将被置为0。 ZwQueryInformationProcess( IN HANDLEProcessHandle, INPROCESSINFOCLASS ProcessInformationClass, OUT PVOIDProcessInformation, IN ULONGProcessInformationLength, OUT PULONGReturnLength OPTIONAL ); .386 .modelflat,stdcall optioncasemap:none include windows.inc include user32.inc includelibuser32.lib include kernel32.inc includelibkernel32.lib include ntdll.inc ;这两个 includelib ntdll.lib .data? dwResult dd ? .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: invoke GetCurrentProcessId invoke OpenProcess,PROCESS_ALL_ACCESS,NULL,eax invoke ZwQueryInformationProcess,eax,7,offsetdwResult,4,NULL cmp dwResult,0 jne debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start 1. SetUnhandledExceptionFilter/ Debugger Interrupts 调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用,Debugger Interrupts就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。注意测试时,在异常处理里取消选中INT3 breaks 和 Singal-stepbreak .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data lpOldHandler dd ? .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code ; ExceptionHandler 异常处理程序 _Handler proc _lpExceptionPoint pushad mov esi,_lpExceptionPoint assume esi:ptr EXCEPTION_POINTERS mov edi,[esi].ContextRecord assume edi:ptr CONTEXT mov [edi].regEax,0FFFFFFFFH ;设置EAX mov [edi].regEip,offset SafePlace assume esi:nothing,edi:nothing popad mov eax,EXCEPTION_CONTINUE_EXECUTION ret _Handler endp start: invoke SetUnhandledExceptionFilter,addr_Handler mov lpOldHandler,eax xor eax,eax ;清零eax int 3 ;产生异常,然后_Handler被调用 SafePlace: test eax,eax je debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found: invoke MessageBox,NULL,addr szFound,addr szCaption,MB_OK exit: invoke SetUnhandledExceptionFilter,lpOldHandler ;取消异常处理函数 invoke ExitProcess,NULL end start 由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。 另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断"和"单步中断"复选框来完成设置。 1. Trap Flag单步标志异常 TF=1的时候,会触发单步异常。该方法属于异常处理,不过比较特殊:未修改的OD无论是F9还是F8都不能处理异常,有插件的OD在F9时能正确处理,F8时不能正确处理。 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data lpOldHandler dd ? szCaption db '检测结果',0 szFound db '程序未收到异常,说明有调试器',0 szNotFound db '程序处理了异常而到达安全位置,没有调试器',0 .code _Handler proc _lpExceptionPoint pushad mov esi,_lpExceptionPoint assume esi:ptr EXCEPTION_POINTERS mov edi,[esi].ContextRecord assume edi:ptr CONTEXT mov [edi].regEip,offset SafePlace assume esi:nothing,edi:nothing popad mov eax,EXCEPTION_CONTINUE_EXECUTION ret _Handler endp start: invoke SetUnhandledExceptionFilter,addr _Handler mov lpOldHandler,eax pushfd ;push eflags or dword ptr [esp],100h ;TF=1 popfd nop jmp die SafePlace: invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit die: invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke SetUnhandledExceptionFilter,lpOldHandler ;取消异常处理函数 invoke ExitProcess,NULL end start 2. SeDebugPrivilege 进程权限 默认情况下进程没有SeDebugPrivilege权限,调试时,会从调试器继承这个权限,可以通过打开CSRSS.EXE进程间接地使用SeDebugPrivilege确定进程是否被调试。注意默认情况下这一权限仅仅授予了Administrators组的成员。可以使用ntdll!CsrGetProcessId() API获取CSRSS.EXE的PID,也可以通过枚举进程来得到CSRSS.EXE的PID。 实例测试中,OD载入后,第一次不能正确检测,第二次可以,不知为何。 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc include kernel32.inc include ntdll.inc includelib user32.lib includelib kernel32.lib includelib ntdll.lib .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: invoke CsrGetProcessId ;ntdll!CsrGetProcessId获取CSRSS.EXE的PID invoke OpenProcess,PROCESS_QUERY_INFORMATION,NULL,eax test eax,eax jnz debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start 3. DebugObject:NtQueryObject() 未完成,期待做过这个测试的朋友指点一下! 除了识别进程是否被调试之外,其他的调试器检测技术牵涉到检查系统当中是否有调试器正在运行。逆向论坛中讨论的一个有趣的方法就是检查DebugObject类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一 个DebugObject类型的对象。 DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3): NTSTATUS NTAPI NtQueryObject( IN HANDLE ObjectHandle, IN OBJECT_INFORMATION_CLASS ObjectInformationClass, OUT PVOID ObjectInformation, IN ULONG Length, OUT PULONG ResultLength ) 这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数: typedef struct _OBJECT_ALL_INFORMATION{ ULONG NumberOfObjectsTypes; OBJECT_TYPE_INFORMATION ObjectTypeInformation[1]; } 检测例程将遍历拥有如下结构的ObjectTypeInformation数组: typedef struct _OBJECT_TYPE_INFORMATION{ [00] UNICODE_STRING TypeName; [08] ULONG TotalNumberofHandles; [0C] ULONG TotalNumberofObjects; ...more fields... } TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects 或 TotalNumberofHandles 是否为非0值。 1. OllyDbg:Guard Pages 这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。 除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。 页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。 示例 下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。 对策 由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会 被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。 如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode, 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION即可。 实例: .386 .modelflat,stdcall optioncasemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data lpOldHandler dd ? dwOldType dd ? .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code _Handler proc _lpExceptionPoint pushad mov esi,_lpExceptionPoint assume esi:ptr EXCEPTION_POINTERS mov edi,[esi].ContextRecord assume edi:ptr CONTEXT mov [edi].regEax,0FFFFFFFFH ;检测标志 mov [edi].regEip,offset SafePlace assume esi:nothing,edi:nothing popad mov eax,EXCEPTION_CONTINUE_EXECUTION ret _Handler endp start: invoke SetUnhandledExceptionFilter,addr_Handler mov lpOldHandler,eax invoke VirtualAlloc,NULL,1000H,MEM_COMMIT,PAGE_READWRITE ;分配内存 push eax mov byte ptr [eax],0C3H ;写一个 RETN 到保留内存,以便下面的调用 invoke VirtualProtect,eax,1000h,PAGE_EXECUTE_READ or PAGE_GUARD,addr dwOldType xor eax,eax ;检测标志 pop ecx call ecx ;执行保留内存代码,触发异常 SafePlace: test eax,eax ;检测标志 je debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found: invoke MessageBox,NULL,addr szFound,addr szCaption,MB_OK exit: invoke VirtualFree,ecx,1000H,MEM_DECOMMIT invoke SetUnhandledExceptionFilter,lpOldHandler ;取消异常处理函数 invoke ExitProcess,NULL end start 1. Software Breakpoint Detection 软件断点是通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。 (1) 实例一 普通断点 注意:在被保护的代码区域下INT3断点进行测试 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: jmp CodeEnd CodeStart: mov eax,ecx ;被保护的程序段 nop push eax push ecx pop ecx pop eax CodeEnd: cld ;检测代码开始 mov edi,offset CodeStart mov ecx,offset CodeEnd -offset CodeStart mov al,0CCH repne scasb jz debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start (1) 实例二 函数断点bp 利用GetProcAddress函数获取API的地址 注意:检测时,BPMessageBoxA .386 .modelflat,stdcall optioncasemap:none includewindows.inc includeuser32.inc includelibuser32.lib includekernel32.inc includelibkernel32.lib .const szKernelDll db 'user32.dll',0 szAPIMessboxdb 'MessageBoxA',0 szCaption db '结果',0 szFound db '发现API断点',0 szNotFound db '未发现断点',0 .code start: invoke GetModuleHandle,addr szKernelDll invoke GetProcAddress,eax,addrszAPIMessbox ;API地址 cld ;检测代码开始 mov edi,eax ;API开始位置 mov ecx,100H ;检测100字节 mov al,0CCH ;CC repne scasb jz debugger_found invoke MessageBox,NULL,addrszNotFound,addr szCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start 1. Hardware Breakpoints 硬件断点是通过设置名为Dr0到Dr7的调试寄存器来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。 由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的CONTEXT结构,该结构可以通过传递给异常处理例程的ContextRecord参数来访问。 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data lpOldHandler dd ? .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code _Handler proc _lpExceptionPoint pushad mov esi,_lpExceptionPoint assume esi:ptr EXCEPTION_POINTERS mov edi,[esi].ContextRecord assume edi:ptr CONTEXT mov [edi].regEip,offset SafePlace cmp [edi].iDr0,0 ;检测硬件断点 jne debugger_found cmp [edi].iDr1,0 jne debugger_found cmp [edi].iDr2,0 jne debugger_found cmp [edi].iDr3,0 jne debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp TestOver debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK TestOver:assume esi:nothing,edi:nothing popad mov eax,EXCEPTION_CONTINUE_EXECUTION ret _Handler endp start: invoke SetUnhandledExceptionFilter,addr _Handler mov lpOldHandler,eax xor eax,eax ;清零eax mov dword ptr [eax],0 ;产生异常,然后_Handler被调用 SafePlace: invoke SetUnhandledExceptionFilter,lpOldHandler ;取消异常处理函数 invoke ExitProcess,NULL end start 1. PatchingDetectionCodeChecksumCalculation补丁检测,代码检验和 补丁检测技术能识别壳的代码是否被修改,也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。 实例:改动被保护代码的话,CHECKSUM需要修改,通过OD等找出该值 注意:在被保护代码段下F2断点或修改字节来测试 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib CHECKSUM EQU 915Ch ;改动被保护代码的话,需要修改 .const szCaption db '检测结果',0 szFound db '检测到调试器',0 szNotFound db '没有调试器',0 .code start: jmp CodeEnd CodeStart: mov eax,ecx ;被保护的程序段 nop push eax push ecx pop ecx pop eax CodeEnd: mov esi,CodeStart mov ecx,CodeEnd - CodeStart xor eax,eax checksum_loop: movzx ebx,byte ptr [esi] add eax,ebx rol eax,1 inc esi loop checksum_loop cmp eax,CHECKSUM jne debugger_found invoke MessageBox,NULL,addr szNotFound,addrszCaption,MB_OK jmp exit debugger_found:invoke MessageBox,NULL,addr szFound,addrszCaption,MB_OK exit: invoke ExitProcess,NULL end start 1. block input封锁键盘、鼠标输入 user32!BlockInput() API 阻断键盘和鼠标的输入。 典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。 示例:源码看附件 BlockInput()参数fBlockIt,true,键盘和鼠标事件被阻断;false,键盘和鼠标事件解除阻断: ; Block input push TRUE call [BlockInput] ;...Unpackingcode... ;Unblock input push FALSE call [BlockInput] 对策 (1)最简单的方法就是补丁 BlockInput()使它直接返回。 (2)同时按CTRL+ALT+DELETE键手工解除阻断。 2. EnableWindow禁用窗口 与BlockInput异曲同工,也是禁用窗口然后再解禁 在资源管理器里直接双击运行的话,会使当前的资源管理器窗口被禁用。 在OD里面的话,就会使OD窗口被禁用。 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .const szCaption db '结果',0 szEnableFalse db '窗口已经禁用',0 szEnableTrue db '窗口已经恢复',0 .code start: invoke GetForegroundWindow mov ebx,eax invoke EnableWindow,eax,FALSE invoke MessageBox,NULL,addr szEnableFalse,addrszCaption,MB_OK nop invoke EnableWindow,ebx,TRUE invoke MessageBox,NULL,addr szEnableTrue,addrszCaption,MB_OK nop invoke ExitProcess,NULL end start 1. ThreadHideFromDebugger ntdll!NtSetInformationThread()用来设置一个线程的相关信息。把ThreadInformationClass参数设为ThreadHideFromDebugger(11H)可以禁止线程产生调试事件。 ntdll!NtSetInformationThread的参数列表如下。ThreadHandle通常设为当前线程的句柄(0xFFFFFFFE): NTSTATUS NTAPI NtSetInformationThread( IN HANDLE ThreadHandle, IN THREAD_INFORMATION_CLASS ThreadInformaitonClass, IN PVOID ThreadInformation, IN ULONG ThreadInformationLength ); ThreadHideFromDebugger内部设置内核结构ETHREAD的HideThreadFromDebugger成员。一旦这个成员设置以后,主要用来向调试器发送事件的内核函数_DbgkpSendApiMessage()将不再被调用。 invoke GetCurrentThread invoke NtSetInformationThread,eax,11H,NULL,NULL 对策: (1)在ntdll!NtSetInformationThread()里下断,断下来后,操纵EIP防止API调用到达内核 (2)Olly Advanced插件也有补这个API的选项。补过之后一旦ThreadInformaitonClass参数为HideThreadFromDebugger,API将不再深入内核仅仅执行一个简单的返回。 .386 .modelflat,stdcall optioncasemap:none include windows.inc include user32.inc include kernel32.inc includelib user32.lib includelib kernel32.lib include ntdll.inc includelib ntdll.lib .const szCaption db '确定以后看看效果',0 szNotice db '汇编代码会消失哦',0 szResult db '看到效果了吗?没有则稍等',0 .code start: invoke MessageBox,NULL,addr szNotice,addrszCaption,MB_OK invoke GetCurrentThread invoke NtSetInformationThread,eax,11H,NULL,NULL invoke MessageBox,NULL,addr szResult,addrszCaption,MB_OK mov eax,ebx ;其它指令 invoke ExitProcess,NULL end start 1. DisablingBreakpoints禁用硬件断点 ;执行过后,OD查看硬件断点还存在,但实际已经不起作用了 ;利用CONTEXT结构,该结构利用异常处理获得,异常处理完后会自动写回 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data lpOldHandler dd ? .code _Handler proc _lpExceptionPoint pushad mov esi,_lpExceptionPoint assume esi:ptr EXCEPTION_POINTERS mov edi,[esi].ContextRecord assume edi:ptr CONTEXT mov [edi].regEip,offset SafePlace xor eax,eax mov [edi].iDr0,eax mov [edi].iDr1,eax mov [edi].iDr2,eax mov [edi].iDr3,eax mov [edi].iDr6,eax mov [edi].iDr7,eax assume esi:nothing,edi:nothing popad mov eax,EXCEPTION_CONTINUE_EXECUTION ret _Handler endp start: invoke SetUnhandledExceptionFilter,addr _Handler mov lpOldHandler,eax xor eax,eax ;清零eax mov dword ptr [eax],0 ;产生异常,然后_Handler被调用 SafePlace: invoke SetUnhandledExceptionFilter,lpOldHandler ;取消异常处理函数 invoke ExitProcess,NULL end start 1. OllyDbg:OutputDebugString() Format String Bug OutputDebugString函数用于向调试器发送一个格式化的串,Ollydbg会在底端显示相应的信息。OllyDbg存在格式化字符串溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于Ollydbg对传递给kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受255个字节,但没对参数进行检查,所以导致缓冲区溢出。 例如:printf函数:%d,当所有参数压栈完毕后调用printf函数的时候,printf并不能检测参数的正确性,只是机械地从栈中取值作为参数,这样堆栈就被破坏了,栈中信息泄漏。。 示例:下面这个简单的示例将导致OllyDbg抛出违规访问异常或不可预期的终止。 szFormatStr db '%s%s',0 push offset szFormatStr call OutputDebugString 对策:补丁 kernel32!OutputDebugStringA()入口使之直接返回 .386 .model flat,stdcall option casemap:none include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib .data szFormatStr db '%s%s',0 szCaption db '呵呵',0 szNotice db '执行已结束,看到效果了吗?',0 .code start: push offset szFormatStr call OutputDebugString invoke MessageBox,NULL,addr szNotice,addrszCaption,MB_OK invoke ExitProcess,NULL end start 2. TLS Callbacks 使用Thread Local Storage (TLS)回调函数可以实现在实际的入口点之前执行反调试的代码,这也是OD载入程序就退出的原因所在。(Anti-OD) 线程本地存储器可以将数据与执行的特定线程联系起来,一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块。动态绑定(运行时)线程特定数据是通过 TLS API(TlsAlloc、TlsGetValue、TlsSetValue 和 TlsFree)的方式支持的。除了现有的 API 实现,Win32 和 Visual C++ 编译器现在还支持静态绑定(加载时间)基于线程的数据。当使用_declspec(thread)声明的TLS变量 时,编译器把它们放入一个叫.tls的区块里。当应用程序加载到内存时,系统寻找可执行文件中的.tls区块,并动态的分配一个足够大的内存块,以便存放TLS变量。系统也将一个指向已分配内存的指针放到TLS数组里,这个数组由FS:[2CH]指向。 数据目录表中第9索引的IMAGE_DIRECTORY_ENTRY_TLS条目的VirtualAddress指向TLS数据,如果非零,这里是一个IMAGE_TLS_DIRECTORY结构,如下: IMAGE_TLS_DIRECTORY32 STRUC StartAddressOfRawData DWORD ? ; 内存起始地址,用于初始化新线程的TLS EndAddressOfRawData DWORD ? ; 内存终止地址 AddressOfIndex DWORD ? ; 运行库使用该索引来定位线程局部数据 AddressOfCallBacks DWORD ? ; PIMAGE_TLS_CALLBACK函数指针数组的地址 SizeOfZeroFill DWORD ? ; 用0填充TLS变量区域的大小 Characteristics DWORD ? ; 保留,目前为0 IMAGE_TLS_DIRECTORY32 ENDS AddressOfCallBacks 是线程建立和退出时的回调函数,包括主线程和其它线程。当一个线程创建或销毁时,在列表中的每一个函数被调用。一般程序没有回调函数,这个列表是空的。TLS数据初始化和TLS回调函数调用都在入口点之前执行,也就是说TLS是程序最开始运行的地方。程序退出时,TLS回调函数再被执行一次。回调函数: TLS_CALLBACK proto Dllhandle : LPVOID, Reason : DWORD,Reserved : LPVOID 参数如下: Dllhandle : 为模块的句柄 Reason可取以下值: DLL_PROCESS_ATTACH 1 : 启动一个新进程被加载 DLL_THREAD_ATTACH 2 : 启动一个新线程被加载 DLL_THREAD_DETACH 3 : 终止一个新线程被加载 DLL_PROCESS_DETACH 0 : 终止一个新进程被加载 Reserverd:用于保留,设置为0 IMAGE_TLS_DIRECTORY结构中的地址是虚拟地址,而不是RVA。这样,如果可执行文件不是从基地址装入,则这些地址会通过基址重定位修正。而且IMAGE_TLS_DIRECTORY本身不在.TLS区块中,而在.rdata里。 TLS回调可以使用诸如pedump之类的PE文件分析工具来识别。如果可执行文件中存在TLS条目,数据条目将会显示出来。 Data directory EXPORT rva:00000000 size:00000000 IMPORT rva:00061000 size:000000E0 ::: TLS rva:000610E0 size:00000018 ::: IAT rva:00000000 size:00000000 DELAY_IMPORT rva:00000000 size:00000000 COM_DESCRPTR rva:00000000 size:00000000 unused rva:00000000 size:00000000 接着显示TLS条目的实际内容。AddressOfCallBacks成员指向一个以null结尾的回调函数数组。 TLS directory: StartAddressOfRawData: 00000000 EndAddressOfRawData: 00000000 AddressOfIndex: 004610F8 AddressOfCallBacks: 004610FC SizeOfZeroFill: 00000000 Characteristics: 00000000 在这个例子中,RVA 0x4610fc指向回调函数指针(0x490f43和0x44654e): 默认情况下OllyDbg载入程序将会暂停在入口点,应该配置一下OllyDbg使其在TLS回调被调用之前中断在实际的loader。 通过“选项->调试选项->事件->第一次中断于->系统断点”来设置中断于ntdll.dll内的实际loader代码。这样设置以后,OllyDbg将会中断在位于执行TLS回调的ntdll!LdrpRunInitializeRoutines()之前的ntdll!_LdrpInitializeProcess(),这时就可以在回调例程中下断并跟踪了。例如,在内存映像的.text代码段上设置内存访问断点,可以断在TLS回调函数。 .386 .model flat,stdcall option casemap:none includewindows.inc includeuser32.inc includekernel32.inc includelibuser32.lib includelibkernel32.lib .data? dwTLS_Indexdd ? OPTION DOTNAME ;; 定义一个TLS节 .tls SEGMENT TLS_StartLABEL DWORD dd 0100h dup ("slt.") TLS_End LABEL DWORD .tls ENDS OPTION NODOTNAME .data TLS_CallBackStart dd TlsCallBack0 TLS_CallBackEnd dd 0 szTitle db "Hello TLS",0 szInTls db "我在TLS里",0 szInNormal db "我在正常代码内",0 szClassName db "ollydbg" ; OD 类名 ;这里需要注意的是,必须要将此结构声明为PUBLIC,用于让连接器连接到指定的位置, ;其次结构名必须为_tls_uesd这是微软的一个规定。编译器引入的位置名称也如此。 PUBLIC_tls_used _tls_usedIMAGE_TLS_DIRECTORY .code ;*************************************************************** ;; TLS的回调函数 TlsCallBack0proc Dllhandle:LPVOID,dwReason:DWORD,lpvReserved:LPVOID mov eax,dwReason ;判断dwReason发生的条件 cmp eax,DLL_PROCESS_ATTACH ; 在进行加载时被调用 jnz ExitTlsCallBack0 invoke FindWindow,addr szClassName,NULL ;通过类名进行检测 .if eax ;找到 invoke SendMessage,eax,WM_CLOSE,NULL,NULL .endif invoke MessageBox,NULL,addr szInTls,addr szTitle,MB_OK mov dword ptr[TLS_Start],0 xor eax,eax inc eax ExitTlsCallBack0: ret TlsCallBack0 ENDP ;**************************************************************** Start: invoke MessageBox,NULL,addr szInNormal,addr szTitle,MB_OK invoke ExitProcess, 1 end Start 反反调试技术 本人脱壳逆向的水平不高,这里仅说一下本人的一点体会: 对于初学者来说主要是利用StrongOD等各种插件,这些插件能够躲过上面所说的很多检测。有了一定基础以后就可以根据各种反调试方法的弱点寻求反反调试的途径了。 |
|