daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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