Description
The executable was designed to send the flag to someone. Are you that someone? Download the binary bin-ins4.zip (password: picoctf).
Download and extract bin-ins4.zip using the password 'picoctf'.
Start a local listener on port 9867 before running the binary.
unzip -P picoctf bin-ins4.zipncat -lvp 9867Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Discover the hardcoded target IP and port with FridaObservationI noticed the binary description said it was designed to 'send the flag to someone', which suggested it opens an outbound TCP connection to a hardcoded destination, and that hooking ws2_32!connect() would reveal both the IP and port from the live sockaddr_in argument before investing time in static disassembly.The binary tries to open a TCP connection to a hardcoded IP address (192.168.29.25) on port 9867. That server is not reachable from your machine, so the connection fails and the flag never arrives. The first step is to confirm this with a Frida hook onws2_32!connect, which receives the destination address as asockaddr_instructure in its second argument.jscat > discover.js << 'EOF' // Hook connect() to log the destination IP and port const connectFn = Module.getExportByName("ws2_32.dll", "connect"); Interceptor.attach(connectFn, { onEnter(args) { // args[1] = pointer to sockaddr_in: [family(2), port(2 BE), ip(4)] const sa = args[1]; const port = (sa.add(2).readU8() << 8) | sa.add(3).readU8(); const ip = [0,1,2,3].map(i => sa.add(4 + i).readU8()).join("."); console.log("connect() -> " + ip + ":" + port); } }); EOFbashfrida -f bin-ins4.exe -l discover.js --no-pauseExpected output
connect() -> 192.168.29.25:9867
What didn't work first
Tried: Run the binary directly without any Frida hook and watch the console or network traffic for a destination address.
The binary silently fails when 192.168.29.25 is unreachable - no output appears and no error is printed to stdout. Wireshark will show a SYN to that IP with no reply, but you still need Frida's connect() hook to get the IP and port in one step without chasing disassembly cross-references.
Tried: Use strings on bin-ins4.exe to find the hardcoded IP address before running any dynamic analysis.
The IP '192.168.29.25' may appear as a plain string, but the port 9867 is stored as a big-endian 16-bit integer in the sockaddr struct and will not appear as a readable decimal. Reading the port from static strings alone is unreliable; the Frida hook reads both the IP and the port from the live sockaddr_in argument, making dynamic analysis more accurate than static string extraction.
Learn more
The connect() syscall is the moment a TCP socket commits to a destination. Its second argument is a
sockaddr_instruct: 2 bytes of address family, 2 bytes of port in big-endian order, then 4 bytes of IPv4 address. Reading those bytes directly from the pointer exposes the hardcoded destination without touching a disassembler.In CTF reverse engineering this is a common pattern: the binary carries a fixed C2 address compiled in at build time. Hooking
connect()is the fastest way to find it because you read the live resolved address rather than chasing cross-references through disassembly.Step 2
Redirect the connection to localhost with a connect() hookObservationI noticed the first hook revealed the binary targets 192.168.29.25:9867, an address unreachable from my machine, which suggested overwriting the IP bytes inside the sockaddr_in struct during onEnter so the kernel connects to my local ncat listener at 127.0.0.1:9867 instead.Once you know the binary targets 192.168.29.25:9867, rewrite the destination address bytes inside theconnect()hook before the call proceeds. The kernel reads the struct after youronEnterreturns, so any in-place write takes effect. At the same time, startncat -lvp 9867on your machine so the redirected connection has somewhere to land.bash# In a separate terminal, start the listener FIRST:bashncat -lvp 9867jscat > redirect.js << 'EOF' // Redirect connect() from the hardcoded IP to 127.0.0.1 const connectFn = Module.getExportByName("ws2_32.dll", "connect"); Interceptor.attach(connectFn, { onEnter(args) { const sa = args[1]; const port = (sa.add(2).readU8() << 8) | sa.add(3).readU8(); if (port === 9867) { // Overwrite the IP bytes to 127.0.0.1 sa.add(4).writeU8(127); sa.add(5).writeU8(0); sa.add(6).writeU8(0); sa.add(7).writeU8(1); console.log("Redirected connect() to 127.0.0.1:9867"); } } }); EOFbashfrida -f bin-ins4.exe -l redirect.js --no-pauseWhat didn't work first
Tried: Use /etc/hosts or a system DNS override to point 192.168.29.25 to 127.0.0.1 instead of patching the sockaddr in-memory.
DNS overrides only affect hostname resolution. The binary connects to a raw IPv4 address, not a hostname, so the OS never does a DNS lookup and /etc/hosts is never consulted. The Frida hook must overwrite the IP bytes directly inside the sockaddr_in struct in memory before connect() fires.
Tried: Use an onLeave handler instead of onEnter to rewrite the destination address after connect() returns.
By the time onLeave runs, the kernel has already consumed the sockaddr struct and attempted the connection. Writing to the struct in onLeave has no effect on the connection that was just made. The destination must be overwritten in onEnter, before the OS reads the pointer.
Learn more
Frida's
onEnterhandler runs synchronously before the hooked function executes. Because the OS reads thesockaddrpointer only whenconnect()actually runs, any bytes you write to that address duringonEnterare what the kernel sees - effectively redirecting the connection without patching the binary on disk.This technique is sometimes called in-memory address patching. It is transient (gone when the process exits) and requires no administrator rights beyond what Frida already needs. Malware analysts use the same approach to redirect malware callbacks to sandbox-controlled listeners for safe traffic capture.
Step 3
Bypass the key check by forcing lstrcmpA to return 0ObservationI noticed the redirected connection triggered an 'Enter the key:' prompt, and logging lstrcmpA arguments confirmed the binary was comparing user input against a secret key using kernel32!lstrcmpA, which suggested forcing its return value to 0 in onLeave to make every comparison appear successful without needing to know the real key.With the connection redirected, the binary sends a prompt asking for a key. It validates the response usingkernel32!lstrcmpA, which returns 0 when both strings match. Hook that function with Frida and unconditionally set the return value to 0 inonLeave. The binary will then believe the key is correct and proceed to send the flag.jscat > solve.js << 'EOF' // Hook 1: redirect connect() to 127.0.0.1:9867 const connectFn = Module.getExportByName("ws2_32.dll", "connect"); Interceptor.attach(connectFn, { onEnter(args) { const sa = args[1]; const port = (sa.add(2).readU8() << 8) | sa.add(3).readU8(); if (port === 9867) { sa.add(4).writeU8(127); sa.add(5).writeU8(0); sa.add(6).writeU8(0); sa.add(7).writeU8(1); console.log("[+] connect() redirected to 127.0.0.1:9867"); } } }); // Hook 2: force lstrcmpA to return 0 (strings equal) to bypass key check const lstrcmpA = Module.getExportByName("kernel32.dll", "lstrcmpA"); Interceptor.attach(lstrcmpA, { onEnter(args) { const s1 = args[0].readCString(); const s2 = args[1].readCString(); console.log("lstrcmpA('" + s1 + "', '" + s2 + "')"); }, onLeave(retval) { retval.replace(0); console.log("[+] lstrcmpA forced to return 0 (match)"); } }); EOFbash# Run with both hooks active:bashfrida -f bin-ins4.exe -l solve.js --no-pausebash# When the listener prints 'Enter the key:', type anything and press Enter.What didn't work first
Tried: Hook strcmp or _strcmp from the C runtime (msvcrt.dll) instead of lstrcmpA from kernel32.dll to bypass the key check.
The binary uses the Windows API lstrcmpA specifically, not the C runtime strcmp. Hooking msvcrt!strcmp produces no output and does not intercept the key comparison - the check still fails. Always log both arguments in an onEnter first so you can confirm which function is actually being called before forcing the return value.
Tried: Use retval.replace(ptr(0)) in onLeave instead of retval.replace(0) to force lstrcmpA to return zero.
retval in onLeave is a NativePointer representing the integer return value, not a pointer that was returned. retval.replace() expects a plain integer argument: retval.replace(0). Passing ptr(0) may silently do nothing or throw depending on the Frida version, leaving the real non-zero return value in EAX/RAX and the key check still failing.
Learn more
lstrcmpA (from kernel32.dll) is the Windows ANSI string comparison function. Like the C standard
strcmp, it returns 0 when both strings are identical, a negative number if the first is lexicographically less, and a positive number if greater. Conditional branches in the binary jump based on whether this return is zero, so overriding it to 0 in Frida'sonLeave- which runs after the real function finishes but before the caller readsEAX/RAX- is enough to make every key check succeed.Logging both arguments first is good practice: it lets you see what the correct key actually is (here,
key9640ed84) even though forcing the return is sufficient to bypass the check. In a real engagement you would record those values for later use rather than purely overriding the result.Step 4
Receive the base64 flag from the local listener and decode itObservationI noticed that once the key check was bypassed, the ncat listener printed a long printable-ASCII string ending with a newline rather than a raw binary payload, which is the signature of base64 encoding and suggested piping it through base64 -d to recover the plaintext flag.After the key check passes, the binary sends the flag to the connected socket as a base64 string. Yourncatlistener receives it. Copy the base64 blob and decode it to recover the plaintext flag.bash# The ncat listener will print the base64 blob, e.g.:bash# cGljb0NURntuM3R3MHJrXzFzXzRQMXNfNFNfVzMzMV85NjQwZWQ4NH0Kbashecho 'cGljb0NURntuM3R3MHJrXzFzXzRQMXNfNFNfVzMzMV85NjQwZWQ4NH0K' | base64 -dLearn more
Sending the flag as base64 over the socket is a common CTF technique to avoid issues with binary bytes in a text-oriented connection. Base64 encodes every 3 bytes of input into 4 printable ASCII characters, so any binary payload can transit safely over a plain TCP stream or be pasted into a terminal without mangling.
The full solution chain here - redirect a hardcoded network destination, bypass an authentication check by forcing a comparison result, then receive and decode the response - mirrors real-world techniques used to analyse C2-connected malware in a sandboxed environment: redirect callbacks to a controlled listener, patch auth checks so the binary reaches its payload delivery stage, and capture what it sends. Frida makes all three steps scriptable without touching the binary on disk.
Flag
Reveal flag
picoCTF{n3tw0rk_1s_4P1s_4S_W331_...}
Three active Frida hooks are required: hook ws2_32 connect() to redirect the hardcoded IP to 127.0.0.1:9867, hook kernel32 lstrcmpA to return 0 and bypass the key check, then receive the base64 flag from the ncat listener and decode it.