Mar 14, 2024 09:18 PM
(This post was last modified: Mar 16, 2026 08:19 AM by Hackers.)
PEB Walking,
& NTDLL
& NTDLL
![[Image: header4.gif]](https://i.ibb.co/C33BJJJg/header4.gif)
Enjoy More Software, Source Code, Tools, and Tutorials
https://hackforums.net/showthread.php?tid=6265944
https://hackforums.net/showthread.php?tid=6266404
https://hackforums.net/showthread.php?tid=6265473
https://hackforums.net/showthread.php?tid=6265665
https://hackforums.net/showthread.php?tid=6266377
https://hackforums.net/showthread.php?tid=6264320
https://hackforums.net/showthread.php?tid=6263383
https://hackforums.net/showthread.php?tid=6264286
https://hackforums.net/showthread.php?tid=6266614
https://hackforums.net/showthread.php?tid=6264522
https://hackforums.net/showthread.php?tid=6266052
https://hackforums.net/showthread.php?tid=6265700
https://hackforums.net/showthread.php?tid=6266289
https://hackforums.net/showthread.php?tid=6265197
https://hackforums.net/showthread.php?tid=6265430
As you may know, I have been learning C++ the last few months and researching low level functions. This has always been part of my goal in life, to better understand operating systems and maybe one day even work on a new one. The following information is for educational purposes only.
You will learn more about:
- PEB Walking
- TEB / Thread Environment Block
- ntdll.dll
- Documented & undocumented Windows functions
- linked research papers for you to discover all the possibilities
... aka 1 step closer to syscalls & interacting directly with, or as close as possible to, the kernel. No, not a popcorn kernel. When it comes to syscalls & really any functionality in Windows, I've learned about a system called 'rings'. Think of it like limbo - how low can you go? Ring 0 & kernel mode is like winning limbo with it an inch off the ground. Ring 3 is like winning participation trophies - anyone can do it.
![[Image: Screenshot-2024-03-14-222353.png]](https://i.ibb.co/ZGpbmRM/Screenshot-2024-03-14-222353.png)
What if I told you that you could use any Windows dll / library, without including windows.h, winternl.h, or any Windows header file?
What if I told you we can do it without a third party tool, and also without even using standard LoadLibrary functions to do it?
The first thing I want to make clear:
You will be able to successfully use any library or Windows function you want, without including any headers in your final program
- This is an exclusive thread for Hack Forums by Brotherhood
![[Image: Screenshot-2024-03-14-221837.png]](https://i.ibb.co/bW6xPLv/Screenshot-2024-03-14-221837.png)
This advanced kernel learning part of my journey began when I was researching Hell's Gate, Windows syscalls, and more. This led me to research a few other related projects: Halos Gate, Tartarus Gate, and more.
I wanted to know more about it, but didn't know enough. I went head down and built 3 crypters that I released, 2 that are unfinished, and I started my master project, Lucid Dream DMT. I had to stop Lucid a few weeks ago to focus on the other projects, learn more, keep researching. I am happy to say that last night I made a huge breakthrough.
Before last night I started with some simple methods to create my own definitions for default functions in Windows:
Code
typedef NTSTATUS (NTAPI *pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect);
typedef NTSTATUS (NTAPI *pNtFreeVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG FreeType);With these type definitions, I could call on them as replacements to the real nt functions. It interfaced with Windows, it worked at obscuring my program (to legally & ethically protect intellectual property & data privacy) in runtime, and it was a good start. Types are very important for making sure your program in user-mode can talk to the kernel. Doing it like this means we can potentially avoid calling on a library to get a function/type because we've defined it in our code ourselves.
The problem? Before last night it was still using LoadLibrary and GetProcAddress functions. Don't worry, I'll show you exactly how it's all done!
First let's cover the basics: finding these typedefs in the first place.
You can head to the official learning docs from MS: https://learn.microsoft.com/en-us/window...i/_kernel/
For my first two functions I created typedefs for, I found them here:
https://learn.microsoft.com/en-us/window...tualmemory
https://learn.microsoft.com/en-us/window...tualmemory
After playing with these for a few days, I discovered guides about walking the PEB, using base addresses, and more.
To do this, one of the first things involved is getting the 'pbi' or Process Basic Information. This is what lets us get the base address of a func. So, I head over to MS' docs and look for the typedef for this.
Code
typedef NTSTATUS (NTAPI *pNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength);During this I also learned I would need to define my own types for PROCESS_BASIC_INFORMATION & more.
Hint: When trying to remove windows.h or winternl.h from your end program, just do it & then go to MS page to find all types and add to your custom .h file. Or, you can ask GPT for types too which (if its documented at least) is pretty reliable.
Great, now I can start to think about PEB a bit more, but at this point in my journey I started slow.
At first I started with code like this:
Code
HMODULE ntdll = LoadLibraryW(L"ntdll.dll");
if (!ntdll) {
std::cerr << "Failed to load ntdll.dll." << std::endl;
return;
}
pNtQueryInformationProcess NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(ntdll, "NtQueryInformationProcess");
if (!NtQueryInformationProcess) {
std::cerr << "Failed to get NtQueryInformationProcess address." << std::endl;
return;
}There are a few key pieces to that code. For one, I am using LoadLibraryW() to get the ntdll module. Then although I am using my own type (for NtQueryInformationProcess), I still was relying on GetProcAddress to load that in originally with my own type.
How can I load in an Nt function, without using GetProcAddress? And how can I load in without LoadLibrary_ funcs?.
I had to keep researching. It was at this time that I found other projects such as Artemis.
Artemis was an important project to take a look at because it clued me into the load order module list - or in other words, the list of all modules / dlls loaded into the current program, in runtime.
I quickly adopted a way to loop through all loaded modules and string-match for ntdll.dll (image from Artemis, not me):
![[Image: Screenshot-2024-03-14-231044.png]](https://i.ibb.co/54dS1Z2/Screenshot-2024-03-14-231044.png)
Why ntdll? It's always loaded. It's also our little "gateway" to the rest of the Windows funcs.
Now that I had a method to find ntdll without using LoadLibraryW like my original code, I ended up with this:
Code
void FindNtdll() {
// Access the PEB directly from the TEB (Thread Environment Block)
PEB* peb = reinterpret_cast<PEB*>(__readgsqword(PEB_OFFSET)); // For x64
// For x86, use: PEB* peb = reinterpret_cast<PEB*>(__readfsdword(PEB_OFFSET));
PPEB_LDR_DATA ldr = peb->Ldr;
LIST_ENTRY* moduleList = &ldr->InMemoryOrderModuleList;
for (LIST_ENTRY* entry = moduleList->Flink; entry != moduleList; entry = entry->Flink) {
LDR_DATA_TABLE_ENTRY* moduleEntry = CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
std::wstring dllName = ReadUnicodeString(moduleEntry->FullDllName);
std::wcout << L"found:" << dllName << std::endl;
if (dllName.find(L"ntdll.dll") != std::wstring::npos) {
std::wcout << L"Found ntdll.dll: " << dllName << std::endl;
std::wcout << L"Base Address: " << moduleEntry->DllBase << std::endl;
// Directly use GetFunctionAddress to find the functions
auto NtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetFunctionAddress(moduleEntry->DllBase, "NtAllocateVirtualMemory");
auto NtFreeVirtualMemory = (pNtFreeVirtualMemory)GetFunctionAddress(moduleEntry->DllBase, "NtFreeVirtualMemory");
break;
}
std::wcout << L"found:" << dllName << std::endl;
}
}First thing I did was update the memory functions to use this new method for testing.
Luckily the rest of the program worked fine! This means ntdll from the loaded module list & finding it via PEB "walking" works! No need to waste time with a silly load library function anymore just to load up ntdll!
Let's break it down....
Code
// Access the PEB directly from the TEB (Thread Environment Block)
PEB* peb = reinterpret_cast<PEB*>(__readgsqword(PEB_OFFSET)); // For x64
// For x86, use: PEB* peb = reinterpret_cast<PEB*>(__readfsdword(PEB_OFFSET));Code
PPEB_LDR_DATA ldr = peb->Ldr;
LIST_ENTRY* moduleList = &ldr->InMemoryOrderModuleList;Code
for (LIST_ENTRY* entry = moduleList->Flink; entry != moduleList; entry = entry->Flink) {
LDR_DATA_TABLE_ENTRY* moduleEntry = CONTAINING_RECORD(entry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
std::wstring dllName = ReadUnicodeString(moduleEntry->FullDllName);
if (dllName.find(L"ntdll.dll") != std::wstring::npos) {Within minutes my mind was racing... How can I load up another library? I can't use the same method because ntdll is auto-loaded and that's the only reason it's in the module list.
kernel32.dll is not auto included for example.
- EDIT: I am not able to see kernel32 or kernelbase on my tests, but documentation does say it is auto loaded. I will have to figure out why I can't see it - perhaps a timing issue or if not that then an obscure bug I suppose.
- However, even if you don't get the auto loaded kernel, you can load it which is what I did after never finding kernel32/base dlls loaded:
Can it be loaded now that we have ntdll? Yes : thanks to ldrLoadDll.
My first undocumented function!
http://undocumented.ntinternals.net/inde...adDll.html
![[Image: OS16WBz.png]](https://i.imgur.com/OS16WBz.png)
Before I jump into this, I want to mention my function GetFunctionAddress - it's how we finish off this first part by not using GetProcAddress.
Code
// Function to parse the export table and get function addresses
void* GetFunctionAddress(PVOID baseAddress, const char* functionName) {
auto dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
auto ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)baseAddress + dosHeader->e_lfanew);
auto exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)baseAddress + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
auto functions = (PDWORD)((PBYTE)baseAddress + exportDirectory->AddressOfFunctions);
auto names = (PDWORD)((PBYTE)baseAddress + exportDirectory->AddressOfNames);
auto ordinals = (PWORD)((PBYTE)baseAddress + exportDirectory->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDirectory->NumberOfNames; i++) {
const char* exportName = (const char*)baseAddress + names[i];
if (_stricmp(exportName, functionName) == 0) {
DWORD functionRVA = functions[ordinals[i]];
return (PBYTE)baseAddress + functionRVA;
}
}
return nullptr;
}The reason we can use this is because:
- we have ntdll's base address / full handle
- we can request a string-value function name such as "ldrLoadDll")
- this works for any dll we load into our program, and for any function - even undocumented
This custom function from me is critical for making sure we're not using GetProcAddress. Why don't we use it? Well, our goal is to protect intellectual property & data in runtime, and using GetProcAddress or a LoadLibrary_ func is a great way for a malicious actor or another software to detect what modules you use & perhaps try to pull analyze your program after you use one. The method in this thread helps with this tremendously, and of course brings us 1 level closer in our ring system (aka "limbo" analogy above ~ how low can we go?).
for ldrLoadDll, let's define our type just like we did for others:
Code
typedef NTSTATUS (NTAPI *pLdrLoadDll)(PWCHAR PathToFile, ULONG Flags, PUNICODE_STRING ModuleFileName, PHANDLE ModuleHandle);Now let's use it to try loading up another library. How about... kernel32?
This uses ntdll's base address & the get function address method above:
Code
auto LdrLoadDllFunc = (pLdrLoadDll)GetFunctionAddress(moduleEntry->DllBase, "LdrLoadDll");
if (!LdrLoadDllFunc) {
std::cerr << "Failed to get address of LdrLoadDll." << std::endl;
return;
}Now let's use it. I can't begin to express how overjoyed I was that this worked:
Code
UNICODE_STRING dllName;
InitializeUnicodeString(&dllName, L"kernel32.dll");
HANDLE moduleHandle = nullptr;
// Attempt to load kernel32.dll
NTSTATUS status = LdrLoadDllFunc(nullptr, 0, &dllName, &moduleHandle);
if (NT_SUCCESS(status)) {
std::wcout << L"kernel32.dll loaded successfully. Module handle: " << moduleHandle << std::endl;
}It worked! I celebrated, jumped up & down, etc. But it was time to try 1 more thing : using kernel32 to create a process.
Code
typedef BOOL (WINAPI *pCreateProcessW)(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);Cool, we got the definitions...
Code
auto CreateProcessWFunc = (pCreateProcessW)GetFunctionAddress(moduleHandle, "CreateProcessW");
if (CreateProcessWFunc) {
std::wcout << L"Address of CreateProcessW retrieved successfully." << std::endl;
}We have the address for create process... Oh my (right??), time to use it!
Code
STARTUPINFOW si = {};
PROCESS_INFORMATION pi = {};
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// Prepare command line - ensure it's mutable as CreateProcessW may modify it
wchar_t cmdLine[] = L"cmd.exe /C dir";
// Call CreateProcessW with minimal, correct parameters
BOOL result = CreateProcessWFunc(
nullptr, // No module name (use command line)
cmdLine, // Command line - needs to be mutable
nullptr, // Process handle not inheritable
nullptr, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
0, // No creation flags
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure
);
if (result) {
std::wcout << L"CreateProcessW succeeded." << std::endl;
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
} else {
std::wcout << L"CreateProcessW failed. Error: " << GetLastError() << std::endl;
}It was at this point that I knew I had done something magical.
I had successfully:
- hooked into ntdll to get all functions
- used ntdll mem functions & now ldrLoadDll
- used ldrLoadDll / a low level function to load another library (kernel32)
- used the loaded kernel32 handle to create a process
Check out the complete POC here: https://github.com/LNodesL/PEB-Crawl-POC
What's next? Well, ntdll has thousands of functions related to it, some documented, many undocumented, and most are useful:
http://undocumented.ntinternals.net/index.html
Possible ways to advance further with the info in this thread:
- Leverage the kernel mode Zw methods/types instead of the Nt versions (user mode)
- use a DLL instead of a PE/exe to access this same information
I'll be working on finishing up my Github repo for this POC (proof of concept) to include the remaining win types in the .h files. Then I'll be bringing this over to my Lucid Dream DMT project for more research.
Now that I am loading ntdll from the module list & finding function addresses dynamically, and able to load any other library, the next step is to actually map out the real syscall IDs and use those for even more direct access to kernal / ring 0. Maybe I'll release another tutorial on that, but for now I have a lot more reading to do to learn more about methods like this:
![[Image: zPWDNj8.png]](https://i.imgur.com/zPWDNj8.png)
Special shoutout to Geoff Chappell who documents so much about this on his website too.
Special thanks to all the members who helped me get started with C++. You know who you are, but I appreciate the assistance. I hope in turn this thread can help others :)



![[Image: getgrass-signature.png]](https://jacobriggs.io/assets/images/misc/hf/getgrass-signature.png)

![[Image: images.jpg]](https://i.ibb.co/GnfCt31/images.jpg)
![[Image: Screenshot-2024-03-16-123749.png]](https://i.ibb.co/mGdhdc5/Screenshot-2024-03-16-123749.png)
![[Image: Screenshot-2024-03-16-123757.png]](https://i.ibb.co/KjzfFqc/Screenshot-2024-03-16-123757.png)
![[Image: Screenshot-2024-03-16-123804.png]](https://i.ibb.co/bJmvySg/Screenshot-2024-03-16-123804.png)
![[Image: Screenshot-2024-03-16-123814.png]](https://i.ibb.co/S7dcMn0/Screenshot-2024-03-16-123814.png)
![[Image: Screenshot-2024-03-16-123821.png]](https://i.ibb.co/zR6Hwpb/Screenshot-2024-03-16-123821.png)
