Windows安全攻防-PEB&TEB

admin 2026-02-02 00:27:16 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析Windows系统的TEB与PEB结构,涵盖定义、字段及内存定位。重点介绍了通过段寄存器访问的方法,并分析相关攻防技术,包括利用PEBWalking绕过监控、断链隐藏模块及修改参数伪装进程,为底层安全与规避技术提供参考。 综合评分: 96 文章分类: 二进制安全,逆向分析,免杀,恶意软件,红队


cover_image

Windows安全攻防-PEB&TEB

原创

R0x7e R0x7e

剑外思归客

2026年1月31日 23:33 江苏

基础介绍

在 Windows 操作系统中,每个运行的程序(即进程)和其中的执行单元(即线程)都由内核对象来表示,但同时在用户空间也存在对应的数据结构,即 PEB 和 TEB,它们为用户模式代码提供了一种快速访问进程和线程信息的方式。

  • TEB (Thread Environment Block): 线程环境块,也称为线程信息块(Thread Information Block, TIB)。顾名思义,它存储了与单个线程相关的特定信息。每个线程都拥有一个独立的 TEB。其内容包括线程ID (TID)、线程的栈基址和栈顶限制、指向线程局部存储 (Thread Local Storage, TLS) 的指针,以及一个指向其所属进程 PEB 的关键指针。
  • PEB (Process Environment Block): 进程环境块。与 TEB 不同,一个进程只有一个 PEB,它存储了该进程范围内的全局信息。这些信息对于进程的整个生命周期都至关重要,例如进程加载了哪些动态链接库 (DLL)、完整的命令行参数、可执行文件的内存基址(Image Base Address)、进程堆信息以及一些用于调试和系统兼容性的标志位。核心关系:TEB 是通往 PEB 的桥梁。在用户模式下,代码首先访问当前线程的 TEB,然后通过 TEB 中的一个特定字段(ProcessEnvironmentBlock)找到其所属进程的 PEB 地址。这种“线程→进程”的链式访问结构,为任何在线程上下文中运行的代码提供了一条稳定、高效的路径来获取关于自身进程的丰富信息。

定义和结构

TEB 线程环境块

TEB 是一个用于存储单个线程状态的结构。其定义在 Windows SDK 的 winternl.h 中,包含大量字段来描述线程的各种属性。由于版本更新,TEB 的结构可能有所变化,微软建议不要直接访问 TEB 结构的字段,除非使用官方提供的 API 。例如,TlsSlots 和 TlsExpansionSlots 字段需要通过 TlsGetValue 等函数访问,ReservedForOle 字段需要通过 CoGetContextToken 访问 。 TEB 的结构非常复杂,包含许多成员。根据微软官方头文件和 Wine 项目的实现,TEB 通常以 _TEB 或 NT_TIB 为前缀定义。下面是一个简化的 TEB 结构定义示例:

typedef struct _TEB {
    NT_TIB NtTib;                         // 包含栈信息、SEH 链表等关键信息
    PVOID EnvironmentPointer;
    CLIENT_ID ClientId;                   // 包含 UniqueProcess (PID) 和 UniqueThread (TID)
    PVOID ActiveRpcHandle;
    PVOID ThreadLocalStoragePointer;      // 指向线程本地存储 (TLS) 数组
    struct _PEB *ProcessEnvironmentBlock; // 指向所属进程的 PEB
    ULONG LastErrorValue;                 // GetLastError() 返回的值就存在这里
    ULONG CountOfOwnedCriticalSections;
    PVOID CsrClientThread;
    PVOID Win32ThreadInfo;                // 指向内核层的 W32THREAD 结构
    // ... 后面还有数百个成员,包括语言 ID、混淆种子等
} TEB, *PTEB;

NT_TIB 子结构

这是 TEB 的头。它包含了栈的基址 (StackBase) 和限制 (StackLimit)。最著名的是 ExceptionList(在 x86 下),它是 SEH(结构化异常处理)链表的头,其结构定义如下:

typedef struct _NT_TIB {
    struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;  // SEH 链头
    PVOID StackBase;                // 堆栈基址
    PVOID StackLimit;               // 堆栈限制
    PVOID SubSystemTib;
    union {
        PVOID FiberData;
        ULONG Version;
    };
    PVOID ArbitraryUserPointer;
    struct _NT_TIB *Self;           // 指向自身的指针(FS:[0x18])
} NT_TIB;

ProcessEnvironmentBlock

指向PEB的指针。这是从线程信息通往进程信息的唯一官方入口,是所有PEB相关操作的起点,在x86环境下偏移为0x30x64环境下偏移为0x60

ClientId

ClientId 是一个 _CLIENT_ID 结构体,包含了两个至关重要的标识符:UniqueProcess(即进程ID, PID)和 UniqueThread(即线程ID, TID)。这两个ID是线程在整个系统中的唯一坐标,恶意软件可用于识别自身或目标进程,在x86环境下偏移为0x20x64环境下偏移为0x40

TlsSlots/TlsExpansionSlots

线程局部存储 (TLS) 槽。可被恶意软件用于在线程级别存储数据,或通过操纵TLS槽来绕过某些EDR的钩子检测。在x86环境下偏移为0xE10x64环境下偏移为0x1480

ExceptionList

指向线程异常处理(SEH)链表的头部。攻击者可以通过覆盖这个链表来劫持异常处理流程,实现代码执行,这是一种经典的漏洞利用和反调试技术。偏移为0x00

LastErrorValue

当调用 SetLastError 或 GetLastError 时,底层其实就是在读写这个字段。

ThreadLocalStoragePointer

指向线程本地存储 (TLS) 数组的指针 。每个线程都有一个 TLS 数组,用于存储线程特定的数据。

| 字段名 | x86 偏移量 (FS) | x64 偏移量 (GS) | 描述 | | — | — | — | — | | ExceptionList (NT_TIB) | 0x00 | 0x00 | 指向SEH链表头的指针 (_EXCEPTION_REGISTRATION_RECORD*) | | StackBase (NT_TIB) | 0x04 | 0x08 | 线程栈的基地址(高地址) | | StackLimit (NT_TIB) | 0x08 | 0x10 | 线程栈的上限地址(低地址) | | Self (NT_TIB) | 0x18 | 0x30 | 指向TEB结构自身的指针 (PTEB) | | ClientId.UniqueProcess | 0x20 | 0x40 | 进程ID (PID) | | ClientId.UniqueThread | 0x24 | 0x48 | 线程ID (TID) | | ThreadLocalStoragePointer | 0x2C | 0x58 | 指向静态TLS数组的指针 | | ProcessEnvironmentBlock | 0x30 | 0x60 | 指向进程环境块的指针 (PPEB) | | LastErrorValue | 0x34 | 0x68 | 线程的最后错误码 (ULONG) | | TlsSlots | 0xE10 | 0x1480 | 动态TLS插槽数组 (PVOID[64]) |

通过WinDbg查看TEB信息

windbg中可以通过dt _teb命令进行查看结构信息

通过!teb可以查看当前线程的基本信息,如:TEB 地址、栈范围、TLS 等

通过dt ntdll!_TEB @$teb可以查看当前线程TEB不同字段的内容

如果你只想看某一个字段具体的值,可以使用dt ntdll!_TEB @$teb 字段名的方式进行查看,如dt ntdll!_TEB @$teb ProcessEnvironmentBlock

PEB 进程环境块

PEB 是连接用户态代码与内核态信息的桥梁,每个进程有且只有一个PEB进程环境块,PEB存储在用户态中,同时也是连接用户态代码与内核态信息的桥梁,一个进程内的所有线程共享同一个 PEB,PEB的结构在不同的windows版本中会有变化,以下是一个简写的PEB结构内容:

typedef struct _PEB {
    BOOLEAN InheritedAddressSpace;      // 0x000
    BOOLEAN ReadImageFileExecOptions;   // 0x001
    BOOLEAN BeingDebugged;              // 0x002 标志进程是否正在被调试
    union {
        BOOLEAN BitField;               // 0x003
        struct {
            BYTE ImageUsesLargePages : 1;
            BYTE IsProtectedProcess : 1;
            // ... 其他位标志
        };
    };
    HANDLE Mutant;                      // 0x004 (x86) / 0x008 (x64)
    PVOID ImageBaseAddress;             // 程序的加载基地址
    PPEB_LDR_DATA Ldr;                  // 指向加载器数据,包含已加载模块列表 (DLLs)
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 包含命令行、路径等信息
    PVOID SubSystemData;
    PVOID ProcessHeap;                  // 进程默认堆地址
    PRTL_CRITICAL_SECTION FastPebLock;
    // ... 后面还有关于环境变量、会话 ID、代码页等数百个成员
} PEB, *PPEB;

本文列举一些较为关键的PEB字段

BeingDebugged (BYTE)

当一个进程被调试器附加时,Windows内核会自动将此位置为1。因此,检查该字段是最基础、最直接的反调试技术之一。程序可以通过直接读取该内存地址来判断自身是否处于被调试状态,IsDebuggerPresent() 这个 API 在底层其实只是简单地读了一下这个字节。

ImageBaseAddress(PVOID)

指向进程主可执行文件(.exe)被加载到内存中的基地址。这个地址是进行内存操作和地址重定位计算的起点。

Ldr(PPEB_LDR_DATA)

这是一个指向 PEB_LDR_DATA 结构的指针。它维护了三个双向链表,记录了进程加载的所有模块(.exe 和 .dllPEB_LDR_DATA结构体定义如下:

typedef struct _PEB_LDR_DATA {
    ULONG Length;                          // 结构体长度
    BOOLEAN Initialized;                   // 是否已初始化
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;      // DLL 被加载到进程中的先后顺序
    LIST_ENTRY InMemoryOrderModuleList;    // DLL 在内存地址空间中的排列顺序
    LIST_ENTRY InInitializationOrderModuleList; // DLL 初始化函数(DllMain)被调用的顺序
    PVOID EntryInProgress;
    BOOLEAN ShutdownInProgress;
    HANDLE ShutdownThreadId;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

其中的三个双向链表InLoadOrderModuleList,InMemoryOrderModuleList,InInitializationOrderModuleList)都指向同一种数据结构LDR_DATA_TABLE_ENTRY,之所以存在三个链表,是为了提供不同的检索视角,LDR_DATA_TABLE_ENTRY的结构定义如下:

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;           // 对应加载顺序链表的指针
    LIST_ENTRY InMemoryOrderLinks;         // 对应内存顺序链表的指针
    LIST_ENTRY InInitializationOrderLinks; // 对应初始化顺序链表的指针
    PVOID DllBase;                         // 关键:DLL 在内存中的基地址
    PVOID EntryPoint;                      // DLL 的入口点 (DllMain)
    ULONG SizeOfImage;                     // DLL 映射在内存中的大小
    UNICODE_STRING FullDllName;            // DLL 的全路径(带路径)
    UNICODE_STRING BaseDllName;            // DLL 的简称(如 kernel32.dll)
    ULONG Flags;
    USHORT LoadCount;
    // ... 后面还有版本信息、时间戳等
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

每个加载的 DLL 都会对应一个这样的结构。 在网络安全攻防中,shellcode和免杀技术通常用到的经典操作:在不调用任何 Windows API 的情况下,找到 kernel32.dll 的基地址,进而通过遍历其导出表来调用 GetProcAddress。这里只单独的提一下逻辑:

  • 从 GS:[0x60] 获取 PEB。
  • 从 PEB 获取 Ldr 指针。
  • 遍历 InMemoryOrderModuleList 链表。
  • 比较每个节点的 BaseDllName,如果是kernel32.dll,则取出 DllBase。 在用户态Rootkit中也会利用Ldr来隐藏自身,Windows 的 API(如 EnumProcessModules 或 Toolhelp32Snapshot)在枚举模块时,本质上也是在遍历这三个链表。如果你手动修改这三个链表的 Flink 和 Blink 指针,将自己的 DLL 从链表中抠掉(断链),那么常规的工具就看不见这个 DLL 了,但它依然存在于内存中并可以运行。

ProcessParameters

这个结构是进程启动环境的快照,存储了进程创建时传递的命令行参数、可执行文件路径、环境变量块指针等。其中:

  • CommandLine:存储了完整的启动命令行字符串。
  • ImagePathName:程序在磁盘上的全路径。
  • Environment:包含了进程的所有环境变量。  在网络安全攻防中,恶意软件可以利用这些信息进行环境感知(例如,检查是否在沙箱环境中运行)。此外,一些高级的持久化或欺骗技术,如进程伪装(Process Masquerading),会尝试修改其他进程内存中的 CommandLine 或 ImagePathName 字段,使其在任务管理器等监控工具中看起来像一个合法的系统进程,从而隐藏其真实身份 。

NtGlobalFlag

这是一个 ULONG 类型的字段,包含了一系列系统级的全局标志。当进程在调试器下创建时(例如,通过 WinDbg 启动),内核会将某些特定的标志位(通常与堆调试相关)设置到这个字段中。常见的标志包括:

  • FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
  • FLG_HEAP_ENABLE_FREE_CHECK (0x20)
  • FLG_HEAP_VALIDATE_PARAMETERS (0x40)**一个正常启动的进程,其 NtGlobalFlag 通常为 0**。因此,恶意软件可以通过检查这个字段是否为非零值,或者是否设置了上述特定标志位,来判断自己是否处于被调试的环境中。这提供了一种比单独检查 BeingDebugged 更为可靠的反调试手段 。

SessionId

标识进程所属的终端服务会话ID,有助于区分不同用户会话下运行的同名进程。

相关偏移数据如下表:

| 字段名 | x86偏移 | x64偏移 | 大小 | 说明 | | — | — | — | — | — | | ImageBaseAddress | 0x008 | 0x010 | 4B/8B | 进程主模块基址 (如exe加载地址) | | Ldr | 0x00C | 0x018 | 4B/8B | 指向PEB_LDR_DATA ,模块加载器核心 theepicpowner.gitlab.io | | ProcessParameters | 0x010 | 0x020 | 4B/8B | 指向RTL_USER_PROCESS_PARAMETERS(命令行/环境变量) | | SubSystemData | 0x014 | 0x028 | 4B/8B | 子系统特定数据 | | ProcessHeap | 0x018 | 0x030 | 4B/8B | 进程默认堆地址 | | FastPebLock | 0x01C | 0x038 | 4B/8B | PEB互斥锁(临界区) | | BeingDebugged | 0x002 | 0x002 | 1B | 关键反调试字段 :1=被调试,0=正常 | | NtGlobalFlag | 0x06C | 0x0BC | 4B | 堆标志检测 :调试时通常为0x70(含FLG_HEAP_ENABLE_TAIL_CHECK等) | | | | | | |

通过windbg查看PEB信息

通过!peb查看当前线程的peb信息:

使用r $peb查看当前PEB的地址

通过dt ntdll!_PEB @$peb查看peb的结构

内存定位与访问机制

内存定位方式

PEB 和 TEB 最引人注目的特性之一,便是其高效且无文档化的访问方式。操作系统通过专门的段寄存器,为每个线程提供了一个直接指向其 TEB 的快捷方式。这种设计使得任何代码,包括自给自足的 Shellcode,都可以在不调用任何高层 Windows API (如 GetCurrentThreadId 或 GetModuleHandle) 的情况下,直接从内存中读取关键信息。

  • x86 架构 (32位)FS 段寄存器被用作指向当前线程 TEB 的基址。因此,可以通过 FS:[offset] 的形式直接访问 TEB 的成员。例如:

  • FS:[0x18]: 存储 TEB 自身的线性地址。

  • FS:[0x30]: 存储指向 PEB 的指针。

  • x64 架构 (64位)GS 段寄存器扮演了同样的角色。访问方式变为 GS:[offset]。例如:

  • GS:[0x30]: 存储 TEB 自身的线性地址。

  • GS:[0x60]: 存储指向 PEB 的指针。

汇编方式访问

通过FS/GS寄存器,代码可以不依赖API直接访问TEB和PEB,这也是其被恶意软件滥用的根本原因。在编写shellcode时通常使用汇编的方式读取TEBPEB,以下是x86架构下通过内联汇编的方式访问TEB的示例

mov eax, fs:[0x18]    ; 获取 TEB 自引用指针
mov eax, fs:[0x30]    ; 获取 PEB 指针
mov eax, fs:[0x34]    ; 获取 LastError

Shellcode定位Kernel32

; x86示例:通过TEB→PEB→Ldr遍历模块
mov eax, fs:[0x30]      ; 获取PEB
mov eax, [eax+0x0C]     ; PEB.Ldr
mov eax, [eax+0x14]     ; InMemoryOrderModuleList
mov eax, [eax]          ; 第二个模块(通常是kernel32.dll)
mov eax, [eax+0x10]     ; BaseAddress

在c代码中也可以使用内联汇编的方式进行获取PEB,然后对其进行操作:

#include&nbsp;<windows.h>
#include&nbsp;<winternl.h>
#include&nbsp;<stdio.h>

PPEB&nbsp;GetPeb_x86()&nbsp;{
&nbsp; &nbsp; PPEB pPeb =&nbsp;NULL;
&nbsp; &nbsp; __asm {
&nbsp; &nbsp; &nbsp; &nbsp; mov eax, fs:[0x30] &nbsp;// 通过FS段寄存器和0x30偏移直接读取PEB指针
&nbsp; &nbsp; &nbsp; &nbsp; mov pPeb, eax &nbsp; &nbsp; &nbsp;&nbsp;// 将地址存入pPeb变量
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;pPeb;
}

int&nbsp;main_x86_example()&nbsp;{
&nbsp; &nbsp; PPEB peb = GetPeb_x86();
&nbsp; &nbsp;&nbsp;if&nbsp;(peb) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("PEB (x86) Address: %p\n", peb);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("BeingDebugged Flag: %d\n", peb->BeingDebugged);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

编译器内联函数方式访问

为了解决内联汇编的平台和编译器限制,现代编译器提供了内联函数(Intrinsics)来执行特定的处理器指令。这是目前推荐的、更安全、更具可移植性的方法,这种方法行对来讲较为直接写汇编代码更简单一些。

  • x86: 使用 __readfsdword(offset)
  • x64: 使用 __readgsqword(offset)以下示例采用了内联函数的方式获取TEBPEB
#include&nbsp;<windows.h>
#include&nbsp;<intrin.h> &nbsp;// 必须包含此头文件

// 获取TEB地址(x86/x64通用封装)
PTEB&nbsp;GetTEB(void)&nbsp;{
#ifdef&nbsp;_M_IX86 &nbsp;// x86
&nbsp; &nbsp;&nbsp;return&nbsp;(PTEB)__readfsdword(0x18); &nbsp;// FS:[0x18] = Self (TEB自身指针)
#elif&nbsp;defined(_M_X64) &nbsp;// x64
&nbsp; &nbsp;&nbsp;return&nbsp;(PTEB)__readgsqword(0x30); &nbsp;// GS:[0x30] = Self
#else
&nbsp; &nbsp;&nbsp;#error&nbsp;"Unsupported architecture"
#endif
}

// 获取PEB地址
PPEB&nbsp;GetPEB(void)&nbsp;{
#ifdef&nbsp;_M_IX86
&nbsp; &nbsp;&nbsp;return&nbsp;(PPEB)__readfsdword(0x30); &nbsp;// FS:[0x30] = PEB指针
#elif&nbsp;defined(_M_X64)
&nbsp; &nbsp;&nbsp;return&nbsp;(PPEB)__readgsqword(0x60); &nbsp;// GS:[0x60] = PEB指针
#else
&nbsp; &nbsp;&nbsp;#error&nbsp;"Unsupported architecture"
#endif
}

int&nbsp;main()&nbsp;{
&nbsp; &nbsp; PPEB peb = GetPEB();
&nbsp; &nbsp;&nbsp;if&nbsp;(peb) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("PEB Address (via Intrinsic): %p\n", peb);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("ImageBaseAddress: %p\n", peb->ImageBaseAddress);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

通过windows API进行访问

NtCurrentTeb

这是一个在winnt.h中定义的宏或内联函数,用于获取当前线程的TEB指针。它本身就是对底层汇编或内联函数的封装,使用NtCurrentTeb()是获取TEB最标准、最便捷的方式。用法示例参考:

#include&nbsp;<winnt.h>
#include&nbsp;<stdio.h>

int&nbsp;main_teb_example()&nbsp;{
&nbsp; &nbsp; PTEB teb = NtCurrentTeb();
&nbsp; &nbsp;&nbsp;if&nbsp;(teb) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("TEB Address: %p\n", teb);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("Thread ID: %lu\n", (ULONG)(ULONG_PTR)teb->ClientId.UniqueThread);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 通过TEB访问PEB
&nbsp; &nbsp; &nbsp; &nbsp; PPEB peb = teb->ProcessEnvironmentBlock;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("PEB Address (from TEB): %p\n", peb);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

RtlGetCurrentPeb

这是一个自Windows XP以来由ntdll.dll导出的未文档化函数。它的实现非常简单,就是返回NtCurrentTeb()->ProcessEnvironmentBlock。虽然未文档化,但由于其长期存在和广泛使用,它被认为是相当稳定的。用法示例参考:

#include&nbsp;<windows.h>
#include&nbsp;<winternl.h>
#include&nbsp;<stdio.h>

// 需要手动声明函数原型,因为它未在公共头文件中声明
extern&nbsp;"C"&nbsp;PPEB NTAPI&nbsp;RtlGetCurrentPeb(VOID);

int&nbsp;main_rtl_example()&nbsp;{
&nbsp; &nbsp; PPEB peb = RtlGetCurrentPeb();
&nbsp; &nbsp;&nbsp;if&nbsp;(peb) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("PEB Address (via RtlGetCurrentPeb): %p\n", peb);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

NtQueryInformationProcess

这是一个功能极其强大的内核函数,可以查询指定进程的各种信息。与前面只能获取当前进程PEB的方法不同,NtQueryInformationProcess可以获取任意进程的PEB地址,只要调用者拥有足够的权限(PROCESS_QUERY_INFORMATION)。 当使用ProcessBasicInformation作为信息类参数时,函数会填充一个PROCESS_BASIC_INFORMATION结构,其中就包含了目标进程的PEB基地址, 示例参考:

#include&nbsp;<windows.h>
#include&nbsp;<winternl.h>
#include&nbsp;<stdio.h>

// 定义函数指针类型
typedef&nbsp;NTSTATUS(NTAPI* pfnNtQueryInformationProcess)(
&nbsp; &nbsp; HANDLE ProcessHandle,
&nbsp; &nbsp; PROCESSINFOCLASS ProcessInformationClass,
&nbsp; &nbsp; PVOID ProcessInformation,
&nbsp; &nbsp; ULONG ProcessInformationLength,
&nbsp; &nbsp; PULONG ReturnLength
);

int&nbsp;main_ntquery_example()&nbsp;{
&nbsp; &nbsp; HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
&nbsp; &nbsp;&nbsp;if&nbsp;(!hNtDll)&nbsp;return&nbsp;1;

&nbsp; &nbsp; pfnNtQueryInformationProcess NtQueryInformationProcess =
&nbsp; &nbsp; &nbsp; &nbsp; (pfnNtQueryInformationProcess)GetProcAddress(hNtDll,&nbsp;"NtQueryInformationProcess");
&nbsp; &nbsp;&nbsp;if&nbsp;(!NtQueryInformationProcess)&nbsp;return&nbsp;1;

&nbsp; &nbsp; PROCESS_BASIC_INFORMATION pbi = {&nbsp;0&nbsp;};
&nbsp; &nbsp; ULONG returnLength =&nbsp;0;

&nbsp; &nbsp;&nbsp;// 获取当前进程的PEB
&nbsp; &nbsp; NTSTATUS status = NtQueryInformationProcess(
&nbsp; &nbsp; &nbsp; &nbsp; GetCurrentProcess(),
&nbsp; &nbsp; &nbsp; &nbsp; ProcessBasicInformation,
&nbsp; &nbsp; &nbsp; &nbsp; &pbi,
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sizeof(pbi),
&nbsp; &nbsp; &nbsp; &nbsp; &returnLength
&nbsp; &nbsp; );

&nbsp; &nbsp;&nbsp;if&nbsp;(NT_SUCCESS(status)) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("PEB Address (via NtQueryInformationProcess): %p\n", pbi.PebBaseAddress);
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("Process ID: %llu\n", (unsigned&nbsp;long&nbsp;long)pbi.UniqueProcessId);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;printf("NtQueryInformationProcess failed with status: 0x%X\n", status);
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;return&nbsp;0;
}

相关攻防手法

在windows 攻防方面攻击者常常会巧妙地操纵PEB和TEB,以实现反调试、模块隐藏、进程伪装和动态API解析等一系列高级规避技术。以下是一些常见的技术场景:

通过PEB Walking绕过导入表监控

传统的Windows程序在编译时会将其依赖的外部函数信息记录在PE文件的导入地址表(IAT)中。程序加载时,Windows加载器会解析IAT,填入所需API的真实地址。然而,这个静态的IAT成为了安全软件和分析师的重点关注对象。通过分析IAT,可以迅速了解一个程序可能具备的功能(如网络通信、文件操作、注册表修改等),从而判断其是否可疑。为了绕过这种静态检测和更高级的API钩子(Hooking)技术,恶意软件发展出了通过PEB Walking进行动态API解析的策略。 该技术完全摒弃了静态的IAT,转而在运行时手动在内存中查找所需API的地址。它利用PEB作为起点,一步步导航到加载到进程空间中的任何DLL(特别是kernel32.dllntdll.dll),然后解析该DLL的导出表(Export Address Table, EAT),最终找到目标函数的内存地址。这种方式使得恶意代码没有任何可疑的导入项

模块隐藏

Windows通过PEB中的Ldr字段(一个指向_PEB_LDR_DATA结构的指针)来管理进程加载的所有模块。_PEB_LDR_DATA结构内部维护了三个独立的双向链表,分别按照模块的加载顺序(InLoadOrderModuleList)、内存布局顺序(InMemoryOrderModuleList)和初始化顺序(InInitializationOrderModuleList)来组织模块信息。每个链表节点都是一个_LDR_DATA_TABLE_ENTRY结构,包含了模块的基地址、名称、大小等详细信息。模块隐藏的精髓就在于,通过精确地修改这三个链表的指针,将恶意模块对应的节点从链表中摘除,即所谓的断链。

修改PEB信息,实现进程伪装

一个进程的身份信息,例如它的可执行文件路径和启动时的命令行参数,并非一成不变的,而是作为元数据存储在PEB中。具体来说,这些信息位于PEB指向的_RTL_USER_PROCESS_PARAMETERS结构中。由于PEB位于用户态内存,拥有足够权限的进程(通常是进程自身或具有调试权限的父进程)可以修改这些数据,从而实现将命令行,映像路径等伪装为合法正常的信息。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:剑外思归客 R0x7e R0x7e《Windows安全攻防-PEB&TEB》

Windows安全攻防-PEB&TEB 网络安全文章

Windows安全攻防-PEB&TEB

文章总结: 本文深入解析Windows系统的TEB与PEB结构,涵盖定义、字段及内存定位。重点介绍了通过段寄存器访问的方法,并分析相关攻防技术,包括利用PEBW
评论:0   参与:  0