反调试技术 2
发现OD的处理 2
1. 窗口类名、窗口名 3
2. 检测调试器进程 4
3. 父进程是否是Explorer 5
4. RDTSC/ GetTickCount时间敏感程序段 6
5. StartupInfo结构 7
6. BeingDebugged 8
7. PEB.NtGlobalFlag , Heap.HeapFlags, Heap.ForceFlags 9
8. DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess() 12
9. SetUnhandledExceptionFilter/ Debugger Interrupts 14
10. Trap Flag单步标志异常 16
11. SeDebugPrivilege 进程权限 16
12. DebugObject: NtQueryObject()犀利 17
13. OllyDbg:Guard Pages 20
14. Software Breakpoint Detection 22
15. Hardware Breakpoints Detection 24
16. PatchingDetection CodeChecksumCalculation补丁检测,代码检验和 25
17. block input封锁键盘、鼠标输入 26
18. EnableWindow禁用窗口 27
19. ThreadHideFromDebugger 27
20. Disabling Breakpoints禁用硬件断点 29
21. OllyDbg:OutputDebugString() Format String Bug 30
22. TLS Callbacks 30
反反调试技术 35
反调试技术 VC版
写意互联网,关注搜索引擎技术,涉猎搜索引擎优化、软件破解、PHP网站建设、Wordpress应用等
声明:这篇文章是本人学习的总结,理论部分参考了《脱壳的艺术》、《加密与解密》以及本人从网络上收集的资料,在此向原作者致敬。本人的贡献在于根据个人理解对各种反调试技术进行了汇总和高度归纳,并提供了本人创作的各种反调试实例及源代码。本人于09年9月份开始学习软件逆向工程的相关知识,在学习过程中得到大量网友的热心帮助,在此向各位致以诚挚谢意。希望本人的这些工作能够对各位有所帮助,浅陋之处,莫要见笑。各种形式的转载都必须保留作者信息及本声明。
由于本人入门较晚、能力有限,部分方法尚未实现,望高手不吝赐教。实现了的方法大都附有实例程序。
很多方法对于修改版的OD已经失效,请用原版OD进行测试。
发现OD的处理
一、如何获取OD窗口的句柄
1.已经获取了窗口类名或标题:FindWindow函数
2.没有获取窗口类名或标题:GetForeGroundWindow返回前台窗口,这里就是OD的窗口句柄了。注意这种方法更为重要,因为大多数情况下不会知道OD的窗口类名。
invoke IsDebuggerPresent
.if eax
invoke GetForegroundWindow ;获得的是OD的窗口句柄
invoke SendMessage,eax,WM_CLOSE,NULL,NULL
.endif
二、获取OD窗口句柄后的处理
(1)向窗口发送WM_CLOSE消息
void CDetectODDlg::OnWndcls()
{
// TODO: Add your control notification handler code here
HWND hWnd;
if(hWnd=::FindWindow("OllyDbg",NULL))
{
MessageBox("发现OD");
::SendMessage(hWnd,WM_CLOSE,NULL,NULL);
}else{
MessageBox("没发现OD");
}
}
(2)使OD窗口不可用
HWND hd_od=FindWindow("ollydbg",NULL);
SetWindowLong(hd_od,GWL_STYLE,WS_DISABLED);
(3)终止相关进程,根据窗口句柄获取进程ID,根据进程ID获取进程句柄,
HWND hWnd;
HANDLE hProc;
DWORD pId;
if(hWnd=::FindWindow("OllyDbg",NULL)) //获取窗口句柄
{
MessageBox("发现OD");
GetWindowThreadProcessId(hWnd,&pId); //获取进程ID
hProc=OpenProcess(PROCESS_TERMINATE,TRUE,pId); //获取进程句柄
TerminateProcess(hProc,200); //终止进程
CloseHandle(hProc);
}else{
MessageBox("没发现OD");
}
(2)程序自身直接退出
1.窗口类名、窗口名
(1)FindWindow
(2)EnumWindow函数调用后,系统枚举所有顶级窗口,为每个窗口调用一次回调函数。在回调函数中用GetWindowText得到窗口标题,用strstr等函数查找有无Ollydbg字符串。StrStr(大小写敏感,对应的StrStrI大小写不敏感)函数返回str2第一次出现在str1中的位置,如果没有找到,返回NULL。
(3)GetForeGroundWindow返回前台窗口(用户当前工作的窗口)。当程序被调试时,调用这个函数将获得Ollydbg的窗口句柄,这样就可以向其发送WM_CLOSE消息将其关闭了。
(1)FindWindow
void CDetectODDlg::OnWndcls()
{
// TODO: Add your control notification handler code here
HWND hWnd;
if(hWnd=::FindWindow("OllyDbg",NULL))
{
MessageBox("发现OD");
::SendMessage(hWnd,WM_CLOSE,NULL,NULL);
}else{
MessageBox("没发现OD");
}
}
(2)EnumWindow
包含头文件:#include "Shlwapi.h"
BOOL CALLBACK EnumWindowsProc(
HWND hwnd, // handle to parent window
LPARAM lParam // application-defined value
)
{
char ch[100];
CString str="Ollydbg";
if(IsWindowVisible(hwnd))
{
::GetWindowText(hwnd,ch,100);
//AfxMessageBox(ch);
if(::StrStrI(ch,str))
{
AfxMessageBox("发现OD");
return FALSE;
}
}
return TRUE;
}
void CDetectODDlg::OnEnumwindow()
{
// TODO: Add your control notification handler code here
EnumWindows(EnumWindowsProc,NULL);
AfxMessageBox("枚举窗口结束,未提示发现OD,则没有OD");
}
2.检测调试器进程
枚举进程列表,看是否有调试器进程(OLLYDBG.EXE,windbg.exe等)。
利用kernel32!ReadProcessMemory()读取进程内存,然后寻找调试器相关的字符串(如”OLLYDBG”)以防止逆向分析人员修改调试器的可执行文件名。
需要头文件:#include "tlhelp32.h"
void CDetectODDlg::OnEnumProcess()
{
// TODO: Add your control notification handler code here
HANDLE hwnd;
PROCESSENTRY32 tp32; //结构体
CString str="OLLYDBG.EXE";
BOOL bFindOD=FALSE;
hwnd=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
if(INVALID_HANDLE_VALUE!=hwnd)
{
Process32First(hwnd,&tp32);
do{
if(0==lstrcmp(str,tp32.szExeFile))
{
AfxMessageBox("发现OD");
bFindOD=TRUE;
break;
}
}while(Process32Next(hwnd,&tp32));
if(!bFindOD)
AfxMessageBox("没有OD");
}
CloseHandle(hwnd);
}
3.父进程是否是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,则目标进程很可能被调试
对策:Olly Advanced提供的方法是让Process32Next()总是返回fail,使进程枚举失效,PID检查将会被跳过。这些是通过补丁 kernel32!Process32NextW()的入口代码(将EAX值设为0然后直接返回)实现的。
(1)通过桌面类和名称获得Explorer的PID 源码见附件
DWORD ExplorerID;
::GetWindowThreadProcessId(::FindWindow("Progman",NULL),&ExplorerID);
(2)通过进程列表快照获得Explorer的PID 源码见附件
void CDetectODDlg::OnExplorer()
{
// TODO: Add your control notification handler code here
HANDLE hwnd;
PROCESSENTRY32 tp32; //结构体
CString str="Explorer.EXE";
DWORD ExplorerID;
DWORD SelfID;
DWORD SelfParentID;
SelfID=GetCurrentProcessId();
::GetWindowThreadProcessId(::FindWindow("Progman",NULL),&ExplorerID);
hwnd=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
if(INVALID_HANDLE_VALUE!=hwnd)
{
Process32First(hwnd,&tp32);
do{
if(0==lstrcmp(str,tp32.szExeFile))
{
// ExplorerID=tp32.th32ProcessID;
// AfxMessageBox("aaa");
}
if(SelfID==tp32.th32ProcessID)
{
SelfParentID=tp32.th32ParentProcessID;
}
}while(Process32Next(hwnd,&tp32));
str.Format("本进程:%d 父进程:%d Explorer进程: %d ",SelfID,SelfParentID,ExplorerID);
MessageBox(str);
if(ExplorerID==SelfParentID)
{
AfxMessageBox("没有OD");
}
else
{
AfxMessageBox("发现OD");
}
}
CloseHandle(hwnd);
}
4.RDTSC/ GetTickCount时间敏感程序段
当进程被调试时,调试器事件处理代码、步过指令等将占用CPU循环。如果相邻指令之间所花费的时间如果大大超出常规,就意味着进程很可能是在被调试。
(1)RDTSC
将计算机启动以来的CPU运行周期数放到EDX:EAX里面,EDX是高位,EAX是低位。
如果CR4的TSD(time stamp disabled)置位,则rdtsc在ring3下运行会导致异常(特权指令),所以进入ring0,把这个标记置上,然后Hook OD的WaitForDebugEvent,拦截异常事件,当异常代码为特权指令时,把异常处的opcode读出检查,如果是rdtsc,把eip加2,SetThreadContext,edx:eax的返回由你了。
(2)GetTickCount 源码见附件
void CDetectODDlg::OnGetTickCount()
{
// TODO: Add your control notification handler code here
DWORD dTime1;
DWORD dTime2;
dTime1=GetTickCount();
GetCurrentProcessId();
GetCurrentProcessId();
GetCurrentProcessId();
GetCurrentProcessId();
dTime2=GetTickCount();
if(dTime2-dTime1>100)
{
AfxMessageBox("发现OD");
}
else{
AfxMessageBox("没有OD");
}
}
5.StartupInfo结构
原理:Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断OD是否在调试程序.
*******************************************************************************
结构体
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
DWORD dwYCountChars; 0024
DWORD dwFillAttribute; 0028
DWORD dwFlags; 002D
WORD wShowWindow; 0030
WORD cbReserved2; 0034
PBYTE lpReserved2; 0038
HANDLE hStdInput; 003D
HANDLE hStdOutput; 0040
HANDLE hStdError; 0044
} STARTUPINFO, *LPSTARTUPINFO;
void CDetectODDlg::OnGetStartupInfo()
{
// TODO: Add your control notification handler code here
STARTUPINFO info;
GetStartupInfo(&info);
if(info.dwX!=0 || info.dwY!=0 || info.dwXCountChars!=0 || info.dwYCountChars!=0
|| info.dwFillAttribute!=0 || info.dwXSize!=0 || info.dwYSize!=0)
{
AfxMessageBox("发现OD");
}
else{
AfxMessageBox("没有OD");
}
}
6.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"可以补丁这个标志
void CDetectODDlg::OnIsdebuggerpresent()
{
// TODO: Add your control notification handler code here
if(IsDebuggerPresent())
{
MessageBox("发现OD");
}
else
{
MessageBox("没有OD");
}
}
7.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\Windows Nt\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
//**********************************************
typedef ULONG NTSTATUS;
typedef ULONG PPEB;
typedef ULONG KAFFINITY;
typedef ULONG KPRIORITY;
typedef struct _PROCESS_BASIC_INFORMATION { // Information Class 0
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
KAFFINITY AffinityMask;
KPRIORITY BasePriority;
ULONG UniqueProcessId;
ULONG InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation, // 0 Y N
ProcessQuotaLimits, // 1 Y Y
ProcessIoCounters, // 2 Y N
ProcessVmCounters, // 3 Y N
ProcessTimes, // 4 Y N
ProcessBasePriority, // 5 N Y
ProcessRaisePriority, // 6 N Y
ProcessDebugPort, // 7 Y Y
ProcessExceptionPort, // 8 N Y
ProcessAccessToken, // 9 N Y
ProcessLdtInformation, // 10 Y Y
ProcessLdtSize, // 11 N Y
ProcessDefaultHardErrorMode, // 12 Y Y
ProcessIoPortHandlers, // 13 N Y
ProcessPooledUsageAndLimits, // 14 Y N
ProcessWorkingSetWatch, // 15 Y Y
ProcessUserModeIOPL, // 16 N Y
ProcessEnableAlignmentFaultFixup, // 17 N Y
ProcessPriorityClass, // 18 N Y
ProcessWx86Information, // 19 Y N
ProcessHandleCount, // 20 Y N
ProcessAffinityMask, // 21 N Y
ProcessPriorityBoost, // 22 Y Y
ProcessDeviceMap,// 23 Y Y
ProcessSessionInformation, // 24 Y Y
ProcessForegroundInformation, // 25 N Y
ProcessWow64Information // 26 Y N
} PROCESSINFOCLASS;
typedef NTSTATUS (_stdcall *ZwQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
); //定义函数指针
void CDetectODDlg::OnPebflags()
{
// TODO: Add your control notification handler code here
//定义函数指针变量
ZwQueryInformationProcess MyZwQueryInformationProcess;
HANDLE hProcess = NULL;
PROCESS_BASIC_INFORMATION pbi = {0};
ULONG peb = 0;
ULONG cnt = 0;
ULONG PebBase = 0;
ULONG AddrBase;
BOOL bFoundOD=FALSE;
WORD flag;
DWORD dwFlag;
DWORD bytesrw;
DWORD ProcessId=GetCurrentProcessId();
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, ProcessId);
if (hProcess != NULL) {
//函数指针变量赋值
MyZwQueryInformationProcess=(ZwQueryInformationProcess)GetProcAddress(LoadLibrary("ntdll.dll"),"ZwQueryInformationProcess");
//函数指针变量调用
if (MyZwQueryInformationProcess(
hProcess,
ProcessBasicInformation,
&pbi,
sizeof(PROCESS_BASIC_INFORMATION),
&cnt) == 0)
{
PebBase = (ULONG)pbi.PebBaseAddress; //获取PEB地址
AddrBase=PebBase;
if (ReadProcessMemory(hProcess,(LPCVOID)(PebBase+0x68),&flag,2,&bytesrw) && bytesrw==2) //读内存地址
{ //PEB.NtGlobalFlag
if(0x70==flag){
bFoundOD=TRUE;
}
}
if (ReadProcessMemory(hProcess,(LPCVOID)(PebBase+0x18),&dwFlag,4,&bytesrw) && bytesrw==4)
{
AddrBase=dwFlag;
}
if (ReadProcessMemory(hProcess,(LPCVOID)(AddrBase+0x0c),&flag,2,&bytesrw) && bytesrw==2)
{//PEB.ProcessHeap.Flags
if(2!=flag){
bFoundOD=TRUE;
}
}
if (ReadProcessMemory(hProcess,(LPCVOID)(AddrBase+0x10),&flag,2,&bytesrw) && bytesrw==2)
{//PEB.ProcessHeap.ForceFlags
if(0!=flag){
bFoundOD=TRUE;
}
}
if(bFoundOD==FALSE)
{
AfxMessageBox("没有OD");
}
else
{
AfxMessageBox("发现OD");
}
}
CloseHandle(hProcess);
}
}
8.DebugPort: CheckRemoteDebuggerPresent()/NtQueryInformationProcess()
Kernel32!CheckRemoteDebuggerPresent()是用于确定是否有调试器被附加到进程。
BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent
)
Kernel32!CheckRemoteDebuggerPresent()接受2个参数,第1个参数是进程句柄,第2个参数是一个指向boolean变量的指针,如果进程被调试,该变量将包含TRUE返回值。
这个API内部调用了ntdll!NtQueryInformationProcess(),由它完成检测工作。
typedef BOOL (WINAPI *CHECK_REMOTE_DEBUGGER_PRESENT)(HANDLE, PBOOL); //定义函数指针
void CDetectODDlg::OnCheckremotedebuggerpresent()
{
// TODO: Add your control notification handler code here
HANDLE hProcess;
HINSTANCE hModule;
BOOL bDebuggerPresent = FALSE;
CHECK_REMOTE_DEBUGGER_PRESENT CheckRemoteDebuggerPresent; //建立函数指针变量
hModule = GetModuleHandleA("Kernel32"); //地址要从模块中动态获得
CheckRemoteDebuggerPresent =
(CHECK_REMOTE_DEBUGGER_PRESENT)GetProcAddress(hModule, "CheckRemoteDebuggerPresent"); //获取地址
hProcess = GetCurrentProcess();
CheckRemoteDebuggerPresent(hProcess,&bDebuggerPresent); //调用
if(bDebuggerPresent==TRUE)
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
}
ntdll!NtQueryInformationProcess()有5个参数。
为了检测调试器的存在,需要将ProcessInformationclass参数设为ProcessDebugPort(7)。
NtQueryInformationProcess()检索内核结构EPROCESS5的DebugPort成员,这个成员是系统用来与调试器通信的端口句柄。非0的DebugPort成员意味着进程正在被用户模式的调试器调试。如果是这样的话,ProcessInformation 将被置为0xFFFFFFFF ,否则ProcessInformation 将被置为0。
ZwQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
//********************************************************
typedef NTSTATUS (_stdcall *ZW_QUERY_INFORMATION_PROCESS)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass, //该参数也需要上面声明的数据结构
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
); //定义函数指针
void CDetectODDlg::OnZwqueryinfomationprocess()
{
// TODO: Add your control notification handler code here
HANDLE hProcess;
HINSTANCE hModule;
DWORD dwResult;
ZW_QUERY_INFORMATION_PROCESS MyFunc;
hModule = GetModuleHandle("ntdll.dll");
MyFunc=(ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule,"ZwQueryInformationProcess");
hProcess = GetCurrentProcess();
MyFunc(
hProcess,
ProcessDebugPort,
&dwResult,
4,
NULL);
if(dwResult!=0)
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
}
9.SetUnhandledExceptionFilter/ Debugger Interrupts
调试器中步过INT3和INT1指令的时候,由于调试器通常会处理这些调试中断,所以设置的异常处理例程默认情况下不会被调用,Debugger Interrupts就利用了这个事实。这样我们可以在异常处理例程中设置标志,通过INT指令后如果这些标志没有被设置则意味着进程正在被调试。另外,kernel32!DebugBreak()内部是调用了INT3来实现的,有些壳也会使用这个API。注意测试时,在异常处理里取消选中INT3 breaks 和 Singal-step break
安全地址的获取是关键
//********************************************************
static DWORD lpOldHandler;
typedef LPTOP_LEVEL_EXCEPTION_FILTER (_stdcall *pSetUnhandledExceptionFilter)(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
pSetUnhandledExceptionFilter lpSetUnhandledExceptionFilter;
LONG WINAPI TopUnhandledExceptionFilter(
struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
_asm pushad
AfxMessageBox("回调函数");
lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER )lpOldHandler);
ExceptionInfo->ContextRecord->Eip=NewEip;//转移到安全位置
_asm popad
return EXCEPTION_CONTINUE_EXECUTION;
}
void CDetectODDlg::OnSetUnhandledExceptionFilter()
{
bool isDebugged=0;
// TODO: Add your control notification handler code here
lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary(("kernel32.dll")),
"SetUnhandledExceptionFilter");
lpOldHandler=(DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilter);
_asm{ //获取这个安全地址
call me //方式一,需要NewEip加上一个偏移值
me:
pop NewEip //方式一结束
mov NewEip,offset safe //方式二,更简单
int 3 //触发异常
}
AfxMessageBox("检测到OD");
isDebugged=1;
_asm{
safe:
}
if(1==isDebugged){
}else{
AfxMessageBox("没有OD");
}
}
//********************************************************
由于调试中断而导致执行停止时,在OllyDbg中识别出异常处理例程(通过视图->SEH链)并下断点,然后Shift+F9将调试中断/异常传递给异常处理例程,最终异常处理例程中的断点会断下来,这时就可以跟踪了。
另一个方法是允许调试中断自动地传递给异常处理例程。在OllyDbg中可以通过 选项-> 调试选项 -> 异常 -> 忽略下列异常 选项卡中钩选"INT3中断"和"单步中断"复选框来完成设置。
10.Trap Flag单步标志异常
TF=1的时候,会触发单步异常。该方法属于异常处理,不过比较特殊:未修改的OD无论是F9还是F8都不能处理异常,有插件的OD在F9时能正确处理,F8时不能正确处理。
void CDetectODDlg::OnTrapFlag()
{
try{
_asm{
pushfd //触发单步异常
or dword ptr [esp],100h ;TF=1
popfd
}
AfxMessageBox("检测到OD");
}catch(...){
AfxMessageBox("没有OD");
}
}
11.SeDebugPrivilege 进程权限
默认情况下进程没有SeDebugPrivilege权限,调试时,会从调试器继承这个权限,可以通过打开CSRSS.EXE进程间接地使用SeDebugPrivilege确定进程是否被调试。注意默认情况下这一权限仅仅授予了Administrators组的成员。可以使用ntdll!CsrGetProcessId() API获取CSRSS.EXE的PID,也可以通过枚举进程来得到CSRSS.EXE的PID。
实例测试中,OD载入后,第一次不能正确检测,第二次可以,不知为何。
void CDetectODDlg::OnSeDebugPrivilege()
{
// TODO: Add your control notification handler code here
HANDLE hProcessSnap;
HANDLE hProcess;
PROCESSENTRY32 tp32; //结构体
CString str="csrss.exe";
hProcessSnap=::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
if(INVALID_HANDLE_VALUE!=hProcessSnap)
{
Process32First(hProcessSnap,&tp32);
do{
if(0==lstrcmpi(str,tp32.szExeFile))
{
hProcess=OpenProcess(PROCESS_QUERY_INFORMATION,NULL,tp32.th32ProcessID);
if(NULL!=hProcess)
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
CloseHandle(hProcess);
}
}while(Process32Next(hProcessSnap,&tp32));
}
CloseHandle(hProcessSnap);
}
12.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值。
#ifndef STATUS_INFO_LENGTH_MISMATCH
#define STATUS_INFO_LENGTH_MISMATCH ((UINT32)0xC0000004L)
#endif
typedef enum _POOL_TYPE {
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;
typedef enum _OBJECT_INFORMATION_CLASS
{
ObjectBasicInformation, // Result is OBJECT_BASIC_INFORMATION structure
ObjectNameInformation, // Result is OBJECT_NAME_INFORMATION structure
ObjectTypeInformation, // Result is OBJECT_TYPE_INFORMATION structure
ObjectAllTypesInformation, // Result is OBJECT_ALL_INFORMATION structure
ObjectDataInformation // Result is OBJECT_DATA_INFORMATION structure
} OBJECT_INFORMATION_CLASS, *POBJECT_INFORMATION_CLASS;
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfHandles;
ULONG TotalNumberOfObjects;
WCHAR Unused1[8];
ULONG HighWaterNumberOfHandles;
ULONG HighWaterNumberOfObjects;
WCHAR Unused2[8];
ACCESS_MASK InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ACCESS_MASK ValidAttributes;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
USHORT MaintainTypeList;
POOL_TYPE PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_INFORMATION {
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
typedef struct _OBJECT_ALL_TYPES_INFORMATION {
ULONG NumberOfTypes;
OBJECT_TYPE_INFORMATION TypeInformation[1];
} OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION;
typedef UINT32 (__stdcall *ZwQueryObject_t) (
IN HANDLE ObjectHandle,
IN OBJECT_INFORMATION_CLASS ObjectInformationClass,
OUT PVOID ObjectInformation,
IN ULONG Length,
OUT PULONG ResultLength );
void CDetectODDlg::OnNTQueryObject()
{
// TODO: Add your control notification handler code here
// 调试器必须正在调试才能检测到,仅打开OD是检测不到的
HMODULE hNtDLL;
DWORD dwSize;
UINT i;
UCHAR KeyType=0;
OBJECT_ALL_TYPES_INFORMATION *Types;
OBJECT_TYPE_INFORMATION *t;
ZwQueryObject_t ZwQueryObject;
hNtDLL = GetModuleHandle("ntdll.dll");
if(hNtDLL){
ZwQueryObject = (ZwQueryObject_t)GetProcAddress(hNtDLL, "ZwQueryObject");
UINT32 iResult = ZwQueryObject(NULL, ObjectAllTypesInformation, NULL, NULL, &dwSize);
if(iResult==STATUS_INFO_LENGTH_MISMATCH)
{
Types = (OBJECT_ALL_TYPES_INFORMATION*)VirtualAlloc(NULL,dwSize,MEM_COMMIT,PAGE_READWRITE);
if (Types == NULL) return;
if (iResult=ZwQueryObject(NULL,ObjectAllTypesInformation, Types, dwSize, &dwSize)) return;
for (t=Types->TypeInformation,i=0;i<Types->NumberOfTypes;i++)
{
if ( !_wcsicmp(t->TypeName.Buffer,L"DebugObject")) //比较两个是否相等,这个L很特殊,本地的意思
{
if(t->TotalNumberOfHandles > 0 || t->TotalNumberOfObjects > 0)
{
AfxMessageBox("发现OD");
VirtualFree (Types,0,MEM_RELEASE);
return;
}
break; // Found Anyways
}
t=(OBJECT_TYPE_INFORMATION *)((char *)t->TypeName.Buffer+((t->TypeName.MaximumLength+3)&~3));
}
}
AfxMessageBox("没有OD!");
VirtualFree (Types,0,MEM_RELEASE);
}
}
13.OllyDbg:Guard Pages
这个检查是针对OllyDbg的,因为它和OllyDbg的内存访问/写入断点特性相关。
除了硬件断点和软件断点外,OllyDbg允许设置一个内存访问/写入断点,这种类型的断点是通过页面保护来实现的。简单地说,页面保护提供了当应用程序的某块内存被访问时获得通知这样一个途径。
页面保护是通过PAGE_GUARD页面保护修改符来设置的,如果访问的内存地址是受保护页面的一部分,将会产生一个STATUS_GUARD_PAGE_VIOLATION(0x80000001)异常。如果进程被OllyDbg调试并且受保护的页面被访问,将不会抛出异常,访问将会被当作内存断点来处理,而壳正好利用了这一点。
示例
下面的示例代码中,将会分配一段内存,并将待执行的代码保存在分配的内存中,然后启用页面的PAGE_GUARD属性。接着初始化标设符EAX为0,然后通过执行内存中的代码来引发STATUS_GUARD_PAGE_VIOLATION异常。如果代码在OllyDbg中被调试,因为异常处理例程不会被调用所以标设符将不会改变。
对策
由于页面保护引发一个异常,逆向分析人员可以故意引发一个异常,这样异常处理例程将会被调用。在示例中,逆向分析人员可以用INT3指令替换掉RETN指令,一旦INT3指令被执行,Shift+F9强制调试器执行异常处理代码。这样当异常处理例程调用后,EAX将被设为正确的值,然后RETN指令将会被执行。
如果异常处理例程里检查异常是否真地是STATUS_GUARD_PAGE_VIOLATION,逆向分析人员可以在异常处理例程中下断点然后修改传入的ExceptionRecord参数,具体来说就是ExceptionCode, 手工将ExceptionCode设为STATUS_GUARD_PAGE_VIOLATION即可。
实例:
//需要用到在UnhandledExceptionHandler 里定义的一些结构
//********************************************************
static bool isDebugged=1;
LONG WINAPI TopUnhandledExceptionFilter2(
struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
_asm pushad
AfxMessageBox("回调函数");
lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER )lpOldHandler);
ExceptionInfo->ContextRecord->Eip=NewEip;
isDebugged=0;
_asm popad
return EXCEPTION_CONTINUE_EXECUTION;
}
void CDetectODDlg::OnGuardPages()
{
// TODO: Add your control notification handler code here
ULONG dwOldType;
DWORD dwPageSize;
LPVOID lpvBase; // 获取内存的基地址
SYSTEM_INFO sSysInfo; // 系统信息
GetSystemInfo(&sSysInfo); // 获取系统信息
dwPageSize=sSysInfo.dwPageSize; //系统内存页大小
lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary(("kernel32.dll")),
"SetUnhandledExceptionFilter");
lpOldHandler=(DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilter2);
// 分配内存
lpvBase = VirtualAlloc(NULL,dwPageSize,MEM_COMMIT,PAGE_READWRITE);
if (lpvBase==NULL) AfxMessageBox("内存分配失败");
_asm{
mov NewEip,offset safe //方式二,更简单
mov eax,lpvBase
push eax
mov byte ptr [eax],0C3H //写一个 RETN 到保留内存,以便下面的调用
}
if(0==::VirtualProtect(lpvBase,dwPageSize,PAGE_EXECUTE_READ | PAGE_GUARD,&dwOldType)){
AfxMessageBox("执行失败");
}
_asm{
pop ecx
call ecx //调用时压栈
safe:
pop ecx //堆栈平衡,弹出调用时的压栈
}
if(1==isDebugged){
AfxMessageBox("发现OD");
}else{
AfxMessageBox("没有OD");
}
VirtualFree(lpvBase,dwPageSize,MEM_DECOMMIT);
}
14.Software Breakpoint Detection
软件断点是通过修改目标地址代码为0xCC(INT3/Breakpoint Interrupt)来设置的断点。通过在受保护的代码段和(或)API函数中扫描字节0xCC来识别软件断点。这里以普通断点和函数断点分别举例。
(1)实例一 普通断点
注意:在被保护的代码区域下INT3断点进行测试
BOOL DetectBreakpoints()
{
BOOL bFoundOD;
bFoundOD=FALSE;
__asm
{
jmp CodeEnd
CodeStart: mov eax,ecx ;被保护的程序段
nop
push eax
push ecx
pop ecx
pop eax
CodeEnd:
cld ;检测代码开始
mov edi,offset CodeStart
mov edx,offset CodeStart
mov ecx,offset CodeEnd
sub ecx,edx
mov al,0CCH
repne scasb
jnz ODNotFound
mov bFoundOD,1
ODNotFound:
}
return bFoundOD;
}
void CDetectODDlg::OnDectectBreakpoints()
{
// TODO: Add your control notification handler code here
HANDLE hProcess;
hProcess=::GetCurrentProcess();
CString str="利用我定位";
if(DetectBreakpoints())
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
}
(2)实例二 函数断点bp
利用GetProcAddress函数获取API的地址
注意:检测时,BP MessageBoxA
BOOL DetectFuncBreakpoints()
{
BOOL bFoundOD;
bFoundOD=FALSE;
DWORD dwAddr;
dwAddr=(DWORD)::GetProcAddress(LoadLibrary("user32.dll"),"MessageBoxA");
__asm
{
cld ;检测代码开始
mov edi,dwAddr ;起始地址
mov ecx,100 ;100bytes ;检测100个字节
mov al,0CCH
repne scasb
jnz ODNotFound
mov bFoundOD,1
ODNotFound:
}
return bFoundOD;
}
void CDetectODDlg::OnDectectFuncBreakpoints()
{
// TODO: Add your control notification handler code here
CString str="利用我定位";
if(DetectFuncBreakpoints())
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
}
15.Hardware Breakpoints Detection
硬件断点是通过设置名为Dr0到Dr7的调试寄存器来实现的。Dr0-Dr3包含至多4个断点的地址,Dr6是个标志,它指示哪个断点被触发了,Dr7包含了控制4个硬件断点诸如启用/禁用或者中断于读/写的标志。
由于调试寄存器无法在Ring3下访问,硬件断点的检测需要执行一小段代码。可以利用含有调试寄存器值的CONTEXT结构,该结构可以通过传递给异常处理例程的ContextRecord参数来访问。
//********************************************************
static bool isDebuggedHBP=0;
LONG WINAPI TopUnhandledExceptionFilterHBP(
struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
_asm pushad
AfxMessageBox("回调函数被调用");
ExceptionInfo->ContextRecord->Eip=NewEip;
if(0!=ExceptionInfo->ContextRecord->Dr0||0!=ExceptionInfo->ContextRecord->Dr1||
0!=ExceptionInfo->ContextRecord->Dr2||0!=ExceptionInfo->ContextRecord->Dr3)
isDebuggedHBP=1; //检测有无硬件断点
ExceptionInfo->ContextRecord->Dr0=0; //禁用硬件断点,置0
ExceptionInfo->ContextRecord->Dr1=0;
ExceptionInfo->ContextRecord->Dr2=0;
ExceptionInfo->ContextRecord->Dr3=0;
ExceptionInfo->ContextRecord->Dr6=0;
ExceptionInfo->ContextRecord->Dr7=0;
ExceptionInfo->ContextRecord->Eip=NewEip; //转移到安全位置
_asm popad
return EXCEPTION_CONTINUE_EXECUTION;
}
void CDetectODDlg::OnHardwarebreakpoint()
{
// TODO: Add your control notification handler code here
lpSetUnhandledExceptionFilter = (pSetUnhandledExceptionFilter)GetProcAddress(LoadLibrary(("kernel32.dll")),
"SetUnhandledExceptionFilter");
lpOldHandler=(DWORD)lpSetUnhandledExceptionFilter(TopUnhandledExceptionFilterHBP);
_asm{
mov NewEip,offset safe //方式二,更简单
int 3
mov isDebuggedHBP,1 //调试时可能也不会触发异常去检测硬件断点
safe:
}
if(1==isDebuggedHBP){
AfxMessageBox("发现OD");
}else{
AfxMessageBox("没有OD");
}
}
//********************************************************
16.PatchingDetection CodeChecksumCalculation补丁检测,代码检验和
补丁检测技术能识别壳的代码是否被修改,也能识别是否设置了软件断点。补丁检测是通过代码校验来实现的,校验计算包括从简单到复杂的校验和/哈希算法。
实例:改动被保护代码的话,CHECKSUM需要修改,通过OD等找出该值
注意:在被保护代码段下F2断点或修改字节来测试
/*********************************************************/
BOOL CheckSum()
{
BOOL bFoundOD;
bFoundOD=FALSE;
DWORD CHECK_SUM=5555; //正确校验值
DWORD dwAddr;
dwAddr=(DWORD)CheckSum;
__asm
{ ;检测代码开始
mov esi,dwAddr
mov ecx,100
xor eax,eax
checksum_loop:
movzx ebx,byte ptr [esi]
add eax,ebx
rol eax,1
inc esi
loop checksum_loop
cmp eax,CHECK_SUM
jz ODNotFound
mov bFoundOD,1
ODNotFound:
}
return bFoundOD;
}
void CDetectODDlg::OnChecksum()
{
// TODO: Add your control notification handler code here
if(CheckSum())
{
AfxMessageBox("发现OD");
}
else
{
AfxMessageBox("没有OD");
}
}
17.block input封锁键盘、鼠标输入
user32!BlockInput() API 阻断键盘和鼠标的输入。
典型的场景可能是逆向分析人员在GetProcAddress()内下断,然后运行脱壳代码直到被断下。但是跳过一段垃圾代码之后壳调用BlockInput()。当GetProcAddress()断点断下来后,逆向分析人员会突然困惑地发现无法控制调试器了,不知究竟发生了什么。
示例:源码看附件
BlockInput()参数fBlockIt,true,键盘和鼠标事件被阻断;false,键盘和鼠标事件解除阻断:
; Block input
push TRUE
call [BlockInput]
;...Unpacking code...
;Unblock input
push FALSE
call [BlockInput]
void CDetectODDlg::OnBlockInput()
{ // #include "Winable.h"
// TODO: Add your control notification handler code here
CString str="利用我定位";
DWORD dwNoUse;
DWORD dwNoUse2;
::BlockInput(TRUE);
dwNoUse=2;
dwNoUse2=3;
dwNoUse=dwNoUse2;
::BlockInput(FALSE);
}
对策
(1)最简单的方法就是补丁 BlockInput()使它直接返回。
(2)同时按CTRL+ALT+DELETE键手工解除阻断。
18.EnableWindow禁用窗口
与BlockInput异曲同工,也是禁用窗口然后再解禁
在资源管理器里直接双击运行的话,会使当前的资源管理器窗口被禁用。
在OD里面的话,就会使OD窗口被禁用。 MFC里对OD貌似无效
void CDetectODDlg::OnEnableWindow()
{
// TODO: Add your control notification handler code here
CString str="利用我定位";
CWnd *wnd;
wnd=GetForegroundWindow();
wnd->EnableWindow(FALSE);
DWORD dwNoUse;
DWORD dwNoUse2;
dwNoUse=2;
dwNoUse2=3;
dwNoUse=dwNoUse2;
wnd->EnableWindow(TRUE);
}t
19.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将不再深入内核仅仅执行一个简单的返回。
/*********************************************************/
typedef enum _THREADINFOCLASS {
ThreadBasicInformation, // 0 Y N
ThreadTimes, // 1 Y N
ThreadPriority, // 2 N Y
ThreadBasePriority, // 3 N Y
ThreadAffinityMask, // 4 N Y
ThreadImpersonationToken, // 5 N Y
ThreadDescriptorTableEntry, // 6 Y N
ThreadEnableAlignmentFaultFixup, // 7 N Y
ThreadEventPair, // 8 N Y
ThreadQuerySetWin32StartAddress, // 9 Y Y
ThreadZeroTlsCell, // 10 N Y
ThreadPerformanceCount, // 11 Y N
ThreadAmILastThread, // 12 Y N
ThreadIdealProcessor, // 13 N Y
ThreadPriorityBoost, // 14 Y Y
ThreadSetTlsArrayAddress, // 15 N Y
ThreadIsIoPending, // 16 Y N
ThreadHideFromDebugger // 17 N Y
} THREAD_INFO_CLASS;
typedef NTSTATUS (NTAPI *ZwSetInformationThread)(
IN HANDLE ThreadHandle,
IN THREAD_INFO_CLASS ThreadInformaitonClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength
);
void CDetectODDlg::OnZwSetInformationThread()
{
// TODO: Add your control notification handler code here
CString str="利用我定位";
HANDLE hwnd;
HMODULE hModule;
hwnd=GetCurrentThread();
hModule=LoadLibrary("ntdll.dll");
ZwSetInformationThread myFunc;
myFunc=(ZwSetInformationThread)GetProcAddress(hModule,"ZwSetInformationThread");
myFunc(hwnd,ThreadHideFromDebugger,NULL,NULL);
}
/*********************************************************/
20.Disabling Breakpoints禁用硬件断点
;执行过后,OD查看硬件断点还存在,但实际已经不起作用了
;利用CONTEXT结构,该结构利用异常处理获得,异常处理完后会自动写回
见 Hardware Breakpoints Detection
21.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()入口使之直接返回
void CDetectODDlg::OnOutputDebugString()
{
// TODO: Add your control notification handler code here
::OutputDebugString("%s%s%s");
}
22.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
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib
.data?
dwTLS_Index dd ?
OPTION DOTNAME
;; 定义一个TLS节
.tls SEGMENT
TLS_Start LABEL 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_used IMAGE_TLS_DIRECTORY <TLS_Start, TLS_End, dwTLS_Index, TLS_CallBackStart, 0, ?>
.code
;***************************************************************
;; TLS的回调函数
TlsCallBack0 proc 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
VC++ 6.0
VC里的TLS回调,总是有一些问题,基本如下:
1、VC6不支持。
2、VS2005的Debug版正常,Release版不正常。
3、VS2005的Release版正常,Debug版不正常。
VC6不支持的原因是VC6带的TLSSUP.OBJ有问题,它已定义了回调表的第一项,并且为0,0意味着回调表的结束,因此我们加的函数都不会被调用。[INDENT]对于第2个问题,我没遇到,倒是遇到了第3个问题。对这个问题进行了一下研究,发现问题所在:在Link过程中节.CRT$XLA和.CRT$XLB合并时,应该是按字母顺序无间隙合并,但在DEBUG版的输出中实事并非如此,顺序没错,但却产生了很大的间隙,间隙填0,相当于在我们的回调表前加0若干个0,又是回调表提前结束,这也许是BUG。针对第二种情况,我没有遇到,不知道是否是这个原因,如果是,则我想应是LINK的BUG。
针对上述问题,本来我想可以使用VS2008的tlssup.obj,但是它与VC6的不兼容,改起来比较麻烦,后来我突然想到,也许我们可以自己创建一个tlssup.obj,基于这个思路,写了自己的tlssup,目前测试结果显示,它可以兼容VC6,VS2005,VS2008。
(1)建立一个控制台工程
(2)创建tlssup.c文件,代码如下
(3)将该文件加入工程
(4)英文版:右键点击该tlssup.c文件,选择Setting->C/C++->Gategory->Precomliled Headers->Not using precompiled headers。中文版:右键点击该tlssup.c文件->设置->C/C++->预编译的头文件->不使用预补偿页眉->确定
// tlssup.c文件代码:
#include <windows.h>
#include <winnt.h>
int _tls_index=0;
#pragma data_seg(".tls")
int _tls_start=0;
#pragma data_seg(".tls$ZZZ")
int _tls_end=0;
#pragma data_seg(".CRT$XLA")
int __xl_a=0;
#pragma data_seg(".CRT$XLZ")
int __xl_z=0;
#pragma data_seg(".rdata$T")
extern PIMAGE_TLS_CALLBACK my_tls_callbacktbl[];
IMAGE_TLS_DIRECTORY32 _tls_used={(DWORD)&_tls_start,(DWORD)&_tls_end,(DWORD)&_tls_index,(DWORD)my_tls_callbacktbl,0,0};
然后,我们在其它CPP文件中定义my_tls_callbacktbl如下即可:
extern "C" PIMAGE_TLS_CALLBACK my_tls_callbacktbl[] = {my_tls_callback1,0}; //可以有多个回调,但一定要在最后加一个空项,否则很可能出错。
当然下面一行也不能少:
#pragma comment(linker, "/INCLUDE:__tls_used")
// 工程cpp文件代码:
// TLS_CallBack_test.cpp : Defines the entry point for the console application.
//
#include <windows.h>
#include <winnt.h>
//下面这行告诉链接器在PE文件中要创建TLS目录
#pragma comment(linker, "/INCLUDE:__tls_used")
void NTAPI my_tls_callback1(PVOID h, DWORD reason, PVOID pv)
{
//仅在进程初始化创建主线程时执行的代码
if( reason == DLL_PROCESS_ATTACH ){
MessageBox(NULL,"hi,this is tls callback","title",MB_OK);
}
return;
}
#pragma data_seg(".CRT$XLB")
extern "C" PIMAGE_TLS_CALLBACK my_tls_callbacktbl[] = {my_tls_callback1,0};
#pragma data_seg()
int main(void)
{
MessageBox(NULL,"hi,this is main()","title",MB_OK);
return 0;
}
MFC里
(1)tlssup.c文件 同样设置
(2)代码
#pragma comment(linker, "/INCLUDE:__tls_used")
/*这是PIMAGE_TLS_CALLBACK()函数的原型,其中第一个和第三个参数保留,第二个参数决定函数在那种情况下*/
void NTAPI my_tls_callback1(PVOID h, DWORD reason, PVOID pv)
{
if( reason == DLL_PROCESS_ATTACH ){
MessageBox(NULL,"hi,this is tls callback","title",MB_OK);
}
return;
}
#pragma data_seg(".CRT$XLB")
extern "C" PIMAGE_TLS_CALLBACK my_tls_callbacktbl[] = {my_tls_callback1,0};
#pragma data_seg()
反反调试技术
本人脱壳逆向的水平不高,这里仅说一下本人的一点体会:
对于初学者来说主要是利用StrongOD等各种插件,这些插件能够躲过上面所说的很多检测。有了一定基础以后就可以根据各种反调试方法的弱点寻求反反调试的途径了。
曾经写过一篇关于ANTI-OD的原理和应对方法的文章,也可以用于增强自己的OD,各位可以看一下:
OD被Anti的原因分析及应对之道:
http://www.ucooper.com/od-anti-reasons.html
各种反调试技术原理与实例 汇编版
http://www.ucooper.com/anti-debug-methods-asm.html
欢迎莅临本人空间:http://ucooper.com
写意互联网,关注搜索引擎技术,涉猎搜索引擎优化、软件破解、PHP网站建设、Wordpress应用等。
失误之处,敬请指教。
参考文献:《脱壳的艺术》、《加密与解密》、看雪论坛、其它资料