• 标 题:[译]The other ways to detect OllyDbg 检测OllyDbg的另类方法
  • 作 者:RoBa
  • 时 间:2004-08-17,15:16
  • 链 接:http://bbs.pediy.com

原文链接:http://bbs.pediy.com/showthread.php?s=&threadid=4013

这是linhanshi兄放在他的网站上的,我觉得不错就翻译了下,水平很烂,多多包涵!

The other ways to detect OllyDbg 检测OllyDbg的另类方法

Pumqara作/RoBa[TT]译

前言

现在是2004年了,RING-3级调试器被越来越多地使用,因为它们有图形界面并且比RING-0级调试器(比如SOFTICE)更加方便。在这篇文章中我将讲述如何检测最好的RING-3调试器之一——OllyDbg。许多人都听说过IsDebugerPresent和fs:[20]检测手段,但是有没有其他的新方法呢?下面我向你介绍我自己的一些检测手段。我会给出详细的解释,因此你可以用你自己的想象力来完善它们。

方法一:FindWindow

这个方法是基于FindWindow函数。像所有的对话框一样,OllyDbg的主对话框(窗口?)也有它的标题和类名。使用这个API函数我们可以判断OllyDbg的主窗口是否打开。Microsoft这样写道:
------------------------------------------------------------------------------------------------
FindWindow函数能够获得窗口类名或标题为特定字符串的顶层窗口的句柄。该函数不搜索子窗口。

HWND FindWindow(

LPCTSTR lpClassName, //窗口类名的地址
LPCTSTR lpWindowName //窗口标题的地址
);

参数说明

lpClassName
指向一个以NULL结尾的表示窗口类名的字符串的指针,或者是一个标识窗口类名字符串的atom。如果该参数是一个atom,它必须是被函数GlobalAddAtom预先建立的一个全局atom。这个16位的atom必须放在lpClassName的低8位,lpClassName的高8位必须为0。

lpWindowName
指向一个以NULL结尾的表示窗口名称(即标题)的字符串的指针。如果这个参数为NULL,所有的窗口都被认为是符合条件的。

返回值

如果搜索成功,返回找到的符合条件的窗口句柄。
如果搜索失败,返回值为NULL。要得到详细的错误信息可以调用GetLastError。
------------------------------------------------------------------------------------------------
我的程序片断:

代码:
.data strOllyClsName db "OLLYDBG",0 .code invoke FindWindow, ADDR strOllyClsName, NULL cmp eax, 00000000h jne Olly_Detected/

方法二:CreateToolhelp32Snapshot, Process32First/Next

这是一个有趣的方法。它基于四个API函数(CreateToolhelp32Snapshot, Process32First, Process32Next, GetCurrentProcessId)和一个结构( PROCESSENTRY32 )。先看看MSDN怎么说:
------------------------------------------------------------------------------------------------
[CreateToolhelp32Snapshot]

CreateToolhelp32Snapshot函数为指定的进程(可以包括进程使用的堆[HEAP],模块[MODULE]和线程[THREAD])建立一个快照[snapshot]。

HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);

参数说明:

dwFlags

[输入]TH32CS_INHERIT - 声明快照句柄是可继承的。
TH32CS_SNAPALL - 在快照中包含系统中所有的进程和线程,还包括在th32ProcessID中指定的进程的堆和模块TH32CS_SNAPHEAPLIST - 在快照中包含在th32ProcessID中指定的进程的所有的堆。要列举出进程的堆,查看Heap32ListFirst。
TH32CS_SNAPMODULE - 在快照中包含在th32ProcessID中指定的进程的所有的模块。要列举出进程的模块,查看Module32ListFirst。
TH32CS_SNAPPROCESS - 在快照中包含系统中所有的进程。要列举出所有进程,查看Process32First.
TH32CS_SNAPTHREAD - 在快照中包含系统中所有的线程。要列举出所有线程,查看Thread32First.

th32ProcessID
[输入]指定将要抓取的进程ID。如果该参数为0表示抓取当前进程。该参数只有在设置了TH32CS_SNAPHEAPLIST,TH32CS_SNAPMOUDLE或TH32CS_SNAPALL后才有效,在其他情况下该参数被忽略,所有的进程都会被抓取。 

Return Values

如果调用成功,返回快照的句柄。
如果调用失败,返回INVAID_HANDLE_VALUE。

Remarks

The snapshot taken by this function is examined by the other tool help functions to provide their results. Access to the snapshot is read only. The snapshot handle acts like an object handle and is subject to the same rules regarding which processes and threads it is valid in.

要列举出所有进程的堆和模块状态,指定TH32CS_SNAPALL并且把th32ProcessID设为0。然后,对于快照中每个新增的进程,再次调用CreateToolhelp32Snapshot,设置要抓取的新进程ID和TH32CS_SNAPHEAPLIST或TH32CS_SNAPMOULE.

要删除快照,使用CloseHandle函数。
------------------------------------------------------------------------------------------------
[Process32First]

Process32First得到一个系统快照里第一个进程的信息。

BOOL WINAPI Process32First(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe
);

参数说明

hSnapshot 
[输入] 先前调用CreateToolhelp32Snapshot函数时返回的指向系统快照的句柄

lppe 
[输入,输出] 指向一个PROCESSENTRY32结构的指针

返回值

如果进程列表的第一个入口已经被复制到缓冲区中则返回TRUE,否则返回FALSE。如果快照中不包含进程信息,通过GetLastError会返回一个ERROR_NO_MORE_FILES错误。

备注

调用该函数的程序必须把PROCESSENTRY32中的成员dwSize设定为该结构的大小(用字节数表示)。Process32First会把dwSize改为写入该结构的字节数。这个值永远不会比dwSize的初始值更大,但是有可能更小。如果这个值变小了,任何偏移量大于这个值的成员都是不可靠的。

要得到同一快照中其它进程的信息,使用Process32Next函数。
------------------------------------------------------------------------------------------------
[Process32Next]

Process32Next可以得到在一个系统快照中下一个进程的信息。

BOOL WINAPI Process32Next(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe
);

参数说明

hSnapshot 
[输入] 先前调用CreateToolhelp32Snapshot函数时返回的指向系统快照的句柄

lppe 
[输出] 指向一个PROCESSENTRY32结构的指针 

返回值

如果进程列表的下一个入口已经被复制到缓冲区中则返回TRUE,否则返回FALSE。如果快照中不包含进程信息,通过GetLastError会返回一个ERROR_NO_MORE_FILES错误。

备注

要得到快照中第一个进程的信息,使用Process32First函数。
------------------------------------------------------------------------------------------------
[PROCESSENTRY32]

当一个快照建立后,PROCESSENTRY32描述了在系统地址空间中一系列进程中的一条。

typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
TCHAR szExeFile[MAX_PATH]; 

} PROCESSENTRY32, *PPROCESSENTRY32;

成员变量

dwSize 
用字节数表示的结构大小。在调用Process32First前,把这个成员设为sizeof(PROCESSENTRY32)。如果你不对dwSize进行初始化,Process32First会调用失败。

cntUsage 
该进程被引用的次数。只有一个进程的引用次数不为0时这个进程才存在。一旦它的引用次数为0,进程就终止了。

th32ProcessID 
该进程的标识。(ID)

th32DefaultHeapID 
Identifier of the default heap for the process. The contents of this member has meaning only to the tool help functions. It is not a handle, nor is it usable by functions other than the ToolHelp functions. 

th32ModuleID 
Module identifier of the process. The contents of this member has meaning only to the tool help functions. It is not a handle, nor is it usable by functions other than the ToolHelp functions. 


cntThreads 
该进程启动的线程数目。 

th32ParentProcessID 
建立该进程的父进程的标识。

pcPriClassBase 
Base priority of any threads created by this process.
 
dwFlags 
未使用,保留

szExeFile 
指向一个以NULL结尾的字符串的指针,该字符串说明了进程所属的可执行文件。

在Windows Me/98/95中,这个文件名包含路径。
------------------------------------------------------------------------------------------------
[GetCurrentProcessId]

GetCurrentProcessId返回调用该函数的进程的标识。

DWORD GetCurrentProcessId(VOID)

参数说明:

该函数没有参数。 

返回值

返回值为调用该函数的进程的标识。

备注

这个进程标识(ID)在系统中确定唯一的进程,直到进程终止。
------------------------------------------------------------------------------------------------
我们的目标是检测我们的程序的父进程是不是OllyDbg.

计划:
1.)用GetCurrentProcessId得到我们程序的进程ID。
2.)用CreateToolhelp32Snapshot和Process32First/Next依次比较每一个PROCESSENTRY32结构中的th32ProcessID成员是不是我们的进程ID,直到找出为止。
3.)得到PROCESSENTRY32结构中的th32ParentProcessID。(译注:即父进程ID)
4.)用CreateToolhelp32Snapshot开始一轮新的比较,但这次比较的是每个th32ProcessID成员是不是我们在第3步得到的父进程的ID,直到找出为止。
5.)得到PROCESSENTRY32中的szExeFile成员,看是不是"Ollydbg.exe"
6.)如果是的话就知道我们的程序正在OllyDbg控制下运行了。

示例代码:
代码:
.586 .model flat, stdcall option casemap:none include d:\masm32\INCLUDE\Windows.inc include d:\masm32\INCLUDE\user32.inc include d:\masm32\INCLUDE\kernel32.inc includelib d:\masm32\lib\user32.lib includelib d:\masm32\lib\kernel32.lib .data strCaption db "OllyDbg Detector!",0 strFound db "OllyDbg found!",0 strNotFound db "OllyDbg NOT found!",0 strOllyDbg db "OLLYDBG.EXE",0h valCurrentPiD dd 0  valParentPiD dd 0 hSnapShot dd 0 .data? proces PROCESSENTRY32 <> .code start:  ; 建立快照 invoke CreateToolhelp32Snapshot, TH32CS_SNAPPROCESS,NULL mov hSnapShot,eax ; 得到当前进程ID invoke GetCurrentProcessId mov valCurrentPiD,eax lea esi,offset proces assume esi:ptr PROCESSENTRY32 mov [esi].dwSize,sizeof PROCESSENTRY32 ; 开始第一轮搜索 ; 用valCurrentPiD查找当前进程 invoke Process32First,hSnapShot,addr proces lea esi,offset proces assume esi:ptr PROCESSENTRY32 mov ebx,valCurrentPiD cmp ebx,[esi].th32ProcessID jne nope1 nope1: invoke Process32Next,hSnapShot,addr proces lea esi,offset proces assume esi:ptr PROCESSENTRY32 mov ebx,valCurrentPiD cmp ebx,[esi].th32ProcessID jne nope1 push [esi].th32ParentProcessID pop valParentPiD invoke CloseHandle,hSnapShot ; 再次建立快照 invoke CreateToolhelp32Snapshot, TH32CS_SNAPPROCESS,NULL mov hSnapShot,eax mov [esi].dwSize,sizeof PROCESSENTRY32 ; 开始第二轮搜索 ; 使用valParentPiD查找当前进程的父进程 invoke Process32First,hSnapShot,addr proces lea esi,offset proces assume esi:ptr PROCESSENTRY32 mov ebx,valParentPiD cmp ebx,[esi].th32ProcessID jne nope2 nope2: invoke Process32Next,hSnapShot,addr proces lea esi,offset proces assume esi:ptr PROCESSENTRY32 mov ebx,valParentPiD cmp ebx,[esi].th32ProcessID jne nope2 ; 从完整路径中提取出文件名 lea eax, [esi].szExeFile push eax invoke lstrlen,eax sub eax,11 pop ebx add ebx,eax ; 把文件名变成大写,与"OLLYDBG.EXE"比较  invoke CharUpper,ebx invoke lstrcmp,ebx,addr strOllyDbg .IF eax==0 invoke MessageBox,0,addr strFound,addr strCaption,0 .ELSE invoke MessageBox,0,addr strNotFound,addr strCaption,0 .ENDIF invoke CloseHandle,hSnapShot invoke ExitProcess,0 end start

方法三:SetUnhandledExceptionFilter

(译注:关于SEH在Hume的<<SEH in Asm>>一文中有经典论述,在此省略,仅列出代码,具体细节请参考英文原文和<<SEH in Asm>>一文)
代码:
.586 .model flat, stdcall option casemap:none include d:\masm32\INCLUDE\Windows.inc include d:\masm32\INCLUDE\user32.inc include d:\masm32\INCLUDE\kernel32.inc includelib d:\masm32\lib\user32.lib includelib d:\masm32\lib\kernel32.lib .data strCaption db "OllyDbg Detector",0 strNotFound db "OllyDbg NOT found!",0 .code ExcpHandler proc mov eax, dword ptr [esp+4] ; eax = EXCEPTION_POINTERS  mov eax, [eax+4] ; eax = CONTEXT  assume eax:ptr CONTEXT mov [eax].regEip, offset safe_address ; Change regEip pushad  invoke MessageBox,0,addr strNotFound,addr strCaption,0 popad xor eax, eax ;\ ; ) Set EXCEPTION_CONTINUE_EXECUTION dec eax ;/ retn 4 ; Normalize stack and return ExcpHandler endp start:  invoke SetUnhandledExceptionFilter,offset ExcpHandler mov ebx,dword ptr [0FFFFFFFFh] ;Exception is here! safe_address: invoke ExitProcess,0 end start 

方法四:API重定位

这是我自己发现的一种方法,它基于OllyDbg调用API函数时的处理方法。当被调试的程序调用API函数时,Oleh Yuschuk(OllyDbg的作者)在他的调试器中使用API重定位方法来处理。下面看一个例子:

0401000 >PUSH ASD.00403033 ; /FileName = "kernel32.dll"
00401005 CALL ; \LoadLibraryA
|
|
'--> 0040105C JMP DWORD PTR DS:[402004] ; JMP DWORD PTR DS:[<&KERNEL32.LoadLibraryA>]
|
|
'--> 87FF4120 PUSH BFF776D0 ; PUSH KERNEL32.LoadLibraryA
87FF4125 JMP KERNEL32.BFF957CA 

看上去他手动装载了输入表并且手动填充了IAT,所有的API地址都被重定到位一个分配好的缓冲区里。在这个缓冲区中他用一种奇怪的方式调用函数。但是对我们来说这里有一个更重要的问题:他同样模仿了GetProcAddress函数,因此这个函数返回的是重定位以后的地址。举个例子来说,IsDebuggerPresent在内存中的实际地址为BFF946F6,但是这个函数返回的是重定位以后的地址87FF4110。我们怎样用这个来实现OllyDbg检测呢?嗯,有很多方法,下面是最简短的一种:

1.)加载 kernel32.dll 库,这时将会返回它的基地址(它加载到内存的位置)
2.)调用 GetProcAddress 函数得到 ExitProcess 的地址
3.)将上面的返回值(用GetProcAddress得到的ExitProcess的地址)和kernel32.dll的基地址比较
4.)如果这个返回值(第2步所得)大于基地址(第1步所得),我们可以肯定这个API函数是被直接调用的,否则这个API就是被间接调用。

示例程序:
代码:
.586 .model flat, stdcall option casemap:none include d:\masm32\INCLUDE\Windows.inc include d:\masm32\INCLUDE\user32.inc include d:\masm32\INCLUDE\kernel32.inc includelib d:\masm32\lib\user32.lib includelib d:\masm32\lib\kernel32.lib .data strCaption db "OllyDbg Detector",0 strFound db "OllyDbg found!",0 strNotFound db "OllyDbg NOT found!",0 strLibrary db "kernel32.dll",0 strFunction db "ExitProcess",0 .code start:  invoke LoadLibrary,addr strLibrary push eax ; EAX为kernel32.dll的基地址 invoke GetProcAddress,eax,addr strFunction ; eax为ExitProcess的地址或者重定位以后的地址 pop ebx ; EBX为kernel32.dll的基地址  cmp eax,ebx ; 是否ExitProcess的地址 < Kernel32.dll的基地址    jl Olly_Detected invoke MessageBox,0,addr strNotFound,addr strCaption,NULL invoke ExitProcess,0 Olly_Detected: invoke MessageBox,0,addr strFound,addr strCaption,NULL invoke ExitProcess,0 end start 

结束语:

希望您喜欢这篇文章。祝您好运,Pumqara。

译者菜评:

这篇文章提出了四种检测OllyDbg的方法,其中第一种应该是最简单最常见的,第三种方法用SEH在看雪的书中也有涉及,但第二种和第四种方法非常新颖,而且只有当程序正被调试的时候才会检测到,这样也更“人性化”一点,不像FindWindow或者传统的遍历进程一样,发现有调试器不管人家在干什么一律拉出去毙了:D。
本来挺通顺的文章,经我折腾后怎么读都别扭,那些进程线程什么的本来我就搅不清,一翻译更乱了,哪位能帮忙改下。看来以后还要好好学习,提高水平。感谢linhanshi兄提供这篇文章,感谢您耐心把它读完。:)