PEB及其武器化
本文首发:https://xz.aliyun.com/t/13556
在Windows操作系统中,PEB是指" Process Environment Block ",它是一个数据结构,用于存储有关进程的信息,每个进程都有一个对应的 PEB 结构体。PEB提供了许多关于进程状态和环境的信息,它是用户模式和内核模式之间的一个关键接口。
我们利用 PEB 可以完成很多事情,比如说动态获取 api,进程伪装,反调试等等。
TEB
在了解 PEB 之前我们要先了解什么是 TEB,TEB指的是线程环境块" Thread Environment Block ",用于存储线程状态信息和线程所需的各种数据。每个线程都有一个对应的TEB结构体,并且 TEB 结构的其中一个成员就是 PEB。
分析过程
接下来我们以 32 位为例进行 TEB 和 PEB 的分析,关于 64 位的结论会在下面一并给出。
我们可以通过 https://www.vergiliusproject.com/ 这个网站来分析,也可以选择使用 windbg 来进行分析。
理论学习
我们首先在 vp 中找到 TEB 结构
下图可以看到,TEB 偏移 0x30 就可以得到 PEB
所以我们得到一个结论
pTEB->0x30 = PEB
我们继续看 PEB 结构,这里我们先关注 0x0c 处的 Ldr,里面存储着有关模块加载的信息。
点进去看看,又看到三个结构体
这三个结构体的结构都是一样的,都是双向链表,不同之处是加载的模块的顺序不同
第一个成员 Flink 指向下一个节点,Blink 指向上一个节点,所以这是一个双向链表,当我们从_PEB_LDR_DATA 结构中取到 InInitializationOrderModuleList 结构时,这个结构中的 Flink 指向真正的模块链表,这个真正的链表的每个成员都是一个 LDR_DATA_TABLE_ENTRY 结构。
之前的 _PEB_LDR_DATA 只是一个入口,这个结构只有一个,它不是链表节点,真正的链表节点结构如下图:
他们之间的对应关系可以由下图来表示,如果学习过链表的概念还是挺好理解的(图片来自https://bbs.kanxue.com/thread-266678.htm)
简化版如下:
可以看到这是一个以PEB_LDR_DATA为起点的一个闭合环形双向链表。
每个_LDR_DATA_TABLE_ENTRY节点结构中偏移为0x30处的成员为dllName,偏移为0x18处的成员为DllBase。
通过遍历链表,比较dllName字符串内容可以找到目标模块的所属节点。
通过节点成员DllBase可以定位该模块的DOS头起始处。
通过对PE结构的解析可以搜索导出表,从而可以取到指定的导出函数地址。
动手调试
我们接下来利用 windbg 手动走一遍定位 dll 过程。
我们先启动一个程序,我这里启动的是 notepad,然后在 windbg 中附加上去。
附加之后,使用dt _TEB @$teb
可以查看当前线程 TEB 相关的信息,并且得到 PEB 相关的信息。
这里直接点过来就可以得到 PEB 相关的信息
这里注意到上图最下方的 Ldr,点一下就可以得到 Ldr 相关的信息
我们以第一个链表进行分析,直接点进去,得到 Flink 和 Blink
同样也可以看到内存地址,我们接下来直接去看内存地址 dt 0x1b678806dc0 _LDR_DATA_TABLE_ENTRY
这里第一个 FullDllName 是 notepad 的路径,我们接着看下一个节点,直接点击下图红框位置
点进去得到如下:
然后用该命令dt 0x1b678806c70 _LDR_DATA_TABLE_ENTRY
看下一个节点
已经得到 ntdll.dll
获取 PEB 的几种方式
还有一个小问题需要解决,那就是我们如何获得TEB 的内存地址呢,在上面分析时我们直接用的是 windbg 中的命令,并没有提到这个问题。
其实,在 x86 下 TEB 结构指针存储在 fs 寄存器中,在 x64 下 TEB 结构指针存储在 gs 寄存器中。另外我们在前面提到过的 x64 和 x86 的区别主要是 TEB 到 PEB 的偏移量不同,在 x64 下该偏移量位 0x60。
也有文章说 fs:[0x18] 是 PEB,接下来我们来看一下该说法原理:
这里我们关注第一个结构体,TIB(线程信息块)
点进去可以看到里面的一些成员,标出来的 self 就是指向自身的指针,也就是指向 0x0 的ExceptionList
0x0+0x18=0x18
,所以说 fs:[0x18] 是 PEB。
下面简单介绍一下获取 PEB 的几种方式
汇编调用
在 x86 下可以直接内联汇编
__asm
{
mov eax, dword ptr fs : [00000030h]
mov peb, eax
}
在 x64 下则需要一个单独的 asm 文件并且进行一些配置,可以参考博客:vs2022 x64 C/C++和汇编混编_vs 嵌入汇编-CSDN博客,代码 demo 如下:
.code
GetPEB PROC
mov rax, gs:[30h] ; TEB from gs in 64 bit only
mov rax, [rax+60h] ; PEB
ret
GetPEB ENDP
end
__readfsdword 与 __readgsqword
可以直接通过 __readfsdword(0x30) 或者 __readgsqword(0x60) 来获取 PEB,demo 如下:
#include <windows.h>
#include <winternl.h>
#ifndef _WIN64
PPEB pPeb = (PPEB)__readfsdword(0x30);
#else
PPEB pPeb = (PPEB)__readgsqword(0x60);
#endif
NtQueryInformationProcess
NtQueryInformationProcess 是一个内核函数,用来查看进程信息,其结构体如下:
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
它的第二个参数可以是一个PROCESS_BASIC_INFORMATION的结构体:
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
第二个值**PebBaseAddress **指向 PEB 结构。据此我们可以有如下 demo:
#include <windows.h>
#include <winternl.h>
// Define the PROCESS_BASIC_INFORMATION structure
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
// Define the NtQueryInformationProcess function
typedef NTSTATUS (NTAPI *PNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
int main() {
// Open the current process
HANDLE hProcess = GetCurrentProcess();
// Define a buffer to hold the information
PROCESS_BASIC_INFORMATION pbi;
// Load NtQueryInformationProcess dynamically
HMODULE hNtDll = GetModuleHandle(L"ntdll.dll");
PNtQueryInformationProcess pNtQueryInformationProcess =
(PNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess");
// Call NtQueryInformationProcess to get the PEB address
NTSTATUS status = pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (NT_SUCCESS(status)) {
// Access PEB address from the structure
PPEB pebAddress = pbi.PebBaseAddress;
wprintf(L"PEB Address: %p\n", pebAddress);
} else {
wprintf(L"Error: NtQueryInformationProcess failed with status 0x%X\n", status);
}
// Close the process handle
CloseHandle(hProcess);
return 0;
}
小结:
通过上述学习,我们已经成功通过 PEB 得到了ntdll.dll,当然也可以获取kerndl32.dll,下面是三个双向链表加载 dll 的顺序:
- InLoadOrderModuleList 模块加载顺序
notepad.exe ntdll.dll kernel32.dll kernelbase.dll
- InMemoryOrderModuleList 模块在内存加载顺序
notepad.exe ntdll.dll kernel32.dll kernelbase.dll
- InInitializationOrderLinks 模块初始化装载顺序
ntdll.dll kernelbase.dll kernel32.dll
还有一张神图:
武器化
接下来介绍 PEB 在免杀中的一些应用。
动态获取 api
我们通过上述的学习已经可以在 windbg 中获取kerndl32.dll 的地址,接下来要做的就是遍历导出表找到所需要的函数,这里需要一些 PE 格式的相关知识,这里不再赘述。
我们的汇编代码如下:
这是 x64 下的,所以偏移量和 x86 下不同,建议大家对照 vp 再去看一遍,其中 mov rax,[rax]
是为了获取下一个节点。
下面是我们验证的代码,比较一下我们获取的函数地址和 GetModuleHandle 函数获取的地址有区别吗,另外不要忘记导出函数。
可以看到结果是没有问题的
接下来要做的就是遍历导出表了
PVOID GetAddressFromExportTable(PVOID pBaseAddress, PCHAR pszFunctionName)
{
PVOID get_address = 0;
DWORD ulFunctionIndex = 0;
// DOS头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
// NT 头
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
// 导出表
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((PUCHAR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress);
//导出表有名字的函数的个数
ULONG NumberOfFunctions = pExportTable->NumberOfFunctions;
//导出函数名称地址表
PULONG AddressOfNamesTable = (PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfNames);
PCHAR Name = NULL;
//循环导出函数名称地址表
for (ULONG i = 0; i < NumberOfFunctions; i++) {
Name = (PCHAR)((PUCHAR)pDosHeader + AddressOfNamesTable[i]);
//如果找到了
if (0 == _strnicmp(pszFunctionName, Name, strlen(pszFunctionName))) {
//找到对应序号
USHORT ordinal = *(USHORT*)((PUCHAR)pDosHeader + pExportTable->AddressOfNameOrdinals + 2 * i);
//根据序号找到RVA
ULONG FuncAddr = *(PULONG)((PUCHAR)pDosHeader + pExportTable->AddressOfFunctions + 4 * ordinal);
get_address = (PVOID)((PUCHAR)pDosHeader + FuncAddr);
return get_address;
}
}
return 0;
}
然后用一小段代码进行验证
typedef VOID(WINAPI* pSleep)(DWORD);
pSleep mySleep = (pSleep)GetAddressFromExportTable(GetKernel32(), (PCHAR)"Sleep");
mySleep(5000);
或者将获取的地址和 GetProcAddress 获取的函数的地址进行比较
pVirtualAlloc alloc = (pVirtualAlloc)GetAddressFromExportTable(getKernel32(), (PCHAR)"Sleep");
pVirtualAlloc a = (pVirtualAlloc)GetProcAddress(GetModuleHandle(L"kernel32"), "Sleep");
由下图可以看到是一样的,所以说实现的并没有问题
进程伪装
我们现在随便启动一个 notepad 的进程,然后用 ProcessExplorer 去观察
其实我们要做的很简单,就是在 peb 中找到对应的数据结构,然后对其进行修改就实现了我们的进程伪装。
首先是进程映像路径和命令行。
还是在上面 vp 的网站上
我们找到 ProcessParameters ,然后点进去就可以发现 ImagePathName 和 CommandLine 两项,就对应着进程映像路径和命令行,
我们红框框了 3 项,还有一个进程当前目录,我们要用 SetCurrentDirectoryW 这个 api 来进行修改,下面是一个简单的 demo
#include <stdlib.h>
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
int main()
{
// 获取TEB中PEB指针的偏移
#ifdef _M_IX86
PPEB PEB = (PPEB)__readfsdword(0x30);
#else
PPEB PEB = (PPEB)__readgsqword(0x60);
#endif
// 改变进程当前目录 注意:路径 必须是存在的,否则会报错
int result = SetCurrentDirectoryW(L"C:\\Users\\James\\");
//改变进程映像路径和命令行
WCHAR path[] = L"c:\\windows\\system32\\notepad.exe\0";
PEB->ProcessParameters->ImagePathName.Buffer = path;
PEB->ProcessParameters->CommandLine.Buffer = path;
getchar();
}
可以看到 ProcessExplorer 中恶意程序的图标已经变成记事本,点进去查看发现已经修改成功,实现了进程伪装。(但是有一点,我使用 Process Hacker2 去查看,并没有修改成功)
反调试
关于 PEB 反调试的内容,主要涉及的有
- BeingDebugged :当有调试器附加的时候该位被置为1
- NtGlobalFlag : 当有调试器附加的时候该位被置为 0x70
- Heap Flags : 在PEB的ProcessHeap位指向_HEAP结构体,该结构体中有俩个字段( HeapFlags 和 ForceFlags )会受到调试器的影响,如果 HeapFlags 的值大于2,或 ForceFlags 的值大于0时,说明被调试。
- 堆 Magic 标志 :当进程被调试器调试时该进程堆会被一些特殊的标志填充,这些特殊标记分别是0xABABABAB , 0xFEEEFEEE 。在调试模式下, NtGlobalFlag 的 HEAP_TAIL_CHECKING_ENABLED 标志将被默认设置,堆内存分配会在末尾追加 0xABABABAB 标志进行安全检查,如果NtGlobalFlag设置了HEAP_FREE_CHECKING_ENABLED 标志,那么当需要额外的字节来填充堆块尾部时, 就会使用 0xFEEEFEEE (或一部分) 来填充。
有一篇文章已经总结的很好了,demo 上面都有,这里就不再过多赘述了,地址:https://anti-debug.checkpoint.com/techniques/debug-flags.html#manual-checks-peb-beingdebugged-flag