TEB 和 PEB学习记录——基础篇
文章目录
概述
注意!该文章讲的所有结构都是32位,且由Windows定义,Windows版本为W10。寻址方式和结构体具体元素与64位会有不同!同时请注意这些结构体在Windows不同版本下的差异!
TEB的全称为Thread Environment Block,该结构用于存储当前进程的相关信息,我们可以通过TEB得到PEB。PEB的全称为 Process Environment Block ,该结构用于存储当前进程的相关信息。TEB和PEB都是Windows定义的两个结构体,对于TEB可以通过FS段寄存器得到它的首地址,由于TEB结构体中有个元素存储了PEB结构体的首地址,所以可以通过ImageBase + offset
的形式得到PEB结构体的首地址。
目前我们只关注几个较为重要的部分:
FS:[0x00] : Current SEH Frame
FS:[0x18] : TEB (Thread Environment Block)
FS:[0x20] : PID
FS:[0x24] : TID
FS:[0x30] : PEB (Process Environment Block)
FS:[0x34] : Last Error Value
SEH
SEH的全称是Structured Exception Handling,这是Windows操作系统的一种异常化处理机制。为了实现这种机制,Windows定义了一个结构体,以链表的形式实现了它,TEB结构体中的第一个元素就是指向该结构体的指针。
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
ExceptionHandler* handler;//a pointer to a function to deal with the exception
};
typedef EXCEPTION_DISPOSITION (CDECL ExceptionHandler)(EXCEPTION_RECORD* ExceptionRecord, EXCEPTION_REGISTRATION* EstablisherFrame, CONTEXT* ContextRecord, DISPATCHER_CONTEXT* DispatcherContext); //same as EXCEPTION_ROUTINE and _except_handler
注意:x64不使用这种异常处理机制。
PEB
PEB结构link:struct PEB (nirsoft.net)
因为是初级阶段的学习,所以目前只讨论PEB中比较常见的几个成员:
0x002 BYTE BeingDebugged;
0x008 void* ImageBaseAddress;
0x00C _PEB_LDR_DATA* Ldr;
0x018 void* ProcessHeap
0x064 DWORD NumberOfProcessors;
0x068 DWORD NtGlobalFlag;
- BeingDebugged:用来检查当前程序是否正在被调试。
- ImageBaseAddress:创建该进程的程序在该进程中的加载基地址。
_PEB_LDR_DATA Ldr:这个结构体提供了载入该进程的dll的相关信息。
typedef struct _PEB_LDR_DATA
{
DWORD Length; //0x00
BYTE Initialized[4]; //0x04
void* SsHandle; //0x08
LIST_ENTRY InLoadOrderModuleList; //0x0C
LIST_ENTRY InMemoryOrderModuleList; //0x14
LIST_ENTRY InInitializationOrderModuleList; //0x1C
void* EntryInProgress; //0x24
} PEB_LDR_DATA;
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; //指向下一个节点
struct _LIST_ENTRY *Blink; //指向前一个节点
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
在 _PEB_LDR_DATA
结构体中,InLoadOrderModuleList
、InMemoryOrderModuleList
、InInitializationOrderModuleList
这三个变量都是 LIST_ENTRY
类型的,它们都是头节点,代表了三个双向链表,这些链表分别按照模块的加载顺序、内存顺序和初始化顺序组织,其中每个节点都包含了模块的信息。双向链表中的每一个元素都指向一个_LDR_DATA_TABLE_ENTRY
结构,这个结构里面存储着具体的导入的dll的相关信息。
- ProcessHeap:a flag under PEB that shows if the first heap memory space of the process was created in debug mode. It can be accessed at offset 0x18 (at PEB).这个是从网上一篇文章里面看到的,很容易能看懂这句话是什么意思,但是为什么这个成员的作用是这个,有待实验证明或者官方文档证明。
- NtGlobalFlag:This field is 0x0 in normal cases and is set to 0x70 when debugging. That is, FLG_HEAP_ENABLE_TAIL_CHECK (0x10), FLG_HEAP_ENABLE_FREE_CHECK (0x20), and FLG_HEAP_VALIDATE_PARAMETERS (0x40) are set. Note that if you attach a running process, it does not change.先看着,链接我待会给在下面。这个成员和上面那个成员一样都与调试有关。
- NumberOfProcessors:表示当前可用处理器的数量。
详细讲一下这个结构体_PEB_LDR_DATA
这个结构体关联了好几个结构体,它们的用处只有一个——用来描述导入该进程的exe/dll的信息,这个元素十分重要!需要完全弄明白!
typedef struct _PEB_LDR_DATA
{
DWORD Length; //0x00
BYTE Initialized[4]; //0x04
void* SsHandle; //0x08
LIST_ENTRY InLoadOrderModuleList; //0x0C
LIST_ENTRY InMemoryOrderModuleList; //0x14
LIST_ENTRY InInitializationOrderModuleList; //0x1C
void* EntryInProgress; //0x24
} PEB_LDR_DATA;
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; //指向下一个节点
struct _LIST_ENTRY *Blink; //指向前一个节点
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
_PEB_LDR_DATA
typedef struct _PEB_LDR_DATA
{
DWORD Length; //0x00
BYTE Initialized[4]; //0x04
void* SsHandle; //0x08
LIST_ENTRY InLoadOrderModuleList; //0x0C
LIST_ENTRY InMemoryOrderModuleList; //0x14
LIST_ENTRY InInitializationOrderModuleList; //0x1C
void* EntryInProgress; //0x24
} PEB_LDR_DATA;
这个结构体用来记录导入进程的用户使用的模块(这里的模块,可以理解为一些被使用到的dll)。它有三个元素分别为三个双向链表的头节点,这三个双向链表结构都为_LDR_DATA_TABLE_ENTRY
,下面是这三个元素的定义:
LIST_ENTRY InLoadOrderModuleList; //0x0C
LIST_ENTRY InMemoryOrderModuleList; //0x14
LIST_ENTRY InInitializationOrderModuleList; //0x1C
LIST_ENTRY
存着两个指针,如果作为头节点,那么我们只考虑第二个指针。无论考虑哪一个元素,当有下一个和上一个节点时,它们指向的结构体为_LDR_DATA_TABLE_ENTRY
,这个结构体,存储着导入dll的真正信息。
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; //指向下一个节点
struct _LIST_ENTRY *Blink; //指向前一个节点
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
_LDR_DATA_TABLE_ENTRY
这个结构体存储着导入的dll的具体信息,同时它是双向链表中的一个节点,通过LIST_ENTRY
存储着上一节点和下一节点的指针。通过一个UNICODE_STRING
存储着这个dll的名字。
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
_UNICODE_STRING
这是一个用来描述字符串信息的结构体,结构如下:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
实验
通过vs2022调试时的反汇编定位PEB,然后找到导入该进程中的一个dll的名字。
#include<Windows.h>
int main()
{
HANDLE test = GetModuleHandle(NULL);
return 0;
}
在第五行下个断点,开始调试,得到反汇编信息。
HANDLE test = GetModuleHandle(NULL);
00481795 8B F4 mov esi,esp
00481797 6A 00 push 0
00481799 FF 15 00 B0 48 00 call dword ptr [__imp__GetModuleHandleW@4 (048B000h)]
F11
步进第四行
771AC7F0 8B FF mov edi,edi
771AC7F2 55 push ebp
771AC7F3 8B EC mov ebp,esp
771AC7F5 5D pop ebp
771AC7F6 FF 25 A0 13 21 77 jmp dword ptr ds:[772113A0h]
F11
步进jmp
的这一行,进入GetModuleHandle
函数内部
7682EC90 8B FF mov edi,edi
7682EC92 55 push ebp
7682EC93 8B EC mov ebp,esp
7682EC95 83 EC 0C sub esp,0Ch
7682EC98 83 7D 08 00 cmp dword ptr [ebp+8],0
7682EC9C 74 2E je 7682ECCC
7682EC9E 56 push esi
7682EC9F FF 75 08 push dword ptr [ebp+8]
7682ECA2 8D 45 F4 lea eax,[ebp-0Ch]
7682ECA5 50 push eax
7682ECA6 FF 15 44 30 94 76 call dword ptr ds:[76943044h]
7682ECAC 8D 45 FC lea eax,[ebp-4]
7682ECAF 33 F6 xor esi,esi
7682ECB1 50 push eax
7682ECB2 8D 45 F4 lea eax,[ebp-0Ch]
7682ECB5 50 push eax
7682ECB6 56 push esi
7682ECB7 56 push esi
7682ECB8 FF 15 3C 34 94 76 call dword ptr ds:[7694343Ch]
7682ECBE 85 C0 test eax,eax
7682ECC0 78 15 js 7682ECD7
7682ECC2 8B 75 FC mov esi,dword ptr [ebp-4]
7682ECC5 8B C6 mov eax,esi
7682ECC7 5E pop esi
7682ECC8 C9 leave
7682ECC9 C2 04 00 ret 4
7682ECCC 64 A1 30 00 00 00 mov eax,dword ptr fs:[00000030h] ;请注意这里
7682ECD2 8B 40 08 mov eax,dword ptr [eax+8]
7682ECD5 EB F1 jmp 7682ECC8
7682ECD7 8B C8 mov ecx,eax
7682ECD9 E8 F9 18 00 00 call 768305D7
7682ECDE EB E5 jmp 7682ECC5
注意标记处,在32位程序中,FS
段寄存器存储了TEB的首地址,TEB首地址加上0x30
得到的地址处的信息就是PEB的首地址,所以标记处的代码执行后,eax中存储了PEB的首地址。故,得到PEB。
0x0086F000 00 00 01 04 ff ff ff ff ........
0x0086F008 00 00 47 00 00 db e7 77 ..G..??w
0x0086F010 90 69 ea 00 00 00 00 00 ?i?.....
0x0086F018 00 00 ea 00 c0 d8 e7 77 ..?.???w
0x0086F020 00 00 00 00 00 00 00 00 ........
0x0086F028 01 00 00 00 00 00 00 00 ........
其中0x0086F000
为PEB首地址,由于我们需要得到的结构体指针在偏移0xc
处,就不将PEB在内存中所有的结构给copy过来了。
注意这里的文本编码方式为ANSI
不是Unicode
。提醒一下,虽然不重要。
接下来我们通过addr + offset
的方式得到_PEB_LDR_DATA
结构体地址0x77e7db00
,并且跟进到该地址。
0x77E7DB00 30 00 00 00 01 00 00 00 0.......
0x77E7DB08 00 00 00 00 28 29 ea 00 ....()?.
0x77E7DB10 e0 a2 ea 00 30 29 ea 00 ???.0)?.
0x77E7DB18 e8 a2 ea 00 40 28 ea 00 ???.@(?.
我们目前只关注上文中提到的三个LIST_ENTRY
元素,所以并未把该结构体中所有的信息copy过来。虽然说有三个链表,但是它们的区别只是链接的顺序不同,我们随便找一个头节点跟进去。找offset:0x14的这个头节点。
0x77E7DB14 30 29 ea 00 e8 a2 ea 00
低位的四字节是上一节点指针,高位四字节是下一节点指针,由于是头节点,所以我们跟进下一节点指针指向的地址。找到_LDR_DATA_TABLE_ENTRY
结构体。
0x00EAA2E8 14 db e7 77 90 a1 ea 00 .??w???.
0x00EAA2F0 98 a1 ea 00 a0 8d ea 00 ???.???.
0x00EAA2F8 00 00 b9 7a 10 bc c1 7a ..?z.??z
0x00EAA300 00 50 1a 00 42 00 44 00 .P..B.D.
0x00EAA308 c8 a6 ea 00 1a 00 1c 00 ???.....
0x00EAA310 f0 a6 ea 00 ec a2 08 00 ???.??..
0x00EAA318 06 00 00 00 a8 d9 e7 77 ....???w
0x00EAA320 a8 d9 e7 77 a9 c1 aa 61 ???w???a
我们的目的是为了得到,这个dll的名字,所以需要关注偏移量0x24和0x2c。
start addr + 0x24
得到_UNICODE_STRING
结构体:
0x00EAA30C 1a 00 1c 00 f0 a6 ea 00 ....???.
高位的四位才是这个字符串的地址,我们跟进去。
0x00EAA6F0 75 00 63 00 72 00 74 00 ucrt
0x00EAA6F8 62 00 61 00 73 00 65 00 base
0x00EAA700 64 00 2e 00 64 00 6c 00 d.dl
0x00EAA708 6c 00 00 00 65 00 64 00 l.ed
得到字符串ucrtbased.dll
。
问题:这个问题我现在没有解决,实际上偏移0x2c处有个元素,这个元素定义:UNICODE_STRING BaseDllName;
,但是我发现当我找到这个元素的时候,它的数据如下:
0x00EAA314 ec a2 08 00 06 00 00 00 ꋬ...
这就很离谱,高位四字节应该是一个指向字符串的指针,但是这个明显不是。问题还未解决…
补充
NtCurrentTeb一个有意思的API
PVOID A = (PVOID)NtCurrentTeb();
可以通过这段代码得到当前进程的TEB的首地址,该函数被定义在Windows.h
中。