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 结构
image.png
下图可以看到,TEB 偏移 0x30 就可以得到 PEB
image.png
所以我们得到一个结论

pTEB->0x30 = PEB

我们继续看 PEB 结构,这里我们先关注 0x0c 处的 Ldr,里面存储着有关模块加载的信息。
image.png
点进去看看,又看到三个结构体
image.png这三个结构体的结构都是一样的,都是双向链表,不同之处是加载的模块的顺序不同
image.png
第一个成员 Flink 指向下一个节点,Blink 指向上一个节点,所以这是一个双向链表,当我们从_PEB_LDR_DATA 结构中取到 InInitializationOrderModuleList 结构时,这个结构中的 Flink 指向真正的模块链表,这个真正的链表的每个成员都是一个 LDR_DATA_TABLE_ENTRY 结构。
之前的 _PEB_LDR_DATA 只是一个入口,这个结构只有一个,它不是链表节点,真正的链表节点结构如下图:
image.png
他们之间的对应关系可以由下图来表示,如果学习过链表的概念还是挺好理解的(图片来自https://bbs.kanxue.com/thread-266678.htm
image.png
简化版如下:
image.png
可以看到这是一个以PEB_LDR_DATA为起点的一个闭合环形双向链表。
每个_LDR_DATA_TABLE_ENTRY节点结构中偏移为0x30处的成员为dllName,偏移为0x18处的成员为DllBase。
通过遍历链表,比较dllName字符串内容可以找到目标模块的所属节点。
通过节点成员DllBase可以定位该模块的DOS头起始处。
通过对PE结构的解析可以搜索导出表,从而可以取到指定的导出函数地址。

动手调试

我们接下来利用 windbg 手动走一遍定位 dll 过程。
我们先启动一个程序,我这里启动的是 notepad,然后在 windbg 中附加上去。
image.png
附加之后,使用dt _TEB @$teb 可以查看当前线程 TEB 相关的信息,并且得到 PEB 相关的信息。
image.png
这里直接点过来就可以得到 PEB 相关的信息
image.png
这里注意到上图最下方的 Ldr,点一下就可以得到 Ldr 相关的信息
image.png
我们以第一个链表进行分析,直接点进去,得到 Flink 和 Blink
image.png
同样也可以看到内存地址,我们接下来直接去看内存地址 dt 0x1b678806dc0 _LDR_DATA_TABLE_ENTRY
image.png
这里第一个 FullDllName 是 notepad 的路径,我们接着看下一个节点,直接点击下图红框位置
image.png
点进去得到如下:
image.png
然后用该命令dt 0x1b678806c70 _LDR_DATA_TABLE_ENTRY看下一个节点
image.png
已经得到 ntdll.dll

获取 PEB 的几种方式

还有一个小问题需要解决,那就是我们如何获得TEB 的内存地址呢,在上面分析时我们直接用的是 windbg 中的命令,并没有提到这个问题。
其实,在 x86 下 TEB 结构指针存储在 fs 寄存器中,在 x64 下 TEB 结构指针存储在 gs 寄存器中。另外我们在前面提到过的 x64 和 x86 的区别主要是 TEB 到 PEB 的偏移量不同,在 x64 下该偏移量位 0x60。
也有文章说 fs:[0x18] 是 PEB,接下来我们来看一下该说法原理:
这里我们关注第一个结构体,TIB(线程信息块)
image.png
点进去可以看到里面的一些成员,标出来的 self 就是指向自身的指针,也就是指向 0x0 的ExceptionList
image.png
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 格式的相关知识,这里不再赘述。
我们的汇编代码如下:
image.png
这是 x64 下的,所以偏移量和 x86 下不同,建议大家对照 vp 再去看一遍,其中 mov rax,[rax]是为了获取下一个节点。
下面是我们验证的代码,比较一下我们获取的函数地址和 GetModuleHandle 函数获取的地址有区别吗,另外不要忘记导出函数。
image.png
可以看到结果是没有问题的
image.png
接下来要做的就是遍历导出表了

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");

由下图可以看到是一样的,所以说实现的并没有问题
image.png

进程伪装

我们现在随便启动一个 notepad 的进程,然后用 ProcessExplorer 去观察
image.png
其实我们要做的很简单,就是在 peb 中找到对应的数据结构,然后对其进行修改就实现了我们的进程伪装。
首先是进程映像路径和命令行。
还是在上面 vp 的网站上
image.png
我们找到 ProcessParameters ,然后点进去就可以发现 ImagePathName 和 CommandLine 两项,就对应着进程映像路径和命令行,
image.png
我们红框框了 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 去查看,并没有修改成功)
image.png
image.png

反调试

关于 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

posted @ 2024-02-18 18:22  fdx_xdf  阅读(121)  评论(0编辑  收藏  举报