← Back to Writeups
CUSTOMMediumReversing

SINO (Socket In, No Out)

XESXOR9/2/20264 min read
#reversing#custom#medium#dlsym_rtld_next#docker_linux_amd64_execution#heap_buffer_exfiltration#ld_preload_free_interception

SINO (Socket In, No Out)

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: dlsym_rtld_next, docker_linux_amd64_execution, heap_buffer_exfiltration, ld_preload_free_interception

Summary

Walkthrough of the CUSTOM Reversing SINO (Socket In, No Out). Task: not-stripped x86-64 ELF network client receives a flag over a socket into a heap buffer, but the print is commented out and stdout/stderr are fclose'd before free(theFlag); get_msg is a 15k-line

Description

Title: "Socket In, No Out" "We accidentally compiled this code and deleted the entire source!"

c
char * theFlag = get_msg(argv[1], atoi(argv[2]));
//printf("The flag is %s\n", theFlag);
fclose(stdout);
fclose(stderr);
free(theFlag);

Download binary: sino.elf Run: ./sino.elf 109.233.56.90 11038

The binary is a network client: it connects to 109.233.56.90:11038, receives the flag over the socket into a heap buffer returned by get_msg(), but the printf that would display it is commented out, and stdout/stderr are fclose()'d before free(theFlag). So the flag is received but never emitted — "Socket In, No Out". ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Goal: recover the flag that the client fetches but refuses to print.

Analysis

file sino.elf → ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped.

main (0x400950)

main faithfully matches the leaked snippet:

asm
400966: cmp   dword [rbp-0x8], 0x3        ; argc == 3 ?
40096a: je    400990
...                                       ; else printf usage, return -1
400994: mov   rdi, [rax+0x8]              ; argv[1]  (IP)
4009a7: call  atoi@plt                    ; atoi(argv[2])  (port)
4009b2: call  get_msg                     ; theFlag = get_msg(argv[1], atoi(argv[2]))
4009b7: mov   [rbp-0x18], rax             ; save theFlag pointer
4009bb: mov   rdi, [0x6110a0]             ; stdout
4009c3: call  fclose@plt                  ; fclose(stdout)
4009c8: mov   rdi, [0x6110c0]             ; stderr
4009d3: call  fclose@plt                  ; fclose(stderr)
4009d8: mov   rdi, [rbp-0x18]             ; theFlag
4009df: call  free@plt                    ; free(theFlag)   <-- flag still intact here

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

At 0x4009df the flag is still in the heap buffer in RDI, right before free(). Everything after get_msg is designed to make the flag unreadable (no print, closed FDs).

get_msg (0x400a00) — obfuscated

get_msg is a ~15,000-line bloated/obfuscated function. It contains ~3 repeated blocks of socket -> connect -> raw syscall() -> read -> close and branches on booleans computed from globals at 0x6110cc / 0x6110d0 (derived from port number / argc parity). It performs the actual byte-wise flag retrieval over the socket. Reversing this to reconstruct the protocol is a large, unnecessary effort. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Anti-analysis: x32-ABI raw syscall

get_msg calls the libc syscall() wrapper using a raw syscall via the x32 ABI. This is the trick that makes analysis tools report the process "runs in x32 mode", and it defeats both strace and gdb/ptrace:

  • strace -f -e trace=network,read,write ./sino.elf ... → strace: exec: Permission denied / "Process runs in x32 mode".
  • gdb -batch breakpoint on the free call (break *0x4009df) to dump RDI → "Couldn't get registers: Input/output error", even with --cap-add=SYS_PTRACE and seccomp=unconfined. The x32-ABI syscall confuses ptrace on an otherwise amd64 process. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Both failures show why static protocol reconstruction and debugger hooking are dead ends here.

Solution

Key insight: the flag lives intact in the heap buffer right up until free(theFlag). No need to reverse the obfuscated get_msg or reconstruct the socket protocol. Since stdout/stderr are closed (so we can't printf), and ptrace is blocked (so we can't debug), intercept free() at runtime with an LD_PRELOAD shim that writes the buffer contents to a file (not to the closed stdout/stderr), then chains to the real free(). LD_PRELOAD needs no ptrace, so the x32 anti-debug is irrelevant. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The binary must actually reach the live server, so run the linux/amd64 ELF inside Docker (ubuntu:22.04, --platform linux/amd64) — on the macOS host the client then talks to the real challenge server.

hookfree.c

c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>

static void (*real_free)(void*) = NULL;

void free(void *p) {
    if (!real_free) real_free = dlsym(RTLD_NEXT, "free");
    if (p) {
        // dump buffer to a file (stdout/stderr are closed by the program)
        FILE *f = fopen("/w/flagdump.bin", "ab");
        if (f) {
            fprintf(f, "--- free(%p) ---\n", p);
            // print first 256 bytes as string + hex
            char *c = (char*)p;
            fprintf(f, "STR: %.256s\n", c);
            fprintf(f, "HEX: ");
            for (int i = 0; i < 128; i++) fprintf(f, "%02x", (unsigned char)c[i]);
            fprintf(f, "\n");
            fclose(f);
        }
    }
    real_free(p);
}

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Build and run (inside Docker linux/amd64)

bash
# Host: launch a linux/amd64 container with the working dir mounted at /w
docker run --rm -it --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:22.04 bash

# Inside the container:
apt-get update && apt-get install -y gcc
gcc -shared -fPIC -o hookfree.so hookfree.c -ldl
LD_PRELOAD=./hookfree.so ./sino.elf 109.233.56.90 11038

The very first free() call is theFlag from main, and /w/flagdump.bin immediately revealed:

STR: spbctf{REDACTED}
HEX: 7370626374667b61663133383861333532383736306338373530326538353133353262373234627d

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

echo <hex> | xxd -r -p confirms the ASCII flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering dlsym_rtld_next, docker_linux_amd64_execution, heap_buffer_exfiltration, ld_preload_free_interception was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-02 • CUSTOM • SINO (Socket In, No Out) • Difficulty: Medium