查看: 417|回复: 1

[转载图文] 由一道CTF对10种反调试的探究

[复制链接]
发表于 2020-5-7 20:54:26 | 显示全部楼层 |阅读模式

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究

  本文为看雪论坛优秀文章
  看雪论坛作者ID:0xDQ
  0x00 前言

最近做的有些ctf中总是出现一些反动态调试的情况。由此对一些常见的反动态调试进行一些总结。既然是调试,趁着这个机会探究了一下调试器如何与被调试进程建立联系的过程。
  
  参考文章:

  • https://blog.csdn.net/hgy413/article/details/7996652

  • https://blog.csdn.net/yiyefangzhou24/article/details/6242459

  • https://www.52pojie.cn/thread-883664-1-1.html

  • https://bbs.pediy.com/thread-223857.htm

  • http://bbs.pediy.com/showthread.php?t=31447

  • https://blog.csdn.net/qq_32400847/article/details/52798050



  0x01 CTF—wp
  先运行一下:
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  到ida里看一下:
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  发现4010D7的位置无缘无故跳到了4010DE,就是这块的问题,在x32dbg搜字符串定位,把4010D7~4010E0都nop掉。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  得到新的文件。重新载入ida。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  经过逆向分析首先经过10次反调试,只有在不被调试的情况下才能组成正确的str1,然后对flag普通base64加密,再进行奇偶位分别与str1与[0ff_404018^3]比较。
  反调试的函数主要在下面分析。
  
  脚本如下:
  
import base64
  str1 = "LKd8gPYWS["
  str2 = "2TVBnx0lnn"
  cipher = [0] * 20
  for i in range(10):
  cipher[2*i] = (ord(str1[i]) ^ 3) - 2
  cipher[2*i+1] = ord(str2[i])
  print''.join(map(chr,cipher))
  #M2FTeV9BbnQxX0RlNnVn
  end_cipher = 'M2FTeV9BbnQxX0RlNnVn'
  print"D0g3{"+end_cipher.decode("base64")+"}"
  #D0g3{3aSy_Ant1_De6ug}
  


  
  0x02 对反调试的探究
  上题中反调试的函数在接下来具体说明。
  

1. IsDebuggerPresent  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  用windbg看一下IsDebuggerPresent的反汇编:
  
kernel32!IsDebuggerPresent:
  7c813133 64a118000000    mov     eax,dword ptr fs:[00000018h]// fs寄存器在3环的时候指向TEB,而+18h偏移处指向teb的开头fs:003b:00000018=7ffdf000
  7c813139 8b4030                mov     eax,dword ptr [eax+30h]//+30h指向PEB
  


由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
7c81313c 0fb64002             movzx   eax,byte ptr [eax+2]//peb->BeingDebugged位来判断是否有调试器。
  


由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  本题第一次的判断直接判断的BeingDebugged不再赘述了。
  

2. CheckRemoteDebuggerPresent这个函数检查的是你获取的进程是否被另一个进程调试。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  看一下反汇编(下面截取了关键的部分):
  
7C85AA3C    50              push eax                           //eax里是 hProcess
  7C85AA3D    6A 07           push 7                            // 这里的7定义是 ProcessDebugPort
  7C85AA3F    FF75 08        push dword ptr SS:[ebp+8]                    //hProcess
  7C85AA42    FF15 AC10807C  call dword ptr DS:[<&ntdll.NtQueryInform> ]           //ntdll.NTQueryInformationProcess
  7C85AA48    85C0            test eax,eax                       //判断
  


  可以看出实际调了NtQueryInformationProcess,它可以将指定类型的进程信息拷贝到某个缓冲。
  
  函数原型如下:
  
NtQueryInformationProcess (
  IN HANDLE ProcessHandle, // 获取进程的句柄
  IN PROCESSINFOCLASS InformationClass, // 信息类型
  OUT PVOID ProcessInformation, // 缓冲区的指针
  IN ULONG ProcessInformationLength, // 缓冲区大小
  OUT PULONG ReturnLength OPTIONAL // 写入缓冲区的字节数
  );
  


  其中ProcessInformationClass中的ProcessDebugPort,它来检索调试器的端口号,只要是非0则有调试器。
  

3. SetLastError & OutputDebugStringA & GetLastError  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  OutputDebugStringA :它可以把调试信息输出到编译器的输出窗口,和调试器进行交谈,(还可以用DbgView来看),如果被调试,那么调用就会成功,SetLastError设置的“12345”就会被覆盖,那么GetLastError也不会成功得到“12345”,由此来检测是否被调试。

4. NtQueryInformationProcess  
上文提到了,这就不说了。

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  5. CloseHandle异常

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  Windows在执行异常处理时,无论是内核异常还是用户异常,在进行异常信息的“包装”后,都会到KiDispatchException进行异常的分发,下面逆了此函数的一部分。
  
  内核异常的分发(部分)
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  用户异常的分发(部分):

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  可以看出来,无论是用户异常还是内核异常,再进行VEH,SEH之前都会先判断是否用调试器,利用该特征可判断进程是正常运行还是调试运行,然后根据不同的结果执行不同来判断程序是否被调试。
  

6. DebugActiveProcess  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  先看一下这个函数:DebugActiveProcess会使调试器附加到获取的进程上并且调试它。此函数的唯一参数是进程的PID。
  
BOOL WINAPI DebugActiveProcess(
  __in DWORD dwProcessId//要被调试的进程标识PID
  );
  



以下是对调试器利用DebugActiveProcess的深究。  
调试器调试程序的时候,一种是直接打开进程,另一种就是用Attach通过附加的形式去调试,而后者利用的就是DebugActiveProcess,废话少说,看代码:

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  先进入kernel32!DbgUiConnectToDbg()这个函数,这里面调用的是ntdll里的DbgUiConnectToDbg(),我们把ntall.dll载到IDA里:
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  在进入ntdll!DbgUiConnectToDbg() 里的ntdll!ZwCreateDebugObject():
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  会发现进0环了,7FFE0300,没记错的话是SystemCall,程序由此进0环。

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  这个函数进0环就是为了创造对象--DebugObject (从ReactOS上找的),在0时无非就是添结构体,并且返回一个句柄。
  
typedef struct _DEBUG_OBJECT {
  KEVENT EventsPresent;
  FAST_MUTEX Mutex;
  LIST_ENTRY EventList;
  union
  {
  ULONG Flags;
  struct
  {
  UCHAR DebuggerInactive:1;
  UCHAR KillProcessOnExit:1;
  };
  }
  } DEBUG_OBJECT, *PDEBUG_OBJECT;
  


  关键是这个这个句柄在3环时放哪了。我们重新回到ntdll里的DbgUiConnectToDbg()。
  
  发现是存到了TEB+0xF24的地方,此时,DebugObject与调试器建立起了关系。
  
  (做反调试的话,可以遍历所有TEB+F24h的位置,如果有值,那肯定在被调试(嘴角疯狂上扬))
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  OK,差不多了,我们重新回到梦开始的地方DebugActiveProcess,往下看
  用到了传进去的参数PID,在下面转换成了被调试进程的句柄存到esi里,紧接着传入了 kernel32!DbgUiDebugActiveProcess(被调试进程句柄),此时,调试器和被调试建立起了联系。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  之后同样进入了ntdll里UiDebugActiveProcess(被调试进程句柄):

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  惊喜来了,创造对象--DebugObject 的句柄和被调试进程的句柄都传入了ntdll!NtDebugActiveProcess。跟进去之后同样通过SystemCall进NtDebugActiveProcess(0环),以下是NtDebugActiveProcess的逆向结果:

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  进入 _DbgkpSetProcessDebugObject:
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  建立上了调试关系。
  

7. GetStartupInfoA

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究

  在使用 CreateProcess 创建进程时,需要传递。STARTUPINFO 的结构的指针,而常常我们并不会一个一个设置其结构的值,连把其他不用的值清0都会忽略,而 ollydbg 也这样做了,我们可以使用 GetStartupInfo 检查启动信息,如果很多值为“不可理解”的,那么就说明自己不是由 explorer 来创建的。(explorer.exe 使用 shell32 中 ShellExecute 的来运行程序,ShellExecute 会清不用的值)还有一点 ollydbg 会向 STARTUPINFO 中的   dwFlags 设置,STARTF_FORCEOFFFEEDBACK,而 explorer 不会。
  
  这篇文章http://bbs.pediy.com/showthread.php?t=31447中要实现的想法,就是这个这道ctf设置的反调试,好巧。
  

8. 检测系统留下的痕迹  
进入sub_401580个函数:
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  
  这种反调试在ctf中很常见。
  
  首先CreateToolhelp32Snapshot照下快照,记录当前进程,线程信息
  利用process32First函数来获得第一个进程的句柄。
  
BOOL WINAPI Process32First(
  HANDLE hSnapshot,//_in
  LPPROCESSENTRY32 lppe//_out
  );
  


  PROCESSENTRY32结构为:
  
typedef struct tagPROCESSENTRY32 {
  DWORD dwSize; // 结构体大小;
  DWORD cntUsage; // 此进程的引用次数;
  DWORD th32ProcessID; // 进程PID;
  DWORD th32DefaultHeapID; // 进程默认堆ID;
  DWORD th32ModuleID; // 进程模块ID;
  DWORD cntThreads; // 此进程开启的线程次数;
  DWORD th32ParentProcessID;// 父进程PID;
  LONG pcPriClassBase; // 线程优先权;
  DWORD dwFlags;
  WCHAR szExeFile[MAX_PATH]; // 进程全名;
  } PROCESSENTRY32;
  


  可以看出本题比较的是szExeFile进程名称这一参数,来判断是否被调试。
  除了本题中查找进程信息,还可以查找窗体信息,和查找调试器引用的注册表项。
  
  查找调试器引用的注册表项:
  

下面是调试器在注册表中的一个常用位置。
  SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug(32位系统)SOFTWARE\Wow6432Node\Microsoft\WindowsNT\CurrentVersion\AeDebug(64位系统)该注册表项指定当应用程序发生错误时,触发哪一个调试器。默认情况下,它被设置为Dr.Watson。如果该这册表的键值被修改为OllyDbg,则恶意代码就可能确定它正在被调试。


  查找窗体信息:
  

FindWindow函数检索处理顶级窗口的类名和窗口名称匹配指定的字符串。
  EnumWindows函数枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数。



9. 时钟检测  
本题采用的是__rdtsc进行的检测。
  
  rdtsc指令将时间标签计数器读入 EDX:EAX。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  Windows系列操作系统的时间间隔10 - 20 毫秒,软件正常运行时的速度比我们分析代码时快得多,所以可以比较上下两句代码的时间戳,来判断程序是否被调试。
  

由一道CTF对10种反调试的探究

由一道CTF对10种反调试的探究
  0x03 总结
  因为一些反调试原理的本质是一样的,所以把一些反调试放到了一块说。本文因为是探究性质,所以会有很多汇编级的逆向,可以注意下在IDA里的注释说明。
  
  如有不正确的地方,还请路过的大牛斧正,希望我的总结可以帮助到看官。
游客,如果您要查看本帖隐藏内容请回复




温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046

0

主题

8

帖子

0

精华

初级会员

Rank: 4

学币
11
荣耀
0
rank
0
违规
0

    发表于 2020-5-12 17:33:30 | 显示全部楼层
    用心讨论,共获提升!
    学逆向论坛-免费的逆向学习论坛
    微信公众号
    快速回复 返回顶部 返回列表