目录
4.3 使用NtQueryInformationProcess
一、反调试技术概览
一般的exe程序可以放到onlydbg等调试器里面进行汇编级别的调试来进行破解,为了不让被别人利用调试手段进行各种分析,这就使用到了反调试手段。反调试技术归属于高级逆向分析技术领域,它的涵盖范围特别广,基本上随便哪种技术,只要经过一番精心设计,都能够变成反调试技术。
反调试技术的关键想法,就是要阻挡逆向工作的人去分析自己的代码。要是没办法在技术上把分析的人彻底拦住,那就想办法从精神层面让他们觉得太难,主动放弃。
所以,对于刚接触逆向分析的新手来说,最管用的反调试手段,就是采用他们不了解的技术,用这个办法来阻碍他们分析程序。开发这类反调试技巧,重点在于找出程序调试时和正常运行时不一样的地方,然后利用这些差异。
但对于逆向分析的高手,就只能从消耗他们的耐心、加大逆向分析难度这个方向去做。开发这类反调试技巧的核心思路是,找到多种完成同一件事的办法,然后随机把这些方法组合起来用,这样就能提高他们分析的成本。
二、什么是反反调试
反反调试是与反调试相对的概念,是针对软件中反调试技术所采取的应对措施,目的是绕过或破解软件里的反调试机制,从而顺利地对软件进行调试和逆向分析。
反调试与反反调试对比
- 反调试:是软件开发者为了保护软件的代码逻辑、知识产权、防止软件被破解、篡改或逆向分析而采用的一系列技术手段。例如检测调试器的存在、修改程序运行环境以干扰调试过程等。
- 反反调试:则是逆向分析人员为了突破这些反调试手段,继续对软件进行深入分析而采取的技术和方法。
常见反反调试方法
- 手动分析与暴力破解:逆向分析人员通过仔细研究软件的反调试代码,找到关键的判断条件和跳转逻辑,然后使用十六进制编辑器直接修改二进制代码,绕过反调试的检查。例如,若软件通过检查某个特定的标志位来判断是否处于调试状态,分析人员可以直接修改该标志位的值。
- Hook技术:通过Hook(钩子)技术拦截和修改软件中与反调试相关的函数调用。例如,当软件调用检测调试器的API函数时,Hook该函数并修改其返回值,使其返回表示未被调试的结果,从而绕过反调试检查。
- 调试器插件与脚本:使用调试器(如WinDbg、IDA等)的插件或编写自定义脚本来自动处理反调试机制。这些插件或脚本可以自动识别和绕过常见的反调试手段,提高调试效率。
- 动态调试技巧:采用一些特殊的调试技巧,如动态修改内存、设置条件断点等,来绕过反调试机制。例如,在软件执行反调试检查之前设置断点,在断点处修改相关的寄存器或内存值,从而绕过检查。
反反调试的应用场景
- 软件安全研究:安全研究人员需要对恶意软件或存在安全漏洞的软件进行调试和分析,以了解其工作原理和攻击方式。反反调试技术可以帮助他们突破软件的反调试保护,深入分析软件的内部逻辑。
- 软件破解与汉化:在软件破解和汉化过程中,需要对软件进行逆向分析和修改。反反调试技术可以帮助破解者绕过软件的反调试机制,实现对软件的破解和汉化。
- 软件漏洞挖掘:在进行软件漏洞挖掘时,需要对软件进行调试和分析,以发现潜在的安全漏洞。反反调试技术可以帮助漏洞挖掘者突破软件的反调试保护,顺利进行漏洞挖掘工作。
三、什么是静态反调试
特点
通常在调试刚开始的时候对调试者进行阻拦。调试者一旦找到阻拦的原因,就可以一次性突破这种反调试手段。
涉及元素
(1)PEB(进程环境块)
- BeginDebug:用于标记程序是否处于调试状态的标记位
- Ldr:反映内存状态的相关信息
- Heap(Flags, ForceFlags):堆状态,通过Flags和ForceFlags这两个属性来体现
- NtGlobalFlag:属于内核全局标记
(2)TEB(线程环境块)
- StaticUnicodeString:静态缓冲区
原始API的使用
(1)NtQueryInformationProcess():这是一个能获取PEB地址的API
-ProcessDebugPort(0x07):用它来获取调试端口
- ProcessDebugObjectHandle(0x1E):用于获取调试句柄
- ProcessDebugFlag(0x1F):可获取调试标记
(2)NtQuerySystemInformation()
- SystemKernelDebuggerInformation(0x23):能够获取系统调试状态(针对双机调试情况)
(3)NtQueryObject():可用于遍历系统内核对象
针对调试器的攻击手段
(1)NtSetInformationThread()
- ThreadHideFormDebugger(0x11):通过这个选项来对调试器进行干扰,让调试器难以追踪线程
其他反调试方式
(1)打开进程检查:借助SeDebugPrivilege权限,检查进程是否具备调试权限
(2)利用TLS回调函数:利用TLS(线程本地存储)回调函数来实现反调试相关操作
(3)使用普通API
- 父进程检查:检查当前进程的父进程情况来判断是否处于调试环境
- 窗口名检查:查看窗口名称是否符合调试环境特征
- 进程名遍历:遍历进程名来排查调试相关进程
- 文件名及文件路径检查:检查程序涉及的文件名和文件路径是否有被调试的迹象
- 注册表检查:通过检查注册表相关项来判断是否被调试
四、什么是动态反调试
特点
动态反调试通常是在调试进行当中对调试者进行阻拦。它在调试进程里能够频繁地被触发,所以调试者得时刻留意着,不然很容易被干扰到。
具体技术
(1)借助SEH(结构化异常处理)
- 异常:利用异常机制来干扰调试。
- 断点:通过设置特定断点情况影响调试。
- SetUnhandleedExceptionFilter() :使用这个函数来处理未处理的异常,干扰调试流程。
(2)时间检查:通过RDTSC这条汇编指令,读取时间戳计数器里的内容,以此来检测调试相关行为。
(3)单步检查:对程序单步执行的情况进行检查,判断是否存在调试操作。
(4)补丁检查
- 0xCC扫描:按照特定规则在程序里扫描0xCC这个值,如果扫描到,就很可能是被设置了断点。
- Hash扫描:对关键代码段的Hash值进行扫描,要是Hash值和预期不一致,那就说明代码在运行时被修改或者正在被调试。
- API断点扫描:检查API的第一个字节是不是0xCC,是的话就证明程序正处于被调试状态。
(5)反反汇编
- 指令截断:把一条指令截断,这样反汇编引擎在解析后续指令时就会出错。
- 指令混淆:将敏感的指令块拆分成好多块,或者变成若干条没关联的指令,让调试者摸不着头脑。
- 指令膨胀:把一条指令扩展成几十条指令,但这些指令整体行为和原来那一条指令是一样的,增加调试难度。
- 代码乱序:在内存层面把原来代码的执行顺序打乱,再用jmp指令把它们链接起来,干扰调试者分析。
(6)偷取代码(Stolen Code)
- 偷取OEP代码:把OEP(程序入口点)代码挪到别的地方,并且进行加密后再执行。
- 偷取API代码:把API的部分起始代码移到其他位置。
(7)分页保护:在程序运行时对分页进行保护,修改代码以及数据段的保护属性,让分析工作难以进行。
(8)壳
- 压缩壳:和OEP加密搭配起来,使得调试者很难顺利完成逆向分析。
- 加密壳:往程序里添加各种各样的反调试手段,干扰调试者的分析过程。
(9)虚拟机
- API虚拟机:把一部分常用的API放到虚拟机里进行模拟执行。
- 指令级虚拟机:能够把任意一段指令放到虚拟机中进行保护 。
五、静态反调试示例
4.1 使用PEB
可以通过两种方式来查看BegingDebugged标记位的情况。一种是利用FS寄存器来访问这个标记位,另一种则是借助API函数IsDebuggerPresent() 进行检查。要是BegingDebugged标记位的值为1 ,那就说明当前这个进程正处于被调试的状态。
bool PEB_BegingDebugged() {
bool BegingDebugged = false;
__asm {
MOV EAX,DWORD PTR FS:[0x30]; //获取PEB地址
MOV AL,BYTE PTR DS:[EAX+0x02];//获取PEB.BegingD...
MOV BegingDebugged,AL;
}
return BegingDebugged;
}
正常情形下,ProcessHeap的Flags值应该是2,ForceFlags值应该是0 。当程序处于调试状态时,这两个值就会发生变化。只是要注意,在NT 5.x及以上版本中,依靠判断这两个值的变化来检测程序是否处于调试状态的方法就不管用了。
bool PEB_ProcessHeap() {
int Flags = 0;
int ForceFlags = 0;
__asm {
PUSHAD;
MOV EAX,DWORD PTR FS:[0x30]; //获取PEB地址
MOV EAX,DWORD PTR DS:[eax+0x18];// 获取PEB.ProcesHeap
MOV EBX,DWORD PTR DS:[eax+0x0C];// 获 取 PEB.Pro...p.Flags
MOV ECX,DWORD PTR DS:[eax+0x10];// 获 取 PEB.Pro...p.Forc...
MOV Flags,EBX;
MOV ForceFlags,ECX;
POPAD;
}
return (Flags==2&&ForceFlags==0)?false:true;
}
当当前进程处于调试状态时,NtGlobalFlag的值会是0x70 。所以,我们可以通过查看这个值,来判断程序是不是正在被调试。
想要实现反反调试,办法很简单,只要在程序启动之后,把相关的值改成正常状态下的值就行。这些值正常时分别是:
- PEB.BegingDebugged 的值为0
- PEB.ProcessHeap.Flags 的值为2
- PEB.ProcessHeap.ForceFlags 的值为0
- PEB.NtGlobalFlag 的值为0
简单综合示例:
#include "stdafx.h"
#include <windows.h>
bool CheckDebug()
{
bool bDebuged = false;
_asm push eax;
_asm mov eax, fs:[0x30];// eax保存PEB首地址
_asm mov al,byte ptr ds:[eax + 2];//BeginDebug字段的值
_asm mov bDebuged, al;
_asm pop eax
return bDebuged;
}
void Test(char chFlag)
{
__try
{
int a = chFlag;
int b = 1/a;
}
//定义异常处理模块
__except (EXCEPTION_EXECUTE_HANDLER)
{
exit(0);
}
}
LONG WINAPI ueh(EXCEPTION_POINTERS* pExcept) {
exit(0);
return EXCEPTION_CONTINUE_SEARCH;
}
int _tmain(int argc, _TCHAR* argv[])
{
SetUnhandledExceptionFilter(ueh);
bool bDebug = CheckDebug();
int IsDebugger = IsDebuggerPresent()-1;
if (bDebug )
{
MessageBox(NULL, L"正在被调试", L"注意", 0);
}
else
{
MessageBox(NULL, L"现在很安全", L"恭喜", 0);
}
/*
表示还有很多代码
*/
Test(IsDebugger);
/*
表示还有很多代码
*/
MessageBox(NULL, L"现在很安全", L"恭喜", 0);
return 0;
}
4.2 使用IsDebuggerPresent
#include "stdafx.h"
#include <windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
bool bDebug = IsDebuggerPresent();//它使用的方式,和上一个是一样的
if (bDebug)
{
MessageBox(NULL, L"正在被调试", L"注意", 0);
}
else
{
MessageBox(NULL, L"现在很安全", L"恭喜", 0);
}
return 0;
}
4.3 使用NtQueryInformationProcess
NtQueryInformationProcess()这个函数比较特殊,它能够在RO和R3这两个环境下都能运行。它的主要用途就是查看跟进程有关的各种信息。
由于要查看的信息种类不一样,所以得给它的第二个参数ProcessInformationClass设置不同的值。从ProcessInformationClass所涵盖的类别来看,通过这个函数差不多能查看60多种进程相关的信息。
具体功能
- ProcessDebugPort:利用这个功能,可以获取目标进程的调试端口。要是目标进程没有处在调试状态,那这个调试端口就是0;要是正在被调试,调试端口就是0xFFFFFFFF。
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
bool NQIP_ProcessDebugPort(){
int nDebugPort = 0;
NtQueryInformationProcess( GetCurrentProcess(),
ProcessDebugPort,
&nDebugPort,
sizeof(nDebugPort), NULL);
return nDebugPort==0xFFFFFFFF?true :false;
}
- ProcessDebugObjectHandle:通过它可以得到目标进程的调试对象句柄。要是目标进程没在调试状态,那么获取到的句柄值就是NULL 。
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
bool NQIP_ProcessDebugObjectHandle() {
HANDLE hProcessDebugObjectHandle = 0;
NtQueryInformationProcess(
GetCurrentProcess(), //目标进程句柄
(PROCESSINFOCLASS)0x1E, //查询信息类型
&hProcessDebugObjectHandle, //输出查询信息
sizeof(hProcessDebugObjectHandle),//查询类型大小
NULL); //实际返回大小
return hProcessDebugObjectHandle?true :false;
}
- ProcessDebugFlag:可获取目标进程的调试标记。若处于调试状态,其值为0,否则为1。
#include<winternl.h>
#pragma comment(lib,"ntdll.lib")
bool NQIP_ProcessDebugFlag() {
BOOL bProcessDebugFlag = 0;
NtQueryInformationProcess(
GetCurrentProcess(),
(PROCESSINFOCLASS)0x1F,
&bProcessDebugFlag,
sizeof(bProcessDebugFlag),
NULL);
return bProcessDebugFlag?false :true;
}
- ProcessBasicInformation:用它能够获取到指定进程的父进程的PID。我们可以把得到的这个父进程PID,和Explorer.exe的PID作比较。要是两者不相符,那就说明这个进程不是通过双击操作启动运行的。
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
bool NQIP_CheckParentProcess() {
struct PROCESS_BASIC_INFORMATION {
ULONG ExitStatus; //进程返回码
PPEB PebBaseAddress; //PEB地址
ULONG AffinityMask; //CPU亲和性掩码
LONG BasePriority; //基本优先级
ULONG UniqueProcessId; //本进程PID
ULONG InheritedFromUniqueProcessId;//父进程PID
}stcProcInfo;
NtQueryInformationProcess(GetCurrentProcess(),
ProcessBasicInformation,
&stcProcInfo,
sizeof(stcProcInfo),NULL);
DWORD ExplorerPID=0;
DWORD CurrentPID = stcProcInfo.InheritedFromUniqueProcessId;
GetWindowThreadProcessId(FindWindow(L"Progman",NULL),&ExplorerPID);
return ExplorerPID==CurrentPID?false :true;
}
破解方法
破解函数反调试最有效的办法,是先找到具体函数调用的地方,分析函数里的条件跳转情况,然后直接暴力破解,或者用Hook的方法过滤函数返回的信息。
在调试的时候,要做好上面这些事,就得记住各个功能号(也就是第二个参数传入的值)是干啥的:
- 0x07:用来获取调试端口(ProcessDebugPort)
- 0x1E:用来获取调试句柄(ProcessDebugObjectHandle)
- 0x1F:用来获取调试标记(ProcessDebugFlag)
4.4 检查系统是否处于调试状态
可以用NtQuerySystemInformation()这个函数,来看看当前系统有没有开启调试模式。
#include "stdafx.h"
#include <windows.h>
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
bool CheckDebug()
{
struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION {
BOOLEAN DebuggerEanbled;
BOOLEAN DebuggerNotPresent;
}DebuggerInfo = { 0 };
NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)0x23,
&DebuggerInfo,
sizeof(DebuggerInfo),
NULL);
//能够检测当前操作系统是否处于调试模式,
//处于调试模式,可能当前正在进行内核调试(Windbg);
return DebuggerInfo.DebuggerEanbled;
}
int _tmain(int argc, _TCHAR* argv[])
{
bool bDebug = CheckDebug();
if (bDebug)
{
MessageBox(NULL, L"正在被调试", L"注意", 0);
}
else
{
MessageBox(NULL, L"现在很安全", L"恭喜", 0);
}
return 0;
}
4.5 使用NtQueryObject
#include "stdafx.h"
#include <windows.h>
#include <winternl.h>
#pragma comment(lib,"ntdll.lib")
bool NQO__NtQueryObject()
{
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfHanders;
ULONG TotalNumberOfObjects;
}OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_INFORMATION {
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInfo[1];
}OBJECT_ALL_INFORMATION, *POBJECT_ALL_INFORMATION;
// 1. 获取欲查询信息大小
ULONG uSize = 0;
NtQueryObject(NULL,
(OBJECT_INFORMATION_CLASS)0x03,
&uSize,
sizeof(uSize),
&uSize);
// 2. 获取对象信息
POBJECT_ALL_INFORMATION pObjectAllInfo = (POBJECT_ALL_INFORMATION)new BYTE[uSize + 200];
NtQueryObject(NULL,
(OBJECT_INFORMATION_CLASS)0x03,
pObjectAllInfo,
uSize,
&uSize);
// 3. 循环遍历并处理对象信息
POBJECT_TYPE_INFORMATION pObjTypeInfo = pObjectAllInfo->ObjectTypeInfo;
for (int i = 0;
i < pObjectAllInfo->NumberOfObjectsTypes;
i++)
{
// 3.1 查看此对象的类型是否为DebugObject,还需要判断对象的数量,大于0则说明有调试对象
if (!wcscmp(L"DebugObject", pObjTypeInfo->TypeName.Buffer))
return true;
// 3.2 获取对象名占用空间的大小(考虑到了结构体对齐问题)
ULONG uNameLength = pObjTypeInfo->TypeName.Length;
ULONG uDataLength = uNameLength - uNameLength%sizeof(ULONG) + sizeof(ULONG);
// 3.3 指向下一个对象信息
pObjTypeInfo = (POBJECT_TYPE_INFORMATION)pObjTypeInfo->TypeName.Buffer;
pObjTypeInfo = (POBJECT_TYPE_INFORMATION)((PBYTE)pObjTypeInfo + uDataLength);
}
delete[] pObjectAllInfo;
return false;
}
int _tmain(int argc, _TCHAR* argv[])
{
bool bDebug = NQO__NtQueryObject();
if (bDebug)
{
MessageBox(NULL, L"正在被调试", L"注意", 0);
}
else
{
MessageBox(NULL, L"现在很安全", L"恭喜", 0);
}
return 0;
}
4.6 使用 ZwSetInformationThread
利用这个函数,能够主动和调试器断开联系,让程序与调试器之间不再有调试关联,这样就能达到反调试的效果 。
#include "stdafx.h"
#include <windows.h>
typedef enum THREAD_INFO_CLASS{
ThreadHideFromDebugger = 17
};
typedef NTSTATUS(NTAPI *ZW_SET_INFORMATION_THREAD)(
IN HANDLE ThreadHandle,
IN THREAD_INFO_CLASS ThreadInformaitonClass,
IN PVOID ThreadInformation,
IN ULONG ThreadInformationLength);
void ZSIT_DetachDebug()
{
ZW_SET_INFORMATION_THREAD Func;
Func = (ZW_SET_INFORMATION_THREAD)GetProcAddress(
LoadLibrary(L"ntdll.dll"), "ZwSetInformationThread");
//攻击调试器,将本进程和调试器分离。
Func(GetCurrentThread(), ThreadHideFromDebugger, NULL, NULL);
}
int _tmain(int argc, _TCHAR* argv[])
{
ZSIT_DetachDebug();
MessageBox(0, 0, 0, 0);
return 0;
}
4.7 指令混淆
#include "stdafx.h"
#include <windows.h>
void _declspec(naked) fun()
{
_asm push 0; // 6A 00
_asm jmp hehe; // EB 02
_asm __emit 0xE8;// E8
_asm __emit 0x12;// 12
hehe:
_asm push 0; // 6A 00
_asm jmp haha;// EB 02
_asm __emit 0x0f;// 0F
_asm __emit 0x15;// 15
haha:
_asm push 0;
_asm jmp heihei;
_asm __emit 0x56;
_asm __emit 0x78;
heihei:
_asm push 0;
_asm call MessageBoxA;
_asm ret;
}
int _tmain(int argc, _TCHAR* argv[])
{
fun();
return 0;
}
这段代码通过指令混淆增加了代码的逆向分析难度,同时实现了弹出一个消息框的功能,但是指令混淆反调试效果有限。