frida-api-tracing.md (11893B)
1 --- 2 title: "API Tracing with Frida" 3 description: "Attaching Frida to a live Windows process, hooking an export with Interceptor, reading arguments on entry and return values on leave, and finding the right function with frida-trace." 4 category: dfir 5 subcategory: "Windows Internals" 6 tags: [dynamic-analysis, reverse-engineering, windows, malware-analysis, instrumentation] 7 tools: [frida, frida-trace, process-hacker] 8 difficulty: intermediate 9 updated: 2026-10-04 10 upstreamName: "ired.team" 11 upstreamUrl: "https://ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/instrumenting-windows-apis-with-frida" 12 upstreamAuthor: "Mantvydas Baranauskas" 13 upstreamLicense: none 14 upstreamRelation: derived 15 references: 16 - name: "Frida" 17 url: "https://frida.re" 18 author: "Ole André Vadla Ravnås and the Frida project" 19 relation: inspired 20 note: "The toolkit itself. Interceptor, Module and NativePointer APIs used throughout." 21 - name: "Frida JavaScript API" 22 url: "https://frida.re/docs/javascript-api/" 23 author: "the Frida project" 24 relation: inspired 25 note: "Signatures for Interceptor.attach, Module.getExportByName and the pointer read methods." 26 - name: "CredUnPackAuthenticationBufferW — Microsoft Learn" 27 url: "https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credunpackauthenticationbufferw" 28 relation: inspired 29 note: "Parameter order for the worked example, and the documented fact that it converts an opaque buffer to plaintext strings." 30 --- 31 32 ## What this is 33 34 Frida attaches to a running process and lets you run JavaScript inside it that intercepts 35 native function calls. For analysis work that solves a specific problem: you want to know 36 what arguments a binary passes to a Windows API, and you want it without building a debugger 37 script, without patching the binary, and with the ability to change your instrumentation 38 while the target keeps running. 39 40 Two tools, two jobs. `frida-trace` answers "is this function called at all, and when" by 41 generating a stub per matching export. `frida` with a script answers "what exactly were the 42 arguments" once you know which function to look at. You almost always use them in that order. 43 44 ## Prerequisites 45 46 - `pip install frida-tools` on the analysis host. On Windows, architecture must match the 47 target — a 32-bit process needs the 32-bit Frida. 48 - Administrator for attaching to a process you do not own. 49 - A disposable VM. Instrumenting a process means executing your code inside it; do this on 50 anything you care about and you are debugging your own instrumentation bugs in production. 51 - Frida cannot attach to a protected process. `lsass.exe` on a host with LSA protection or 52 Credential Guard enabled is out of reach, which is a constraint worth knowing before you 53 plan an analysis around it. 54 55 ## Walkthrough 56 57 ### Spawn or attach 58 59 ```powershell 60 frida C:\Windows\System32\notepad.exe 61 frida -p 10964 62 frida -n notepad.exe 63 ``` 64 65 Spawning holds the process suspended until you resume it, which is how you instrument 66 something that does its interesting work during start-up. Attaching catches a process already 67 running, and anything that happened before you attached is gone. 68 69 <figure class="shot"> 70 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28742%29.png" 71 alt="Frida REPL console attached to a freshly spawned notepad.exe process, showing the interactive prompt" 72 loading="lazy" referrerpolicy="no-referrer"> 73 <figcaption>The Frida REPL inside a spawned process. 74 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 75 </figcaption> 76 </figure> 77 78 ### Find out which function to care about 79 80 Pattern-match export names across the target's loaded modules and let Frida generate a 81 handler for each hit: 82 83 ```powershell 84 frida-trace -i "WriteFile" C:\Windows\System32\notepad.exe 85 frida-trace -i "*Cred*" -n explorer.exe 86 ``` 87 88 Each match produces a JavaScript file under `__handlers__/`, and editing one takes effect 89 immediately. Start wide with a wildcard to see which functions fire, then narrow. 90 91 <figure class="shot"> 92 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/frida-trace.gif" 93 alt="frida-trace console printing a running count of WriteFile calls made by notepad.exe as the user saves a file" 94 loading="lazy" referrerpolicy="no-referrer"> 95 <figcaption><code>frida-trace</code> counting <code>WriteFile</code> calls as they happen. 96 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 97 </figcaption> 98 </figure> 99 100 Wildcarding a whole family is how you find the function you did not know to look for. Tracing 101 `*Cred*` in `explorer.exe` and then invoking the "run as different user" prompt shows which 102 credential APIs the shell actually calls, in order. 103 104 <figure class="shot"> 105 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/credential-popup-trace.gif" 106 alt="frida-trace output showing CredUIPromptForWindowsCredentialsW being called the moment the Windows credential prompt appears" 107 loading="lazy" referrerpolicy="no-referrer"> 108 <figcaption>Wildcard tracing names the function the prompt goes through. 109 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 110 </figcaption> 111 </figure> 112 113 ### Hook one function properly 114 115 `Interceptor.attach` takes an address and a pair of callbacks. `onEnter` receives the 116 argument array; `onLeave` receives the return value, and is where you read output parameters, 117 because on entry they are uninitialised. 118 119 ```javascript 120 const writeFile = Module.getExportByName(null, 'WriteFile'); 121 122 Interceptor.attach(writeFile, { 123 onEnter(args) { 124 this.len = args[2].toInt32(); 125 console.log(`WriteFile handle=${args[0]} bytes=${this.len}`); 126 console.log(hexdump(args[1], { length: Math.min(this.len, 0x80) })); 127 }, 128 onLeave(retval) { 129 console.log(` -> ${retval.toInt32()}`); 130 } 131 }); 132 ``` 133 134 ```powershell 135 frida C:\Windows\System32\notepad.exe -l .\hooking.js 136 ``` 137 138 `null` as the module in `getExportByName` searches every loaded module, which is what you 139 want for an API that may be exported from `kernel32` or `kernelbase` depending on build. 140 Pass an explicit module name when the symbol is ambiguous. `this` is shared between `onEnter` 141 and `onLeave` for a single call, which is how you carry a length or a pointer across. 142 143 ### Reading output parameters on leave 144 145 The pattern that makes this technique useful: a function takes an opaque input and writes 146 decoded output to caller-supplied buffers. Capture the pointers on entry, read them on leave. 147 148 `CredUnPackAuthenticationBufferW` is the textbook case, because its documented purpose is to 149 turn the credential blob from the Windows credential prompt into separate username, domain 150 and password strings. Its parameter order puts `pszUserName` at index 3 and `pszPassword` at 151 index 7: 152 153 ```javascript 154 const fn = Module.getExportByName('credui.dll', 'CredUnPackAuthenticationBufferW'); 155 156 Interceptor.attach(fn, { 157 onEnter(args) { 158 this.user = args[3]; 159 this.pass = args[7]; 160 }, 161 onLeave(retval) { 162 if (retval.toInt32() === 0) return; 163 console.log(`${this.user.readUtf16String()}:${this.pass.readUtf16String()}`); 164 } 165 }); 166 ``` 167 168 On entry those buffers are empty; on leave they hold plaintext. That is not a flaw in Frida 169 or in the API — it is what the function is for, and it is the general shape of the problem: 170 anything that must exist in cleartext in a process's address space can be read by code 171 running in that process. 172 173 <figure class="shot"> 174 <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/image%20%28744%29.png" 175 alt="Traced list of credential API calls made during a credential prompt, with the unpack function highlighted among them" 176 loading="lazy" referrerpolicy="no-referrer"> 177 <figcaption>The credential API call sequence, with the unpack call that handles plaintext. 178 <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> 179 </figcaption> 180 </figure> 181 182 ### Useful idioms 183 184 ```javascript 185 Process.enumerateModules().forEach(m => console.log(m.name, m.base, m.size)); 186 Module.enumerateExports('ntdll.dll').filter(e => e.name.startsWith('NtCreate')); 187 Thread.backtrace(this.context, Backtracer.ACCURATE) 188 .map(DebugSymbol.fromAddress).join('\n'); 189 Process.enumerateRanges('r-x').filter(r => r.file === undefined); 190 ``` 191 192 The backtrace inside an `onEnter` is the one that changes an analysis: it tells you *which* 193 code called the API, not just that it was called. The last line enumerates executable ranges 194 with no backing file — the same condition the 195 [injected-thread](/sheets/dfir/injected-thread-hunting) check tests, reachable from a script. 196 197 ## What it tells you 198 199 **Where a value exists in cleartext, and therefore what is worth protecting.** An API trace 200 turns "the password must be decrypted somewhere" into a function name and an argument index. 201 That is the analysis that justifies Credential Guard, LSA protection and protected processes 202 — and the reason those mitigations are the ones that break this technique, since Frida cannot 203 attach to a process it cannot open. 204 205 **What a sample does, when static analysis stalls.** Packed or obfuscated code still has to 206 call the OS to do anything observable. Tracing `CreateFile`, `WriteFile`, registry and 207 `Nt*` memory calls gives you behaviour without ever unpacking anything. Add a backtrace in 208 each handler and you also get where in the unpacked image the call came from, which is where 209 to point a disassembler next. 210 211 **Confirmation that a detection's assumptions hold.** The injection sheets' 212 detection advice names specific call sequences — 213 `NtUnmapViewOfSection` then `VirtualAllocEx` then `WriteProcessMemory` for 214 [process hollowing](/sheets/exploitation/process-hollowing), 215 `VirtualAllocEx` then `WriteProcessMemory` then `CreateRemoteThread` for 216 [DLL injection](/sheets/exploitation/dll-injection), 217 `SetThreadContext` for [thread execution hijacking](/sheets/exploitation/thread-execution-hijacking). 218 Hooking those exports in a lab and watching the order they fire in is how you verify that a 219 rule written against that sequence will actually match, and how you discover that a sample 220 reaches the syscall directly and never touches the export you hooked. 221 222 **The limits, which are the same limits an EDR's user-mode hooks have.** Frida patches the 223 function prologue in the target's own address space. Code that resolves the syscall number 224 and issues `syscall` itself never reaches your hook. Code that maps a fresh copy of `ntdll` 225 and calls into that copy bypasses it. This is worth internalising, because it is exactly why 226 user-mode API hooking is a weak detection foundation and why the 227 [kernel callbacks](/sheets/dfir/kernel-notification-callbacks) and 228 [ETW](/sheets/dfir/etw-telemetry) routes exist: they observe from a place the target cannot 229 patch. 230 231 **And the obvious one.** The same instrumentation that reads credentials for analysis reads 232 them for theft, from a process the attacker already controls. Treat the ability to attach to 233 a process as equivalent to the ability to read everything that process handles, and scope 234 debug privileges accordingly. 235 236 ## References 237 238 - [Frida](https://frida.re) — the Frida project 239 - [Frida JavaScript API](https://frida.re/docs/javascript-api/) 240 - [CredUnPackAuthenticationBufferW — Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credunpackauthenticationbufferw) 241 - [Kernel notification callbacks](/sheets/dfir/kernel-notification-callbacks) — observation 242 from where the target cannot reach