Winter is coming
Author: 0xIDA
Platform: FlagYard · Category: rev · Difficulty: Hard
Solvers
Summary
32-bit ELF password checker that mmaps seven XOR-encrypted shellcode stages and runs each against a slice of the 39-byte input. Recover each stage's transform, invert it, then resolve stage-4's incomplete custom-base64 underconstraint among hex candidates against the platform.
Solution
Step 1: Triage and stage decrypt
Binary is a 32-bit PIE ELF. Main:
- Banner via
system/puts fgetspassword (needstrlen > 0x26→ 39+ chars with newline)- Loop stage
s = 0..6:mmapRWX → copy encrypted blob from.data→ decrypt stub XOR each byte with(0x40 + s)→ call shellcode(chunk, len, expected, extra)
| Stage | Input offset | Len | Transform |
|---|---|---|---|
| 0 | 0 | 6 | plaintext FlagY{ |
| 1 | 6 | 5 | custom 32-bit tribonacci/fib G(ch) == expected dword |
| 2 | 11 | 6 | CRC32 (zlib poly, init/final XOR 0xffffffff) |
| 3 | 17 | 5 | RC4 encrypt, key Fl4gY4rD_Sup3r_K3y (18 bytes) |
| 4 | 22 | 6 | incomplete custom base64 (alphabet in .rodata) |
| 5 | 28 | 5 | input[i] + 0x0d == expected[i] |
| 6 | 33 | 6 | input[i] ^ 0x2a == expected[i] |
Expected dwords live in .data (GOT-relative ebx+off).
Step 2: Invert stages 0–3, 5–6 (unique)
stage0: FlagY{
stage1: b5187 # unique preimages of five fib dwords
stage2: dbe8b6 # unique 6-hex CRC32 == 0x7fa4e767
stage3: 3fa27 # RC4 decrypt of five expected bytes
stage5: 1c493 # expected - 0x0d
stage6: 6d53c} # expected ^ 0x2a
Step 3: Stage 4 — incomplete base64
Custom alphabet (64 chars):
SbIj8G4oDi2MVKcUX3kdW9xpAN1FPeEn7lrtwCqv5JugBLO0Qy\+H6TzRmhZsfaY
Shellcode emits only one base64 index per input byte (standard base64's 4th char of each 3-byte group is never checked). Expected six chars: VjVKxi.
Bit recovery (unique high bits only):
part4 = 03 + b2 + 5b + b5
b2 high2 = 00 → b2 ∈ 0x00..0x3f
b5 high2 = 01 → b5 ∈ 0x40..0x7f
Live bit-flip oracle on the binary confirms only positions 24 and 27 are free (low 6 bits each). Everything else is fixed.
Orphan dword 0x72 (r) sits immediately after the six expected chars; it equals standard-base64 c7 when b5 = 'b' (b5 & 0x3f == index('r') == 34). Hex style on the rest of the flag narrows free bits to 60 candidates 03[0-9]5b[a-f].
Platform string-match (not the buggy local checker alone) selects:
part4 = 0345bb
Full password / flag:
FlagY{b5187dbe8b63fa270345bb1c4936d53c}
Step 4: One-shot solver
#!/usr/bin/env python3
"""Winter is coming — recover flag from winter_is_coming stages."""
import struct
import zlib
from pathlib import Path
ALPH = b"SbIj8G4oDi2MVKcUX3kdW9xpAN1FPeEn7lrtwCqv5JugBLO0Qy\\+H6TzRmhZsfaY"
DATA = Path(__file__).with_name("data_section.bin").read_bytes()
def u32(off):
return struct.unpack_from("<I", DATA, off)[0]
def fib_preimage(target):
t0, t1, t2 = 0, 1, 0
for n in range(256):
if n and t0 == target:
return n
t0 = (t1 + t2) & 0xFFFFFFFF
t2, t1 = t1, t0
raise ValueError(hex(target))
def rc4_ks(key, n):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
i = j = 0
out = []
for _ in range(n):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
out.append(S[(S[i] + S[j]) % 256])
return out
def stage4_fixed():
exp = bytes(u32(0x6F8 + 4 * i) for i in range(6)) # VjVKxi
idxs = [ALPH.index(c) for c in exp]
i0, i1, i2, i3, i4, i5 = idxs
b0 = ((i0 << 2) | (i1 >> 4)) & 0xFF
b1 = (((i1 & 15) << 4) | (i2 >> 2)) & 0xFF
b2_hi = i2 & 3
b3 = ((i3 << 2) | (i4 >> 4)) & 0xFF
b4 = (((i4 & 15) << 4) | (i5 >> 2)) & 0xFF
b5_hi = i5 & 3
return b0, b1, b2_hi, b3, b4, b5_hi
def main():
s0 = bytes(u32(0x6A0 + 4 * i) for i in range(6))
s1 = bytes(fib_preimage(u32(0x6B8 + 4 * i)) for i in range(5))
crc_t = u32(0x6CC)
# unique hex CRC among 16^6
s2 = None
for n in range(16**6):
cand = f"{n:06x}".encode()
if (zlib.crc32(cand) & 0xFFFFFFFF) == crc_t:
s2 = cand
break
assert s2
key = b"Fl4gY4rD_Sup3r_K3y"
ct = [u32(0x6E4 + 4 * i) for i in range(5)]
ks = rc4_ks(key, 5)
s3 = bytes(ct[i] ^ ks[i] for i in range(5))
b0, b1, b2_hi, b3, b4, b5_hi = stage4_fixed()
# intended free bits (platform): b2='4', b5='b'
s4 = bytes([b0, b1, ord("4"), b3, b4, ord("b")])
assert (s4[2] >> 6) == b2_hi and (s4[5] >> 6) == b5_hi
s5 = bytes(u32(0x714 + 4 * i) - 0x0D for i in range(5))
s6 = bytes(u32(0x728 + 4 * i) ^ 0x2A for i in range(6))
flag = s0 + s1 + s2 + s3 + s4 + s5 + s6
print(flag.decode())
if __name__ == "__main__":
main()
Flag
FlagY{b5187dbe8b63fa270345bb1c4936d53c}
Submitted live to Flagyard lab 6 (already solved / 300 pts). Local binary prints The flag is: Right.
Notes / pitfalls
- Stage decrypt key is
0x40 + stage_index, constant per stage. - Stage 4 is underconstrained by design (missing 4th base64 digit per group → 12 free bits). Local checker accepts many passwords; platform stores one. Hex +
b5='b'(orphanras std c7) cuts to 10; platform match is0345bb. - Prefer matching glibc/
ctypesonly whenrandis involved (not here). - Rebuild expected offsets from runtime
ebx(GOT) or dump.dataafter rebase; static VMA alone is fine for file-backed.datacontents.