Skip to main content

Binary Instrumentation 4 picoCTF 2026 Solution

Frida reveals a hardcoded target, so hook connect() to redirect it at your listener and force the key check to pass.

Published: March 20, 2026Updated: September 22, 2026

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.
bash
unzip -P picoctf bin-ins4.zip
bash
ncat -lvp 9867

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Discover the hardcoded target IP and port with Frida
    Observation
    The description says the binary sends the flag to someone, so it opens an outbound TCP connection to a hardcoded destination. Hook connect() and read the IP and port off the live sockaddr argument before touching a disassembler.
    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 on ws2_32!connect, which receives the destination address as a sockaddr_in structure in its second argument.
    js
    cat > 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);
        }
    });
    EOF
    bash
    frida -f bin-ins4.exe -l discover.js --no-pause

    Expected 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.

    With the target unreachable the binary fails silently: no output, no error. Wireshark shows a SYN and no reply, but the connect() hook gives you both the IP and the port in one step without chasing cross-references.

    Tried: Use strings on bin-ins4.exe to find the hardcoded IP address before running any dynamic analysis.

    The IP may appear as a plain string, but the port is a big-endian 16-bit integer in the sockaddr struct and never shows up as readable decimal. Static strings will not give it to you reliably; the hook reads both fields from the live argument.

    Learn more

    The connect() syscall is the moment a TCP socket commits to a destination. Its second argument is a sockaddr_in struct: 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.

  2. Step 2Redirect the connection to localhost with a connect() hook
    Observation
    The hook shows a target of 192.168.29.25:9867, which is unreachable from here. Overwrite the IP bytes inside the sockaddr struct on entry and the kernel connects to a local listener instead.
    Once you know the binary targets 192.168.29.25:9867, rewrite the destination address bytes inside the connect() hook before the call proceeds. The kernel reads the struct after your onEnter returns, so any in-place write takes effect. At the same time, start ncat -lvp 9867 on your machine so the redirected connection has somewhere to land.
    bash
    # In a separate terminal, start the listener FIRST:
    bash
    ncat -lvp 9867
    js
    cat > 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");
            }
        }
    });
    EOF
    bash
    frida -f bin-ins4.exe -l redirect.js --no-pause
    What 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.

    A hosts-file override only affects hostname resolution. This binary connects to a raw IPv4 address, so no lookup happens and the file is never read. The hook has to overwrite the IP bytes in the struct 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 read the struct and attempted the connection, so writing to it changes nothing. Overwrite the destination on entry, before the OS reads the pointer.

    Learn more

    Frida's onEnter handler runs synchronously before the hooked function executes. Because the OS reads the sockaddr pointer only when connect() actually runs, any bytes you write to that address during onEnter are 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.

  3. Step 3Bypass the key check by forcing lstrcmpA to return 0
    Observation
    The redirected connection produces an 'Enter the key' prompt, and logging arguments confirms the check runs through lstrcmpA. Force its return value to zero on the way out and every comparison succeeds without knowing the key.
    With the connection redirected, the binary sends a prompt asking for a key. It validates the response using kernel32!lstrcmpA, which returns 0 when both strings match. Hook that function with Frida and unconditionally set the return value to 0 in onLeave. The binary will then believe the key is correct and proceed to send the flag.
    js
    cat > 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)");
        }
    });
    EOF
    bash
    # Run with both hooks active:
    bash
    frida -f bin-ins4.exe -l solve.js --no-pause
    bash
    # 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.

    This binary uses the Windows API lstrcmpA, not the C runtime strcmp, so hooking strcmp produces no output and intercepts nothing. Log both arguments on entry first and confirm which function actually runs before forcing a return value.

    Tried: Use retval.replace(ptr(0)) in onLeave instead of retval.replace(0) to force lstrcmpA to return zero.

    In onLeave, retval is a NativePointer wrapping the integer return value, not a pointer that was returned, and replace() wants a plain integer. Passing ptr(0) either throws or does nothing depending on the Frida version, leaving the real value in place and the 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's onLeave - which runs after the real function finishes but before the caller reads EAX/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.

  4. Step 4Receive the base64 flag from the local listener and decode it
    Observation
    With the key check bypassed, the listener prints a long printable-ASCII string rather than a binary payload. That is base64; decode it.
    After the key check passes, the binary sends the flag to the connected socket as a base64 string. Your ncat listener 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
    # cGljb0NURntuM3R3MHJrXzFzXzRQMXNfNFNfVzMxMV8uLi59Cg==
    bash
    echo 'cGljb0NURntuM3R3MHJrXzFzXzRQMXNfNFNfVzMxMV8uLi59Cg==' | base64 -d

    Expected output

    picoCTF{n3tw0rk_1s_4P1s_4S_W311_...}
    Learn 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.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{n3tw0rk_1s_4P1s_4S_W311_...}

Two 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. The flag is shown abbreviated on this page; work the steps above to recover the full value.

Key takeaway

A binary with a hardcoded network destination and a compiled-in authentication check can be redirected and bypassed at runtime without touching the file on disk. Dynamic instrumentation intercepts OS API calls as they happen, so an analyst reads or rewrites arguments and return values before the program sees them. Malware sandboxes and endpoint products use the same workflow to capture C2 traffic, study payload delivery, and pull indicators out of live samples.

Related reading

Useful tools for Reverse Engineering

Where to go next