- 反调试技术定义:恶意代码用它识别是否被调试,或者让调试器失效,当恶意代码意识到自己被调试时,它们可能改变 正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。
16.1 探测windows调试器
- 多种手段
- 使用Windows API
- 手动检测包含调试器痕迹的内存结构
- 查询调试器遗留在系统中的痕迹
- 使用Windows API(最简单)
- Windows 操作系统提供一些API,应用程序可以通过调用这些API,来探测自己是否正在被调试。
- 防止恶意代码使用API进行反调试:
- 最简单方法是在恶意代码运行期间修改恶意代码,使其不能调用探测调试器的API函数,或者修改这些函数的返回值。
- 高级方法:使用hook、rootkit技术等
- 常用来探测调试器的API
- IsDebuggerPresent:查看自己进程是否被调试
- 查询进程环境块(PEB)中的IsDebugged标志。如果进程没有运行在调试器环境中,函数返回0;如果调试附加了进程,函数返回一个非零值
- CheckRemoteDebuggerPresent:查看自己或者其他进程是否被调试
- 它也检查PEB结构中的IsDebugged属性。 它不仅可以探测进程自身是否被调试,同时可以探测系统其他进程是否被调试,检查进程句柄对应的进程是否被调试器附加
- NtQueryInformationProcess
- 提取一个给定进程的信息。它的第一个参数是进程句柄,第二个 参数告诉我们它需要提取进程信息的类型,例如将该参数置为 ProcessDebugPort (值为0x7),将会告诉你这个句柄标识的进程是否正在被调试。如果进程正在被调试,则返回调试端口,否则返回0
- OutputDebugString
- 在调试器中显示一个字符串
- 也可以用来探测调试器的存在,使用SetLastError函数将当前错误码设置为任意值,如果进程没有被调试器福建,那么调用OutputDebugString会失败,当前错误码被重写,GetLastError获取到的错误码不是之前手工设置的,反之说明有调试器的存在
- IsDebuggerPresent:查看自己进程是否被调试
手工检测数据结构(最常使用)
- Windows操作系统维护着每个正在运行的进程的PEB结构,PEB结构中的一些标志暴露了调试器存在的信息
- PEB结构中包含与这个进程相关的所有用户态参数,包括调试器状态,可以利用fs:[30h]获取PEB结构的基地址
- 检测调试器存在常使用的一些标志
- 检测BeingDebugged属性(偏移0x2)
- 恶意代码标志进程是否正在被调试
- 调试者解决方法:
- 在执行跳转指令前,手动修改零标志,强制执行跳转(或者不跳转)
- 手动设置BeingDebugged属性值为0
- OllyDbg 的 Hide Debugger、StrongOD等插件可以帮助我们修改 BeingDebugged 标志
- 恶意代码标志进程是否正在被调试
检测ProcessHeap 属性(偏移0x18)
- Reserved4数组中一个非公开的位置叫作ProcessHeap,位于PEB结构的0x18处。第一个堆头部有一个属性字段,它告诉内核这个堆是否在调试器中创建。这些属性叫作ForceFlags(Flags标志位作用类似)
Windows XP系统中,ForceFlags属性位于堆头部偏移量0x10处; Windows 7 x86系统中位于偏移量0x44处
BOOL CheckDebug()
{
int result = 0;
DWORD dwVersion = GetVersion();
DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
//for xp
if (dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 10h]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 44h]
mov result, eax
}
}
return result != 0;
}
Flags属性在Windows XP系统中相对ProcessHeap偏移量0x0C处,或者Windows 7系统中偏移量0x40处的。这个属性总与ForceFlags属性大致相同,但通常情况下Flags与值2进行比较。
BOOL CheckDebug()
{
int result = 0;
DWORD dwVersion = GetVersion();
DWORD dwWindowsMajorVersion = (DWORD)(LOBYTE(LOWORD(dwVersion)));
//for xp
if (dwWindowsMajorVersion == 5)
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 0ch]
mov result, eax
}
}
else
{
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 18h]
mov eax, [eax + 40h]
mov result, eax
}
}
return result != 2;
}
调试者解决方法:
- 在执行跳转指令前,手动修改零标志,强制执行跳转(或者不跳转)
- 手动修改ProcessHeap标志
- 用OllyDbg的命令行插件修改,输入的命令为dump ds:[fs:[30]+0x18]+0x10
- 使用Hide Debugger、StrongOD等调试器隐藏插件
- 用PhantOm插件,它会禁用调试堆创建功能而不需要手动设置
- 如果使用的调试器是WinDbg,则以禁用调试堆栈的方式启动进程 ,如windbg–hd notepad.exe
检测NTGlobalFlag(偏移0x68)
由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为 0x70,则进程正运行在调试器中
BOOL CheckDebug()
{
int result = 0;
__asm
{
mov eax, fs:[30h]
mov eax, [eax + 68h]
and eax, 0x70
mov result, eax
}
return result != 0;
}
解决方法:同上,如果用OllyDbg的命令行插件修改,输入的命令为dump fs:[30]+0x68。如果用PhantOm插件,它会逃避使用NTGlobalFlag的反调试技术而不需要手动设置。
系统痕迹检测
- 调试工具分析恶意代码时在系统中驻留一些痕迹
- 注册表
- 进程
- 窗体
查找调试器引用的注册表项
- 调试器在注册表中的常用位置
- SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)
- SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)
- 指定当应用程序发生错误时触发哪一个调试器,默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。
BOOL CheckDebug()
{
BOOL is_64;
IsWow64Process(GetCurrentProcess(), &is_64);
HKEY hkey = NULL;
char key[] = "Debugger"; ////要查询注册表键值的名字字符串,注册表键的名字,以空字符结束
char reg_dir_32bit[] = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug";
char reg_dir_64bit[] = "SOFTWARE\\Wow6432Node\\Microsoft\\WindowsNT\\CurrentVersion\\AeDebug";
DWORD ret = 0;
if (is_64)
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_64bit, &hkey); //用于创建或打开注册表项
}
else
{
ret = RegCreateKeyA(HKEY_LOCAL_MACHINE, reg_dir_32bit, &hkey);
}
if (ret != ERROR_SUCCESS)
{
return FALSE;
}
char tmp[256]; //用于装载指定值的一个缓冲区
DWORD len = 256; //用于装载lpData缓冲区长度的一个变量。 一旦返回,它会设为实际装载到缓冲区的字节数
DWORD type; //用于装载取回数据类型的一个变量
ret = RegQueryValueExA(hkey, key, NULL, &type, (LPBYTE)tmp, &len);
if (strstr(tmp, "OllyIce")!=NULL || strstr(tmp, "OllyDBG")!=NULL || strstr(tmp, "WinDbg")!=NULL || strstr(tmp, "x64dbg")!=NULL || strstr(tmp, "Immunity")!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
- 调试器在注册表中的常用位置
查找系统中的文件和目录
- 调试器可执行文件会在恶意代码分析期间存在,可以查找进程信息
BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); //在快照中包含系统中所有的进程( TH32CS_SNAPPROCESS),返回进程句柄
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32); //获取第一个进程的句柄,配合Process32Next遍历所有进程
while(bMore)
{
if (stricmp(pe32.szExeFile, "OllyDBG.EXE")==0 || stricmp(pe32.szExeFile, "OllyICE.exe")==0 || stricmp(pe32.szExeFile, "x64_dbg.exe")==0 || stricmp(pe32.szExeFile, "windbg.exe")==0 || stricmp(pe32.szExeFile, "ImmunityDebugger.exe")==0)
{
return TRUE;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
return FALSE;
}
- 调试器可执行文件会在恶意代码分析期间存在,可以查找进程信息
查找窗体信息
通过FindWindow来查找调试器,看是否存在调试器窗口
BOOL CheckDebug()
{
if(FindWindowA("OLLYDBG", NULL)!=NULL || FindWindowA("WinDbgFrameClass", NULL)!=NULL || FindWindowA("QWidget", NULL)!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
EnumWindows函数枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数,在回调函数中判断窗口名称是否存在调试器 ```c BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lParam)
{
char cur_window[1024];
GetWindowTextA(hwnd, cur_window, 1023);
if (strstr(cur_window, “WinDbg”)!=NULL || strstr(cur_window, “x64_dbg”)!=NULL || strstr(cur_window, “OllyICE”)!=NULL || strstr(cur_window, “OllyDBG”)!=NULL || strstr(cur_window, “Immunity”)!=NULL)
{
((BOOL)lParam) = TRUE;
}
return TRUE;
}
- 调试工具分析恶意代码时在系统中驻留一些痕迹
BOOL CheckDebug()
{
BOOL ret = FALSE;
EnumWindows(EnumWndProc, (LPARAM)&ret);
return ret;
}
- GetForegroundWindow函数获取一个前台窗口的句柄
```c
BOOL CheckDebug()
{
char fore_window[1024];
GetWindowTextA(GetForegroundWindow(), fore_window, 1023);
if (strstr(fore_window, "WinDbg")!=NULL || strstr(fore_window, "x64_dbg")!=NULL || strstr(fore_window, "OllyICE")!=NULL || strstr(fore_window, "OllyDBG")!=NULL || strstr(fore_window, "Immunity")!=NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
- 对抗反调试方法:在OllyDbg的PhantOm插件中勾选hide OllyDbg windows
16.2 识别调试器行为
- 在逆向工程中,为了帮助恶意代码分析人员进行分析,可以使用调试器设置一个断点,或是单步执行一个进程,在调试器中执行这些操作时,它们会修改进程中的代码,恶意代码通过检测这些修改来识别是否有调试器。直接运行恶意代码与在调试器中运行恶意代码也会在一些细节上不同,如父进程信息、STARTUPINFO信息、SeDebugPrivilege权限等。
- 断点
- 代码和校验
- 时钟检测
- 判断父进程
软件断点检测——INT扫描
- 设置软中断常用INT 3(机器码0xCC),用软件中断指令INT 3临时替换运行程序中的一条指令,然后当程序运行到这条指令时,调用调试异常处理例程。
反调试技术:探测0xCC指令的存在
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage; //RVA+base=实际地址
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
BOOL Found = FALSE;
__asm //遍历section中的字节
{
cld
mov edi,dwAddr //EDI需指向缓冲区地址
mov ecx,dwCodeSize //ECX设为缓冲区的长度
mov al,0CCH //AL则包含要找的字节
repne scasb //repne scasb指令用于在一段数据缓冲区中搜索一个字节
jnz NotFound //当ECX=0或找到该字节时,比较停止
mov Found,1
NotFound:
}
return Found;
}
对抗反调试技术方法:使用硬件断点代替软件断点
硬件断点检查
- 在OllyDbg的寄存器窗口按下右键,点击View debug registers可以看到DR0、DR1、DR2、DR3、DR6和DR7这几个寄存器。DR0、Dr1、Dr2、Dr3用于设置硬件断点,由于只有4个硬件断点寄存器,所以同时最多只能设置4个硬件断点。
- DR4、DR5由系统保留。DR6、DR7用于记录Dr0~Dr3中断点的相关属性。
- 如果没有硬件断点,那么DR0、DR1、DR2、DR3这4个寄存器的值都为0。
BOOL CheckDebug()
{
CONTEXT context;
HANDLE hThread = GetCurrentThread();
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &context);
if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0)
{
return TRUE;
}
return FALSE;
}
执行代码校验和检查
恶意代码可以计算代码段的校验并实现与扫描中断相同的目的,CRC(循环冗余校验)或者MD5校验和检查
BOOL CheckDebug()
{
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS32 pNtHeaders;
PIMAGE_SECTION_HEADER pSectionHeader;
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
DWORD checksum = 0;
__asm
{
cld
mov esi, dwAddr
mov ecx, dwCodeSize
xor eax, eax
checksum_loop :
movzx ebx, byte ptr[esi]
add eax, ebx
rol eax, 1
inc esi
loop checksum_loop
mov checksum, eax
}
if (checksum != 0x46ea24)
{
return FALSE;
}
else
{
return TRUE;
}
}
对抗反调试技术方法:使用硬件断点,或者在代码运行过程中用调试器手动修改执行路径来对抗这种反调试技术
时钟检测
- 被调试时,进程的运行速度大大降低,正常运行快于调试
- 方法
- 运行一段代码前后时间戳比较
- 异常处理前后时间戳比较(调试器处理异常时需要人为干预,导致调试器处理异常的速度异常慢)
- 使用rdstc指令(操作码0x0F31)
- 返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中,恶意代码运行两次rdstc指令,比较两次读数间的差值
BOOL CheckDebug()
{
DWORD time1, time2;
__asm
{
rdtsc
mov time1, eax
rdtsc
mov time2, eax
}
if (time2 - time1 < 0xff)
{
return FALSE;
}
else
{
return TRUE;
}
}
- 使用QueryPerformanceCounter,恶意代码运行两次QueryPerformanceCounter函数,比较两次读数间的差值,若两次调用之间花费的时间过于长,则可以认为正在使用调试器
- 存储处理器活跃的时钟数
- 使用GetTickCount,恶意代码运行两次GetTickCount函数,比较两次读数间的差值
GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数(由于时钟计数器的大小原因,计数器每49.7天就被重置一次)
BOOL CheckDebug()
{
DWORD time1 = GetTickCount();
__asm
{
mov ecx,10
mov edx,6
mov ecx,10
}
DWORD time2 = GetTickCount();
if (time2-time1 > 0x1A)
{
return TRUE;
}
else
{
return FALSE;
}
}
绕过方法
- 时钟检测之后再设置断点
- 修改比较结果,强制程序跳转
判断父进程是否是explorer.exe
一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。
BOOL CheckDebug()
{
LONG status;
DWORD dwParentPID = 0;
HANDLE hProcess;
PROCESS_BASIC_INFORMATION pbi;
int pid = getpid(); //进程pid
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); //获取进程的令牌、退出码和优先级等信息(PROCESS_QUERY_INFORMATION)
if(!hProcess)
return -1;
PNTQUERYINFORMATIONPROCESS NtQueryInformationProcess = (PNTQUERYINFORMATIONPROCESS)GetProcAddress(GetModuleHandleA("ntdll"),"NtQueryInformationProcess");
status = NtQueryInformationProcess(hProcess,SystemBasicInformation,(PVOID)&pbi,sizeof(PROCESS_BASIC_INFORMATION),NULL); //能够接收到父进程ID,PROCESS_BASIC_INFORMATION结构体)
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); //拍摄进程快照
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore) //遍历所有进程
{
if (pbi.InheritedFromUniqueProcessId == pe32.th32ProcessID) //在进程列表中查找到和父进程PID相同的那个进程
{
if (stricmp(pe32.szExeFile, "explorer.exe")==0)
{
CloseHandle(hProcessSnap);
return FALSE;
}
else
{
CloseHandle(hProcessSnap);
return TRUE;
}
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
}
父进程相关:判断STARTUPINFO信息
- explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0。所以可以利用STARTUPINFO来判断程序是否在被调试。
BOOL CheckDebug()
{
STARTUPINFO si;
GetStartupInfo(&si);
if (si.dwX!=0 || si.dwY!=0 || si.dwFillAttribute!=0 || si.dwXSize!=0 || si.dwYSize!=0 || si.dwXCountChars!=0 || si.dwYCountChars!=0)
{
return TRUE;
}
else
{
return FALSE;
}
}
- explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0。所以可以利用STARTUPINFO来判断程序是否在被调试。
父进程相关:判断是否具有SeDebugPrivilege权限
- 默认情况下进程是没有SeDebugPrivilege权限的,但是当进程通过调试器启动时,由于调试器本身启动了SeDebugPrivilege权限,当调试进程被加载时SeDebugPrivilege也就被继承了。
- 可以检测进程的SeDebugPrivilege权限来间接判断是否存在调试器,而对SeDebugPrivilege权限的判断可以用能否打开csrss.exe进程来判断。
BOOL CheckDebug()
{
DWORD ID;
DWORD ret = 0;
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(pe32);
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
BOOL bMore = Process32First(hProcessSnap, &pe32);
while(bMore)
{
if (strcmp(pe32.szExeFile, "csrss.exe")==0)
{
ID = pe32.th32ProcessID;
break;
}
bMore = Process32Next(hProcessSnap, &pe32);
}
CloseHandle(hProcessSnap);
if (OpenProcess(PROCESS_QUERY_INFORMATION, NULL, ID) != NULL)
{
return TRUE;
}
else
{
return FALSE;
}
}
16.3 干扰调试器功能
- 恶意代码可以用一些技术来干扰调试器的正常运行
- TLS回调
- 异常处理
使用TLS回调
- TLS回调函数被用来在程序入口点执行之前运行代码,也就是说恶意代码刚被加载到调试器TLS回调就会运行,所以可能会漏掉一些恶意代码的行为
- TLS(线程本地存储)
- 用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
- windows的一个存储类,允许每个线程维护一个TLS声明的专有变量,在应用程序实现TLS的情况下,可执行程序的PE头部会包含一个.tls段,提供初始化和终止TLS数据对象的回调函数,在入口点之前运行
- 如果在可执行程序中看到了.tls,可以怀疑使用了反调试技术
常用来反调试的TLS回调函数
使调试器窗口无效,使用FindWindow()查找指定的窗口,并使用SetWindowsLong()将其设为不可用
HWND hd_od=FindWindow("ollydbg",NULL);
SetWindowLong(hd_od,GWL_STYLE,WS_DISABLED);
堵塞输入
- 调用BlockInput(TRUE),阻塞用户的鼠标和键盘 输入,接下来在main()函数中发起一个异常后 再取消阻塞。
- 在main()函数中发起异常将导致调试器捕获异常,并暂停等待用户输入。而此时用户输入是被锁定的,那么程序就相当于被变相锁死了,没有办法继续调试。而当调试器不存在时,代码中的__except()部分将直接获得执行权,并取消阻塞,程序正常运行
- 创建监视线程
- 为了防止OllyDbg等调试器以进程附加的形式对 程序进行调试。
- 创建一个子线程,监视调试器窗口的出现,如 果发现调试器窗口,将其设为不可用。
- 解决方法
- 如果需要调试TLS回调函数,需要设置Debug->Events->System breakpoint作为第一个暂停的位置,这样就可以让OllyDbg在TLS回调执行前暂停
- 在IDA Pro中按Ctrl+E快捷键看到二进制的入口点,该组合键的作用是显示应用程序所有的入口点,其中包括TLS回调。双击函数名可以浏览回调函数。
- 使用异常
- SEH异常实现的反调试
- 恶意代码可以使用异常来破坏或者探测调 试器
- 调试器捕获异常后不会立即将处理权返回被调试进程处理
- 调试器如果不能将异常结果正确返回被调试进程,则恶意终止运行
- 解决方法:建议设置调试器的选项,把所有异常传递给应用程序(OD:Options->Debugging Options -> Exceptions)
插入中断
- 通过在合法指令序 列中插入中断,破坏程序的正常运行
- 插入INT 3(0xCC或0xCD03)
- 双字节操作码0xCD03可以产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法
使用_try和_except:
BOOL CheckDebug()
{
__try
{
__asm int 3
}
__except(1)
{
return FALSE;
}
return TRUE;
}
使用汇编代码安装SEH(SEH(“Structured Exception Handling”),即结构化异常处理·是(windows)操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。),在下面的代码中如果进程没有处于调试中,则正常终止;如果进程处于调试中,则跳转到非法地址0xFFFFFFFF处,无法继续调试。 ```c
include “stdio.h”
include “windows.h”
include “tchar.h”
void AD_BreakPoint()
{
printf(“SEH : BreakPoint\n”);
__asm {
// install SEH
push handler
push DWORD ptr fs:[0]
mov DWORD ptr fs:[0], esp
// generating exception
int 3
// 1) debugging
// go to terminating code
mov eax, 0xFFFFFFFF
jmp eax // process terminating!!!
// 2) not debugging
// go to normal code
handler:
mov eax, dword ptr ss:[esp+0xc]
mov ebx, normal_code
mov dword ptr ds:[eax+0xb8], ebx
xor eax, eax
retn
normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}
printf(" => Not debugging...\n\n");
}
int _tmain(int argc, TCHAR* argv[])
{
AD_BreakPoint();
return 0;
}
- 双字节操作码0xCD03也可以产生INT 3中断,这是恶意代码干扰WinDbg调试器的有效方法。在调试器外,0xCD03指令产生一个STATUS_BREAKPOINT异常。然而在WinDbg调试器内,由于断点通常是单字节机器码0xCC,因此WinDbg会捕获这个断点然后将EIP加1字节。这可能导致程序在被正常运行的WinDbg调试时,执行不同的指令集(OllyDbg可以避免双字节INT 3的攻击)。
```c
BOOL CheckDebug()
{
__try
{
__asm
{
__emit 0xCD
__emit 0x03
}
}
__except(1)
{
return FALSE;
}
return TRUE;
}
- 插入 INT 2D断点
- 与INT 3类似,但INT 0x2D指令用来探测内核态的调试器
- INT 2D指令在ollydbg中有两个有趣的特性。在调试模式中执行INT 2D指令,下一条指令的第一个字节将被忽略。使用StepInto(F7)或者StepOver(F8)命令跟踪INT 2D指令,程序不会停在下一条指令开始的地方,而是一直运行,就像RUN(F9)一样。
在下面的代码中,程序调试运行时,执行INT 2D之后不会运行SEH,而是跳过NOP,把bDebugging标志设置为1,跳转到normal_code;程序正常运行时,执行INT 2D之后触发SEH,在异常处理器中设置EIP并把bDebugging标志设置为0。 ```c BOOL CheckDebug()
{
BOOL bDebugging = FALSE;__asm {
// install SEH
push handler
push DWORD ptr fs:[0]
mov DWORD ptr fs:[0], espint 0x2d
nop
mov bDebugging, 1
jmp normal_code
handler:
mov eax, dword ptr ss:[esp+0xc]
mov dword ptr ds:[eax+0xb8], offset normal_code
mov bDebugging, 0
xor eax, eax
retn
normal_code:
// remove SEH
pop dword ptr fs:[0]
add esp, 4
}
printf("Trap Flag (INT 2D)\n");
if( bDebugging ) return 1;
else return 0;
}
3. **插入ICE断点指令icebp(0xF1) **
- 片内仿真器(ICE)断点指令ICEBP(操作码0xF1) ,用于产生一个单步异常。如果通过单步调试跟踪程序,调试器会认为这是单步调试产生的异常,从而不执行先前设置的异常处理例程。
```c
BOOL CheckDebug()
{
__try
{
__asm __emit 0xF1
}
__except(1)
{
return FALSE;
}
return TRUE;
}
- 对抗反调试:执行ICEBP指令时不要使用单步
设置陷阱标志位
- EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。
BOOL CheckDebug() { __try { __asm { pushfd or word ptr[esp], 0x100 popfd nop } } __except(1) { return FALSE; } return TRUE; }
- EFLAGS寄存器的第八个比特位是陷阱标志位。如果设置了,就会产生一个单步异常。
使用异常
- 前面已经讨论了各种使用异常机制的反调试手段。
- RaiseException
- RaiseException函数产生的若干不同类型的异常可以被调试器捕获。
```c
BOOL TestExceptionCode(DWORD dwCode)
{
__try
{
}RaiseException(dwCode, 0, 0, 0);
__except(1)
{
}return FALSE;
return TRUE;
}
- RaiseException函数产生的若干不同类型的异常可以被调试器捕获。
```c
BOOL TestExceptionCode(DWORD dwCode)
BOOL CheckDebug()
{
return TestExceptionCode(DBG_RIPEXCEPTION);
}
- SetUnhandledExceptionFilter
- 进程中发生异常时若SEH未处理或注册的SEH不存在,会调用UnhandledExceptionFilter,它会运行系统最后的异常处理器。
- UnhandledExceptionFilter内部调用了前面提到过的NtQueryInformationProcess以判断是否正在调试进程。若进程正常运行,则运行最后的异常处理器;若进程处于调试,则将异常派送给调试器。
- SetUnhandledExceptionFilter函数可以修改系统最后的异常处理器。下面的代码先触发异常,然后在新注册的最后的异常处理器内部判断进程正常运行还是调试运行。
- 进程正常运行时pExcept->ContextRecord->Eip+=4;将发生异常的代码地址加4使得其能够继续运行;进程调试运行时产生无效的内存访问异常,从而无法继续调试。
```c
#include "stdio.h"
#include "windows.h"
#include "tchar.h"
LPVOID g_pOrgFilter = 0;
LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS pExcept)
{
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)g_pOrgFilter);
// 8900 MOV DWORD PTR DS:[EAX], EAX
// FFE0 JMP EAX
pExcept->ContextRecord->Eip += 4;
return EXCEPTION_CONTINUE_EXECUTION;
}
void AD_SetUnhandledExceptionFilter()
{
printf("SEH : SetUnhandledExceptionFilter()\n");
g_pOrgFilter = (LPVOID)SetUnhandledExceptionFilter
((LPTOP_LEVEL_EXCEPTION_FILTER)ExceptionFilter);
__asm {
xor eax, eax;
mov dword ptr [eax], eax
jmp eax
}
printf(" => Not debugging...\n\n");
}
int _tmain(int argc, TCHAR* argv[])
{
AD_SetUnhandledExceptionFilter();
return 0;
}
- 反对抗方法:在OllyDbg中,选择Options->Debugging Options->Exceptions来设置把异常传递给应用程序。
16.4 调试器漏洞
- 与所有软件一样,调试器也存在漏洞,有时恶意代码编写者为了防止被调试,会攻击这些漏洞。
- PE头漏洞
- DataDirectory数组中的元素个数
- OllyDbg非常严格地遵循了微软对PE文件头部的规定。在PE文件的头部,通常存在一个叫作IMAGE_OPTIONAL_HEADER的结构。
- 需要特别注意这个结构中的最后几个元素。NumberOfRvaAndSizes属性标识后面DataDirectory数组中的元素个数
- DataDirectory数组表示在这个可执行文件中的什么地方可找到其他导入可执行模块的位置,它位于可选头部结构的末尾,是一个比IMAGE_DATA_DIRECTORY略大一些的数组。数组中每个结构目录都指明了目录的相对虚拟地址和大小。DataDirectory数组的大小被设置为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,它等于0x10。因为DataDirectory数组不足以容纳超过0x10个目录项,所以当NumberOfRvaAndSizes大于0x10时,Windows加载器将会忽略NumberOfRvaAndSizes。
- OllyDbg遵循了这个标准,并且无论NumberOfRvaAndSizes是什么值,OllyDbg都使用它。因此,设置NumberOfRvaAndSizes为一个超过0x10的值,会导致在程序退出前,OllyDbg对用户弹出一个窗口。提示错误Bad or unknown format of 32-bit executable file。
- 节头部
- 文件内容中包含的节包括代码节、数据节、资源节,以及一些其他信息节。每个节都拥有一个IMAGE_SECTION_HEADER结构的头部。
- VirtualSize和SizeOfRawData是其中两个比较重要的属性。根据微软对PE的规定,VirtualSize应该包含载入到内存的节大小,SizeOfRawData应该包含节在硬盘中的大小。Windows加载器使用VirtualSize和SizeOfRawData中的最小值将节数据映射到内存。如果SizeOfRawData大于VirtualSize,则仅将VirtualSize大小的数据复制入内存,忽略其余数据。因为OllyDbg仅使用SizeOfRawData,所以设置SizeOfRawData为一个类似0x77777777的大数值时,会导致OllyDbg崩溃。
- DataDirectory数组中的元素个数
- OutputDebugString漏洞
- 恶意代码常尝试利用OllyDbg1.1的格式化字符串漏洞,为OutputDebugString函数提供一个%s字符串的参数,让OllyDbg崩溃。因此,需要注意程序中可疑的OutputDebugString调用,例如OutputDebugString(“%s%s%s%s%s%s%s%s%s”)。如果执行了这个调用,OllyDbg将会崩溃。
- 开头补充