反调试技巧总结-原理和实现

总结:

1.  FindWindow。比如 FindWindowA("OLLYDBG", NULL);

2.  EnumWindow函数调用后,系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用 GetWindowText得到窗口标题,进行检测。

3.  GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,再用GetWindowTextA检测。

4.  枚举进程列表,看是否有调试器进程(OLLYDBG.EXE,windbg.exe等)

5.  父进程是否是Explorer。通常进程的父进程是explorer.exe(双击执行的情况下),否则可能程序被调试。

6.  RDTSC/ GetTickCount时间敏感程序段当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。

7.  StartupInfo结构检测Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断OD是否在调试程序。

8.  BeingDebuggedkernel32!IsDebuggerPresent() API检测进程环境块(PEB)中的BeingDebugged标志检查这个标志以确定进程是否正在被用户模式的调试器调试。

9.  PEB.NtGlobalFlag,Heap.HeapFlags, Heap.ForceFlags。通常程序没有被调试时,PEB另一个成员NtGlobalFlag(偏移0x68)值为0,如果进程被调试通常值为0x70(代表下述标志被设置)由于NtGlobalFlag标志的设置,堆也会打开几个标志,这个变化可以在ntdll!RtlCreateHeap()里观测到。正常情况下系统为进程创建第一个堆时会将Flags和ForceFlags分别设为2(HEAP_GROWABLE)和0 。当进程被调试时,这两个标志通常被设为50000062(取决于NtGlobalFlag)和0x40000060(等于Flags AND 0x6001007D)。

10.EPROCESS的DebugPort成员: CheckRemoteDebuggerPresent() /NtQueryInformationProcess()。Kernel32!CheckRemoteDebuggerPresent() 是用于确定是否有调试器被附加到进程,内部调用了ntdll!NtQueryInformationProcess()检索内核结构EPROCESS的DebugPort成员。

11.SetUnhandledExceptionFilter/Debugger Interrupts调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用,这样我们可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。

12.Trap Flag单步标志异常TF=1的时候,会触发单步异常,在异常中设定检测,正常程序可进入,未修改OD调试程序不能进入此异常,从而检测调试。

13.SeDebugPrivilege进程权限.默认情况下进程没有SeDebugPrivilege权限,调试时,会从调试器继承这个权限。

14.DebugObject:NtQueryObject()内核对象检测。

15.OllyDbg:Guard Pages.OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的, 页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个 STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛 出异常,访问将会被当作内存断点来处理,从而检测到。

16.Software Breakpoint.通过修改目标地址代码为0xCC(INT3/BreakpointInterrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。

17.HardwareBreakpoints.通过传递给异常处理例程的ContextRecord参数来访问, 含有调试寄存器值的CONTEXT结构,判断Dr0-Dr3是否设置了值,来判断调试。

18.PatchingDetectionCodeChecksumCalculation补丁检测,代码检验和.能识别壳的代码是否被修改,或软件断点。

19.block input封锁键盘、鼠标输入。

20.EnableWindow禁用窗口。

21.ThreadHideFromDebugger. ntdll!NtSetInformationThread()用来设置一个线程的相关信息。把ThreadInformationClass参数设为ThreadHideFromDebugger(11H) 可以禁止线可以禁止线程产生调试事件。

22.DisablingBreakpoints禁用硬件断点。利用CONTEXT结构,该结构利用异常处理获得,异常处理完后会自动写回。

23.OllyDbg:OutputDebugString()Format String Bug。OutputDebugString函数用于向调试器发送一个格式化的串,Ollydbg会在底端显示相应的信息。OllyDbg存在格式化字符串 溢出漏洞,非常严重,轻则崩溃,重则执行任意代码。这个漏洞是由于Ollydbg对传递给kernel32!OutputDebugString()的字符串参数过滤不严导致的,它只对参数进行那个长度检查,只接受255个字节,但没对参数进行检查,所以导致缓冲区溢出溢出。

24.TLS Callbacks使用Thread Local Storage (TLS)回调函数可以实现在实际的入口点之前执行反调试的代码,这也是OD载入程序就退出的原因所在。

25.CreateFile检测。win32程序对vxd程序通信时,是通过调用DeviceIoControl函数进入vxd,此函数的一个参数就是由createfile获得的设备句柄。这样,同样生成或打开文件的调用也能够打开一个到vxd的通道。要用createfile打开一个vxd,而不是一个通常的文件,在文件名的地方必须使用特殊形式。


1.FindWindow

比如 FindWindowA("OLLYDBG", NULL);

szClassName     db     'ollydbg',0

        invoke FindWindow,addr szClassName,NULL ;通过类名进行检测

              .if       eax     ;找到

                    jmp   debugger_found    

              .endif               


2.EnumWindow

系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用 GetWindowText得到窗口标题,进行检测。

.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


3.GetForeGroundWindow返回前台窗口

GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,这样就可以向其发送WM_CLOSE消息将其关闭了。

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


4. 枚举进程列表,看是否有调试器进程

枚举进程列表,看是否有调试器进程(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


5. 父进程是否是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 ==@stProcess.th32ProcessID               ;是当前进程吗?

                      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


6.RDTSC/ GetTickCount时间敏感程序段

当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。

(1)RDTSC

将计算机启动以来的CPU运行周期数放到EDXEAX里面,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                  


7.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


8. 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


9. 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


10.EPROCESS的DebugPort成员

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


11.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中断"和"单步中断"复选框来完成设置。


12.单步标志异常

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


13.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.EXEPID

              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


14.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值。


15.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


16.Software Breakpoint.

软件断点是通过修改目标地址代码为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


17.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


18.补丁检测,代码检验和

补丁检测技术能识别壳的代码是否被修改,也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。

实例:改动被保护代码的话,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


19.封锁键盘、鼠标输入

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键手工解除阻断。


20.禁用窗口

在资源管理器里直接双击运行的话,会使当前的资源管理器窗口被禁用。

在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


21.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            


22.禁用硬件断点

;执行过后,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


23.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


24.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 <TLS_Start, TLS_End, dwTLS_Index, TLS_CallBackStart, 0,?>

 

.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

 

25.CreateFile检测

win32程序对vxd程序通信时,是通过调用DeviceIoControl函数进入vxd,此函数的一个参数就是由createfile获得的设备句柄。这样,同样生成或打开文件的调用也能够打开一个到vxd的通道。要用createfile打开一个vxd,而不是一个通常的文件,在文件名的地方必须使用特殊形式。

function SoftIce9x32: Boolean; //探测Win9x下的SoftIce, 发现为True,否则为False
begin {Detect Softice Win9x 32bit}
	if CreateFile('\\.\SICE', GENERIC_READ or GENERIC_WRITE,
                            LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                            EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function SoftIce9x16: Boolean; //探测Dos下的SoftIce, 发现为True,否则为False
begin {Detect Softice Win9x 16bit}
	if _lopen(PChar('\\.\SICE'), OF_READWRITE) <> HFILE_ERROR then
  	Result := True
  else Result := False;
end;

function SoftIceNT32: Boolean; //探测WinNt下的SoftIce, 发现为True,否则为False
begin {Detect Softice WinNt 32bit}
	if CreateFile('\\.\NTICE', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function SoftIceNt16: Boolean; //探测WinNt下的16位的SoftIce, 发现为True,否则为False
begin {Detect Softice WinNt 16bit}
	if _lopen(PChar('\\.\NTICE'), OF_READWRITE) <> HFILE_ERROR then
  	Result := True
  else Result := False;
end;

function SIWDEBUG: Boolean; //探测Win9x/Nt下的SoftIce, 发现为True,否则为False
begin {Detect Softice SIWDEBUG}
	if CreateFile('\\.\SIWDEBUG', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function SIWVID: Boolean; 探测Win9x/Nt下的SoftIce, 发现为True,否则为False
begin {Detect Softice SIWVID}
	if CreateFile('\\.\SIWVID', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function FileMon: Boolean; //探测File Monitor, 发现为True,否则为False
begin {Detect File Monitor}
	if CreateFile('\\.\FILEMON', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function RegMon: Boolean; //探测Reg Monitor 发现为True,否则为False
begin {Detect File Monitor}
	if CreateFile('\\.\REGMON', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

function Trw: Boolean; //探测Win9x下的TRW, 发现为True,否则为False
var
  Trw, TrwDebug: Boolean;
begin {Detect Trw}
	if CreateFile('\\.\Trw', GENERIC_READ or GENERIC_WRITE,
                           LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                           EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Trw := True
  else Trw := False;
	if CreateFile('\\.\TRWDEBUG', GENERIC_READ or GENERIC_WRITE,
                                LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                                EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    TrwDebug := True
  else TrwDebug := False;
  Result := Trw or TrwDebug;
end;

function IceDump: Boolean; //探测Win9x下的IceDump, 发现为True,否则为False
begin {Detect IceDump}
	if CreateFile('\\.\ICEDUMP', GENERIC_READ or GENERIC_WRITE,
                             LE_SHARE_READ or FILE_SHARE_WRITE, nil,
                             EN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) <> INVALID_HANDLE_VALUE then
    Result := True
  else Result := False;
end;

procedure FindSoftIce; //探测Win9x/WinNt 下的 SoftIce
begin 
  try //容错代码
    asm
      mov     ebp, 04243484Bh //Bounds Checker为SoftICE预留的后门 
      mov     ax, 04h
      int     3
      cmp     al,4
      jnz     GotSoftIce
    end;
  except
  end;
end;

procedure FindSoftIce9x; //探测Win9x 下的 SoftIce
begin 
  try
    asm
      mov ah, 43h
      int 68h
      cmp ax, 0f386h //检测此处是否被调试器设置0f386h
      jz  GotSoftIce
    end;
  except
  end;
end;


©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页