写在前面

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

BeingDebuged

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

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

  1. int main(int argc, char *argv[])
  2. {
  3. DWORD dwBeingDebuged;
  4. TCHAR szTest[] = TEXT("已检测到调试器!");
  5. TCHAR szSuccess[] = TEXT("运行正常!");
  6. dwBeingDebuged = IsDebuggerPresent();
  7. if(0 != dwBeingDebuged)
  8. {
  9. MessageBox(NULL, szTest, NULL, MB_OK);
  10. ExitProcess(NULL);
  11. }
  12. MessageBox(NULL, szSuccess, NULL, MB_OK);
  13. return 0;
  14. }

NtGlobalFlag

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

  1. int main(int argc, char *argv[])
  2. {
  3. DWORD dwNtGlobalFlag;
  4. TCHAR szTest[] = TEXT("已检测到调试器!");
  5. TCHAR szSuccess[] = TEXT("运行正常!");
  6. dwNtGlobalFlag = _AntiDebug();
  7. if(0 != dwNtGlobalFlag)
  8. {
  9. MessageBox(NULL, szTest, NULL, MB_OK);
  10. ExitProcess(NULL);
  11. }
  12. MessageBox(NULL, szSuccess, NULL, MB_OK);
  13. return 0;
  14. }
  15. DWORD _AntiDebug()
  16. {
  17. _asm{
  18. mov eax,fs:[0x30]
  19. mov eax,[eax + 0x68]
  20. and eax,0x70
  21. }
  22. }

ProcessHeap的Flags和ForceFlags

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

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

  1. int main(int argc, char *argv[])
  2. {
  3. DWORD dwReturn;
  4. TCHAR szTest[] = TEXT("已检测到调试器!");
  5. TCHAR szSuccess[] = TEXT("运行正常!");
  6. dwReturn = _AntiDebug();
  7. if(0 != dwReturn)
  8. {
  9. MessageBox(NULL, szTest, NULL, MB_OK);
  10. ExitProcess(NULL);
  11. }
  12. MessageBox(NULL, szSuccess, NULL, MB_OK);
  13. return 0;
  14. }
  15. DWORD _AntiDebug()
  16. {
  17. _asm{
  18. push ebx
  19. mov eax,fs:[0x30]
  20. mov eax,[eax + 0x18]
  21. mov ebx,dword ptr [eax + 0x40]
  22. and ebx,0x00000002
  23. cmp ebx,0
  24. je end0
  25. mov ebx,dword ptr [eax + 0x44]
  26. cmp ebx,0
  27. jne end0
  28. xor eax,eax
  29. jmp end
  30. end0:
  31. mov eax,1
  32. end:
  33. pop ebx
  34. }
  35. }

Heap Magic

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

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

  1. int main(int argc, char *argv[])
  2. {
  3. DWORD dwNum = 0;
  4. PDWORD pHeap;
  5. TCHAR szTest[] = TEXT("已检测到调试器!");
  6. TCHAR szSuccess[] = TEXT("运行正常!");
  7. pHeap = (DWORD*)HeapAlloc(GetProcessHeap(), NULL, 0x100); //通过在自己的进程默认堆中申请内存得到堆中内存的地址,(还可以通过PEB的LDR_MODULE字段中得到这些标志)
  8. _try{
  9. for(;;){ //检测Heap Magic标志。
  10. switch(*pHeap++){
  11. case 0xABABABAB:
  12. case 0xBAADF00D:
  13. case 0xFEEEFEEE:
  14. dwNum++;
  15. break;
  16. }
  17. }
  18. }
  19. _except(EXCEPTION_EXECUTE_HANDLER){
  20. if(dwNum > 10)
  21. {
  22. MessageBox(NULL, szTest, NULL, MB_OK);
  23. ExitProcess(NULL);
  24. }
  25. }
  26. MessageBox(NULL, szSuccess, NULL, MB_OK);
  27. return 0;
  28. }

ProcessDebugPort

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

  1. int main(int argc, char *argv[])
  2. {
  3. BOOL bDebuggerPresent;
  4. TCHAR szTest[] = TEXT("已检测到调试器!");
  5. TCHAR szSuccess[] = TEXT("运行正常!");
  6. CheckRemoteDebuggerPresent(GetCurrentProcess(), &bDebuggerPresent);
  7. if(TRUE == bDebuggerPresent)
  8. {
  9. MessageBox(NULL, szTest, NULL, MB_OK);
  10. ExitProcess(NULL);
  11. }
  12. MessageBox(NULL, szSuccess, NULL, MB_OK);
  13. return 0;
  14. }

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

  1. typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)( //typedef后可以掩饰复合类型
  2. _In_ HANDLE ProcessHandle,
  3. _In_ UINT ProcessInformationClass,
  4. _Out_ PVOID ProcessInformation,
  5. _In_ ULONG ProcessInformationLength,
  6. _Out_opt_ PULONG ReturnLength
  7. );
  8. //表示检测进程的调试端口
  9. UINT ProcessDebugPort = 7;
  10. int main(int argc, char *argv[])
  11. {
  12. TCHAR szTest[] = TEXT("已检测到调试器!");
  13. TCHAR szSuccess[] = TEXT("运行正常!");
  14. DWORD dwDebuggerPresent = 0;
  15. NTSTATUS stNtstatus;
  16. pfnNtQueryInformationProcess NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
  17. stNtstatus = NtQueryInformationProcess(
  18. GetCurrentProcess(),
  19. ProcessDebugPort,
  20. &dwDebuggerPresent,
  21. sizeof(DWORD),
  22. NULL);
  23. if(0 != dwDebuggerPresent && stNtstatus == 0x00000000)
  24. {
  25. MessageBox(NULL, szTest, NULL, MB_OK);
  26. ExitProcess(NULL);
  27. }
  28. MessageBox(NULL, szSuccess, NULL, MB_OK);
  29. return 0;
  30. }

ProcessDebugObjectHandle

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

  1. typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
  2. _In_ HANDLE ProcessHandle,
  3. _In_ UINT ProcessInformationClass,
  4. _Out_ PVOID ProcessInformation,
  5. _In_ ULONG ProcessInformationLength,
  6. _Out_opt_ PULONG ReturnLength
  7. );
  8. UINT ProcessDebugObjectHandle = 0x1E;
  9. int main(int argc, char* argv[])
  10. {
  11. HANDLE hDebugHandle;
  12. NTSTATUS stNtstatus;
  13. pfnNtQueryInformationProcess NtQueryInformationProcess;
  14. NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
  15. stNtstatus = NtQueryInformationProcess(
  16. GetCurrentProcess(),
  17. ProcessDebugObjectHandle,
  18. &hDebugHandle,
  19. sizeof(HANDLE),
  20. NULL);
  21. if(0x00000000 == stNtstatus && NULL != isDebuggerPresent)
  22. {
  23. MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
  24. ExitProcess(NULL);
  25. }
  26. MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
  27. return 0;
  28. }

ProcessDebugFlags

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

  1. typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
  2. _In_ HANDLE ProcessHandle,
  3. _In_ UINT ProcessInformationClass,
  4. _Out_ PVOID ProcessInformation,
  5. _In_ ULONG ProcessInformationLength,
  6. _Out_opt_ PULONG ReturnLength
  7. );
  8. UINT ProcessDebugFlags = 0x1F;
  9. int main(int argc, char* argv[])
  10. {
  11. ULONG DebugFlags;
  12. NTSTATUS stNtstatus;
  13. pfnNtQueryInformationProcess NtQueryInformationProcess;
  14. NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
  15. stNtstatus = NtQueryInformationProcess(
  16. GetCurrentProcess(),
  17. ProcessDebugFlags,
  18. &DebugFlags,
  19. sizeof(ULONG),
  20. NULL);
  21. if(0x00000000 == stNtstatus && NULL == DebugFlags)
  22. {
  23. MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
  24. ExitProcess(NULL);
  25. }
  26. MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
  27. return 0;
  28. }

ProcessBasicInformation

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

  1. #include <Windows.h>
  2. #include <winternl.h>
  3. #include <TlHelp32.h>
  4. typedef NTSTATUS(NTAPI *pfnNtQueryInformationProcess)(
  5. _In_ HANDLE ProcessHandle,
  6. _In_ UINT ProcessInformationClass,
  7. _Out_ PVOID ProcessInformation,
  8. _In_ ULONG ProcessInformationLength,
  9. _Out_opt_ PULONG ReturnLength
  10. );
  11. const UINT uProcessBasicInformation = 0;
  12. DWORD GetParentPID(DWORD dwPid);
  13. BOOL GetExeNameByPID(char * szExeName, DWORD dwPid);
  14. int main(int argc, char* argv[])
  15. {
  16. char szExeName[256] = {0};
  17. DWORD dwParent;
  18. NTSTATUS stNtstatus;
  19. pfnNtQueryInformationProcess NtQueryInformationProcess;
  20. PROCESS_BASIC_INFORMATION stProcessBasicInformation;
  21. NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtQueryInformationProcess"));
  22. stNtstatus = NtQueryInformationProcess(
  23. GetCurrentProcess(),
  24. uProcessBasicInformation,
  25. &stProcessBasicInformation,
  26. sizeof(PROCESS_BASIC_INFORMATION),
  27. NULL);
  28. dwParent = GetParentPID(stProcessBasicInformation.UniqueProcessId);
  29. GetExeNameByPID(szExeName, dwParent);
  30. if(0x00000000 == stNtstatus && (lstrcmp(szExeName, "explorer.exe") && lstrcmp(szExeName, "cmd.exe") && lstrcmp(szExeName, "services.exe")))
  31. {
  32. MessageBox(NULL, TEXT("已检测到调试器!"),NULL, MB_OK);
  33. ExitProcess(NULL);
  34. }
  35. MessageBox(NULL, TEXT("程序正常运行!"), NULL, MB_OK);
  36. return 0;
  37. }
  38. //获取父进程的PID
  39. DWORD GetParentPID(DWORD dwPid)
  40. {
  41. HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  42. PROCESSENTRY32 pe32;
  43. pe32.dwSize = sizeof(PROCESSENTRY32);
  44. if(hProcessSnap == INVALID_HANDLE_VALUE)
  45. {
  46. CloseHandle(hProcessSnap);
  47. return 0;
  48. }
  49. if(!Process32First(hProcessSnap, &pe32))
  50. {
  51. CloseHandle(hProcessSnap);
  52. return 0;
  53. }
  54. do{
  55. if(pe32.th32ProcessID == dwPid)
  56. break;
  57. }while(Process32Next(hProcessSnap, &pe32));
  58. return pe32.th32ParentProcessID;
  59. }
  60. //获取对应PID进程的程序名称
  61. BOOL GetExeNameByPID(char szExeName[], DWORD dwPid)
  62. {
  63. HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  64. PROCESSENTRY32 pe32;
  65. pe32.dwSize = sizeof(PROCESSENTRY32);
  66. if(hProcessSnap == INVALID_HANDLE_VALUE)
  67. {
  68. CloseHandle(hProcessSnap);
  69. return 0;
  70. }
  71. if(!Process32First(hProcessSnap, &pe32))
  72. {
  73. CloseHandle(hProcessSnap);
  74. return 0;
  75. }
  76. do{
  77. if(pe32.th32ProcessID == dwPid)
  78. {
  79. lstrcpy(szExeName, pe32.szExeFile);
  80. break;
  81. }
  82. }while(Process32Next(hProcessSnap, &pe32));
  83. return 1;
  84. }

HideFromDebugger

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

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

  1. #include <winternl.h>
  2. typedef NTSTATUS (NTAPI *pfnZwSetInformationThread)(
  3. _In_ HANDLE ThreadHandle,
  4. _In_ UINT ThreadInformationClass,
  5. _In_ PVOID ThreadInformation,
  6. _In_ ULONG ThreadInformationLength
  7. );
  8. UINT uThreadHideFromDebugger = 0x11;
  9. int main(int argc, char * argv[])
  10. {
  11. NTSTATUS stNtstatus;
  12. pfnZwSetInformationThread ZwSetInformationThread;
  13. ZwSetInformationThread = (pfnZwSetInformationThread)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("ZwSetInformationThread"));
  14. ZwSetInformationThread(
  15. GetCurrentThread(),
  16. uThreadHideFromDebugger,
  17. 0,
  18. 0);
  19. return 0;
  20. }

NtCreateThreadEx

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

  1. //通过CreateThread( )的时候利用THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志,则在创建该线程时,它将对调试器隐藏。
  2. //#include <Windows.h>
  3. typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx) (
  4. _Out_ PHANDLE ThreadHandle,
  5. _In_ ACCESS_MASK DesiredAccess,
  6. _In_opt_ PVOID ObjectAttributes, //POBJECT_ATTRIBUTES
  7. _In_ HANDLE ProcessHandle,
  8. _In_ PVOID StartRoutine,
  9. _In_opt_ PVOID Argument,
  10. _In_ ULONG CreateFlags,
  11. _In_opt_ ULONG_PTR ZeroBits,
  12. _In_opt_ SIZE_T StackSize,
  13. _In_opt_ SIZE_T MaximumStackSize,
  14. _In_opt_ PVOID AttributeList
  15. );
  16. void ThreadProc();
  17. int main(int argc, char* argv[])
  18. {
  19. HANDLE hThread;
  20. pfnNtCreateThreadEx NtCreateThreadEx;
  21. NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(LoadLibrary(TEXT("ntdll.dll")), TEXT("NtCreateThreadEx"));
  22. NtCreateThreadEx(
  23. &hThread,
  24. 0x1FFFFF,
  25. NULL,
  26. GetCurrentProcess(),
  27. (LPTHREAD_START_ROUTINE)ThreadProc,
  28. NULL,
  29. 0x00000004, //直接执行并且对调试器隐藏THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER
  30. NULL,
  31. NULL,
  32. NULL,
  33. NULL
  34. );
  35. WaitForSingleObject(hThread,INFINITE);
  36. int n = GetLastError();
  37. return 0;
  38. }
  39. //线程回调函数
  40. void ThreadProc()
  41. {
  42. MessageBox(NULL, TEXT("我是一个新线程!"), NULL, MB_OK);
  43. }

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

张银奎《软件调试》

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. springboot源码解析-管中窥豹系列之BeanDefine如何加载(十三)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  2. Java进阶专题(二十六) 将近2万字的Dubbo原理解析,彻底搞懂dubbo

    前言 ​ 前面我们研究了RPC的原理,市面上有很多基于RPC思想实现的框架,比如有Dubbo.今天就从Dubbo的SPI机制.服务注册与发现源码及网络通信过程去深入剖析下Dubbo. Dubbo架构 ...

  3. python登陆界面尝试

    示例1: """ 编写一个程序 用户可以输入用户名和密码 用户有三次机会 登录成功可以进行相应的操作 输入Q退出系统 """ name_li ...

  4. Logtash遇到的异常和注意点

    1.Logtash遇到的异常和注意点 1.1 logstash使用kafka插件和es集成 如果logstash使用kafka插件和es集成,必须设置kafka插件参数 session_timeout ...

  5. 解决wampserver 服务无法启动

    如图左击选中apache的httpd.conf把文本中的80端口,改成未被占用的端口.

  6. [2020年10月28日普级组]1408.MSWORLD

    1408. M S W O R L D 1408.MSWORLD 1408.MSWORLD 题目描述 Bessie , Farmer John 的优选牛,刚刚获得了一个牛科动物选美比赛的冠军!并得到了 ...

  7. 学习笔记-vue 打包去#和页面空白问题

    文件资源路径是对的,但是页面空白.百度了很久找了一篇文章解决了. 1.vue项目中config文件下index.js中打包配置 build: { // Template for index.html ...

  8. C# Linq 延迟查询的执行

    在定义linq查询表达式时,查询是不会执行,查询会在迭代数据项时运行.它使用yield return 语句返回谓词为true的元素. var names = new List<string> ...

  9. 02 . MongoDB复制集,分片集,备份与恢复

    复制集 MongoDB复制集RS(ReplicationSet): 基本构成是1主2从的结构,自带互相监控投票机制(Raft(MongoDB)Paxos(mysql MGR 用的是变种)) 如果发生主 ...

  10. Object o = new Object()占多少个字节?-对象的内存布局

    一.先上答案 这个问题有坑,有两种回答 第一种解释: object实例对象,占16个字节. 第二种解释: Object o:普通对象指针(ordinary object pointer),占4个字节. ...