PEB及其武器化

admin 2024年2月5日13:40:23评论12 views字数 7452阅读24分50秒阅读模式

在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 结构
PEB及其武器化
下图可以看到,TEB 偏移 0x30 就可以得到 PEB
PEB及其武器化
所以我们得到一个结论

pTEB->0x30 = PEB

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

动手调试

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

获取 PEB 的几种方式

还有一个小问题需要解决,那就是我们如何获得TEB 的内存地址呢,在上面分析时我们直接用的是 windbg 中的命令,并没有提到这个问题。
其实,在 x86 下 TEB 结构指针存储在 fs 寄存器中,在 x64 下 TEB 结构指针存储在 gs 寄存器中。另外我们在前面提到过的 x64 和 x86 的区别主要是 TEB 到 PEB 的偏移量不同,在 x64 下该偏移量位 0x60。
也有文章说 fs:[0x18] 是 PEB,接下来我们来看一下该说法原理:
这里我们关注第一个结构体,TIB(线程信息块)
PEB及其武器化
点进去可以看到里面的一些成员,标出来的 self 就是指向自身的指针,也就是指向 0x0 的ExceptionList
PEB及其武器化
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: %pn", pebAddress);
} else {
wprintf(L"Error: NtQueryInformationProcess failed with status 0x%Xn", 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及其武器化

武器化

接下来介绍 PEB 在免杀中的一些应用。

动态获取 api

我们通过上述的学习已经可以在 windbg 中获取kerndl32.dll 的地址,接下来要做的就是遍历导出表找到所需要的函数,这里需要一些 PE 格式的相关知识,这里不再赘述。
我们的汇编代码如下:
PEB及其武器化
这是 x64 下的,所以偏移量和 x86 下不同,建议大家对照 vp 再去看一遍,其中 mov rax,[rax]是为了获取下一个节点。
下面是我们验证的代码,比较一下我们获取的函数地址和 GetModuleHandle 函数获取的地址有区别吗,另外不要忘记导出函数。
PEB及其武器化
可以看到结果是没有问题的
PEB及其武器化
接下来要做的就是遍历导出表了

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

由下图可以看到是一样的,所以说实现的并没有问题
PEB及其武器化

进程伪装

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

getchar();

}

可以看到 ProcessExplorer 的图标已经变成记事本,点进去查看发现已经修改成功,实现了进程伪装。(但是有一点,我使用 Process Hacker2 去查看,并没有修改成功)
PEB及其武器化
PEB及其武器化

反调试

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

 

 

来源:【https://xz.aliyun.com/】,感谢【fdx 】

原文始发于微信公众号(船山信安):PEB及其武器化

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月5日13:40:23
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PEB及其武器化http://cn-sec.com/archives/2469878.html

发表评论

匿名网友 填写信息