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/MemorySnitcherREADME
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.
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.
Index
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.
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:

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:

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

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;
}

It is straightforward, but not very OPSEC-safe:

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
node-connect
337.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.2kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
337.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.2kCommit, push, and open a PR
