Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5

NTDLL, PEB, LDr, and F**king with Winternals

#1
For reference, here's the final code so you can follow along: https://pastebin.com/33uqTh84



I posted this like two days ago on a site that a friend invited me to for old time's sake, but didn't get much attention so I'll post it here for its informativity.

Long story short, another old friend of mine in the security community once challenged someone to do the following. I was more or less clueless about WinAPI and C at the time so I kept my mouth shut, but that challenge always kept itself at the back of my head whenever I wrote anything that used WinAPI to do anything.

The challenge (or rather the question) was something as follows:
Quote:How can you find the base address of kernelbase.dll as it's loaded in memory using the PEB on Windows?

So I interpreted the challenge as an opportunity to write my own little PoC to do exactly that.



PEB:

PEB is an abbreviation of a data structure present in Windows since Windows NT. The full name is the Process Environment Block, and is essentially a data structure used by the windows kernel to manage the process as its running and keep track of whatever important data is needed.

According to MSDN, the structure is defined as follows:

Code:
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;

Now, take note of a couple things. First of all, lots of the data types are only listed as "Reserved" and according to MSDN, that just means it's "reserved for kernel/OS use only." In other words, they want to hide functionality, so they only tell you what SOME parts of the PEB are for.

Luckily, enough people have reversed ntdll.dll to find out exactly what all the reserved values represent, and we will encounter them later on, but as it stands, all the data we need is for the most part "open source." You'll also notice that the types for each entry aren't for the most part standard. Rather, they're all Windows internal types. PVOID actually represents a pointer to void, BYTE represents a wide char, and of course, ULONG would be an unsigned long.

The structure can also be referenced as PPEB which would be a pointer to the structure in memory. That might be useful later on.



LDR:

To be entirely honest, I have no idea what this officially stands for, I can't actually find a single post online that references its full name. My best guess would be 'Loaded Dynamic Resources' or something of that sort.

Essentially, the LDR serves as a table that keeps track of all libraries that get loaded into memory.

Take a DLL for instance.
It's a library that's been compiled. So all its functions are already in a bytecode format, and when your application tries to use a DLL file, it actually needs to load that file into memory, load the symbol table for the file as well, and find the offset of whatever function you want to call.

Kernelbase.dll is one of the DLLs that gets loaded into essentially every single application because for the kernel to do its work (or other winapi processes,) it needs to have some of its own code in there to change access tokens or retrieve handles or whatever. So ideally, we should be able to find the base address of kernelbase.dll using the LDR.

And, as we can see in the PEB data structure declaration, one of the values is of the type PPEB_LDR_DATA.

Looking at the MSDN documentation for the structure, we have the following:

Code:
typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

So PPEB_LDR_DATA will refer to a pointer to the structure in question. Again, might be useful to note that.

Also, OhGreatMoreReservedBullshit.png.mp3.ogg.exe.scr
Seriously, kinda pissing me off having to rely on some bloke's personal docs instead of MSDN's own.

LIST_ENTRY is probably what I'm looking for, and looking at that structure...
Code:
typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
Tells me at least a little bit.

In short: It's your regular run-of-the-mill doubly-linked list. So it's circular (i.e. the last element points forward to the first element, and the first element points backwards to the last element), Flink represents the next item in the list, and Blink represents the previous item in the list.

According to MSDN once again, each element of the LIST_ENTRY structure found in PEB_LDR_DATA should be an actual entry in the list containing a LDR_DATA_TABLE_ENTRY.

Code:
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;

Well, there's a pretty major couple of problems with this, but we'll get to it later. Let's start writing some code.




Setting Up our Project:

Alright, I'm going to try and do everything in raw C to the best degree I can. No extra abstraction, nothing extra like streams, just Windows typedefs and structs.

I promise, the code isn't too difficult to understand, but there are a few hurdles to jump over because of Windows's closed-sourced nature and poor documentation.

First, from MSDN, the structures I mentioned above are all given in the <winternl.h> header. We're also using <Windows.h> so the kernel knows to load kernel32.dll and kernelbase.dll when we launch the program (and se we get all the types and functions we need later.)

I'm going to write everything in main() so ideally this shouldn't be too terrible for a template:

Code:
#include <stdio.h>
#include <Windows.h>
#include <Winternl.h>

int main() {

    return 0;
}

Pretty straight forward.

Now comes the first hurdle.



Dynamic Loading and Function Pointers:

So how exactly does a function get called in a program?

Basically, every function is just a segment of bytecode that the program can jump to when the function gets called. That segment also contains a ret instruction which will jump back to the address stored in EAX on 32bit systems and RAX on 64bit.

So, in short, a function is just a memory offset.
Or, in other words again, it's just a pointer to some more bytecode.

So it would make sense that we can call a function by a pointer to where that function resides in memory, right?
GladWeHaveAnUnderstanding

You see, there's a really easy way to get the address of the PEB of a process in Windows. No ASM needed, rather, there's a WinAPI function that does it for us:

Code:
__kernel_entry NTSTATUS NtQueryInformationProcess(
  IN HANDLE           ProcessHandle,
  IN PROCESSINFOCLASS ProcessInformationClass,
  OUT PVOID           ProcessInformation,
  IN ULONG            ProcessInformationLength,
  OUT PULONG          ReturnLength
);

And following documentation, if we set ProcessInformationClass to 0 (or ProcessBasicInformation), then it should return the following structure:

Code:
typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

Well, would you look at that? It contains a PPEB value, which coincidentally, we already found above to be a pointer to the PEB structure.

So how does this function work?
Quote:
  • ProcessHandle (INPUT) takes in the type HANDLE for the process to try and retrieve the information about
  • ProcessInformationClass (INPUT) is the type of information to retrieve (we already specified ProcessBasicInformation which is an enum type pointing to 0)
  • ProcessInformation (OUTPUT) is a void pointer to where the structure should be stored in memory.
  • ProcessInformationLength (INPUT) should be the size of the buffer specified, so no overflows happen.
  • ReturnLength (OUTPUT) is an address to where the function should return the length of bytes written.

The return value of the function is of type NTSTATUS which is essentially a way to check error codes on old Windows functions that have been around since NT.

Now, remember how we already established functions work on computers?

The problem we run into here is that Ntdll is, well, a DLL. Yeah, I can add <Winternl.h> to get the function definitions and structures and all that, but it doesn't actually contain ANY CODE from Ntdll. So, although I can technically write the functions into my code without VisualStudio giving me a hard time, there's no easy way to actually LINK the functions from the DLL to the application.

And thus, now I need to actually load the assembled file into memory, find the function as it's loaded, then call the function.

Thank god, there are WinAPI functions that help you do that.
But first, I need to actually build the structure that the function follows.

To do so, I add the following lines between my #includes and main():

Code:
typedef NTSTATUS(__stdcall* NT_QUERY_INFO) (
    IN HANDLE           ProcessHandle,
    IN PROCESSINFOCLASS ProcessInformationClass,
    OUT PVOID           ProcessInformation,
    IN ULONG            ProcessInformationLength,
    OUT PULONG          ReturnLength
    );

NT_QUERY_INFO NtQueryInfo;

Yeah, it's basically a straight copy-paste job from what MSDN documented.
I'm grateful for at least that much from them.

Just a couple things to note: We're essentially declaring NT_QUERY_INFO as a pointer to a function that returns an NTSTATUS type, and follows the calling convention of __stdcall* (the typical calling convention of WinAPI functions compared to the typical Linux method of cdecl). Within that type, we allow it to pass the structure defined within the brackets.

You can essentially think of that line as a really complicated function prototype. But unfortunately, the compiler has no way of knowing what's being loaded since it'll be happening at runtime and not compile-time, so we need to specify all those nitty-gritty details to make sure everything works.

We then actually declare a 'variable' with that function. Or in other words, we're making a buffer to load that function into (or rather, loading a pointer to the function into.)

The compiler will handle the rest.

Now, let's load the DLL and find the function (and of course save it into our buffer we declared.
As mentioned, WinAPI provides two nice functions to help with this: LoadLibrary() and GetProcAddress().

Code:
int main() {

    //Dynamically loading NtQueryInformationProcess()...
    HMODULE hModule = LoadLibrary(TEXT("ntdll.dll"));
    NtQueryInfo = (NT_QUERY_INFO)GetProcAddress(hModule, "NtQueryInformationProcess");
    if (hModule == NULL) {
        printf("Could not find ntdll. Exiting...\n");
        return 1;
    }
    return 0;
}

Simple enough?
We load the library and get a handle (HMODULE type) with LoadLibrary(). Since LoadLibrary() takes in a type of LPCSTR instead of a normal char*, we need to also convert our normal string to that type using the TEXT() macro.

Side note: LPCSTR = Long Pointer to Const STRing
So basically, the size of the pointer to the string varies and will break the code if it doesn't match (out-of-bounds read.)

Then, we use GetProcAddress() to find the actual virtual address of the function NtQueryInformationProcess() in the loaded address space of ntdll.dll. Finally, we cast it to the type of the typedef we declared earlier (NT_QUERY_INFO) and save its address to NtQueryInfo.

Finally, to make sure the DLL was loaded properly, we have a little conditional that will break execution if it can't get a proper HANDLE from LoadLibrary().



Calling Our NT Function:

Now we have our function pointer set up.

Let's go back to our checklist of how we're supposed to find the address of kernelbase.dll:

Code:
- Retrieve the PEB
- Read LDR
----> Count number of entries in LDR since it's circular
----> Read the name and base address of each entry and get all the data we can.

And all we've done so far is just so that we could do step one and retrieve the PEB.

Oh well.
Let's call our hard work.

Code:
//Setting up the data for the call...
    HANDLE hProc = GetCurrentProcess();
    PROCESS_BASIC_INFORMATION info;
    ZeroMemory(&info, sizeof(info));
    DWORD retLength;

//Function call!
    NTSTATUS status = NtQueryInfo(hProc, ProcessBasicInformation, &info, sizeof(info), &retLength);
    if (!NT_SUCCESS(status)) {
        printf("Failed in calling NtQueryInformationProcess(). Error code: 0x%16x\n", status);
        return 1;
    }

    printf("Succeeded in calling NtQueryInformationProcess().\n");
    printf("PEB located @ 0x%016x\n\n", info.PebBaseAddress);

So, let's go down the list:
GetCurrentProcess() returns the handle of the currently-running process that the function is called from.
I opted for this instead of OpenProcess() because OpenProcess() requires specifying permissions and may run into errors. NtQueryInformationProcess requires a handle with the permissions for PROCESS_QUERY_INFORMATION. You might not be able to open a process with specific rights, so instead I opt for GetCurrentProcess() which returns a handle with PROCESS_ALL_ACCESS to ensure we don't have access problems. We save this value to hProc.

Next is the Info Class. We mentioned it's an enum type pointing to 0, but since it's declared in <winternl.h>, we can put it in there literally to make stuff easier to read.

Now, the buffer.
We've established what the structure is above, and we have the structure declaration from the header file again, so let's go ahead and just declare it.

Since memory allocation is finnicky when native, I've also opted to zero all the memory out using the ZeroMemory() macro just to make sure there's no weird residual data lying around there.

Finally, I specify a DWORD value where the ReturnLength can safely output as well. Now, I know, you're thinking "But the function call was defined to take in a PULONG type, not a DWORD!" Well, PULONG is a pointer to an unsigned long type. And funnily enough, in Windows.h, a DWORD is typedef'd as an unsigned long. So HA. We pass its address as the past parameter.

Then, we run the NTSUCCESS macro to make sure the call succeeded (remember, the NTSTATUS return type is how we check if execution succeeded or failed, and what its error code is.) So the printf() in the case of failure will give us the error code.

Otherwise, if it succeeds, we can now retrieve a pointer to the PEB from info->PebBaseAddress, and printf() that address/pointer as well.




Reading the PEB and Counting the LDR:

We have our PEB now, or at least a pointer that refers to it in memory.
Since our PEB struct declaration provided by MSDN also included PPEB as a reference, we can store the PEB as the following:

Code:
PPEB pPeb = info->PebBaseAddress;

Simple, yeah?

Now, if you're unfamiliar with the -> notation that I'm using, remember, QueryInfo returned a buffer. We're getting that buffer with a pointer. Naturally, we would need to retrieve PebBaseAddress using some weird hack like this:
Code:
(*info).PebBaseAddress
But, the arrow notation lets us implicitly tell the compiler that it's a pointer, and it'll fix itself accordingly.

To continue, we now need to actually read the PEB to find the LDR.
But that's easy, since the LDR is not one of the undocumented fields.

Rather, we can now save the pointer to the LDR as the following:
Code:
PPEB_LDR_DATA pLdr = pPeb->Ldr;
Again, just one line.

Honestly, we could combine the two to make one minified version, but I'm not a sadist. I write this shit so people can learn from it and see what mistakes I encountered and what I considered. Learn from the code, I try to make it easy to do so.

Now, like I mentioned, the LDR is basically just one big linked list of pointers to other pointers. And it's circular.

So from the LDR_DATA table, let's actually go into the table and save the first entry. Then we'll loop around until our current position becomes the same as the starting position in the table.

Code:
PLIST_ENTRY startPos = &pLdr->InMemoryOrderModuleList;
PLIST_ENTRY currPos = startPos->Flink;
int moduleCount = 0;
while (currPos != startPos) {
    moduleCount++;
    currPos = currPos->Flink;
}
printf("Found %d modules loaded.\n", modulecount);

The last and arguably most difficult problem to debug is coming very soon.

so far, we've been able to load ntdll dynamically, find a function in it and call it using a function pointer, get the address of the PEB, get the LDR table from the PEB, and find the number of modules loaded into the address space based on the number of entries in the LDR.

So even though it's about to get tough, we've gotten pretty d*** far.




Poor MSDN Documentation and Bad Explanations:

You see, according to MSDN, each LIST_ENTRY we get from the LDR space is actually a pointer to a struct of the type LDR_DATA_TABLE_ENTRY. This struct is defined as follows from MSDN.

Code:
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;

So, there's a f*** of Reserved data in here.
Not to mention, the FIRST EIGHT BYTES of the struct are reserved.
I quite literally have NO WAY of knowing WHERE this LIST_ENTRY I got from the LDR points to.

I mean, LIST_ENTRY is basically just storing PVOIDs, so maybe it really is at the beginning of the struct.

So I tried that.
Casting the value of currPos to a void pointer, then to PLDR_DATA_TABLE_ENTRY.

I'll save you the trouble. It didn't work. Bad values everywhere.

Ideally, I wanted to print this for each assembly:
Code:
printf(
    "Path: %s, Base Address @ 0x%p, Entry Point @ 0x%p\n",
    pLdrEntry->FullDllName,
    pLdrEntry->DllBase,
    pLdrEntry->EntryPoint
);

There are problems with that too, but that's a basic idea of what I wanted to output.
So when I was getting undefined gibberish after the simple casting, I knew something was up.

By then I noticed that there's a LIST_ENTRY struct located in the LDR_ENTRY.
So I decided to take advantage of a different WinAPI macro to help out:

Code:
PLDR_DATA_TABLE_ENTRY pLdrEntry = CONTAINING_RECORD(
    currPos,
    LDR_DATA_TABLE_ENTRY,
    InMemoryOrderLinks
);

The CONTAINING_RECORD() macro takes three parameters in. It expands to weird pointer arithmetic BS, but the gist is as follows:

The first parameter is the pointer to the structure in question.
The second parameter is the struct definition that we want to try and cast to.
The third parameter is the name of the field in the struct (defined by second param) that we believe the first parameter is pointing to.

And using that data, CONTAINING_RECORD() will now offset the pointer accordingly so that the beginning/base of the struct (second param) is where the returned pointer is pointing to.

Now, if we run the printf() from earlier, well, you'll still get some issues.

You see, the first problem lies in the fact that FullDllName isn't a normal char* string as you'd expect a string to be. It's a UNICODE_STRING struct. So:
1. We can't print it using the %s format specifier for printf()
2. We need to get the text data from the struct.

So instead, we need to use a wide-char string specifier for unicode strings, and we also need to get the buffer of the string and not its other length/max-length properties.

But there's another issue.
Although MSDN defines an EntryPoint property of the struct for the LDR_DATA_TABLE_ENTRY, in the header files included with Windows/VisualStudio 2019, it's merged with Reserved3 which is also of type PVOID.

So instead of this...
Code:
[...]
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
[...]

We've got this:
Code:
[...]
    PVOID DllBase;
    PVOID Reserved3[2];
[...]

So, altogether, our new iterating/printing loop (th elast bit of our code) will look like this:

Code:
PLDR_DATA_TABLE_ENTRY pLdrEntry;
    for (int i = 0; i < moduleCount; ++i) {

        pLdrEntry = CONTAINING_RECORD(
            currPos,
            LDR_DATA_TABLE_ENTRY,
            InMemoryOrderLinks
        );

        printf(
            "Path: %ls, Base Address @ 0x%p, Entry Point @ 0x%p\n",
            pLdrEntry->FullDllName.Buffer,
            pLdrEntry->DllBase,
            pLdrEntry->Reserved3[0]
        );

        currPos = currPos->Flink;
    }

And we're done!

Let's finally run the code and see what we can find...



Build and Execute:

I wrote the code in Visual Studio 2019 using more or less default settings for everything.
I kept it all C-like, but I mean it still uses the MSVC compiler so whatever. Maybe a header in there uses C++ and I'm unaware.

I set it to Release settings, built for 64bit since I have a 64bit machine and didn't want SysWOW64 emulation. The result filesize was 12KB.

Here's the final code:
https://pastebin.com/33uqTh84

Here's the output:
[Image: DhsvDGS.png]

Forgive the blue, it's just default powershell.

But as you can see, all the memory addresses get printed out.
We now have the address of kernelbase.dll in memory, just as the question asked.

Now, one interesting thing to note is that if you run this over and over again, the PEB address changes, yeah, but the offsets for the loaded modules never changes. the kernel just loads them into those base addresses by default.

So you could find those offsets in memory by running your own process, to find the offsets of another process. Remember when I said OpenProcess() might run into permission errors? Now we can entirely avoid that by making your own process.

Let's take it a step further and why it might be important to note stuff like this:
Antivirus software tends to only scan for certain signatures, and make sure that whatever libraries and functions are being imported aren't too shady. well, if you're loading dynamically (and maybe getting some help from LD_PRELOAD), then you might get lower detection rates.

Anticheat software tries to make sure you aren't doing anything on the sidelines like this. Again, if you know where stuff is loaded in memory, you can dynamically load it ideally without getting detected.

Now, ASLR is tough, but not impossible to work around. Using some ROP techniques as outlined here, you could also abuse win32 functions for privilege escalation and shellcoding at your own will.
Reply


Messages In This Thread
NTDLL, PEB, LDr, and F**king with Winternals - by Lain - June 14th, 2020 at 10:37 PM



Users browsing this thread: 1 Guest(s)

Dark/Light Theme Selector

Contact Us | Makestation | Return to Top | Lite (Archive) Mode | RSS Syndication 
Proudly powered by MyBB 1.8, © 2002-2024
Forum design by Makestation Team © 2013-2024