Hey gang. Today’s free post will discuss the Process Environment Block (PEB) and Thread Environment Block (TEB). Both of these are very useful from an OS and security development perspective. First, here’s a meme that I made.
Secondly, if you’re reading this from your phone, you have a lot of scrolling to do. I have a lot of code that I’m going to spit at you.
Before You Read
This is mostly an introductory post, but the following posts reference the PEB. You don’t have to read them, but if you want some use-cases, feel free to.
What
Let’s first talk about the TEB. To understand the TEB, you need to know what threads are. There’s a nice summary in the Thread Execution Hijack post but I’ll copy and paste it here.
A process is a container. This container holds threads, modules, and code (and a few other things that aren’t that important right now). In order to execute the code you have written and then built into a process, it must be executed via a thread.
Threads are what run your code. And there are usually a bunch of them. Think of threads like workers in a factory. Each worker executes his or her job within the context of the factory (the process!). Threads work together to keep the factory producing what it needs to produce, and they also help spread out the work that needs to be done. Considering threads can be created, suspended, prioritized, interrogated, executed, killed, put to sleep, and even hijacked, this analogy feels very appropriate.
Each thread has it’s own storage system called Thread Local Storage (TLS). Inside of this TLS lies the TEB. And the TEB is a data structure that holds a chunk of information about the thread. Here is its winternl.h declaration:
typedef struct _TEB {
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle; // Windows 2000 only
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, *PTEB;
A lot of this looks like gobbledegook, and it kind of is. The main fields that you really should care about is the ProcessEnvironmentBlock (the PEB, duh), the TlsSlots (pointers to TLS storage), and TlsExpansionSlots (a pointer to more slots in case the TlsSlots aren’t enough).
“What about all the Reserved fields?”
Windows likes to have reserved fields in their data structures. These are held for system specific or future use. In Windows 10 22621, TEB[0x48] is the thread ID of the current thread. The PDB of KernelBase.dll has a better declaration of the TEB with fewer reserved fields. I’ve extracted it for you here.
struct
{
NT_TIB NtTib;
PVOID EnvironmentPointer;
CLIENT_ID ClientId;
PVOID ActiveRpcHandle;
PVOID ThreadLocalStoragePointer;
PPEB ProcessEnvironmentBlock;
ULONG LastErrorValue;
ULONG CountOfOwnedCriticalSections;
PVOID CsrClientThread;
PVOID Win32ThreadInfo;
ULONG User32Reserved[26];
ULONG UserReserved[5];
PVOID WOW32Reserved;
LCID CurrentLocale;
ULONG FpSoftwareStatusRegister;
PVOID SystemReserved1[54];
LONG ExceptionCode;
UCHAR Padding0[4];
PACTIVATION_CONTEXT_STACK ActivationContextStackPointer;
UCHAR SpareBytes[24];
ULONG TxFsContext;
GDI_TEB_BATCH GdiTebBatch;
CLIENT_ID RealClientId;
PVOID GdiCachedProcessHandle;
ULONG GdiClientPID;
ULONG GdiClientTID;
PVOID GdiThreadLocalInfo;
SIZE_T Win32ClientInfo[62];
PVOID glDispatchTable[233];
SIZE_T glReserved1[29];
PVOID glReserved2;
PVOID glSectionInfo;
PVOID glSection;
PVOID glTable;
PVOID glCurrentRC;
PVOID glContext;
ULONG LastStatusValue;
UCHAR Padding2[4];
UNICODE_STRING StaticUnicodeString;
WCHAR StaticUnicodeBuffer[261];
UCHAR Padding3[6];
PVOID DeallocationStack;
PVOID TlsSlots[64];
LIST_ENTRY TlsLinks;
PVOID Vdm;
PVOID ReservedForNtRpc;
PVOID DbgSsReserved[2];
ULONG HardErrorMode;
UCHAR Padding4[4];
PVOID Instrumentation[11];
GUID ActivityId;
PVOID SubProcessTag;
PVOID EtwLocalData;
PVOID EtwTraceData;
PVOID WinSockData;
ULONG GdiBatchCount;
union
{
PROCESSOR_NUMBER CurrentIdealProcessor;
ULONG32 IdealProcessorValue;
struct
{
UCHAR ReservedPad0;
UCHAR ReservedPad1;
UCHAR ReservedPad2;
UCHAR IdealProcessor;
};
};
ULONG GuaranteedStackBytes;
UCHAR Padding5[4];
PVOID ReservedForPerf;
PVOID ReservedForOle;
ULONG WaitingOnLoaderLock;
UCHAR Padding6[4];
PVOID SavedPriorityState;
ULONG_PTR SoftPatchPtr1;
ULONG_PTR ThreadPoolData;
PVOID *TlsExpansionSlots;
PVOID DeallocationBStore;
PVOID BStoreLimit;
ULONG ImpersonationLocale;
ULONG IsImpersonating;
PVOID NlsCache;
PVOID pShimData;
ULONG HeapVirtualAffinity;
UCHAR Padding7[4];
HANDLE CurrentTransactionHandle;
PTEB_ACTIVE_FRAME ActiveFrame;
PVOID FlsData;
PVOID PreferredLanguages;
PVOID UserPrefLanguages;
PVOID MergedPrefLanguages;
ULONG MuiImpersonation;
union
{
USHORT CrossTebFlags;
struct
{
unsigned __int16 SpareCrossTebBits : 16;
};
};
union
{
USHORT SameTebFlags;
struct
{
unsigned __int16 DbgSafeThunkCall : 1;
unsigned __int16 DbgInDebugPrint : 1;
unsigned __int16 DbgHasFiberData : 1;
unsigned __int16 DbgSkipThreadAttach : 1;
unsigned __int16 DbgWerInShipAssertCode : 1;
unsigned __int16 DbgIssuedInitialBp : 1;
unsigned __int16 DbgClonedThread : 1;
unsigned __int16 SpareSameTebBits : 9;
};
};
PVOID TxnScopeEnterCallback;
PVOID TxnScopeExitCallback;
PVOID TxnScopeContext;
ULONG LockCount;
ULONG SpareUlong0;
PVOID ResourceRetValue;
}
Now there’s a bunch more fun information here to mess with.
Want to know what a threat actor could do with this? Well, you’ll have to read the why section to get that one.
TEBs point to the PEB. Makes sense. Think about it. Threads lie within a process, and there are often multiple threads. Each thread in the same process will point to the same PEB, hopefully ;).
Side note, that could be a pretty cool exploit. Let’s say your target process’ PEB is read-only and has a system in place to prevent permissions changing of the PEB memory space. If you wanted to debug it, just force the TEB to point to your own PEB, given that the TEB isn’t protected like the PEB.
The PEB is much cooler than the TEB. It’s similar to the TEB (duh) but holds information about the entire process instead. Here’s its winternl.h declaration:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
There’s more interesting stuff here. You have the BeingDebugged field. Guess what that one stands for? Go on, guess.
There’s also the Ldr which holds all of the information of the modules that are loaded into the process. This can be used for malicious purposes like the hijacking of DLLs (which we are all too familiar with).
Here’s some code from one of my projects that walks the PEB Ldr:
void *getPEBFunction(const char *func)
{
PPEB peb = NtCurrentTeb()->ProcessEnvironmentBlock;
PPEB_LDR_DATA ldr = peb->Ldr;
for (PLIST_ENTRY entry = ldr->InMemoryOrderModuleList.Flink; entry != &ldr->InMemoryOrderModuleList; entry = entry->Flink)
{
PLDR_DATA_TABLE_ENTRY mod = CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
if (mod->DllBase == nullptr)
continue;
// Parse IAT, EAT, etc.
address = getStuff(mod->DllBase, func);
if (address != nullptr)
return address;
}
return nullptr;
}
And I’ve also extracted the W10 22621 structure for you too!
struct
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
union
{
BOOLEAN BitField;
struct
{
unsigned __int8 ImageUsesLargePages : 1;
unsigned __int8 IsProtectedProcess : 1;
unsigned __int8 IsLegacyProcess : 1;
unsigned __int8 IsImageDynamicallyRelocated : 1;
unsigned __int8 SkipPatchingUser32Forwarders : 1;
unsigned __int8 SpareBits : 3;
};
};
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
struct _RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
struct _RTL_CRITICAL_SECTION *FastPebLock;
PVOID AltThunkSListPtr;
PVOID IFEOKey;
union
{
ULONG CrossProcessFlags;
struct
{
unsigned __int32 ProcessInJob : 1;
unsigned __int32 ProcessInitializing : 1;
unsigned __int32 ProcessUsingVEH : 1;
unsigned __int32 ProcessUsingVCH : 1;
unsigned __int32 ReservedBits0 : 28;
};
};
union
{
PVOID KernelCallbackTable;
PVOID UserSharedInfoPtr;
};
ULONG SystemReserved[1];
ULONG SpareUlong;
PPEB_FREE_BLOCK FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[2];
PVOID ReadOnlySharedMemoryBase;
PVOID HotpatchInformation;
PVOID *ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
LARGE_INTEGER CriticalSectionTimeout;
ULONG_PTR HeapSegmentReserve;
ULONG_PTR HeapSegmentCommit;
ULONG_PTR HeapDeCommitTotalFreeThreshold;
ULONG_PTR HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID *ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
ULONG GdiDCAttributeList;
struct _RTL_CRITICAL_SECTION *LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
USHORT OSBuildNumber;
USHORT OSCSDVersion;
ULONG OSPlatformId;
ULONG ImageSubsystem;
ULONG ImageSubsystemMajorVersion;
ULONG ImageSubsystemMinorVersion;
ULONG_PTR ImageProcessAffinityMask;
ULONG GdiHandleBuffer[60];
PPOST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
PVOID TlsExpansionBitmap;
ULONG TlsExpansionBitmapBits[32];
ULONG SessionId;
ULARGE_INTEGER AppCompatFlags;
ULARGE_INTEGER AppCompatFlagsUser;
PVOID pShimData;
PVOID AppCompatInfo;
UNICODE_STRING CSDVersion;
struct _ACTIVATION_CONTEXT_DATA *ActivationContextData;
struct _ASSEMBLY_STORAGE_MAP *ProcessAssemblyStorageMap;
struct _ACTIVATION_CONTEXT_DATA *SystemDefaultActivationContextData;
struct _ASSEMBLY_STORAGE_MAP *SystemAssemblyStorageMap;
ULONG_PTR MinimumStackCommit;
PVOID *FlsCallback;
LIST_ENTRY FlsListHead;
PVOID FlsBitmap;
ULONG FlsBitmapBits[4];
ULONG FlsHighIndex;
PVOID WerRegistrationData;
PVOID WerShipAssertPtr;
}
There’s a lot of shit here. What can we do with it all?
Why
Let’s once again reflect on the meme (I’ll post it again so you don’t have to scroll up).
Quick refresher. GetProcAddress literally expands to “Get Procedure Address”. So, if you wanted to get the address of a function (e.g. MessageBoxA), you would first get a handle to the owning module (User32.dll) via GetModuleHandle, and then call GetProcAddress on it with “MessageBoxA” as the second parameter.
If you’re trying to be discrete, this is loud. If you’re trying to load up socket, encryption, or HTTP functions, you will be detected quite easily by EDR solutions.
Walking the PEB, however is not as loud. It’s still detectable if you’re being silly, but it is simply more discrete than using GetProcAddress. Beyond dynamically acquiring functions, there’s a lot more that you can do with the TEB and PEB.
TEB
Stack Manipulation
The TLS contains information about the thread’s stack limits. You may be able to mess with this and cause a stack overflow.
Exception Handler Abuse
The TEB holds a linked list of exception handlers for the thread. You could force the handlers to point to your malicious code, cause an exception, and then be able to execution it.
Alter PEB Pointer
I mentioned this earlier, but if you force a TEB to point to a custom PEB, you could possibly convince the thread that it is in an environment that it is not.
Alter TLSExpansionSlots
If you force this field to point to your custom pointer of slots, then you may be able to force the thread to execute arbitrary code
PEB
Change BeingDebugged
Kind of simple but just change that field value so you can debug a program that uses that detection method.
Walk the Ldr and Do Bad Stuff
By walking the PEB you can manipulate the loaded modules or possibly inject your own.
Change ProcessParameters
This field points to information about the process that is relevant to how it was started. This is another structure that holds a lot of information that you can fiddle with. I’ll drop it here:
struct
{
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
HANDLE ConsoleHandle;
ULONG ConsoleFlags;
HANDLE StandardInput;
HANDLE StandardOutput;
HANDLE StandardError;
CURDIR CurrentDirectory;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PWSTR Environment;
ULONG StartingX;
ULONG StartingY;
ULONG CountX;
ULONG CountY;
ULONG CountCharsX;
ULONG CountCharsY;
ULONG FillAttribute;
ULONG WindowFlags;
ULONG ShowWindowFlags;
UNICODE_STRING WindowTitle;
UNICODE_STRING DesktopInfo;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeData;
RTL_DRIVE_LETTER_CURDIR CurrentDirectories[32];
SIZE_T EnvironmentSize;
SIZE_T EnvironmentVersion;
}
Change PostProcessInitRoutine
This is a field that points to a function that executes whenever the process is initialized. If you can overwrite this with your own function, then you can execute some malicious code whenever the process is created.
Hijack SessionId
Rather straightforward, but this can be used for a session hijack attack.
And more!
That’s all for this post. There is no how section today. But that doesn’t mean you can’t learn how. Learn by doing. This is why I always sign off with-
Go!
-BowTiedCrawfish