Reversing a non-stripped Linux binary with length-based branch obfuscation and magic-number division.
The provided file is e4sy_RE.bdsec. Despite the unusual extension, file identifies it as
a standard Linux binary:
e4sy_RE.bdsec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ... not stripped
So step one is just cp e4sy_RE.bdsec e4sy_RE && chmod +x e4sy_RE and treat it as a normal ELF.
Since it's not stripped, objdump -t and objdump -d give full symbol names, which
makes this much easier than a stripped binary.
Running it shows a banner, a "lucky number" printed via rand()/srand(time^clock)
(a red herring — it's never checked against anything), and a flag prompt:
[*] Your lucky number: 41544
Enter the flag:
Grepping strings shows no plaintext flag — the check is done arithmetically, so we need
to actually reverse the comparison logic in main.
main is the only interesting function (objdump -d shows just main, plus libc PLT stubs
and startup glue). Disassembling it reveals that after reading the input with fgets and
trimming the trailing newline with strcspn, the code branches entirely on the length of
the input string:
strlen == 0x29 (41) -> real check (main+0x21b, "the" path)
strlen == 0x1d (29) -> decoy check #1 (main+0x102 area)
strlen == 0x1a (26) -> decoy check #2 (main+0x3cf area)
strlen == 0x18 (24) -> decoy check #3 (main+0x4f1 area)
anything else -> immediate failure
Each of the four length classes runs its own independent obfuscated cipher and compares
the result against its own embedded constant array (expected.0, expected.1, expected.2,
expected.3). Three of the four are decoys; feeding a correctly-shaped string of the wrong
length just walks you into a dead-end path that always prints [-] Incorrect flag.
The genuinely solvable path is the 41-character branch, which matches the expected
BDSEC{...} flag length. This is a common CTF trick to waste solvers' time reversing the
wrong function, so identifying "which length is real" up front saves a lot of effort.
The real branch (main+0x21b onward) runs a loop for i = 0 .. 40 over the input buffer.
Below is the annotated disassembly of the core loop body, with meaning worked out instruction
by instruction:
mov rax, rdi ; rax = i
mov rcx, rdi ; rcx = i
add r8, 1 ; advance input pointer
and eax, 0x7 ; eax = i & 7
movzx esi, [r13+rax] ; esi = key_part_a[i % 8]
xor sil, [rbp+rax] ; sil ^= key_part_b[i % 8]
xor sil, [r8-1] ; sil ^= input[i]
add rdi, 1 ; i++
... ; (magic-number division tricks -> rcx = i % 7)
add ecx, 1 ; rcx = (i % 7) + 1
rol sil, cl ; rotate sil left by (i%7)+1 bits
... ; (magic-number division tricks -> rdx = (13*i_old) % 41)
mov eax, r10d ; eax = r10 (r10 tracks 11*i across iterations)
add r10d, 0xb ; r10 += 11 (prep for next iteration)
xor eax, 0x23 ; eax = (11*i) ^ 0x23
add esi, eax ; sil += that value (mod 256)
mov [rsp+rdx], sil ; STORE at permuted destination index rdx
cmp rdi, 0x29
jne loop
The two "magic number" multiply/shift blocks are compiler-generated reciprocal division —
a well-known pattern for turning x / 7 and x / 41 into a multiply+shift instead of an
actual div instruction. Recognizing this pattern (constants like 0x4924924924924925
for division by 7, 0xc7ce0c7ce0c7ce0d for division by 41) avoids having to hand-simulate
those blocks; they're just:
rcx = i % 7
dest = (13 * i) % 41 ; note: this uses i *before* increment
So, cleanly, per character i (0-indexed) the transform is:
key = key_part_a[i % 8] ^ key_part_b[i % 8]
mixed = input[i] ^ key
rotated = rol8(mixed, (i % 7) + 1)
add_val = ((11 * i) & 0xFF) ^ 0x23
out_byte = (rotated + add_val) & 0xFF
dest = (13 * i) % 41
buffer[dest] = out_byte
Crucially, the destination index and every keystream value depend only on the loop
counter i, never on the input's actual byte values. That means the permutation and
keystream can be computed independently of the flag; we don't need to brute-force
anything, just invert the arithmetic.
After the transform loop, the code does this:
movdqa xmm0, [key_2460] ; 16 bytes
movdqa xmm1, [key_2470] ; 16 bytes
pxor xmm0, [rsp] ; buffer[0:16] XOR key_2460
pxor xmm1, [rsp+0x10] ; buffer[16:32] XOR key_2470
por xmm0, xmm1
... (shift/or fold tree) ... ; collapse 32 bytes down to 1 byte (dl)
; then a second loop:
for k in 32..41:
dl |= buffer[k] ^ expected_3[k]
test dl, dl
je success
The SSE "fold" (psrldq + por repeated) is just an optimized way of checking
"are all 32 bytes zero" without a real loop — if dl ends up 0, every byte matched.
Dumping the actual bytes from .rodata confirms:
key_2460 == expected.3[0:16]
key_2470 == expected.3[16:32]
So the two "keys" are literally just the first 32 bytes of the expected.3 constant,
duplicated by the compiler for the SIMD comparison. The full 41-byte target buffer
is simply expected.3 in its entirety; no separate reconstruction needed.
Since dest(i) and the keystream are independent of the input, we can invert everything
symbolically:
target = expected.3[dest(i)] # buffer[dest(i)] required value
add_val = ((11*i) & 0xFF) ^ 0x23
after_add = (target - add_val) & 0xFF # undo the "+= add_val"
rot = (i % 7) + 1
pre_rotate = ror8(after_add, rot) # undo the rol8
input[i] = pre_rotate ^ key_part_a[i%8] ^ key_part_b[i%8]
data = open('e4sy_RE', 'rb').read()
key_a = data[0x2458:0x2458+8]
key_b = data[0x2450:0x2450+8]
expected3 = data[0x2420:0x2420+0x29] # 41 bytes
def ror8(v, r):
r %= 8
return ((v >> r) | (v << (8 - r))) & 0xFF
flag = [0] * 41
for i in range(41):
dest = (13 * i) % 41
target = expected3[dest]
add_val = ((11 * i) & 0xFF) ^ 0x23
after_add = (target - add_val) & 0xFF
rot = (i % 7) + 1
pre_rotate = ror8(after_add, rot)
flag[i] = pre_rotate ^ key_a[i % 8] ^ key_b[i % 8]
print(bytes(flag))
(Note: .rodata starts at both file offset and virtual address 0x2000, since this
is a non-relocated section in a PIE binary loaded at its default base; so raw file offsets
can be used directly to index into .rodata symbols like key_part_a, key_part_b, and
expected.3.)
b'BDSEC{e4SY_r3v3rS3_eNg1N33r1nG_cH4LL4ng3}'
Feeding the recovered string back into the binary confirms the solve:
$ echo "BDSEC{e4SY_r3v3rS3_eNg1N33r1nG_cH4LL4ng3}" | ./e4sy_RE
...
[+] Excellent work, reverse engineer!
[+] Submit the flag to the CTF platform to receive your points.
pxor + repeated psrldq/por halving) are a compact
compiler/obfuscator trick for an all-zero-bytes check; recognize the pattern
instead of tracing it byte-by-byte.imul by a large odd constant + shifts) is standard
reciprocal-multiplication codegen for integer division/modulo by a constant. Spotting
known magic constants (or just testing the block against a few sample inputs) quickly
reveals the intended % 7 / % 41 operations.