解析PEB

前言:

学习syscall和免杀绕不开PEB这个部分,我们要对相关结构熟悉,既要会手动解析,也能利用代码解析。

PEB结构:

经典大图,永不过时:

注:PEB的结构微软并未完全公开,以上图片可以参考但不一定权威

如果你对偏移比较熟悉的话可以直接用指针+偏移找,不需要特地定义结构体,而且双向链表_LIST_ENTRY这个是公开的。

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

自定义PEB的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBase;
PPEB_LDR_DATA LoaderData; // 0x0c
PVOID ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PVOID FastPebLockRoutine;
PVOID FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
PVOID* KernelCallbackTable;
PVOID EventLogSection;
PVOID EventLog;
PVOID FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[0x2];
PVOID ReadOnlySharedMemoryBase;
PVOID ReadOnlySharedMemoryHeap;
PVOID* ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
BYTE Spare2[0x4];
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID** ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, * PPEB;

我们主要是需要0x0c(64位为0x18)偏移处的PPEB_LDR_DATA结构

1
2
3
4
5
6
7
8
typedef struct _PEB_LDR_DATA {
ULONG Length;
ULONG Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

PEB_LDR_DATA的0x0c、0x14、0x1c中为三个双向链表LIST_ENTRY
三个链表加载模块分别代表模块加载顺序,模块在内存中的加载顺序以及模块初始化装载的顺序PEB_LDR_DATA结构类似于一个火车头,三个双向链表LIST_ENTRY意思相同,只是顺序不一样,都指向下一个_LDR_DATA_TABLE_ENTRY结构(也有的定义为_LDR_MODULE 结构)毕竟微软没有公开结构,只要我们所需的偏移一致就行。
_LDR_DATA_TABLE_ENTRY 32位版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
ULONG Flags; //0x34
USHORT LoadCount; //0x38
USHORT TlsIndex; //0x3a
union
{
struct _LIST_ENTRY HashLinks; //0x3c
struct
{
VOID* SectionPointer; //0x3c
ULONG CheckSum; //0x40
};
};
union
{
ULONG TimeDateStamp; //0x44
VOID* LoadedImports; //0x44
};
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x48
VOID* PatchInformation; //0x4c
struct _LIST_ENTRY ForwarderLinks; //0x50
struct _LIST_ENTRY ServiceTagLinks; //0x58
struct _LIST_ENTRY StaticLinks; //0x60
VOID* ContextInformation; //0x68
ULONG OriginalBase; //0x6c
union _LARGE_INTEGER LoadTime; //0x70
};

_LDR_DATA_TABLE_ENTRY64 64位版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef struct _LDR_DATA_TABLE_ENTRY64
{
struct LIST_ENTRY64 InLoadOrderLinks; //0x0
struct LIST_ENTRY64 InMemoryOrderLinks; //0x10
struct LIST_ENTRY64 InInitializationOrderLinks; //0x20
VOID* DllBase; //0x30
VOID* EntryPoint; //0x38
ULONG SizeOfImage; //0x40
struct _UNICODE_STRING FullDllName; //0x48
struct _UNICODE_STRING BaseDllName; //0x58
ULONG Flags; //0x68
USHORT LoadCount; //0x6c
USHORT TlsIndex; //0x6e
union
{
struct LIST_ENTRY64 HashLinks; //0x70
struct
{
VOID* SectionPointer; //0x70
ULONG CheckSum; //0x78
};
};
union
{
ULONG TimeDateStamp; //0x80
VOID* LoadedImports; //0x80
};
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x88
VOID* PatchInformation; //0x90
struct LIST_ENTRY64 ForwarderLinks; //0x98
struct LIST_ENTRY64 ServiceTagLinks; //0xa8
struct LIST_ENTRY64 StaticLinks; //0xb8
VOID* ContextInformation; //0xc8
ULONGLONG OriginalBase; //0xd0
union _LARGE_INTEGER LoadTime; //0xd8
}LDR_DATA_TABLE_ENTRY64, * PLDR_DATA_TABLE_ENTRY64;

_LDR_MODULE 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, * PLDR_MODULE;

PPEB_LDR_DATA结构是一个入口,类似于火车头,里面_LIST_ENTRY->Flink指向第一个_LDR_DATA_TABLE_ENTRY,然后第一个指向第二个以此类推,最后一个指向第一个,而_LIST_ENTRY->Blink指向的是上一个火车,第二个指向火车头,火车头指向最后一个。

这是广为流传的一张图,大多数文章都会拿它做案例,但其实它有一点点错误(吐槽:这么多人都没发现),Blink指向的并不是上一个Blink,而是指向上一个LIST_ENTRY的地址,下面进行详细分析。

解析PEB:

我们如何找到PEB的地址?这里有三种方法:

  1. 查看OD等调试器的寄存器窗口,fs段寄存器的后面会接着TEB结构指针。直接在内存窗口跳过去即可
  2. 通过fs的值拆分成段选择子,通过GDT表查找段描述符,得到一个3环的调用门….#*#%$()#….. 很显然我不会,直接放弃。
  3. 第三种是最方便的,32位:fs:[0x18]存储着TEB结构指针,fs:[0x30]存储着PEB结构指针。64位:gs:[0x60]存储着PEB结构指针。

为什么不能通过fs直接过去呢?因为fs存储的是段选择子,不是真正的地址所以需要通过偏移。
接下来尝试手动解析:这里用的是64位exe,工具x64dbg

利用gs:[0x60]获取PEB地址,也可以直接用peb()快捷获取,得到地址:000000CA38C50000,ctrl+g跳转,此时就是PEB的结构指针。我们在0x18h(在64位下偏移是18h)处找到PPEB_LDR_DATA地址00007FF8E7B1C4C0

跳转到PPEB_LDR_DATA处,0X20处偏移是我们要找的第一个LDR_DATA_TABLE_ENTRYInMemoryOrderModuleList地址:0000021892B02820,此时

  • Flink地址:0000021892B02650
  • Blink地址:00007FF8E7B1C4E0
  • EntryPoint:00007FF7A96916E0
  • FullDllName:(0x58-0x60)
  • BaseDllName:(0x68-0x70)

根据Flink跳转至下一个LDR_DATA_TABLE_ENTRY

下一个_LDR_DATA_TABLE_ENTRY结构的InMemoryOrderModuleList
此时Flink:0000021892B02D70指向下一个InMemoryOrderModuleList地址,Blink:0000021892B02820刚好指向了上一个InMemoryOrderModuleList地址

  • FullDllName:c:\windows\system32\ntdll.dll
  • BaseDllName:ntdll.dll

继续下一跳,Flink:0000021892B02D70,Blink:0000021892B02650刚好指向ntdll.dll的InMemoryOrderModuleList起始地址

我们可以依次遍历下去,找到我们所需要的基址

代码实现:

微软很贴心的提供了两个获取PEB的api

我们也可以不用Windows的api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 64位
.CODE
GetPeb64 PROC
mov rax,gs:[60h]
ret
GetPeb64 ENDP
END

//32位
.CODE
GetPeb32 PROC
mov rax, fs:[30h];
ret;
GetPeb32 ENDP
END

// 也可以直接找到ldr地址
.CODE
GetPebLdr PROC
mov rax, gs:[60h];
add rax, 18h;
mov rax, [rax];
ret;
GetPebLdr ENDP
END

asm.h

1
2
3
#define __ASMCODE_H

extern "C" PVOID64 _cdecl GetPeb();

定义结构体查找导出函数地址:

注:结构体定义太长了没贴上来,对着上面抄就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <windows.h>
#include <winternl.h>
#include "global.h"
#include <stdio.h>
int main()
{
PIMAGE_DOS_HEADER pDos = NULL;
PIMAGE_FILE_HEADER pFile = NULL;
PIMAGE_OPTIONAL_HEADER pOptional = NULL;
PIMAGE_EXPORT_DIRECTORY pExport = NULL;
PMPEB peb = (PMPEB)__readgsqword(0x60);
PLDR_DATA_TABLE_ENTRY64 pLdr = NULL;
PBYTE imageBase = NULL;

// use PEB to find the base address of ntdll.dll
pLdr = (PLDR_DATA_TABLE_ENTRY64)((PBYTE)peb->LoaderData->InMemoryOrderModuleList.Flink - 0x10);
wprintf(L"[*]name:%s\n", pLdr->BaseDllName.Buffer);
printf("[*]DllBase:%p\n", (PBYTE)pLdr->DllBase);
while ((_wcsicmp(pLdr->BaseDllName.Buffer, L"ntdll.dll")) != 0)
{
pLdr = (PLDR_DATA_TABLE_ENTRY64)((PBYTE)pLdr->InMemoryOrderLinks.Flink - 0x10);
imageBase = (PBYTE)pLdr->DllBase;
wprintf(L"[*]name: %s\n",pLdr->BaseDllName.Buffer);
printf("[*]DllBase: %p\n", imageBase);
}
printf("[*]find ntdll.dll address.....\n");

// find the exported functions of ntdll.dll
pDos = (PIMAGE_DOS_HEADER)imageBase;
if (*(PWORD)pDos != 0x5A4D)
{
printf("[*]pe parsing failed..");
}
printf("\n");
pOptional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)pDos + pDos->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER));
pExport = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)imageBase + pOptional->DataDirectory[0].VirtualAddress);

PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(imageBase + pExport->AddressOfFunctions));
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)imageBase + pExport->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)imageBase + pExport->AddressOfNameOrdinals);
for (WORD cx = 0; cx < pExport->NumberOfNames; cx++)
{
PCHAR pczFunctionName = (PCHAR)((PBYTE)imageBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)imageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
printf("[*]Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress);
}
return 0;
}

直接利用偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <windows.h>
#include <winternl.h>
#include "getPeb.h";
#include <stdio.h>

// custom VirtualProtect function
typedef BOOL(WINAPI* CustomVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
CustomVirtualProtect myVirtualProtect;

PVOID GetFunctionAddress(PVOID pBaseAddress, PCHAR pFunctionName)
{
PIMAGE_DOS_HEADER pDos = NULL;
PIMAGE_FILE_HEADER pFile = NULL;
PIMAGE_OPTIONAL_HEADER pOptional = NULL;
PIMAGE_EXPORT_DIRECTORY pExport = NULL;
PVOID getFunctionAddress = NULL;

pDos = (PIMAGE_DOS_HEADER)pBaseAddress;
if (*(PWORD)pDos != 0x5A4D)
{
printf("[*]pe parsing failed..");
}
printf("\n");
pOptional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)pDos + pDos->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER));
pExport = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)pBaseAddress + pOptional->DataDirectory[0].VirtualAddress);

PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pBaseAddress + pExport->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pBaseAddress + pExport->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pBaseAddress + pExport->AddressOfNameOrdinals);
for (WORD cx = 0; cx < pExport->NumberOfNames; cx++)
{
PCHAR pczFunctionName = (PCHAR)((PBYTE)pBaseAddress + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pBaseAddress + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (0 == _strnicmp(pczFunctionName, pFunctionName, strlen(pczFunctionName)))
{
printf("[*]Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress);
getFunctionAddress = pFunctionAddress;
break;
}

}
return getFunctionAddress;
}

int main()
{
PVOID64 peb = GetPeb64();
PVOID64 PEB_LDR_DATA = *(PVOID64**)((PBYTE)peb + 0x18);
PVOID64 PEB_LDR_TABLE_ENTRY = (PVOID64)((PBYTE)PEB_LDR_DATA + 0x20);
UNICODE_STRING* fullName = NULL;
LIST_ENTRY* pListEntry = NULL;
HMODULE hKernel32 = NULL;
PVOID apiID = NULL;

pListEntry = (LIST_ENTRY*)PEB_LDR_TABLE_ENTRY;
do
{
pListEntry = pListEntry->Flink;
fullName = (UNICODE_STRING*)((PBYTE)pListEntry + 0x48);
hKernel32 = (HMODULE)(*((PULONG64)((PBYTE)pListEntry + 0x20)));
wprintf(L"[*]moduleNaame: %s\n", fullName->Buffer);
wprintf(L"[*]moduleAddress: %p\n", hKernel32);


} while ((_wcsicmp(fullName->Buffer, L"kernel32.dll")) != 0);
printf("find address....\n");

apiID = GetFunctionAddress((PVOID)hKernel32, (PCHAR)"VirtualProtect");
myVirtualProtect = (CustomVirtualProtect)(DWORD)apiID;

return 0;
}

注意事项:取值时要注意是指针还是二重指针,数据宽度要注意

参考文章: