总结:
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. BeingDebugged。kernel32!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,[email protected],sizeof @szBuffer
invoke StrStrI,[email protected],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,[email protected] ;手工填写结构大小
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
6.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
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.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
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;