Short C sample to read LDR data from PEB
#1
Wrote this today just for some quick pointer practice, totals around to about 70-80 lines including comments and whitespace.

Here's the code, posted as unlisted so it stays relatively private within the community here: https://pastebin.com/33uqTh84

Basically, it dynamically loads Ntdll.dll since there's no real header for windows as it's mostly closed-source kernel functions and structures, then creates a function pointer to NtQueryInformationProcess() and pulls ProcessBasicInformation which includes a pointer for the PEB of the process in question.

Note: To actually get ProcessBasicInformation using NtQueryInformationProcess(), you need to get a process handle with PROCESS_QUERY_INFORMATION privileges. In this sample, I just use GetCurrentProcess() to get the handle of the process it's called from. As a result, it returns the handle with PROCESS_ALL_ACCESS which means I won't run into any other issues. This will vary if you get other handles instead using OpenProcess().

So from the structure pulled with NtQuery, it gets a pointer to the PEB. From the Microsoft PEB docs, a pointer to the LDR Data Table is included in the PEB. The LDR is basically another structure that contains a (doubly-linked) list of all loaded modules in the process. Since each process on windows typically has kernel32.dll, kernelbase.dll, ntdll.dll, and runtimes loaded by default by the kernel, we can find their base addresses using the PEB of any process, and by extension, their entry-points in the process.

Each entry in the LDR pointer provided by the PEB (in the list rather) only has a pointer to the next item and the previous item. It doesn't really tell you much. Microsoft docs say that the entry is the structure, but there's a catch: It's not the 'base' of the structure so you need to do some offset work.

But, luckily, there's a Winapi macro to deal with that. Line 63 calls CONTAINS_RECORD which allows you to cast the pointer from the first parameter (the LIST_ENTRY) to the type of the second parameter (LDR_DATA_TABLE_ENTRY) and set its base to the offset that is given by the third parameter. In this case, the list_entry actually points to the InMemoryOrderLinks struct which is the LIST_ENTRY pointer that's given from the LDR pointer in PEB.

Once it's casted properly, we can just read the FullName (path of the DLL loaded, but only the buffer since it's a UNICODE_STRING data type and not just a normal cstring) as well as the DllBase (its base address in memory) and finally its entry point (which is an undocumented pointer given by Reserved3[0]).

The code then prints out the data for each module loaded, including its own base and entry-point.

I'll add more details soon in an edit, as well as some cool stuff you can do with some small mods and maybe why it's useful for exploit dev.
Reply
#2
So this is something related to exploit dev? I don't know enough about the windows C library to understand what was going on in the code for the most part
Reply
#3
Ah, approved, nice.

The short answer: Yes.
The long answer: It goes into how much data you can get just from running a program on Windows and only reading data from the program's space.



The first part of the code is loading Ntdll.dll. Although WINTERNL.H (the supposed header file for ntdll) contains most of the information needed regarding function calls, enum types, structs, and typedefs, it doesn't actually contain any code for them, so you can't just include the header and start calling functions the same way you can write "#include <stdio.h>" and call printf().

Now, for most people who have done Windows development work with C# know that you can dynamically load a DLL file using Assembly.LoadFile("[path to the DLL]"). That's essentially what I'm doing here since LoadLibrary() also searches for the file specified in $PATH. Then, the function retrieves the symbol table, and the next line uses GetProcAddress to find the actual location of that function in its address space now that the DLL is loaded.

While it's retrieving that function, it's also casting it to the type declared on line 5. The function returns an NTSTATUS, so that's what we declare it as, but since it's a WinAPI function, we also need to let the compiler know it uses the calling convention of __stdcall. The, the name is arbitrary, and we just follow the IN/OUT parameters of the function call as documented by MSDN.

Since Windows.h is already linked to the file, it'll also load kernel32.dll, kernelbase.dll and any runtimes requires like MSVC on its own when executed. So when we go find their base addresses in PEB/LDR, we're more or less sure to find them there.



Now, as for its practical use-case:

A fact that isn't so well known is that the offset for loading DLLs like kernel32 is going to be the same in one process and another. ASLR might stop people from finding where other processes in memory but there are still ways around that, and at the end of the day, it'll load the same libraries in the same order to the same memory offsets, and since DLLs also have static placement of functions and data in memory (e.g. it's not easily randomized by ASLR and typically just has a static pointer table at a fixed address,) you can then get the address of any function you want.

Now, consider the application in question has a buffer overflow somewhere. You don't need to do too much work to start calling API functions from within the vulnerable application if you know exactly where they're located in memory, now do you?

Rather, when you can read the PEB with basically one line of ASM code (just changing the offset of the fs register) then it'll actually be remarkably easier to shellcode because you can find what you need in the given memory space.

See, if you try to break out of your memory space assigned by the kernel, you get out-of-bounds errors, as expected.
But what if your shellcode could find kernel32.dll, then from there find the address of OpenProcess() or OpenProcessToken(). Using those functions you could now open up a completely different memory space to start working with, given you have permissions to do so.

And of course knowing where kernel32.dll is also allows you to get cool functions like Read/WriteProcessMemory(), OpenFile(), GetGeoInfoA, and finally VirtualProtect() (which leads to a really good time since you can disable DEP with it.)


Read more here: https://h0mbre.github.io/Creating_Win32_ROP_Chains/#
He does a pretty good job of explaining what can be done using Python instead, so that might be more up your alley.
Reply
#4
I'm somewhat busy now, but i've 'earmarked' this thread for further and in-depth reading later. I just wanted to mention this real quick for now. Providing high quality contributions makes you eligible to become part of the Contributor Usergroup. As a part of that usergroup you will have access to one of our private sections.

It's a great way of sharing materials that you'd prefer to keep for "GS Eyes Only" if you will. You'd be able to share stuff like this without worrying about some skid copying your source code and other stuff and claiming it's theirs.

Insider is the one that can add you to the Contributors Usergroup. If you plan on helping with GS Specific tooling and research publicly or otherwise send me a PM, we'll have a chat and i may add you to the GS Devs usergroup, if that is something you'd be interested in. Needless to say we have our private sections as well as repos under GS Github organization.

In any case i'll go over the content you posted in depth when finish up some work.
Reply