0xmrerror@terminal:~/writeups

Binary Instrumentation 2 — Intercepting File Writes With Frida

picoCTF 2025 · 300 pts. Hook the file-write API with Frida and rewrite the payload before it ever touches disk.

MediumpicoCTFReverse engineering · 13 min readfridahookinginstrumentationfile-writepictoctf

picoCTF 2025, 300 points. A Windows binary that "was supposed to create a file and write the flag to it" — but when you run it, no file appears and no flag prints. The challenge's name is the hint: instrument the binary at runtime and fix what its WinAPI calls do as they happen.

1 · Run It — Nothing Happens

The program is a GUI-free console bininst2.exe that exits immediately doing, apparently, nothing interesting. Strings inspection points to CreateFileA and WriteFile (KERNEL32). The bug is upstream, in the code that called them.

2 · Trace File APIs With Frida

Frida's frida-trace auto-generates a JS handler per matched function. The *File* wildcard + -X export filter narrows the trace to KERNEL32's file functions.

frida-trace -i *File* -f bininst2.exe -X KERNEL32

Stubs land in __handlers__/KERNEL32.DLL/, including CreateFileA.js. Running under the trace shows two smoking guns:

# 1. CreateFileA is called with a literal placeholder path
CreateFileA (lpFileName="<Insert path here>") = -1
#    → INVALID_HANDLE_VALUE, the file can never be created
# 2. WriteFile is called with nNumberOfBytesToWrite=0
WriteFile (nNumberOfBytesToWrite=0)

3 · Fix the Filename at Runtime

In the generated CreateFileA.js handler, overwrite the first argument (lpFileName) with a real path before the call executes. The handler gets an args array of pointers, so replacing it needs a native pointer to a fresh string.

# __handlers__/KERNEL32.DLL/CreateFileA.js
onEnter(log, args, state) {
  const path = Memory.allocUtf8String('flag.txt');
  args[0] = path;    # swap the placeholder for a writable path
  log('CreateFileA lpFileName=' + args[0].readUtf8String());
}

Re-run the trace: CreateFileA("flag.txt") = 0x... — a valid handle, and a flag.txt now appears next to the binary.

4 · Harvest the Buffer

WriteFile's output is still empty because of the zero byte-count — but the trace's WriteFile.js stub lets you read the real payload from the buffer pointer. It's base64:

echo 'cGljb0NURntmcjFkYV9mMHJfYjFuX2luNXRydW0zbnQ0dGlvbiFfYjIxYWVmMzl9' | base64 -d
picoCTF{fr1da_f0r_b1n_in5trum3nt4tion!_b21aef39}
# the trailing hex is baked into the binary's (public) build

5 · What Made It Work

Neither bug is interesting on its own. A placeholder path and a zero byte count would each be a five-second fix in source, and neither gets you the flag. What gets you the flag is noticing that the program still built the payload and handed it to WriteFile, so the data is in the buffer the whole time. The trace shows you the arguments, and from there the payload is just a buffer read plus base64 -d.