shadow_spider_network
The attachment contains RC4 logic, but it is not actually used: When the input string overflows the buffer, the __stack_chk_fail is called, which is changed to some logic that triggers the signal handler, which actually checks the flag.
The rest is solved by AI.
Attack code:
# decompilation of the signal handler
# e.g.
# if ( *(_WORD *)(a3[15] + 17LL) == 125 )
# else if ( *(_BYTE *)(a3[15] - 19LL) == 109 )
flag = bytearray(82)
for line in open("decompiled.txt", "r"):
if "a3[15]" in line:
if "LL" in line.split()[5]:
sign = line.split()[4]
offset = int(line.split()[5].removesuffix("LL)"))
elif "LL" in line.split()[6]:
sign = line.split()[5]
offset = int(line.split()[6].removesuffix("LL)"))
else:
sign = "+"
offset = 0
value = int(line.split()[-2])
if sign == "+":
offset = -offset
flag[64 - offset] = value
print(flag)
Analysis by AI:
TSGCTF 2025 - Shadow Spider Network Writeup
Category: Reverse Engineering
Difficulty: Hard
Author: TSG
Flag: TSGCTF{Inv3571ga710n_1n70_BOF_Or13n73d_Pr0gramm1ng_a5_a_73chn1qu3_f0r_0bfu5ca710n}
Challenge Overview
We are given a binary that appears to implement RC4 encryption/decryption for flag verification. The binary prompts for a flag and checks it against what seems to be RC4-encrypted data. However, there's a contradiction: the code checks strlen(input) == 40 but then compares 48 bytes against RC4-decrypted data.
Initial Analysis
Basic Reconnaissance
$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped
$ checksec chall
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Main Function Analysis
The main function is simple:
void main() {
printf("FLAG> ");
if (verify_function()) {
puts("Wrong");
} else {
puts("Correct!");
}
exit(0);
}
Verification Function
The verification function sub_40419F appears to implement RC4:
- Initializes S-box with 0-255
- Uses a key from global variable
s(37 bytes: "the_flying_cabbage_eats_purple_clocks") - Performs RC4 key scheduling
- Checks
strlen(input) == 40 - Generates RC4 keystream and compares 48 bytes against encrypted data at
byte_4050A0
Contradiction: The function checks for 40-character input but compares 48 bytes!
Deep Dive: Signal Handler Obfuscation
Signal Handler Discovery
Looking at initialization functions, we find sub_403F4F which sets up signal handlers:
void setup_signals() {
// Set up alternate signal stack
struct sigaltstack s;
memset(&s, 0, sizeof(s));
s.ss_sp = malloc(sysconf(_SC_PAGESIZE));
s.ss_size = sysconf(_SC_PAGESIZE);
s.ss_flags = 0;
sigaltstack(&s, 0);
// Set up signal handler
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = (__sighandler_t)sub_401363; // Signal handler
sigemptyset(&act.sa_mask);
act.sa_flags = SA_ONSTACK; // Use alternate stack
// Register for SIGSEGV (11) and SIGILL (4)
sigaction(11, &act, 0); // SIGSEGV
sigaction(4, &act, 0); // SIGILL
}
Signal Handler Analysis
The signal handler sub_401363 is complex (1455 lines of decompiled code). It implements a state machine that:
- Checks the current RIP (
a3[21]) to determine state - For each state, checks a specific byte at an offset from
a3[15] - If byte matches expected value, sets next RIP to continue
- If mismatch, sets RIP to crash function (
sub_401080)
The handler has 82 unique character checks with offsets from -64 to +17.
The Real Verification Mechanism
Understanding the Signal Handler Checks
The signal handler checks take the form:
if (*(_BYTE *)(a3[15] + offset) == expected_char)
Where:
a3[15]is the faulting address (RIP in signal context)offsetranges from -64 to +17expected_charis an ASCII value
The Buffer Overflow Trick
The verification function uses:
_BYTE v1[56]; // 56-byte buffer
__isoc99_scanf("%s", v1); // Unsafe - no length limit!
The program:
- Reads input with
scanf("%s", v1)which can overflow the 56-byte buffer - Intentionally causes faults at
input + 64 - Signal handler checks 82 bytes around the faulting address
Mapping Offsets to Input
If the program faults at input + 64, then:
a3[15](RIP) =input + 64input[0]is at offset -64input[1]is at offset -63- ...
input[81]is at offset +17
Thus, the input needs to be 82 characters to satisfy all checks.
Extracting the Flag
Signal Handler Character Checks
By analyzing the signal handler, we extract all 82 (offset, char) pairs:
| Offset | Char | Decimal | Description |
|---|---|---|---|
| -64 | T | 84 | Start of flag |
| -63 | S | 83 | |
| -62 | G | 71 | |
| -61 | C | 67 | |
| -60 | T | 84 | |
| -59 | F | 70 | |
| -58 | { | 123 | Flag opening |
| -57 | I | 73 | |
| ... | ... | ... | ... |
| +17 | } | 125 | Flag closing |
Reconstructing the Flag
We wrote a Python script to reconstruct the flag from the checks:
# Map offsets to indices: index = offset + 64
checks = [
(-64, 84, 'T'), (-63, 83, 'S'), (-62, 71, 'G'),
(-61, 67, 'C'), (-60, 84, 'T'), (-59, 70, 'F'),
(-58, 123, '{'), (-57, 73, 'I'), (-56, 110, 'n'),
# ... all 82 checks ...
(+17, 125, '}')
]
# Reconstruct
flag_chars = ['?'] * 82
for offset, decimal, char in checks:
index = offset + 64
flag_chars[index] = char
flag = ''.join(flag_chars)
The Complete Flag
TSGCTF{Inv3571ga710n_1n70_BOF_Or13n73d_Pr0gramm1ng_a5_a_73chn1qu3_f0r_0bfu5ca710n}
Translation (with leetspeak decoded):
"Investigation into BOF Oriented Programming as a technique for obfuscation"
Solving the Contradiction
How the 40 vs 48 Byte Issue is Resolved
- Normal execution path:
strlen(input) == 40check fails for 82-byte input - Signal handler intervention: The program intentionally causes signals
- Signal handler modifies execution: Changes RIP or other registers to bypass the check
- State machine completion: If all 82 checks pass, signal handler sets success state
RC4's Actual Role
The RC4 verification at loc_404486 is just one state in the larger signal handler state machine. It's not the primary verification mechanism but rather part of the obfuscation.
Solution Script
#!/usr/bin/env python3
import subprocess
flag = "TSGCTF{Inv3571ga710n_1n70_BOF_Or13n73d_Pr0gramm1ng_a5_a_73chn1qu3_f0r_0bfu5ca710n}"
# Run the binary
proc = subprocess.run(
["./chall"],
input=flag + "\n",
text=True,
capture_output=True
)
print(proc.stdout)
# Output: FLAG> Correct!
Key Takeaways
- Signal-based Control Flow: The challenge uses signal handlers (SIGSEGV/SIGILL) for control flow obfuscation
- Buffer Overflow as Feature: Intentional buffer overflow is part of the verification mechanism
- State Machine Verification: Flag verification happens through a state machine in the signal handler
- Red Herrings: RC4 and the 40-byte check are distractions from the real verification
- Context Awareness: Understanding signal context (
ucontext_t) is crucial for solving
Tools Used
- Python for analysis and automation
- GDB for debugging (though patching was needed to bypass anti-debugging)
- Signal handler analysis tools
Flag
TSGCTF{Inv3571ga710n_1n70_BOF_Or13n73d_Pr0gramm1ng_a5_a_73chn1qu3_f0r_0bfu5ca710n}
References
- Linux
ucontext_tstructure and signal handling - RC4 algorithm implementation
- Buffer overflow exploitation techniques
- Signal-based control flow obfuscation patterns