吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 9980|回复: 61
收起左侧

[系统底层] 从内核角度认识反调试基本原理

  [复制链接]
ICEY 发表于 2022-8-28 00:19

从内核角度认识反调试基本原理

概述

反调试、反反调试 两种技术同根同源,原理基本一致, 本篇文章将以内核的角度,探究 调试与未被调试情况下 EPROCESS、PEB下各个标志位的区别,认识简单反调试技术的基本原理,本文章讨论的是基础的反调试手段(通过标志位检测之类的),不讨论一些特殊的反调试手段(计时、检查内存、VT等)。文章最后以 VMP 3.6 作为例子,进行反反调试实战(只分析原理,不提供(不制作)反反调试插件)。

系统版本

系统版本.png

同上篇文章。(文章末尾提供PDF版本下载地址)

检测进程是否被调试

基础

EPROCESS(执行体进程块)

NT内核使用EPROCESS结构体来描述每一个进程,就像档案一样,EPROCESS结构内包含进程的各种信息,和相关结构的指针。

可以自行使用WinDbg查看EPROCESS结构体。例:(节选)

4: kd> ?? sizeof(_eprocess) //查看结构大小
unsigned int64 0x850
4: 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 0x7c8
4: 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.exe

2: kd> .writemem E:\ls\Debugging_EPROCESS.a ffff9d0e59bc3080 l0x850  //写出EPROCESS
Writing 850 bytes..
2: kd> .process /p ffff9d0e59bc3080;.writemem E:\ls\Debugging_PEB.a 002e4000 l0x7c8  //写出PEB
Implicit 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.exe

2: kd> .writemem E:\ls\Non_Debugging_EPROCESS.a ffff9d0e5b8f50c0 l0x850  //写出EPROCESS
Writing 850 bytes..
2: kd> .process /p ffff9d0e5b8f50c0;.writemem E:\ls\Non_Debugging_PEB.a 00323000 l0x7c8  //写出PEB
Implicit process is now ffff9d0e`5b8f50c0
.cache forcedecodeuser done
Writing 7c8 bytes.
通过WinHex进行对比

将写出来的文件分别拖进WinHex,点击  查看->同步和比较 例:

Eprocess、peb对比.png

图中标黑的就是对比出来不同的地方。再结合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函数.png

"Kd"开头的内核函数,一般就是我上述提到的 “内核调试支持”函数。

那么,有 “内核调试支持”函数,那就应该有 “内核调试支持”结构或变量。我们一样在名称窗口搜索 "Kd",找到"Kd"开头的变量,例:

kd变量.png

基本思路

对比 开启内核调试器 和 未开启内核调试器 情况下,"内核调试支持"变量的使用情况。

开启内核调试的情况

我们可以直接在WinDbg下查看这些变量的值,例:(节选)

5: kd> dq KdComponentTable  l1
fffff804`2b953a10  fffff804`2badd714

5: 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`2ba23340

5: kd> dq KdPrintWritePointer l1
fffff804`2ba022b0  fffff804`2ba23382

5: 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    0b

ic>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  未开启内核调试:1

dd KdpTimeSlipPending l1
已开启内核调试:1  未开启内核调试:0

db KdpBootedNodebug  l1
已开启内核调试:0  未开启内核调试:1

dd KdDebuggerLockMaxWaitTime  l1
已开启内核调试:有值  未开启内核调试:0

dd KdDebuggerEnteredCount  l1
已开启内核调试:有值  未开启内核调试:0

db KdpContextSent   l1
已开启内核调试:1   未开启内核调试:0

db KdpBreakpointTable l4
下内核断点:有值  未下内核断点:0

dq KdTimerStop     l1
已开启内核调试:有值  未开启内核调试:0

db KdpPathBuffer   l4
已开启内核调试:有值  未开启内核调试:0

dq KdTimerDifference l1
已开启内核调试:有值  未开启内核调试:0

db KdpControlCPressed l1
挂起:1  恢复:0

dd KdUmBreakMarker l1
已开启内核调试:有值  未开启内核调试:0

dd KdEnteredDebugger l1
挂起:1  恢复:0

dq KdDebugDevice   l1
已开启内核调试:有值  未开启内核调试:0

db KdPageDebuggerSection l1
已开启内核调试:0  未开启内核调试:1

db KdpDebuggerStructuresInitialized l1
已开启内核调试:1  未开启内核调试:0

dq KdTimerStart    l1
已开启内核调试:有值  未开启内核调试:0

db KdLogBuffer     l4
已开启内核调试:有值  未开启内核调试:0

db KdDebuggerEnabled l1
已开启内核调试:1   未开启内核调试:0

dq KdDebuggerNotPresent l1
已开启内核调试:0  未开启内核调试:1

db KdpContext l4
已开启内核调试:有值  未开启内核调试:0

db KdBreakAfterSymbolLoad l1
已开启内核调试:1  未开启内核调试:0

db KdpDataBlockEncoded l1
已开启内核调试:0  未开启内核调试:1

dd KdpDebugRoutineSelect l1
已开启内核调试:1  未开启内核调试:0

dq KdpTimeSlipTimer l1
已开启内核调试:有值  未开启内核调试:0

db KdPortLocked    l1
已开启内核调试:1  未开启内核调试:0

db KdpMessageBuffer l4
已开启内核调试:有值  未开启内核调试:0

dq 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下载地址)

ken demo.png

目的:

在测试模式下,通过原版 x64dbg(无反反调试功能) 成功运行demo,并且能通过int 3 断点中断在 user32!MessageBoxA 处。

第一步:

将加好壳的程序直接拖进x64dbg(或PE工具),查看内存区段

调用栈参考.png

这里的地址我们要记录一下,除了.text、rdata、data、reloc、rsrc区段以外的都有可能调用检测。

因为检测函数总是壳的部分进行调用。

Kernel-mode:

测试模式下直接双击运行

测试模式直接运行.png

检测到开启测试模式,无法运行。

分析

因为此壳只是运行在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 否则:返回0
bool 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:

01.png

KdDebuggerNotPresent

02.png

对应地址:

03.png

可以在内核调试器先在ExpQuerySystemInformation下条件断点,断下来后再在KdDebuggerEnabled与KdDebuggerNotPresent下以线程为条件设置访问断点。(如果有用户调试器,需要先将这之前的反调试过掉)

0: kd> !process 0 0 vmp3.6.exe  //获取EPROCESS
PROCESS ffff9d0e5ac83080
    SessionId: 1  Cid: 0e84    Peb: efce1d3000  ParentCid: 1e80
FreezeCount 1
    DirBase: 166100000  ObjectTable: ffffc40fb6f48080  HandleCount:  34.
    Image: zy1.exe

0: kd> bp /p ffff9d0e5ac83080 nt!NtQuerySystemInformation  //在ExpQuerySystemInformation下条件断点
0: kd> g
Breakpoint 0 hit  //命中断点
nt!NtQuerySystemInformation:
fffff804`2bc9ec50 4883ec38        sub     rsp,38h
2: kd> .thread  //获取当前线程
Implicit thread is now ffff9d0e`59f8f080
2: 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,断下来后的调用栈:

内核检测调用栈.png

vmp3_6 + 0x39d6cc 明显属于壳的区段了(是壳调用了这个函数进行检测),说明我们的方向是正确的。

然后将其读取的KdDebuggerEnabled置0和KdDebuggerNotPresent置1(修改al或rbx)。

也可以激进一点直接将KdDebuggerEnabled置0和KdDebuggerNotPresent置1,例:

1: kd> eb KdDebuggerEnabled 0  
1: kd> eb KdDebuggerNotPresent 1

继续运行!

触发异常.png

此时触发了一个异常,vmp_3.6 + 0x3819ab 也属于壳的区段,说明这也是一个检测。

结合我前一篇文章说的,异常会先发往调试器,如果调试器解决这个异常,就不会调用进程的 向量化、结构化异常处理。

此处肯定是不能让调试器处理这个异常的,一旦调试器处理,就说明有调试器了。

此处的逻辑,例:(这只是一个逻辑例子,并不能运行)

//存在调试器:返回1 否则:返回0
bool check1() {
        _try{
        __asm rdtsc;//举例:触发一个异常
        return TRUE;//被解决了,有调试器
        }
        _except(EXCEPTION_EXECUTE_HANDLER) {
                return FALSE;//没被解决,无调试器
        }
}

所以我们在命令框中输入 “gn”(不处理异常继续运行)

此时进程成功在测试模式下运行,未被检测。

测试模式下成功运行.png

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: 0e68
FreezeCount 1
    DirBase: 192d00000  ObjectTable: ffffc40fadb1cdc0  HandleCount:  43.
    Image: vmp3.6.exe

0: 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 否则:返回0
bool 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  //为获取检测示例的EPROCESS
PROCESS ffff9d0e59bc3080
    SessionId: 1  Cid: 06b0    Peb: a5abcf5000  ParentCid: 058c
FreezeCount 1
    DirBase: 106400000  ObjectTable: ffffc40fb395c680  HandleCount:  34.
    Image: zy3.exe

2: kd> bp /p ffff9d0e59bc3080 nt!NtQueryInformationProcess //检测示例运行到nt!NtQueryInformationProcess中断
2: kd> g  //运行
Breakpoint 0 hit  //符合断点,中断
nt!NtQueryInformationProcess:
fffff804`2bbdbdf0 4053            push    rbx
2: kd> dt _eprocess ffff9d0e59bc3080 -y flags  //为获取flags的偏移
nt!_EPROCESS
   +0x300 Flags2 : 0xd014
   +0x304 Flags : 0x144d0c03  //这个
   +0x6cc Flags3 : 0x40c008
2: kd> r @$thread  //获取当前线程,通过线程设置条件断点
$thread=ffff9d0e58cf1080
2: kd> ba r4 ffff9d0e59bc3080+0x304 ".if(@$thread == ffff9d0e58cf1080) {} .else{gc}"  //这个线程访问flags时中断
2: kd> g
nt!NtQueryInformationProcess+0x1a6260:  //符合条件中断,是断在符合条件的代码的下一行
fffff804`2bd82050 d1e8            shr     eax,1

此时调用栈:

flags.png

汇编注释:

dwProcessDebugFlags.png

通过NtQueryInformationProcess可以获取EPROCESS.flags.NoDebugInherit的相反值。

修改示例:

2: kd> !process 0 0 zy3.exe  //为获取检测示例的EPROCESS
PROCESS ffff9d0e59bc3080
    SessionId: 1  Cid: 06b0    Peb: a5abcf5000  ParentCid: 058c
FreezeCount 1
    DirBase: 106400000  ObjectTable: ffffc40fb395c680  HandleCount:  34.
    Image: zy3.exe

2: kd> dt _eprocess ffff9d0e59bc3080 -y flags  //为获取flags的偏移
nt!_EPROCESS
   +0x300 Flags2 : 0xd014
   +0x304 Flags : 0x144d0c03  //这个
   +0x6cc Flags3 : 0x40c008
2: kd> ed ffff9d0e59bc3080+0x304 0x144d0c01  //将第二位(NoDebugInherit)修改成0
InheritedFromUniqueProcessId(选读):

检测父进程是一个比较蠢的反调试手段(VMP3.6并没有采取这个检测),这里只提供修改示例:

0: kd> !process 0 0 vmp3.6.exe  //获取demo EPROCESS
PROCESS ffff9d0e5aad6080
    SessionId: 1  Cid: 17f4    Peb: 25a749000  ParentCid: 0e68
FreezeCount 1
    DirBase: 192d00000  ObjectTable: ffffc40fadb1cdc0  HandleCount:  43.
    Image: vmp3.6.exe

2: kd> !process 0 0 explorer.exe  //有些反调试会检测父进程是否为explorer.exe
PROCESS ffff9d0e59daf080
    SessionId: 1  Cid: 14f8    Peb: 00c26000  ParentCid: 14e0
    DirBase: 2220b0000  ObjectTable: ffffc40fab968dc0  HandleCount: 2193.
    Image: explorer.exe

2: kd> dt _eprocess ffff9d0e59daf080  -y UniqueProcessId  //获取explorer.exe的PID
ntdll!_EPROCESS
   +0x2e0 UniqueProcessId : 0x00000000`000014f8 Void

2: kd> dt _eprocess ffff9d0e5aad6080 -y InheritedFromUniqueProcessId  //此时demo的父进程为X64dbg
ntdll!_EPROCESS
   +0x3e0 InheritedFromUniqueProcessId : 0x00000000`00000e68 Void

2: 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  //获取 EPROCESS
PROCESS ffff9d0e5aad6080
    SessionId: 1  Cid: 2148    Peb: 3a0a56f000  ParentCid: 211c
FreezeCount 1
    DirBase: 1c9b00000  ObjectTable: ffffc40fb0640b40  HandleCount:  43.
    Image: vmp3.6.exe

1: kd> .process /p ffff9d0e5aad6080  //切换上下文,方便修改PEB
Implicit process is now ffff9d0e`5aad6080
.cache forcedecodeuser done

1: 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。

断点第一次中断:

第一次 informationprocess.png

从壳区段发出的 syscall,调用了NtQueryInformationProcess,来检测是否处于调试状态。

代码例:

//存在调试器:返回1 否则:返回0
bool 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部分:

检测debugport1.png

检测debugport2.png

解决: zf = 1

断点第二次中断:

opendebugport.png

代码例:

//存在调试器:返回1 否则:返回0
bool 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

断点第三次中断:

第三次中断.png

同第一次中断。

解决:置zf = 1

断点第四次中断:

第四次中断.png

代码例:

bool check5()
{
        __try
        {
                CloseHandle((HANDLE)0x999999);  //如果debugport值不为空,则CloseHandle会触发调试器可解决的异常
                return false;  //调试器处理了,有调试器
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
                return true;  //没有调试器处理,无调试器
        }
}

解决:置zf = 1

断点第五次中断(异常):

第五次中断.png

看过我之前的“WinDows10 x64 异常处理”的帖子就知道,这里为啥会断在(nt!KiDispatchException)这里了

(上面说过这个异常)

解决:忽视异常继续运行

断点第六次中断以及其他中断处:

第六次中断.png

是从 vmp3.6 + 0x101d 处调用的,不属于壳的区段,可以忽略。继续运行

(只要不是从壳区段开启的调用,都可无视)

成功在调试器中运行:

成功运行.png

尝试使用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],rbx
3: kd> gu  //跳出此函数
nt!KiDispatchException+0x2bb:  //到达这里,看下图
fffff804`2b66bfeb 84c0            test    al,al

中断后跳出此函数(gu),发现DbgkForwardException返回 0 ,不处理此异常。例:

隐藏调试.png

看调用栈!断点是发挥了作用了,但是DbgkForwardException返回0。

我们分析一下DbgkForwardException这个函数。例:(节选)

隐藏调试2.png

根本原因就是 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 : 0x5406

3: 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-319

3: 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-319

3: kd> ed @$thread+0x6d0 0x5402  //修改CrossThreadFlags为0x5402,再取消DbgkForwardException断点
3: kd> g

x64dbg成功中断在 user32!MessageBoxA,例:

成功中断.png

总结

此系统检测进程是否被调试,只要抓住

EPROCESS.flags 、EPROCESS.DebugPort 、EPROCESS.InheritedFromUniqueProcessId 、

PEB.NtGlobalFlag 、PEB.BeingDebugged

就可以解决80%+的问题。

检测系统是否被调试,只要抓住上述列出的几个变量的值,就可以解决90%+的问题。

使用内核调试器调试仅有RING3反调试的进程简直就是降维打击。

资料下载(文章PDF版本、demo):https://pan.baidu.com/s/11V6iZEN6FjvVrT6NGE0A9w
提取码:ICEY



免费评分

参与人数 43吾爱币 +45 热心值 +38 收起 理由
lsz7575 + 1 + 1 谢谢@Thanks!
Zomtooe + 1 用心讨论,共获提升!
魔道书生 + 2 + 1 我很赞同!
dengsu + 1 + 1 用心讨论,共获提升!
DSUPER + 1 + 1 我很赞同!
X1a0 + 1 用心讨论,共获提升!
ILOVEHuhu + 1 + 1 用心讨论,共获提升!
随风起舞 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
procurve + 1 + 1 谢谢@Thanks!
crl7-key + 1 + 1 我很赞同!
sheep426 + 1 我很赞同!
AOTY1018 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
caryguo + 1 + 1 我很赞同!
jiawe + 1 + 1 我很赞同!
tinyu + 1 + 1 用心讨论,共获提升!
m52pj + 1 + 1 热心回复!
努力加载中 + 1 + 1 谢谢@Thanks!
xlwllm + 1 + 1 我很赞同!
iyangl + 1 + 1 用心讨论,共获提升!
victos + 1 + 1 谢谢@Thanks!
WUXING_TIANCHEN + 1 + 1 谢谢@Thanks!
xzhtx + 1 + 1 谢谢@Thanks!
manyou + 1 + 1 谢谢@Thanks!
zhczf + 1 我很赞同!
DearDavies + 1 + 1 谢谢@Thanks!
quitidle + 1 + 1 用心讨论,共获提升!
奈何不得 + 1 + 1 谢谢@Thanks!
学习使我快乐1 + 1 + 1 热心回复!
无知灰灰 + 1 + 1 用心讨论,共获提升!
zjun777 + 1 + 1 用心讨论,共获提升!
Fan2115 + 1 + 1 大佬厉害
cheesw + 1 + 1 这么认真的帖子不多了
lingyun011 + 1 + 1 我很赞同!
Black_山猫 + 1 + 1 热心回复!
Roamtiantits + 2 + 1 我很赞同!
忆魂丶天雷 + 2 + 1 谢谢@Thanks!
甜萝 + 1 我很赞同!
hui-shao + 1 + 1 谢谢@Thanks!
coder9527 + 1 + 1 热心回复!
malEvOleNce52 + 1 + 1 谢谢@Thanks!
oxygen1a1 + 1 + 1 热心回复!
FuSu_ChunQiu + 1 + 1 我很赞同!
为之奈何? + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

lsz7575 发表于 2023-8-18 00:37
楼主在这埋了一个坑,,这个值要写0..我说我怎么复现不了..差点重装win10了..
Microsoft Windows [版本 10.0.18362.356]    1903

1: kd> eb KdDebuggerNotPresent 1
继续运行!

文章真不错.感谢分享..

575071031 发表于 2022-8-28 05:42
oxygen1a1 发表于 2022-8-28 08:47
hszt 发表于 2022-8-28 09:08
感谢分享,很难得的一篇文章,建议精华
hui-shao 发表于 2022-8-28 10:19
很实用的文章!反调试
外酥内嫩 发表于 2022-8-28 10:24
大佬太牛啦·
灰灰。 发表于 2022-8-28 11:44
大佬太强了,果然从内核入手,ring3的反调试真是砍瓜切菜啊
ydna1234 发表于 2022-8-28 11:49
大佬太厉害了~~~
yzitcast843 发表于 2022-8-28 12:16
讲的很好,精华。
zgdtianya 发表于 2022-8-28 14:32
不懂,回帖是美得
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-22 01:19

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表