SkillAgentSearch skills...

MemorySnitcher

Vulnerable (on purpose) programs to leak NtReadVirtualMemory address for stealthier API resolution (no GetProcAddress, GetModuleHandle or LoadLibrary in the IAT)

Install / Use

/learn @ricardojoserf/MemorySnitcher
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

MemorySnitcher

Update - 12/2025: Rethinking the Approach

Important note: This article was written with the assumption that using NtReadVirtualMemory was necessary for stealthy Export Address Table (EAT) traversal. However, this approach has been correctly criticized as unnecessary complexity. When reading your own process's memory, direct pointer access (e.g., *(DWORD*)(address)) is not only more efficient but also less suspicious than using NtReadVirtualMemory. The technique described here demonstrates creative thinking about API resolution, but simpler alternatives exist that avoid both IAT entries and the overhead of NT system calls for self-process memory reading.

<br>

TL;DR

  • Using dynamic API resolution to avoid functions appearing in the IAT requires only the NtReadVirtualMemory address.

  • A separate application which just prints the address is easily detected.

  • Using an application with a vulnerability which leaks memory addresses (on purpose) may be stealthier.

<br>

Index

  1. Motivation

  2. NtReadVirtualMemory for API resolution

  3. Approach 1: Print the address

  4. Approach 2: More code!

  5. Approach 3: Address leak by design

  6. Putting it into practice: NativeBypassCredGuard example

  7. Conclusion

<br>

Motivation

Over the last few months, I have made public some tools that use "only" low-level functions in the ntdll.dll library, also known as NTAPIs. This DLL contains the lowest level functions in user-mode because it interacts directly with ntoskrnl.exe, which is already kernel-mode.

Some of these projects have been NativeDump and TrickDump to dump the LSASS process; NativeBypassCredGuard to patch Credential Guard; NativeTokenImpersonate to impersonate tokens and NativeNtdllRemap to remap ntdll.dll.

It bothered me to have all the necessary functions in the Import Address Table (IAT) of the binary, because this could hint at the true intentions of the compiled binary, so I implemented dynamic API resolution. However, using it, I could not avoid calling GetModuleHandle or LoadLibrary and GetProcAddress - which are part of kernel32.dll, not ntdll.dll!

<br>

NtReadVirtualMemory for API resolution

To use dynamic API resolution, I created functions mimicking GetModuleHandle and GetProcAddress. GetModuleHandle returns the address of a loaded DLL given the library name (LoadLibrary works too, but I would only use it if the DLL is not already loaded in the process) and GetProcAddress returns the address of a function given the DLL address and the function name.

By walking the PEB, it is possible to do this using only functions in ntdll.dll:

  • Custom implementation of GetProcAddress requires only NtReadVirtualMemory.

  • Custom implementation of GetModuleHandle requires NtReadVirtualMemory, NtQueryInformationProcess and RtlUnicodeStringToAnsiString.

The problem: you need some way to resolve at least NtReadVirtualMemory, and for that you need the address of ntdll.dll. With those two addresses, you can use your custom implementation of GetProcAddress to get the function address of any function in ntdll.dll.

And, resolving NtQueryInformationProcess and RtlUnicodeStringToAnsiString, you can use your custom GetModuleHandle to get the base address of any DLL, in case you are not sticking to using only NTAPIs.

The way I did it until now is:

#include <windows.h>

// First, the function delegates are defined at the top of the program:
typedef NTSTATUS(WINAPI* NtReadVirtualMemoryFn)(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T);
typedef NTSTATUS(WINAPI* NtQueryInformationProcessFn)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);

NtQueryInformationProcessFn NtQueryInformationProcess;
NtReadVirtualMemoryFn NtReadVirtualMemory;

int main() {
    // NTAPI
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    NtReadVirtualMemory = (NtReadVirtualMemoryFn)GetProcAddress((HMODULE)hNtdll, "NtReadVirtualMemory");
    void* pNtapi = CustomGetProcAddress(hNtdll, "NtClose");
    
    // Function in other DLL
    NtQueryInformationProcess = (NtQueryInformationProcessFn)CustomGetProcAddress(hNtdll, "NtQueryInformationProcess");
    RtlUnicodeStringToAnsiString = (RtlUnicodeStringToAnsiStringFn)CustomGetProcAddress(hNtdll, "RtlUnicodeStringToAnsiString");
    uintptr_t hDLL = CustomGetModuleHandle((HANDLE)(-1), "kernel32.dll");
    void* pFunction = CustomGetProcAddress(hDLL, "CloseHandle");
    ...
}
  • First, NtReadVirtualMemory address is calculated using GetModuleHandleA and GetProcAddress. The ntdll.dll library is the first one to get loaded in any process, so there is no need for LoadLibrary.

  • NtQueryInformationProcess and RtlUnicodeStringToAnsiString get resolved with the custom implementation of GetProcAddress, using ntdll.dll base address.

  • Then, any function address in any DLL can be calculated dynamically using the custom implementation of GetModuleHandle.

<br>

From this code, we find we only call GetModuleHandleA once to get ntdll.dll address; and GetProcAddress once to get NtReadVirtualMemory address. The rest of the addresses can be calculated dynamically!

The problem is, even if we only call them once, we would have GetModuleHandleA and GetProcAddress functions in the Import Address Table of the binary, which can be considered suspicious. Let's verify it using PE-BEAR:

it1

There are 17 imported functions from Kernel32.dll, the first 2 in the list are the suspicious-looking ones.

Regarding the other 15 functions, these appear because the binary was compiled with the C Runtime (CRT) included, which embeds the necessary runtime support directly into the executable. I created a very simple program to test it, and those same 15 functions appear in the IAT:

it2

There are many blogs about compiling without CRT so I will not do it here (also, maybe not having any function in the IAT looks even worse).

<br>

After some tests, I found that the ntdll.dll base address can be bruteforced given the NtReadVirtualMemory address. For example, if the function address is 0x7FFE5424D7B0, the module's base address can be any value from 0x7FFE54100000 to 0x7FFE542D0000. So I created a small script to test all addresses like 0x7FFE54100000, 0x7FFE54120000, ... , 0x7FFE541F0000, 0x7FFE54200000, 0x7FFE54210000, ... , 0x7FFE542F0000.

The file resolve.c contains the code to resolve the function in any DLL given three parameters:

  • The NtReadVirtualMemory address.

  • The DLL containing that function

  • The function name

r1

<br>

Approach 1: Print the address

The easiest way to obtain the address is just to resolve the function and print the value:

#include <iostream>
#include <windows.h>

int main(int argc, char* argv[]) {
    HMODULE hNtdll = LoadLibraryA("ntdll.dll");
    FARPROC pNtReadVirtualMemory = GetProcAddress(hNtdll, "NtReadVirtualMemory");
    printf("[+] NtReadVirtualMemory address: \t0x%p\n", pNtReadVirtualMemory);
    return 0;
}

ra

It is straightforward, but not very OPSEC-safe:

rav

The function address value will change for every system and reboot, so hardcoding it is not useful, but we can use the output from a program like read_addresses.exe as input parameters for a program such as resolve.exe.

<br>

Approach 2: More code!

The code probably does too much for so few lines of code, so I will rely on AI to create the most generic application (a Task Management program):

Give me the code for a C++ application of at least 300 lines that under no circumstances could be considered malicious by an antivirus or EDR. For example, a Task management program

The code prompts the user to press a key from 1 to 6, but we will add a secret option 33:

switch (choice) {
    case 1: addTask(); break;
    ...
    case 33: test(); break;
    case 0: cout << "Exiting...\n"; break;
    default: cout << "Invalid choice. Try again.\n";
}

The called function will print the addresses:

void test() {
    HMODULE hNtdll 

Related Skills

View on GitHub
GitHub Stars42
CategoryDevelopment
Updated16d ago
Forks6

Languages

C++

Security Score

80/100

Audited on Mar 10, 2026

No findings