rev
Challenges

hidden string

This binary seems to be giving me some prompts but where are these strings coming from??
note: for the flag use the part of it that looks like it might sense. There is a slight bug in the challenge :)
tl;dr: The binary stores its prompts and checks inside a XOR-obfuscated string table in .data
. A verifier walks a byte program in .data
as (index, offset)
pairs, pulls characters from that table, XORs them with 0x61
, and compares against your input (also XORed with 0x61
). Dump the table, follow the pairs to reconstruct the flag, and wrap it as ENO{…}
.
⚠️There’s a tiny challenge bug that leaves padding pairs (0,0)
at the end (which would add www
); stop at }
or the first (
0,0)
after you’ve started constructing, this is later announced in the announcement but it did tank my accuracy a lil bit.🐧
My initial reconnaissance didn't yield anything meaningful, strings, file carve or obj dump didn't really provide much information due to the output is obfuscated.. So ...its binary ninja huh, I just like the theme and the layout of this app so much. Open it and we jumped to
main
, and noticed the helpersub_4015e0
and the leafsub_401550
. All three toucheddata_404160
,data_404168
, anddata_404170
,They all interact with the data section which...is the flag being scatter everywhere?
A three-column table in
.data
. Later figured but the way I called these are per-entry(pointer, length, xorKey)
layout.See it in
Binary Ninja
the function and the data arrays:

LOGICALLY SPEAKING:
sub_401550(id)
XOR-decodes the id-th string with its key, printslength-1
bytes, then XORs it back. BN shows the table layout clearly:
int64_t sub_401550(int64_t arg1) {}
uint64_t count
if (*(arg1 * 0x18 + 0x404168) == 0)
count = -1
else
int64_t rax_1 = 0
int64_t rdx_3
do
char* rdx_2 = (&data_404160)[arg1 * 3] + rax_1
rax_1 += 1
*rdx_2 ^= *(arg1 * 0x18 + 0x404170)
rdx_3 = *(arg1 * 0x18 + 0x404168)
while (rax_1 u< rdx_3)
count = rdx_3 - 1
fwrite(buf: (&data_404160)[arg1 * 3], size: 1, count, fp: stdout)
int64_t i = 0
if (*(arg1 * 0x18 + 0x404168) != 0)
do
char* rdx_5 = (&data_404160)[arg1 * 3] + i
i += 1
*rdx_5 ^= *(arg1 * 0x18 + 0x404170)
while (i u< *(arg1 * 0x18 + 0x404168))
return i
}
The stride is therefore 0x18 per entry; valid IDs are 0..0x2f (guarded in both the printer and checker).
(&data_404160)[id * 3]
base 0x404160
+ id * 0x18
+ 0x00
pointer to bytes
*(id * 0x18 + 0x404168)
base 0x404168
+ id * 0x18
length (incl. \0
)
*(id * 0x18 + 0x404170)
base 0x404170
+ id * 0x18
XOR key (1 byte)
Focus on the highlighted lines (important piece), phewwwww, that's a lot of low level code (\tuf), now lets run the binary, we will see prompts like:
❯ ./challenge
welcome to ENO challenge
enter flag
We already know
main
callssub_4015e0
with small integer arrays. Each element is an ID into a string table.sub_4015e0
prints the corresponding strings with spaces, and optionally a newline:
// prints: welcome to ENO challenge (where it all starts)
__builtin_memcpy(&var_170, "\x00\x00...\x03", 0x18);
sub_4015e0(&var_178, 4, 1);
Inside that,
sub_401550(id)
does it things (explained above)We also know that the prefix check ENO{???} is also included:
if (buf == 0x7b4f4e45) { // 0x7b 4f 4e 45
...
if (*(&buf + sx.q((rax_7 - 1).d)) == 0x7d) { ... } // 0x7d = '}'
}
The machine is little-endian, so
0x7b4f4e45
in a 32-bit register corresponds in memory to bytes (you get the idea):45 4E 4F 7B == 'E' 'N' 'O' '{'
Now we know where it all starts...to not bore you, but after this, you pretty much just traced the line after the ENO{ to see what encryption logic is being applied, and reverse it; The moment you have a string that pass the check "enter flag", done.
First -- open the string table in
.data
(starting at0x4160
), treating each 24-byte row as(ptr, len, key)
XOR-decoding (0x61) (they cancels out) the bytes so we can read the hidden words.
Then it walks the byte stream at
0x2020
as(index, offset)
pairs and plucks the corresponding characters out of that table to rebuild the message.It stops on the first
}
or the first(0,0)
pair (to skip the padding that would addwww
) <-- thatwww
is what the announcement meant.
Finally, wraps the result with
ENO{…}
for full flagasano.
from pwn import ELF, u64
# da table warudooooooooo
elf = ELF("./challenge", checksec=False)
TABLE = 0x4160
PAIRS = 0x2020
ENTRY_SZ = 0x18
MAX_IDX = 0x30
# upper bound for safe reads
max_addr = max(sec.header.sh_addr + sec.header.sh_size for sec in elf.sections)
def ok(a, n=1):
return 0 <= a <= max_addr - n
# decode table rows: (ptr,len,key) with stride 0x18
decoded, i = {}, 0
while i < MAX_IDX:
row = TABLE + i * ENTRY_SZ
if not ok(row, ENTRY_SZ):
break
ptr = u64(elf.read(row + 0x00, 8))
length = u64(elf.read(row + 0x08, 8))
key = elf.read(row + 0x10, 1)[0]
if length and ok(ptr, length):
buf = bytearray(elf.read(ptr, max(length, 0x40)))
for j in range(len(buf)):
buf[j] ^= key
decoded[i] = bytes(buf)
i += 1
# walk (idx,off) pairs
pairs = elf.read(PAIRS, 0x100)
chars = []
for k in range(0, 64):
idx, off = pairs[2 * k], pairs[2 * k + 1]
if (idx, off) == (0, 0) and chars:
break # sentinel to avoid trailing 'www'
s = decoded.get(idx)
if not s:
break
if off >= max(0, len(s) - 1):
break
chars.append(chr(s[off]))
body = "".join(chars)
flag = (
f"ENO{{{body}}}"
if not body.startswith("ENO{")
else (body if body.endswith("}") else body + "}")
)
print(flag)
ENO{stackxorrocks}
Last updated