本帖最后由 Aspark 于 2022-8-30 13:49 编辑
概述反调试、反反调试 两种技术同根同源,原理基本一致, 本篇文章将以内核的角度,探究 调试与未被调试情况下 EPROCESS、PEB下各个标志位的区别,认识简单反调试技术的基本原理,本文章讨论的是基础的反调试手段(通过标志位检测之类的),不讨论一些特殊的反调试手段(计时、检查内存、VT等)。文章最后以 VMP 3.6 作为例子,进行反反调试实战(只分析原理,不提供(不制作)反反调试插件)。
系统版本
 编辑
同上篇文章。(文章末尾提供PDF版本下载地址)
检测进程是否被调试基础EPROCESS(执行体进程块)
NT内核使用EPROCESS结构体来描述每一个进程,就像档案一样,EPROCESS结构内包含进程的各种信息,和相关结构的指针。
可以自行使用WinDbg查看EPROCESS结构体。例:(节选)
- 4: kd> ?? sizeof(_eprocess) //查看结构大小unsigned int64 0x8504: kd> dt _eprocess //查看结构nt!_EPROCESS
- +0x000 Pcb : _KPROCESS //进程控制块 +0x2d8 ProcessLock : _EX_PUSH_LOCK
- +0x2e0 UniqueProcessId : Ptr64 Void
- +0x2e8 ActiveProcessLinks : _LIST_ENTRY
- +0x2f8 RundownProtect : _EX_RUNDOWN_REF
- +0x300 Flags2 : Uint4B
- +0x304 Flags : Uint4B
- +0x308 CreateTime : _LARGE_INTEGER
- ....//省略(减少篇幅) +0x3e0 InheritedFromUniqueProcessId : Ptr64 Void //父进程PID ....//省略(减少篇幅) +0x3f8 Peb : Ptr64 _PEB //进程环境块指针(Ring 3) ....//省略(减少篇幅) +0x420 DebugPort : Ptr64 Void //调试端口 ....//省略(减少篇幅) +0x838 ParentSecurityDomain : Uint8B
- +0x840 CoverageSamplerContext : Ptr64 Void
- +0x848 MmHotPatchContext : Ptr64 Void
复制代码 KPROCESS(进程控制块)(选读)KPROCESS也处于EPROCESS内,属于EPROCESS的一部分,EPROCESS前0x2d8字节的数据构成KPROCESS。KPROCESS的作用主要是提供给内核使用。(包含 分发器、调度器等),使用WinDbg查看,例:(节选) - 4: kd> dt _kprocess
- nt!_KPROCESS
- +0x000 Header : _DISPATCHER_HEADER
- +0x018 ProfileListHead : _LIST_ENTRY
- +0x028 DirectoryTableBase : Uint8B
- +0x030 ThreadListHead : _LIST_ENTRY
- ....//省略(减少篇幅) +0x26c KernelTime : Uint4B
- +0x270 UserTime : Uint4B
- +0x274 ReadyTime : Uint4B
- +0x278 UserDirectoryTableBase : Uint8B
- +0x280 AddressPolicy : UChar
- +0x281 Spare2 : [71] UChar
- +0x2c8 InstrumentationCallback : Ptr64 Void
- +0x2d0 SecureState : <unnamed-tag>
复制代码 PEB(进程环境块)PEB处于用户空间(用户模式下的虚拟地址),包含了进程大多数用户模式下的信息。使用WinDbg查看,例:(节选) - 4: kd> ?? sizeof(_peb) //查看peb大小unsigned int64 0x7c84: kd> dt _peb
- nt!_PEB
- +0x000 InheritedAddressSpace : UChar
- +0x001 ReadImageFileExecOptions : UChar
- +0x002 BeingDebugged : UChar //是否开始了调试 +0x003 BitField : UChar
- +0x004 Padding0 : [4] UChar
- +0x008 Mutant : Ptr64 Void
- +0x010 ImageBaseAddress : Ptr64 Void
- ....//省略(减少篇幅) +0x0bc NtGlobalFlag : Uint4B
- ....//省略(减少篇幅) +0x7b8 LeapSecondData : Ptr64 _LEAP_SECOND_DATA
- +0x7c0 LeapSecondFlags : Uint4B
- +0x7c0 SixtySecondEnabled : Pos 0, 1 Bit
- +0x7c0 Reserved : Pos 1, 31 Bits
- +0x7c4 NtGlobalFlag2 : Uint4B
复制代码 分析方法虚拟机内两个相同的EXE,一个直接运行,一个通过WinDbg运行,然后通过内核调试器,将这两个EXE的EPROCESS结构(包含KPROCESS)、PEB结构内存以字节形式写出来。再通过WinHex分析其不同之处。(多次执行,寻找普遍规律) 例: 写出正在调试的进程的EPROCESS(包含KPROCESS)和PEB- 2: kd> !process 0 0 Debugging.exe //正在调试的进程PROCESS ffff9d0e59bc3080
- SessionId: 1 Cid: 16a4 Peb: 002e4000 ParentCid: 2048 DirBase: 117c00000 ObjectTable: ffffc40fb7ce6cc0 HandleCount: 48. Image: Debugging.exe2: kd> .writemem E:\ls\Debugging_EPROCESS.a ffff9d0e59bc3080 l0x850 //写出EPROCESSWriting 850 bytes..2: kd> .process /p ffff9d0e59bc3080;.writemem E:\ls\Debugging_PEB.a 002e4000 l0x7c8 //写出PEBImplicit process is now ffff9d0e`59bc3080
- .cache forcedecodeuser done
- Writing 7c8 bytes.
复制代码 写出非调试状态的进程的EPROCESS(包含KPROCESS)和PEB- 2: kd> !process 0 0 Non_Debugging.exe //非调试状态下的进程PROCESS ffff9d0e5b8f50c0
- SessionId: 1 Cid: 06c0 Peb: 00323000 ParentCid: 14f8
- DirBase: 1bda00000 ObjectTable: ffffc40fb5026dc0 HandleCount: 52. Image: Non_Debugging.exe2: kd> .writemem E:\ls\Non_Debugging_EPROCESS.a ffff9d0e5b8f50c0 l0x850 //写出EPROCESSWriting 850 bytes..2: kd> .process /p ffff9d0e5b8f50c0;.writemem E:\ls\Non_Debugging_PEB.a 00323000 l0x7c8 //写出PEBImplicit process is now ffff9d0e`5b8f50c0
- .cache forcedecodeuser done
- Writing 7c8 bytes.
复制代码 通过WinHex进行对比
将写出来的文件分别拖进WinHex,点击 查看->同步和比较 例:
 编辑
图中标黑的就是对比出来不同的地方。再结合EPROCESS结构(包含KPROCESS)和PEB结构,可以得出哪些变量是不同的。
结论
去除EPROCESS和PEB中每次都不一定相同的各种指针、变量。
最后可以得出下面列出的几项变量是可以区分 进程是否处于被调试状态:
EPROCESS:
- +0x304 Flags : Uint4B
- +0x304 CreateReported : Pos 0, 1 Bit
- +0x304 NoDebugInherit : Pos 1, 1 Bit //未调试时置0,调试时置1 +0x304 ProcessExiting : Pos 2, 1 Bit
- +0x304 ProcessDelete : Pos 3, 1 Bit
- +0x304 ManageExecutableMemoryWrites : Pos 4, 1 Bit
- +0x304 VmDeleted : Pos 5, 1 Bit
- +0x304 OutswapEnabled : Pos 6, 1 Bit
- +0x304 Outswapped : Pos 7, 1 Bit
- +0x304 FailFastOnCommitFail : Pos 8, 1 Bit
- +0x304 Wow64VaSpace4Gb : Pos 9, 1 Bit
- +0x304 AddressSpaceInitialized : Pos 10, 2 Bits
- +0x304 SetTimerResolution : Pos 12, 1 Bit
- +0x304 BreakOnTermination : Pos 13, 1 Bit
- +0x304 DeprioritizeViews : Pos 14, 1 Bit
- +0x304 WriteWatch : Pos 15, 1 Bit
- +0x304 ProcessInSession : Pos 16, 1 Bit
- +0x304 OverrideAddressSpace : Pos 17, 1 Bit
- +0x304 HasAddressSpace : Pos 18, 1 Bit
- +0x304 LaunchPrefetched : Pos 19, 1 Bit
- +0x304 Background : Pos 20, 1 Bit
- +0x304 VmTopDown : Pos 21, 1 Bit
- +0x304 ImageNotifyDone : Pos 22, 1 Bit
- +0x304 PdeUpdateNeeded : Pos 23, 1 Bit
- +0x304 VdmAllowed : Pos 24, 1 Bit
- +0x304 ProcessRundown : Pos 25, 1 Bit
- +0x304 ProcessInserted : Pos 26, 1 Bit
- +0x304 DefaultIoPriority : Pos 27, 3 Bits
- +0x304 ProcessSelfDelete : Pos 30, 1 Bit
- +0x304 SetTimerResolutionLink : Pos 31, 1 Bit
复制代码- +0x3e0 InheritedFromUniqueProcessId : Ptr64 Void //父进程
复制代码- +0x420 DebugPort : Ptr64 Void //调试端口 未调试时为0
复制代码 PEB:
- +0x0bc NtGlobalFlag : Uint4B//未开启调试 00 00 00 00//已开启调试 70 00 00 00
复制代码- +0x002 BeingDebugged : UChar//未开启调试 0//已开启调试 1
复制代码 也就是说,在此系统,如果要通过某些标志位来检测进程是否处于被调试状态,上面列出来的5处地方,是无论如何也绕不开的。
检测是否存在内核调试器相信使用过内核调试器的同学,一定都知道,输入命令的时候,前缀总是 “kd>” 或“lkd>”这样的。
(可查看上面我输入的命令,前面总有 "kd>"前缀)
其中 “kd”就是“Kernel debug”的意思,而"lkd"就是“Local kernel debug”的意思了。
常见的内核调试,是要有内核的支持的,也就是说,我们输入的命令,是要通过内核中的一些 “内核调试支持”函数来帮助实现。
例:(我们IDA导入内核ntoskrnl.exe 和对应pdb文件,名称窗口搜索 "Kd"开头的函数)
 编辑
"Kd"开头的内核函数,一般就是我上述提到的 “内核调试支持”函数。
那么,有 “内核调试支持”函数,那就应该有 “内核调试支持”结构或变量。我们一样在名称窗口搜索 "Kd",找到"Kd"开头的变量,例:
 编辑
基本思路对比 开启内核调试器 和 未开启内核调试器 情况下,"内核调试支持"变量的使用情况。 开启内核调试的情况
我们可以直接在WinDbg下查看这些变量的值,例:(节选) - 5: kd> dq KdComponentTable l1
- fffff804`2b953a10 fffff804`2badd7145: kd> db KDskEvt_Flush l1
- fffff804`2b974048 0e .5: kd> db KDskEvt_Write l1
- fffff804`2b974058 0b .5: kd> db KDskEvt_Read l1
- fffff804`2b974068 0a .5: kd> db KdDebuggerDataBlock l4
- fffff804`2b9ff5e0 80 19 a3 2b ...+5: kd> dq KdPrintCircularBuffer l1
- fffff804`2ba022a8 fffff804`2ba233405: kd> dq KdPrintWritePointer l1
- fffff804`2ba022b0 fffff804`2ba233825: kd> db KdPitchDebugger l1
- fffff804`2ba02db8 00 .
- ........ //省略很多
复制代码 未开启内核调试的情况因为没有开启内核调试,所以不能使用WinDbg直接通过符号进行内存读取了。因此我写了个 “内核符号化阅读工具”,可以在不进行内核调试的情况下,符号化读取内核的内存,详情可看我另外一篇帖子。 - ic>dq KdComponentTable l1
- FFFFF8055C96CA10 FFFFF8055CAF6714
- ic>db KDskEvt_Flush l1
- FFFFF8055C98D048 0e
- ic>db KDskEvt_Write l1
- FFFFF8055C98D058 0bic>db KDskEvt_Read l1
- FFFFF8055C98D068 0a
- ic>db KdDebuggerDataBlock l4
- FFFFF8055CA185E0 f7 64 8f 7d
- ic>dq KdPrintCircularBuffer l1
- FFFFF8055CA1B2A8 FFFFF8055CA3C340
- ic>dq KdPrintWritePointer l1
- FFFFF8055CA1B2B0 FFFFF8055CA3C340
- ic>db KdPitchDebugger l1
- FFFFF8055CA1BDB8 01........ //省略很多
复制代码 结果- db KdPitchDebugger l1
- 已开启内核调试:0 未开启内核调试:1dd KdpTimeSlipPending l1
- 已开启内核调试:1 未开启内核调试:0db KdpBootedNodebug l1
- 已开启内核调试:0 未开启内核调试:1dd KdDebuggerLockMaxWaitTime l1
- 已开启内核调试:有值 未开启内核调试:0dd KdDebuggerEnteredCount l1
- 已开启内核调试:有值 未开启内核调试:0db KdpContextSent l1
- 已开启内核调试:1 未开启内核调试:0db KdpBreakpointTable l4
- 下内核断点:有值 未下内核断点:0dq KdTimerStop l1
- 已开启内核调试:有值 未开启内核调试:0db KdpPathBuffer l4
- 已开启内核调试:有值 未开启内核调试:0dq KdTimerDifference l1
- 已开启内核调试:有值 未开启内核调试:0db KdpControlCPressed l1
- 挂起:1 恢复:0dd KdUmBreakMarker l1
- 已开启内核调试:有值 未开启内核调试:0dd KdEnteredDebugger l1
- 挂起:1 恢复:0dq KdDebugDevice l1
- 已开启内核调试:有值 未开启内核调试:0db KdPageDebuggerSection l1
- 已开启内核调试:0 未开启内核调试:1db KdpDebuggerStructuresInitialized l1
- 已开启内核调试:1 未开启内核调试:0dq KdTimerStart l1
- 已开启内核调试:有值 未开启内核调试:0db KdLogBuffer l4
- 已开启内核调试:有值 未开启内核调试:0db KdDebuggerEnabled l1
- 已开启内核调试:1 未开启内核调试:0dq KdDebuggerNotPresent l1
- 已开启内核调试:0 未开启内核调试:1db KdpContext l4
- 已开启内核调试:有值 未开启内核调试:0db KdBreakAfterSymbolLoad l1
- 已开启内核调试:1 未开启内核调试:0db KdpDataBlockEncoded l1
- 已开启内核调试:0 未开启内核调试:1dd KdpDebugRoutineSelect l1
- 已开启内核调试:1 未开启内核调试:0dq KdpTimeSlipTimer l1
- 已开启内核调试:有值 未开启内核调试:0db KdPortLocked l1
- 已开启内核调试:1 未开启内核调试:0db KdpMessageBuffer l4
- 已开启内核调试:有值 未开启内核调试:0dq KdDebuggerLock l1
- 挂起:1 恢复:0
复制代码以上这些是我通过对比大概总结出来的可以利用的检测点,有些网上有公开,有些是网上没有资料,但是确实有效的。 因为我们只需要的是一个结果,这些变量是用来做什么的并不需要太清楚。但如果有兴趣的可以继续研究。 如果只是ring3进行测试模式检测,只需要注意KdDebuggerEnabled和KdDebuggerNotPresent即可,其他变量Ring3是不能获取的。
探究反调试实例demo(x64):- #include<Windows.h>int main() {
- while (1) {
- system("pause");
- MessageBoxA(0, "hello!", 0, 0);
- }
- }
复制代码 +VMProtect 3.6.0 检测调试器:User-mode + Kernel-mode (文章末尾提供demo下载地址)
 编辑
目的:在测试模式下,通过原版 x64dbg(无反反调试功能) 成功运行demo,并且能通过int 3 断点中断在 user32!MessageBoxA 处。
第一步:将加好壳的程序直接拖进x64dbg(或PE工具),查看内存区段
 编辑
这里的地址我们要记录一下,除了.text、rdata、data、reloc、rsrc区段以外的都有可能调用检测。
因为检测函数总是壳的部分进行调用。
Kernel-mode:测试模式下直接双击运行
 编辑
检测到开启测试模式,无法运行。
分析
因为此壳只是运行在ring3层上,且没有安装驱动进行驱动保护,所以它能做的检测就十分有限。
只能进行系统提供的api去检测当前 系统状态是处于 正常模式 还是 测试模式。
目前Ring3能做到的就只能通过NtQuerySystemInformation获取KdDebuggerEnabled和KdDebuggerNotPresent再进行检测了。
原理例:
- #include<Windows.h>typedef struct _SYSTEM_KERNEL_DEBUGGER_INFORMATION { BOOLEAN KdDebuggerEnabled;
- BOOLEAN KdDebuggerNotPresent;
- } SYSTEM_KERNEL_DEBUGGER_INFORMATION, * PSYSTEM_KERNEL_DEBUGGER_INFORMATION;typedef NTSTATUS(WINAPI* pNtQuerySystemInformation)(IN UINT SystemInformationClass, OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength);//存在内核调试器:返回1 否则:返回0bool check0(){
- // 取 NtQuerySystemInformation 地址 pNtQuerySystemInformation NtQuerySystemInformation
- = (pNtQuerySystemInformation)GetProcAddress(LoadLibrary(L"ntdll.dll"), "ZwQuerySystemInformation");
- // 获取系统信息 SYSTEM_KERNEL_DEBUGGER_INFORMATION KdDebuggerInfo;
- NtQuerySystemInformation(0x23, &KdDebuggerInfo, sizeof(SYSTEM_KERNEL_DEBUGGER_INFORMATION), NULL);
- // 判断调试器 if (KdDebuggerInfo.KdDebuggerEnabled == 0 && KdDebuggerInfo.KdDebuggerNotPresent == 1)
- return FALSE;
- else return TRUE;
- }
复制代码 因此我们可以直接在IDA上寻找KdDebuggerEnabled和KdDebuggerNotPresent的引用,例:
KdDebuggerEnabled:
 编辑
KdDebuggerNotPresent
 编辑
对应地址:
 编辑
可以在内核调试器先在ExpQuerySystemInformation下条件断点,断下来后再在KdDebuggerEnabled与KdDebuggerNotPresent下以线程为条件设置访问断点。(如果有用户调试器,需要先将这之前的反调试过掉)
- 0: kd> !process 0 0 vmp3.6.exe //获取EPROCESSPROCESS ffff9d0e5ac83080
- SessionId: 1 Cid: 0e84 Peb: efce1d3000 ParentCid: 1e80FreezeCount 1 DirBase: 166100000 ObjectTable: ffffc40fb6f48080 HandleCount: 34. Image: zy1.exe0: kd> bp /p ffff9d0e5ac83080 nt!NtQuerySystemInformation //在ExpQuerySystemInformation下条件断点0: kd> g
- Breakpoint 0 hit //命中断点nt!NtQuerySystemInformation:
- fffff804`2bc9ec50 4883ec38 sub rsp,38h2: kd> .thread //获取当前线程Implicit thread is now ffff9d0e`59f8f0802: kd> ba r1 KdDebuggerEnabled ".if(@$thread == ffff9d0e`59f8f080){} .else{gc}" //以线程为条件设置访问断点2: kd> g
- nt!ExpQuerySystemInformation+0xc7a:
- fffff804`2bc9fa1a 8803 mov byte ptr [rbx],al //看下图的位置
复制代码也可也像下方一样设置断点。 条件断点格式如下:(如果有用户调试器,需要先将这之前的反调试过掉) 若是在用户调试器下启动,需要用户调试置将demo断在入口点后,挂起系统,再在内核调试器下输入: - bp /p <EPROCESS> <Address> //EPROCESS输入demo的EPROCESS,Address输入ExpQuerySystemInformation对应位置
复制代码若是直接双击运行demo,需要在运行demo前挂起系统,在内核调试器下输入: - bp <Address> ".if(dwo(@$proc+0x450)==0x33706d76){} .else{gc}"//Address输入ExpQuerySystemInformation对应位置,dwo(@$proc+0x450)是取进程名的前四字节。//这里的条件就是,运行到address这一行时,此时的进程必须满足进程名为XXXX,0x33706d76 就是 “vmp3” 的ascii码
复制代码 运行demo,断下来后的调用栈:
 编辑
vmp3_6 + 0x39d6cc 明显属于壳的区段了(是壳调用了这个函数进行检测),说明我们的方向是正确的。
然后将其读取的KdDebuggerEnabled置0和KdDebuggerNotPresent置1(修改al或rbx)。
也可以激进一点直接将KdDebuggerEnabled置0和KdDebuggerNotPresent置1,例:
复制代码 隐藏代码1: kd> eb KdDebuggerEnabled 0 1: kd> eb KdDebuggerNotPresent 1继续运行!
 编辑
此时触发了一个异常,vmp_3.6 + 0x3819ab 也属于壳的区段,说明这也是一个检测。
结合我前一篇文章说的,异常会先发往调试器,如果调试器解决这个异常,就不会调用进程的 向量化、结构化异常处理。
此处肯定是不能让调试器处理这个异常的,一旦调试器处理,就说明有调试器了。
此处的逻辑,例:(这只是一个逻辑例子,并不能运行)
- //存在调试器:返回1 否则:返回0bool check1() {
- _try{
- __asm rdtsc;//举例:触发一个异常 return TRUE;//被解决了,有调试器 }
- _except(EXCEPTION_EXECUTE_HANDLER) {
- return FALSE;//没被解决,无调试器 }
- }
复制代码 所以我们在命令框中输入 “gn”(不处理异常继续运行)
此时进程成功在测试模式下运行,未被检测。
 编辑
User-mode:我们通过原版的x64dbg启动这个demo,会发现一开始会自动断在入口点(ntdll处的或是程序入口点都无所谓)。然后内核调试器挂起系统。
结合我说过的那几个地方。在做处理。
(EPROCESS.flag 、EPROCESS.InheritedFromUniqueProcessId 、EPROCESS.DebugPort)
(PEB.BeingDebugged 、 PEB.NtGlobalFlag)
注意:
因为我们是重新运行了demo,所以还需要重新解决一次 内核调试器的检测,此处不再赘述,上面有讲方法。
PEB部分解决:
PEB那两个标志位我们就不设访问断点了,因为此处的内存可以在RING3直接访问,设访问断点的意义不大。我们直接将它们置0。
- 0: kd> !process 0 0 vmp3.6.exe //获取demo的信息PROCESS ffff9d0e5aad6080
- SessionId: 1 Cid: 17f4 Peb: 25a749000 ParentCid: 0e68FreezeCount 1 DirBase: 192d00000 ObjectTable: ffffc40fadb1cdc0 HandleCount: 43. Image: vmp3.6.exe0: kd> .process /p ffff9d0e5aad6080;eb 25a749000+0x2 0;ed 25a749000+0xbc 0 // PEB.BeingDebugged 和 PEB.NtGlobalFlag 置 0
复制代码
EPROCESS部分解决:我们可以在EPROCESS.flags.NoDebugInherit 、EPROCESS.InheritedFromUniqueProcessId 、EPROCESS.DebugPort三处下内存访问断点。 (我们可以在 nt!NtQueryInformationProcess 处先下断点,再在上述三个地方设置条件访问断点,因为Ring3一般通过这个函数获取这三个变量的信息) NtQueryInformationProcess为未导出函数,需要通过GetProcAddress获取。 - typedef NTSTATUS(NTAPI * TNtQueryInformationProcess)(
- IN HANDLE ProcessHandle,
- IN DWORD ProcessInformationClass,
- OUT PVOID ProcessInformation,
- IN ULONG ProcessInformationLength,
- OUT PULONG ReturnLength
- );
复制代码 flags.NoDebugInherit(选读):选读是因为VMP3.6并没有检测这个点。 因此我这里提供一个检测示例来说明这个检测是如何做到的: 原理例子: - //存在调试器:返回1 否则:返回0bool check2() {
- HMODULE hNtdll = LoadLibraryA("ntdll.dll");
- if (hNtdll)
- {
- auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
- hNtdll, "NtQueryInformationProcess");
- if (pfnNtQueryInformationProcess)
- {
- DWORD ProcessDebugFlags, Returned;
- const DWORD CProcessDebugFlags = 0x1f;
- NTSTATUS status = pfnNtQueryInformationProcess( //获取NoDebugInherit取反 GetCurrentProcess(),
- CProcessDebugFlags,
- &ProcessDebugFlags,
- sizeof(DWORD),
- &Returned);
- if (0 == ProcessDebugFlags)
- return TRUE;
- }
- }
- return FALSE;
- }
复制代码命令示例: - 2: kd> !process 0 0 zy3.exe //为获取检测示例的EPROCESSPROCESS ffff9d0e59bc3080
- SessionId: 1 Cid: 06b0 Peb: a5abcf5000 ParentCid: 058c
- FreezeCount 1 DirBase: 106400000 ObjectTable: ffffc40fb395c680 HandleCount: 34. Image: zy3.exe2: kd> bp /p ffff9d0e59bc3080 nt!NtQueryInformationProcess //检测示例运行到nt!NtQueryInformationProcess中断2: kd> g //运行Breakpoint 0 hit //符合断点,中断nt!NtQueryInformationProcess:
- fffff804`2bbdbdf0 4053 push rbx2: kd> dt _eprocess ffff9d0e59bc3080 -y flags //为获取flags的偏移nt!_EPROCESS
- +0x300 Flags2 : 0xd014 +0x304 Flags : 0x144d0c03 //这个 +0x6cc Flags3 : 0x40c0082: kd> r @$thread //获取当前线程,通过线程设置条件断点$thread=ffff9d0e58cf10802: kd> ba r4 ffff9d0e59bc3080+0x304 ".if(@$thread == ffff9d0e58cf1080) {} .else{gc}" //这个线程访问flags时中断2: kd> g
复制代码 此时调用栈:
 编辑
汇编注释:
 编辑
通过NtQueryInformationProcess可以获取EPROCESS.flags.NoDebugInherit的相反值。
修改示例:
- 2: kd> !process 0 0 zy3.exe //为获取检测示例的EPROCESSPROCESS ffff9d0e59bc3080
- SessionId: 1 Cid: 06b0 Peb: a5abcf5000 ParentCid: 058c
- FreezeCount 1 DirBase: 106400000 ObjectTable: ffffc40fb395c680 HandleCount: 34. Image: zy3.exe2: kd> dt _eprocess ffff9d0e59bc3080 -y flags //为获取flags的偏移nt!_EPROCESS
- +0x300 Flags2 : 0xd014 +0x304 Flags : 0x144d0c03 //这个 +0x6cc Flags3 : 0x40c0082: kd> ed ffff9d0e59bc3080+0x304 0x144d0c01 //将第二位(NoDebugInherit)修改成0
复制代码 InheritedFromUniqueProcessId(选读):检测父进程是一个比较蠢的反调试手段(VMP3.6并没有采取这个检测),这里只提供修改示例: - 0: kd> !process 0 0 vmp3.6.exe //获取demo EPROCESSPROCESS ffff9d0e5aad6080
- SessionId: 1 Cid: 17f4 Peb: 25a749000 ParentCid: 0e68FreezeCount 1 DirBase: 192d00000 ObjectTable: ffffc40fadb1cdc0 HandleCount: 43. Image: vmp3.6.exe2: kd> !process 0 0 explorer.exe //有些反调试会检测父进程是否为explorer.exePROCESS ffff9d0e59daf080
- SessionId: 1 Cid: 14f8 Peb: 00c26000 ParentCid: 14e0 DirBase: 2220b0000 ObjectTable: ffffc40fab968dc0 HandleCount: 2193. Image: explorer.exe2: kd> dt _eprocess ffff9d0e59daf080 -y UniqueProcessId //获取explorer.exe的PIDntdll!_EPROCESS
- +0x2e0 UniqueProcessId : 0x00000000`000014f8 Void2: kd> dt _eprocess ffff9d0e5aad6080 -y InheritedFromUniqueProcessId //此时demo的父进程为X64dbgntdll!_EPROCESS
- +0x3e0 InheritedFromUniqueProcessId : 0x00000000`00000e68 Void2: kd> eq ffff9d0e5aad6080+3e0 0x00000000`000014f8 //将父进程修改为explorer.exe
复制代码一样可以先在NtQueryInformationProcess下条件断点,然后再在EPROCESS.UniqueProcessId设置条件访问断点。 (详细方法类似于 flags.NoDebugInherit(选读).命令示例) DebugPort:大多检测自身是否处于调试状态的API,都会去检测EPROCESS中的DebugPort字段,这个字段才是反调试的“根”。 初始工作(下断点):- 1: kd> !process 0 0 vmp3.6.exe //获取 EPROCESSPROCESS ffff9d0e5aad6080
- SessionId: 1 Cid: 2148 Peb: 3a0a56f000 ParentCid: 211c
- FreezeCount 1 DirBase: 1c9b00000 ObjectTable: ffffc40fb0640b40 HandleCount: 43. Image: vmp3.6.exe1: kd> .process /p ffff9d0e5aad6080 //切换上下文,方便修改PEBImplicit process is now ffff9d0e`5aad6080
- .cache forcedecodeuser done1: kd> dt _peb 3a0a56f000 -y BeingDebugged //获取BeingDebugged偏移ntdll!_PEB
- +0x002 BeingDebugged : 0x1 ''
- 1: kd> eb 3a0a56f000+0x2 0 //修改BeingDebugged为0,排除干扰
- 1: kd> dt _peb 3a0a56f000 -y NtGlobalFlag //获取NtGlobalFlag偏移
- ntdll!_PEB
- +0x0bc NtGlobalFlag : 0x70 //这个
- +0x7c4 NtGlobalFlag2 : 0
- 1: kd> ed 3a0a56f000+0xbc 0 //修改NtGlobalFlag为0,排除干扰
- 1: kd> eb KdDebuggerEnabled 0 //排除内核调试器检测干扰
- 1: kd> eb KdDebuggerNotPresent 1 //排除内核调试器检测干扰
- 1: kd> dt _eprocess ffff9d0e5aad6080 -y debugport //获取debugport偏移
- ntdll!_EPROCESS
- +0x420 DebugPort : 0xffff9d0e`5b8867b0 Void
- 1: kd> ba r8 ffff9d0e5aad6080+0x420 ".if(@$proc == ffff9d0e5aad6080){} .else{gc}" //下条件访问断点
- 1: kd> g //取消系统挂起
复制代码再在x64dbg中,运行demo。 断点第一次中断:
 编辑
从壳区段发出的 syscall,调用了NtQueryInformationProcess,来检测是否处于调试状态。
代码例:
- //存在调试器:返回1 否则:返回0bool check3() {
- HMODULE hNtdll = LoadLibraryA("ntdll.dll");
- if (hNtdll)
- {
- auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
- hNtdll, "NtQueryInformationProcess");
- if (pfnNtQueryInformationProcess)
- {
- DWORD ProcessDebugPort = 0x7, dwReturned; //检测Debugport DWORD64 dwProcessDebugPort;
- NTSTATUS status = pfnNtQueryInformationProcess(
- GetCurrentProcess(),
- ProcessDebugPort,
- &dwProcessDebugPort, //Debugport不为空则返回-1 sizeof(DWORD64),
- &dwReturned);
- if (-1 == dwProcessDebugPort)
- return TRUE;
- }
- }
- return FALSE;
- }
复制代码 NtQueryInformationProcess部分:
 编辑
解决: zf = 1
断点第二次中断:
 编辑
代码例:
- //存在调试器:返回1 否则:返回0bool check4() {
- HMODULE hNtdll = LoadLibraryA("ntdll.dll");
- if (hNtdll)
- {
- auto pfnNtQueryInformationProcess = (TNtQueryInformationProcess)GetProcAddress(
- hNtdll, "NtQueryInformationProcess");
- if (pfnNtQueryInformationProcess)
- {
- DWORD dwReturned;
- HANDLE hProcessDebugObject = 0;
- const DWORD ProcessDebugObjectHandle = 0x1e; //查询ProcessDebugObjectHandle NTSTATUS status = pfnNtQueryInformationProcess(
- GetCurrentProcess(),
- ProcessDebugObjectHandle,
- &hProcessDebugObject,
- sizeof(HANDLE),
- &dwReturned);
- if (0 != hProcessDebugObject) //DebugObject不为空说明有调试器 return TRUE;
- return FALSE;
- }
- }
- }
复制代码解决:置zf = 1 断点第三次中断:
 编辑
同第一次中断。
解决:置zf = 1
断点第四次中断:
 编辑
代码例:
- bool check5(){
- __try
- {
- CloseHandle((HANDLE)0x999999); //如果debugport值不为空,则CloseHandle会触发调试器可解决的异常 return false; //调试器处理了,有调试器 }
- __except (EXCEPTION_EXECUTE_HANDLER)
- {
- return true; //没有调试器处理,无调试器 }
- }
复制代码 解决:置zf = 1
断点第五次中断(异常):
 编辑
看过我之前的“WinDows10 x64 异常处理”的帖子就知道,这里为啥会断在(nt!KiDispatchException)这里了
(上面说过这个异常)
解决:忽视异常继续运行
断点第六次中断以及其他中断处:
 编辑
是从 vmp3.6 + 0x101d 处调用的,不属于壳的区段,可以忽略。继续运行
(只要不是从壳区段开启的调用,都可无视)
成功在调试器中运行:
 编辑
尝试使用x64dbg在user32!MessageBoxA处下断点,运行,发现程序结束了。并没有断下来。
用户调试器无法通过断点中断:
通过我上一篇文章 “Windows 10 x64 异常处理”处所说:
KiDispatchException第一次收到一个用户态异常的时候,如果内核调试器不处理,就会先判断此进程的DebugPort字段是否为空,如果不为空就会调用DbgkForwardException发往用户态调试器,如果用户态调试器不处理,就会进行向量化、结构化异常处理.....首先!DebugPort字段不可能为空,因为我们是调试器打开的进程。所以我们在DbgkForwardException处下断点。 - 0: kd> bp /p ffff9d0e5aad6080 nt!DbgkForwardException //下条件断点0: kd> g //恢复系统后到,用户调试器继续运行Breakpoint 1 hit //符合条件,中断nt!DbgkForwardException:
- fffff804`2bcbc20c 48895c2410 mov qword ptr [rsp+10h],rbx3: kd> gu //跳出此函数nt!KiDispatchException+0x2bb: //到达这里,看下图fffff804`2b66bfeb 84c0 test al,al
复制代码 中断后跳出此函数(gu),发现DbgkForwardException返回 0 ,不处理此异常。例:
 编辑
看调用栈!断点是发挥了作用了,但是DbgkForwardException返回0。
我们分析一下DbgkForwardException这个函数。例:(节选)
 编辑
根本原因就是 ETHREAD.CrossThreadFlags.HideFromDebugger 为 1,所以用户调试无法接收这个异常。
代码例:
- 复制代码 隐藏代码
- typedef NTSTATUS(WINAPI* NtSetInformationThreadPtr)(
- HANDLE threadHandle,
- int threadInformationNum,
- PVOID threadInformation,
- ULONG threadInformationLength
- );void HideFromDebugger() {
- HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
- NtSetInformationThreadPtr NtSetInformationThread = (NtSetInformationThreadPtr)GetProcAddress(hNtDll, "NtSetInformationThread");
- NTSTATUS status = NtSetInformationThread(GetCurrentThread(), 17, NULL, 0);//设置隐藏调试}
复制代码解决: 我们重新运行demo,解决上述所说的那些反调试手段后,再在DbgkForwardException下条件断点,然后返回用户调试器在user32!MessageBoxA下断点。运行。内核调试器中断后,修改 ETHREAD.CrossThreadFlags.HideFromDebugger 为 0 ,例: - 3: kd> dt _ethread @$thread -y CrossThreadFlags
- ntdll!_ETHREAD
- +0x6d0 CrossThreadFlags : 0x54063: kd> .formats 0x5406 //查看0x5406的二进制Evaluate expression:
- Hex: 00000000`00005406 Decimal: 21510 Octal: 0000000000000000052006 Binary: 00000000 00000000 00000000 00000000 00000000 00000000 01010100 00000110 //第三位为1 Chars: ......T.
- Time: Thu Jan 1 13:58:30 1970 Float: low 3.01419e-041 high 0 Double: 1.06274e-3193: kd> .formats 0y0000000000000000000000000000000000000000000000000101010000000010
- //修改第三位为0后,查看16进制Evaluate expression:
- Hex: 00000000`00005402 //这个 Decimal: 21506 Octal: 0000000000000000052002 Binary: 00000000 00000000 00000000 00000000 00000000 00000000 01010100 00000010 Chars: ......T.
- Time: Thu Jan 1 13:58:26 1970 Float: low 3.01363e-041 high 0 Double: 1.06254e-3193: kd> ed @$thread+0x6d0 0x5402 //修改CrossThreadFlags为0x5402,再取消DbgkForwardException断点3: kd> g
复制代码 x64dbg成功中断在 user32!MessageBoxA,例:
 编辑
总结此系统检测进程是否被调试,只要抓住
EPROCESS.flags 、EPROCESS.DebugPort 、EPROCESS.InheritedFromUniqueProcessId 、
PEB.NtGlobalFlag 、PEB.BeingDebugged
就可以解决80%+的问题。
检测系统是否被调试,只要抓住上述列出的几个变量的值,就可以解决90%+的问题。
使用内核调试器调试仅有RING3反调试的进程简直就是降维打击。
|