目录
引言
一、利用SetUnhandledExceptionFilter/Debugger Interrupts
二、Trap Flag 单步标志异常
三、利用SeDebugPrivilege 进程权限
四、利用DebugObject:NtQueryObject()
五、OllyDbg:Guard
六、Software Breakpoint Detection
引言
在程序反调试领域,存在多种检测调试器的手段。接下来将介绍其他反调试手段中篇,诸如SetUnhandledExceptionFilter/Debugger Interrupts、Trap Flag单步标志异常等一系列其他反调试方法,这些方法通过不同的原理来识别程序是否正被调试,为程序的安全防护提供多维度保障 ,给新手学习带来灵感。
一、利用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 pushadAfxMessageBox("回调函数");lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)lpOldHandler);ExceptionInfo->ContextRecord->Eip = NewEip; // 转移到安全位置__asm popadreturn EXCEPTION_CONTINUE_EXECUTION;
}void CDetectODDlg::OnSetUnhandledExceptionFilter() {bool isDebugged = 0;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) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}
}
当程序因调试中断而停止执行时,我们可以在OllyDbg(简称OD)里,通过“视图->SEH链”的路径找到异常处理例程,并在该例程处设置断点。随后按下“Shift+F9”组合键,将中断或异常传递给异常处理例程,最终异常处理例程里设置的断点就会生效,此时便能对程序进行跟踪调试了。
还有一种方式,就是让调试中断自动传递到异常处理例程。在OllyDbg中,具体操作是依次点击“选项->调试选项->异常”,接着在“忽略下列异常”选项卡中,勾选“INT3中断”和“单步中断”这两个复选框,如此便可完成相关设置。
二、Trap Flag 单步标志异常
当`TF`的值设为1时,会触发单步异常。这种检测方法属于异常处理的范畴,但有其特殊性。对于未经过修改的OllyDbg(OD),不管按`F9`键还是`F8`键,都无法处理这个异常。而装有插件的OllyDbg,按`F9`键时能正常处理异常,按`F8`键时则不能正确处理。
关键代码示例:
void CDetectODDlg::OnTrapFlag() {try {__asm {pushfd // 触发单步异常or dword ptr [esp], 100h ; TP = 1popfd}AfxMessageBox(" 检测到OD");}catch (...) {AfxMessageBox(" 没有OD");}
}
三、利用SeDebugPrivilege 进程权限
正常情况下,进程并不具备`SeDebugPrivilege`权限。但在调试进程时,它会从调试器那里继承到该权限。我们可以通过打开`CSRSS.EXE`进程,间接地利用`SeDebugPrivilege`权限来判断进程是否正在被调试。需要留意的是,默认状态下,只有`Administrators`组成员才被赋予这一权限。
获取`CSRSS.EXE`进程的PID有两种方式,一是借助`ntdll!CsrGetProcessId()`这个API,二是通过枚举进程来实现。在实际的实例测试中,发现当`OD`加载后,首次检测无法得到正确结果,第二次检测却能正常判断,至于其中原因暂不明确 。
关键代码示例:
void CDetectODDlg::OnSeDebugPrivilege() {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);
}
四、利用DebugObject:NtQueryObject()
除了判断进程是否正被调试外,还有一些调试器检测手段是去检查系统中有没有正在运行的调试器。在逆向论坛上,大家讨论过一种有意思的办法,即查看`DebugObject`类型的内核对象数量。该方法可行的原因在于,每当一个应用程序进入调试状态,内核就会为这次调试对话创建一个`DebugObject`类型的对象。
要获取`DebugObject`的数量,可以利用`ntdll!NtQueryObject()`函数来检索所有对象类型的相关信息。`NtQueryObject`函数需要5个参数,若要查询所有对象类型,就得把`ObjectHandle`参数设为`NULL`,将`ObjectInformationClass`参数设为`ObjectAllTypeInformation(3)` 。
`NtQueryObject` 函数原型为:
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];
} OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
检测例程将遍历拥有如下结构的 `ObjectTypeInformation` 数组:
typedef struct _OBJECT_TYPE_INFORMATION {UNICODE_STRING TypeName;ULONG TotalNumberOfHandles;ULONG TotalNumberOfObjects;//...more fields...
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
将`TypeName`成员的值与`UNICODE`字符串“DebugObject”进行比对,随后查看`TotalNumberOfObjects`或者`TotalNumberOfHandles`的值是不是不为0。 下面是相关的类型定义内容:
#ifndef STATUS_INFO_LENGTH_MISMATCH
#define STATUS_INFO_LENGTH_MISMATCH((UINT32)0xC0000004L)
#endiftypedef 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,ObjectNameInformation,ObjectTypeInformation,ObjectAllTypesInformation,ObjectDataInformation
} 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")) {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);}}
}
五、OllyDbg:Guard
这个检查专门针对OllyDbg,因为它跟OllyDbg的内存访问/写入断点功能有关。
除了硬件断点和软件断点,OllyDbg能设置一种内存访问/写入断点,它靠页面保护来实现。说白了,页面保护就是让应用程序在访问某块内存时能收到通知。
页面保护通过`PAGE_GUARD`页面保护修改符来设置。要是访问的内存地址属于受保护页面,就会产生`STATUS_GUARD_PAGE_VIOLATION(0x80000001)`异常。但要是进程正被OllyDbg调试,而且受保护页面被访问,就不会抛出这个异常,而是把访问当成内存断点处理,一些壳程序就利用了这一特性。
举例说明:下面的示例代码会先分配一段内存,把要执行的代码存进去,接着开启页面的`PAGE_GUARD`属性。然后把标志符`EAX`初始化为0,通过执行内存里的代码触发`STATUS_GUARD_PAGE_VIOLATION`异常。要是代码在OllyDbg里调试,由于异常处理例程不会被调用,标志符`EAX`的值就不会变。
应对办法:因为页面保护会引发异常,逆向分析人员可以主动引发异常,让异常处理例程被调用。在这个例子里,逆向分析人员可以把`RETN`指令换成`INT3`指令,一旦执行`INT3`指令,按`Shift+F9`就能强制调试器执行异常处理代码。等异常处理例程调用后,`EAX`就会被设为正确值,接着`RETN`指令执行。
要是异常处理例程检查异常是不是真的`STATUS_GUARD_PAGE_VIOLATION`,逆向分析人员可以在异常处理例程里下断点,修改传入的`ExceptionRecord`参数,具体就是把`ExceptionCode`手动改成`STATUS_GUARD_PAGE_VIOLATION`就行。
关键示例代码:
//需要用到在UnhandledExceptionHandler 里定义的一些结构
//********************************************************
static bool isDebugged = 1;
LONG WINAPI TopUnhandledExceptionFilter2(struct _EXCEPTION_POINTERS *ExceptionInfo
) {__asm pushadAfxMessageBox(" 回调函数");lpSetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)lpOldHandler);ExceptionInfo->ContextRecord->Eip = NewEip;isDebugged = 0;__asm popadreturn EXCEPTION_CONTINUE_EXECUTION;
}void CDetectODDlg::OnGuardPages() {//TODO: Add your control notification handler code hereULONG 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, lpvBasepush eaxmov byte ptr [eax], 0C3H //写一个 RETN 到保留内存,以便下面的调用}if (0 == ::VirtualProtect(lpvBase, dwPageSize, PAGE_EXECUTE_READ | PAGE_GUARD, &dwOldType)) {AfxMessageBox(" 执行失败");}__asm {pop ecxcall ecx //调用时压栈safe:pop ecx //堆栈平衡,弹出调用时的压栈}if (1 == isDebugged) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}VirtualFree(lpvBase, dwPageSize, MEM_DECOMMIT);
}
六、Software Breakpoint Detection
软件断点的设置,是把目标地址的代码改成`0xCC`(也就是`INT3/Breakpoint Interrupt`) 。要识别软件断点,可以在受保护的代码段以及(或者)API函数里,对字节`0xCC`进行扫描。下面分别以普通断点和函数断点为例展开说明。
(1) 实例一 :普通断点
要注意,测试时需要在受保护的代码区域设置`INT3`断点。
关键示例代码:
BOOL DetectBreakpoints() {BOOL bFoundOD;bFoundOD = FALSE;__asm {jmp CodeEndCodeStart:mov eax, ecx ;被保护的程序段noppush eaxpush ecxpop ecxpop eaxCodeEnd:cld ;检测代码开始mov edi, offset CodeStartmov edx, offset CodeStartmov ecx, offset CodeEndsub ecx, edxmov al, 0CCHscasbrepnejnz ODNotFoundmov bFoundOD, 1ODNotFound:}return bFoundOD;
}void CDetectODDlg::OnDectectBreakpoints() {//TODO:Add your control notification handler code hereHANDLE 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, 0CCHrepnescasbjnz ODNotFoundmov bFoundOD, 1ODNotFound:}return bFoundOD;
}void CDetectODDlg::OnDectectFuncBreakpoints() {//TODO:Add your control notification handler code hereCString str = "利用我定位";if (DetectFuncBreakpoints()) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}
}