写在前面

在逆向工程中为了防止破解者调试软件,通常都会在软件中采用一些反调试技术来防破解。下面就是一些在逆向工程中常见的反调试技巧与示例。

BeingDebuged

利用调试器加载程序时调试器会通过CreateProcess()创建调试进程,同时会创建调试内核对象并保存在当前线程环境块的DbgSsReserved[1]字段中,此时此线程就不同于普通线程了(一般称为调试器线程)。接着CreateProcess()会判断是否有调试器,无则直接返回,有则调用PspCreateProcess(),此函数会根据DbgSsReserved[1]字段设置创建进程的EPROCESS的DebugPort字段,这样此被创建进程就可以称为被调试进程。接着PspCreateProcess()会调用MmCreatePeb(),此函数会根据EPROCESS的Debugport字段设置PEB的BeingDebuged字段。(无调试器时其值为0)

我们可以直接通过内联汇编来访问BeingDebuged字段,也可以通过IsDebuggerPresent()这个API来得到此字段的值。下面是示例程序。

int main(int argc, char *argv[])
{
DWORD dwBeingDebuged;
TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!"); dwBeingDebuged = IsDebuggerPresent();
if(0 != dwBeingDebuged)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}

NtGlobalFlag

当BeingDebuged被设为“TRUE”后程序会将PEB的NtGlobalFlag设置0x70h标志。我们通过内联汇编检测进程环境快的NtGlobalFlag字段。

int main(int argc, char *argv[])
{
DWORD dwNtGlobalFlag;
TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!"); dwNtGlobalFlag = _AntiDebug();
if(0 != dwNtGlobalFlag)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
} DWORD _AntiDebug()
{
_asm{
mov eax,fs:[0x30]
mov eax,[eax + 0x68]
and eax,0x70
}
}

ProcessHeap的Flags和ForceFlags

当NtGlobalFlag字段被设置0x70标志后,其会改变PEB的字段ProcessHeap所指向的Flags和ForceFlags的值。

对于x86平台而言Flags和ForceFlags偏移地址分别是相对于ProcessHeap指向地址的0x40和0x44处。当程序未被调试器调试时Flags的值应包含0x00000002标志,ForceFlags的值应为0,下面是通过内联汇编检测二者的值。

int main(int argc, char *argv[])
{
DWORD dwReturn;
TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!"); dwReturn = _AntiDebug();
if(0 != dwReturn)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
} MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
} DWORD _AntiDebug()
{
_asm{
push ebx
mov eax,fs:[0x30]
mov eax,[eax + 0x18] mov ebx,dword ptr [eax + 0x40]
and ebx,0x00000002
cmp ebx,0
je end0
mov ebx,dword ptr [eax + 0x44]
cmp ebx,0
jne end0
xor eax,eax
jmp end
end0:
mov eax,1
end:
pop ebx
}
}

Heap Magic

当进程被调试器调试时其进程堆会被一些特殊的标记填充,这些特殊标记分别是0xABABABAB , 0xBAADF00D , 0xFEEEFEEE。

通过从进程堆中创建内存块来获得进程堆的起始地址,然后通过对整个进程堆双字遍历来计算这些标志的数量。(如果大于10则证明进程被调试器调试了)

int main(int argc, char *argv[])
{
DWORD dwNum = 0;
PDWORD pHeap;
TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!"); pHeap = (DWORD*)HeapAlloc(GetProcessHeap(), NULL, 0x100); //通过在自己的进程默认堆中申请内存得到堆中内存的地址,(还可以通过PEB的LDR_MODULE字段中得到这些标志)
_try{
for(;;){ //检测Heap Magic标志。
switch(*pHeap++){
case 0xABABABAB:
case 0xBAADF00D:
case 0xFEEEFEEE:
dwNum++;
break;
}
}
}
_except(EXCEPTION_EXECUTE_HANDLER){
if(dwNum > 10)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0; }

ProcessDebugPort

如果进程被调试则进程会通过一个调试端口与调试子系统通信,进而与调试器通信。我们可以通过CheckRemoteDebuggerPresent()来检测调试端口是否存在ProcessDebugPort实际就是PEB的DebugPort字段。同时CheckRemoteDebuggerPresent()不仅可以检测自身是否存在调试端口,还可以检测其他进程是否存在调试端口。

int main(int argc, char *argv[])
{ BOOL bDebuggerPresent;
TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!"); CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent); if(TRUE == bDebuggerPresent)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
}
MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}

实际CheckRemoteDebuggerPresent()函数内部是通过调用NtQueryInformationProcess()来检测进程是否存在调试端口的,所以我们也可以直接通过动态调用NtQueryInformationProcess()来检测是否存在调试端口。

typedef NTSTATUS(NTAPI  *pfnNtQueryInformationProcess)(						//typedef后可以掩饰复合类型
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
//表示检测进程的调试端口
UINT ProcessDebugPort = 7; int main(int argc, char *argv[])
{ TCHAR szTest[] = TEXT("已检测到调试器!");
TCHAR szSuccess[] = TEXT("运行正常!");
DWORD dwDebuggerPresent = 0;
NTSTATUS stNtstatus; pfnNtQueryInformationProcess NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugPort,
&dwDebuggerPresent,
sizeof(DWORD),
NULL); if(0 != dwDebuggerPresent && stNtstatus == 0x00000000)
{
MessageBox(NULL, szTest, NULL, MB_OK);
ExitProcess(NULL);
} MessageBox(NULL, szSuccess, NULL, MB_OK);
return 0;
}

ProcessDebugObjectHandle

如果进程被调试会创建一个调试内核对象,通过NtQueryInformationProcess()可以检测是否存在调试内核对象句柄。

typedef NTSTATUS(NTAPI  *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
); UINT ProcessDebugObjectHandle = 0x1E; int main(int argc, char* argv[])
{
HANDLE hDebugHandle;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess; NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess")); stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugObjectHandle,
&hDebugHandle,
sizeof(HANDLE),
NULL);
if(0x00000000 == stNtstatus && NULL != isDebuggerPresent)
{ MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
ExitProcess(NULL);
} MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
return 0;
}

ProcessDebugFlags

当进程被调试时其进程内核对象EPROCESS的NoDebugInherit字段会被置空,调用NtQueryInformationProcess可以检测此字段的值。

typedef NTSTATUS(NTAPI  *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
); UINT ProcessDebugFlags = 0x1F; int main(int argc, char* argv[])
{
ULONG DebugFlags;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess; NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess")); stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
ProcessDebugFlags,
&DebugFlags,
sizeof(ULONG),
NULL);
if(0x00000000 == stNtstatus && NULL == DebugFlags)
{ MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
ExitProcess(NULL);
} MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
return 0;
}

ProcessBasicInformation

调用NtQuertyInformationProcess检测ProcessBasicInformation得到一个数据结构。通过这个数据结构可以得到进程ID,进一步通过PID创建进程快照得到父进程的PID从而进一步得到父进程的用户名。通过判断父进程的用户名是不是调试器而进行反调试。

#include <Windows.h>
#include <winternl.h>
#include <TlHelp32.h>
typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
_In_ HANDLE ProcessHandle,
_In_ UINT ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
); const UINT uProcessBasicInformation = 0; DWORD GetParentPID(DWORD dwPid);
BOOL GetExeNameByPID(char * szExeName, DWORD dwPid);
int main(int argc, char* argv[])
{
char szExeName[256] = {0};
DWORD dwParent;
NTSTATUS stNtstatus;
pfnNtQueryInformationProcess NtQueryInformationProcess;
PROCESS_BASIC_INFORMATION stProcessBasicInformation; NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess")); stNtstatus = NtQueryInformationProcess(
GetCurrentProcess(),
uProcessBasicInformation,
&stProcessBasicInformation,
sizeof(PROCESS_BASIC_INFORMATION),
NULL);
dwParent = GetParentPID(stProcessBasicInformation.UniqueProcessId);
GetExeNameByPID(szExeName, dwParent); if(0x00000000 == stNtstatus && (lstrcmp(szExeName, "explorer.exe") && lstrcmp(szExeName, "cmd.exe") && lstrcmp(szExeName, "services.exe")))
{ MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
ExitProcess(NULL);
} MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
return 0;
} //获取父进程的PID
DWORD GetParentPID(DWORD dwPid)
{
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32); if(hProcessSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcessSnap);
return 0;
} if(!Process32First(hProcessSnap, &pe32))
{
CloseHandle(hProcessSnap);
return 0; }
do{ if(pe32.th32ProcessID == dwPid)
break;
}while(Process32Next(hProcessSnap, &pe32));
return pe32.th32ParentProcessID; } //获取对应PID进程的程序名称
BOOL GetExeNameByPID(char szExeName[], DWORD dwPid)
{
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32); if(hProcessSnap == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcessSnap);
return 0;
} if(!Process32First(hProcessSnap, &pe32))
{
CloseHandle(hProcessSnap);
return 0; }
do{ if(pe32.th32ProcessID == dwPid)
{ lstrcpy(szExeName, pe32.szExeFile);
break;
} }while(Process32Next(hProcessSnap, &pe32));
return 1;
}

HideFromDebugger

被调试进程通过调试端口DebugPort与调试子系统通信进一步与调试器通信。如果我们能将DebugPort清除就可以阻止进程与调试器通信达到反调试的目的,但是DebugPort一旦被创建就无法被在设置,所以我们这种思路走不通。

我们发现进程如果产生异常,其会检测ETHREAD的HideFromDebugger成员是否为“FALSE”,如果是就会向当前进程的调试端口发送调试事件消息(为"TRUE"表示该异常对用户调试器不可见)。我们可以通过ZwSetInformationThread()设置HideFromDebugger字段的值为TRUE从而令用户调试器接收不到调试事件,达到反调试的目的。

#include <winternl.h>
typedef NTSTATUS (NTAPI *pfnZwSetInformationThread)(
_In_ HANDLE ThreadHandle,
_In_ UINT ThreadInformationClass,
_In_ PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
); UINT uThreadHideFromDebugger = 0x11; int main(int argc, char * argv[])
{
NTSTATUS stNtstatus;
pfnZwSetInformationThread ZwSetInformationThread; ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("ZwSetInformationThread")); ZwSetInformationThread(
GetCurrentThread(),
uThreadHideFromDebugger,
0,
0); return 0;
}

NtCreateThreadEx

通过CreateThread( )的时候利用THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则在创建该线程时,它将对调试器隐藏。

//通过CreateThread( )的时候利用THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则在创建该线程时,它将对调试器隐藏。
//#include <Windows.h>
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx) (
_Out_ PHANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ PVOID ObjectAttributes, //POBJECT_ATTRIBUTES
_In_ HANDLE ProcessHandle,
_In_ PVOID StartRoutine,
_In_opt_ PVOID Argument,
_In_ ULONG CreateFlags,
_In_opt_ ULONG_PTR ZeroBits,
_In_opt_ SIZE_T StackSize,
_In_opt_ SIZE_T MaximumStackSize,
_In_opt_ PVOID AttributeList
);
void ThreadProc(); int main(int argc, char* argv[])
{
HANDLE hThread;
pfnNtCreateThreadEx NtCreateThreadEx;
NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtCreateThreadEx"));
NtCreateThreadEx(
&hThread,
0x1FFFFF,
NULL,
GetCurrentProcess(),
(LPTHREAD_START_ROUTINE)ThreadProc,
NULL,
0x00000004, //直接执行并且对调试器隐藏THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
NULL,
NULL,
NULL,
NULL
); WaitForSingleObject(hThread,INFINITE);
int n = GetLastError();
return 0;
}
//线程回调函数
void ThreadProc()
{
MessageBox(NULL, TEXT("我是一个新线程!"), NULL, MB_OK);
}

参考资料: 看雪学院《加密解密》

张银奎《软件调试》

https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software

Windows反调试技术(上)的更多相关文章

  1. Windows 反调试技术——OpenProcess 权限过滤 - ObRegisterCallback

    转载: https://blog.xpnsec.com/anti-debug-openprocess/ 看雪翻译:https://bbs.pediy.com/thread-223857.htm 本周我 ...

  2. Windows反调试技术(下)

    OD的DBGHELP模块 检测DBGHELP模块,此模块是用来加载调试符号的,所以一般加载此模块的进程的进程就是调试器.绕过方法也很简单,将DBGHELP.DLL改名. #include <Wi ...

  3. 反调试技术常用API,用来对付检测od和自动退出程序

    在调试一些病毒程序的时候,可能会碰到一些反调试技术,也就是说,被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编啦之类的方法破解自己.为了了解如何破解反调试技术 ...

  4. 基于TLS的反调试技术

    TLS(Thread Local Storage 线程局部存储) 一个进程中的每个线程在访问同一个线程局部存储时,访问到的都是独立的绑定于该线程的数据块.在PEB(进程环境块)中TLS存储槽共64个( ...

  5. Windows Kernel Way 1:Windows内核调试技术

    掌握Windows内核调试技术是学习与研究Windows内核的基础,调试Windows内核的方式大致分为两种: (1)通过Windbg工具在Windows系统运行之初连接到Windows内核,连接成功 ...

  6. Linux下的反调试技术

    Linux下的反调试技术 2014年01月30日 ⁄ 综合 ⁄ 共 2669字 ⁄ 字号 小 中 大 ⁄ 评论关闭 转自  http://wangcong.org/blog/archives/310 ...

  7. Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码) (转)

    1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...

  8. Delphi_OD_代码_调试_Delphi反调试技术(以OD为例附核心原代码)

    1.程序窗口[chuang kou]句柄[ju bing]检测原理:用FindWindow函数[han shu]查找[cha zhao]具有相同窗口[chuang kou]类名和标题的窗口[chuan ...

  9. 反调试技术(Delphi版)

    1.程序窗口句柄检测原理:用FindWindow函数查找具有相同窗口类名和标题的窗口,如果找到就说明有OD在运行//****************************************** ...

随机推荐

  1. 设置beeline连接hive的数据展示格式

    问题描述:beeline -u 方式导出数据,结果文件中含有"|"(竖杠). 执行的sql为:beeline -u jdbc:hive2://hadoop1:10000/defau ...

  2. VUE移动端音乐APP学习【四】:scroll组件及loading组件开发

    scroll组件 制作scroll 组件,然后嵌套一个 DOM 节点,使得该节点就能够滚动.该组件中需要引入 BetterScroll 插件. scroll.vue: <template> ...

  3. 输出质数(Java)

    输出质数 一.什么是质数 质数又称素数.一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数,否则称为合数(规定1既不是质数也不是合数). 二.代码实现 1.输出100以内的质数 i ...

  4. ABP 适用性改造 - 精简 ABP CLI 生成的项目结构

    Overview 不管是公司或者个人都会有不同的开发习惯,通过建立项目模板,既可以使开发人员聚焦于业务功能的开发,也可以在一定程度上统一不同开发人员之间的开发风格.在使用 ABP 框架的过程中,对于 ...

  5. docker部署kafka集群

    利用docker可以很方便的在一台机子上搭建kafka集群并进行测试.为了简化配置流程,采用docker-compose进行进行搭建. kafka搭建过程如下: 编写docker-compose.ym ...

  6. 你要 if 还是 case 呢?-- Shell十三问<第十二问>

    你要 if 还是 case 呢?-- Shell十三问<第十二问> 还记得我们在第 10 章所介绍的 return value 吗? 是的,接下来介绍的内容与之有关,若你的记忆也被假期的欢 ...

  7. 如何快速在odoo中创建自己的菜单

    上一篇内容:如何快速创建odoo模块,使用脚手架快速创建自己的odoo应用app 前言 在上一个教程中,我们已经实现了对模块的创建,现在我们要对创建的模块添加内容,这个教程将教你如何在odoo菜单中创 ...

  8. 如何在 NET 程序万种死法中有效的生成 Dump (上)

    一:背景 相信很多人都知道通过 任务管理器 抓取dump,虽然简单粗暴,但无法满足程序的无数种死法,比如: 内存膨胀,程序爆炸 CPU爆高,程序累死 应用无响应,用户气死 意外退出,和人生一样 既然手 ...

  9. JavaScript深入理解-Set、Map、WeakSet和WeakMap

    Set Set 对象允许储存任何类型的唯一值,无论是原始值或者是对象引用 本质:构造函数,用来生成 Set 数据结构 描述 Set 对象是值的集合,你可以按照插入的顺序迭代它的元素.Set 中的元素只 ...

  10. 安装maven工程报错"Failed to execute goal on project...Could not resolve dependencies for project..."

    我在qingcheng_interface中Lifecycle目录下执行install命令后报错"Failed to execute goal on project...Could not ...