🌘
By Justin Tan
Repair a Corrupted ELF and Reverse RC6
A CTF writeup about repairing a corrupted ELF binary and recovering a flag with RC6 analysis.
The challenge presents a binary that claims to be ELF64 but reports an i386 machine type and an unrealistically large section table. We begin by repairing those header fields, then go through the obfuscation layers.
Recon
Running file shows:
- Claims to be ELF64
- Machine type says Intel i386
- Section header count is absurdly large
Assume it is a 32-bit binary with corrupted header.
Header Repair
# Patch offsets: 0x04 (Class), 0x30 (Shnum)
with open("chall", "rb") as f: data = bytearray(f.read())
data[0x04] = 0x01 # Set Class to ELF32
data[0x30] = 0x00 # Zero out section header count
data[0x31] = 0x00
with open("chall_fixed_32", "wb") as f: f.write(data)
Analysis
Once decompiled, open in Ghidra shows:
- Use of RC6 constants (
0xB7E15163,0x9E3779B9) - Use of
2*x*x + xmixing pattern - Hardcoded ciphertext
The “stored bytes” aren’t compared directly. Each byte is rotated right by 3 bits before comparison.
Check: RC6(Input) == ROR3(Stored_Bytes)
Reverse Check
To recover the flag, reverse the operations:
- Apply ROR 3 to the extracted bytes (undo the binary’s transform)
- Run RC6 decryption using the hardcoded key
At this point, the output is readable but still wrong.
Code:
import struct
# RC6
w = 32
r = 20
P32 = 0xB7E15163
Q32 = 0x9E3779B9
def rotr(x, y):
return ((x >> (y & 31)) | (x << (32 - (y & 31)))) & 0xFFFFFFFF
def rotl(x, y):
return ((x << (y & 31)) | (x >> (32 - (y & 31)))) & 0xFFFFFFFF
def rc6_key_schedule(key):
c = len(key) // 4
if len(key) % 4 != 0: c += 1
L = [0] * c
for i in range(len(key)):
L[i // 4] = (L[i // 4] | (key[i] << (8 * (i % 4)))) & 0xFFFFFFFF
S = [(P32 + i * Q32) & 0xFFFFFFFF for i in range(2 * r + 4)]
A = B = i = j = 0
v = 3 * max(c, 2 * r + 4)
for _ in range(v):
A = S[i] = rotl((S[i] + A + B) & 0xFFFFFFFF, 3)
B = L[j] = rotl((L[j] + A + B) & 0xFFFFFFFF, (A + B))
i = (i + 1) % (2 * r + 4)
j = (j + 1) % c
return S
def rc6_decrypt_block(ciphertext, S):
A = int.from_bytes(ciphertext[0:4], 'little')
B = int.from_bytes(ciphertext[4:8], 'little')
C = int.from_bytes(ciphertext[8:12], 'little')
D = int.from_bytes(ciphertext[12:16], 'little')
C = (C - S[2 * r + 3]) & 0xFFFFFFFF
A = (A - S[2 * r + 2]) & 0xFFFFFFFF
for i in range(r, 0, -1):
(A, B, C, D) = (D, A, B, C)
u = rotl((D * (2 * D + 1)) & 0xFFFFFFFF, 5)
t = rotl((B * (2 * B + 1)) & 0xFFFFFFFF, 5)
C = (rotr((C - S[2 * i + 1]) & 0xFFFFFFFF, t) ^ u)
A = (rotr((A - S[2 * i]) & 0xFFFFFFFF, u) ^ t)
D = (D - S[1]) & 0xFFFFFFFF
B = (B - S[0]) & 0xFFFFFFFF
return struct.pack('<4I', A, B, C, D)
def decrypt_attempt(encrypted_bytes, key, rot_func, rot_amt):
# 1. Apply Rotation to all bytes
transformed = bytearray()
for b in encrypted_bytes:
transformed.append(rot_func(b, rot_amt))
# 2. Decrypt
S = rc6_key_schedule(key)
result = b""
for i in range(0, len(transformed), 16):
block = transformed[i : i+16]
if len(block) == 16:
result += rc6_decrypt_block(block, S)
return result
# keystring from binary
key_ascii = b"0123456789abcdef0123456789abcdef"
# ciphertext from 'od' output
enc_hex = [
0x3f, 0x70, 0xbd, 0xc8, 0xed, 0xb2, 0x96, 0x20, 0x21, 0x1b, 0x1a, 0xc1, 0x70, 0x7f, 0xc5, 0xcb,
0x5a, 0x92, 0xdd, 0x56, 0xf0, 0xea, 0xf6, 0x12, 0x3a, 0xce, 0x0f, 0x5c, 0xa3, 0xb3, 0x7c, 0x87,
0xdb, 0x49, 0x80, 0x0b, 0xbc, 0xe8, 0x65, 0x3e, 0x1f, 0x22, 0x10, 0x89, 0x87, 0x56, 0x1e, 0x22
]
# Rotation helpers (8-bit)
def ror3(b, amt): return ((b >> amt) | (b << (8-amt))) & 0xFF
def rol3(b, amt): return ((b << amt) | (b >> (8-amt))) & 0xFF
def no_rot(b, amt): return b
# bruteforce
print("Attempting decryption variants...\n")
attempts = [
("Key=ASCII, Rot=Right 3", key_ascii, ror3, 3),
("Key=ASCII, Rot=Left 3", key_ascii, rol3, 3),
("Key=ASCII, No Rot", key_ascii, no_rot, 0),
]
for name, k, r_func, r_amt in attempts:
try:
res = decrypt_attempt(enc_hex, k, r_func, r_amt)
# Check if result looks like a flag
print(f"[{name}] Result: {res}")
if b"CTF" in res or b"flag" in res:
print(f"\n>>> SUCCESS! Flag found in [{name}]:\n{res.decode('utf-8', errors='ignore')}\n")
except Exception as e:
print(f"[{name}] Error: {e}")
Two obfuscation layers remain:
- Bit rotation: every byte is rotated left by 1 -> fixed with
ROR 1 - Block shuffling: data is scrambled in 8-byte blocks. The first 2 bytes of each block are moved to the end (
{bRE:CTF->RE:CTF{b})
Logic:
Extract encrypted bytes
↓
ROR 3
↓
RC6 decrypt
↓
ROR 1
↓
Fix 8-byte block order
Code:
import struct
decrypted_raw = b'\xf6\xc4\xa4\x8at\x86\xa8\x8ch\xa4bh\x86\x96\xa6n\xben\xbe\x8ch\x98\x98\xe6B\xa6\xe4\xaa\xa8\xd0\xbe\xe4\x00\x00f\xa6\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
def ror1(b):
# Rotate Right 1 bit
return ((b >> 1) | (b << 7)) & 0xFF
flag_chars = []
for b in decrypted_raw:
# Apply the correction
fixed_byte = ror1(b)
flag_chars.append(fixed_byte)
# Decode and print
flag_bytes = bytes(flag_chars)
print("Recovered Flag String:", flag_bytes)
# Attempt to locate the substring 'CTF{'
try:
flag_str = flag_bytes.decode('utf-8', errors='ignore')
print("\nFINAL FLAG: " + flag_str)
except:
pass