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_4015e0and 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 Ninjathe function and the data arrays:

LOGICALLY SPEAKING:
sub_401550(id)XOR-decodes the id-th string with its key, printslength-1bytes, 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)
int32_t main(int32_t argc, char** argv, char** envp)
void* fsbase
int64_t rax = *(fsbase + 0x28)
int64_t var_178 = 0
int64_t var_170
__builtin_memcpy(dest: &var_170,
src: "\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x"
"00\x00\x00\x00\x00",
count: 0x18)
sub_4015e0(&var_178, 4, 1)
int64_t var_188 = 4
int64_t var_180 = 5
sub_4015e0(&var_188, 2, 0)
fputc(c: 0x20, fp: stdout)
int32_t buf
int32_t result
if (fgets(&buf, n: 0x100, fp: stdin) == 0)
result = 1
else
result = 0
*(&buf + strcspn(&buf, "\r\n")) = 0
if (buf == 0x7b4f4e45)
uint64_t rax_7 = strlen(&buf)
if (*(&buf + sx.q((rax_7 - 1).d)) == 0x7d)
if (rax_7 != 0)
int32_t* i = &buf
do
uint32_t rcx_1 = zx.d(*i)
uint64_t rdx_2 = zx.q(0x61 - &buf + i.d) & 3
char rsi_1 = rcx_1.b ^ 0x61
# 0x61 🥀🥀🥀🥀🥀
if (rdx_2 != 1 && rdx_2 == 3)
rsi_1 = (rcx_1.b | 0x61) & (not.d(rcx_1 & 0x61)).b
*i = rsi_1
i += 1
while (i != &buf + rax_7)
int64_t var_158
if (rax_7 == 0 || rax_7 u<= 5)
label_401397:
__builtin_memcpy(dest: &var_158,
src: "\x06\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x"
"00\x26\x00\x00\x00\x00\x00\x00\x00\x27\x00\x00\x00\x00\x00\x00\x00"
"28\x00\x00\x00\x00\x00\x00\x00",
count: 0x28)
result = 2
sub_4015e0(&var_158, 5, 1)
else
void var_124
void* i_1 = &var_124
void* const r10_1 = &data_402021
void var_117
do
uint64_t r9_2 = zx.q(*(r10_1 - 1))
char rbx_1 = 0
if (r9_2 u<= 0x2f)
uint64_t rbp_1 = zx.q(*r10_1)
int64_t rax_11 = *(r9_2 * 0x18 + 0x404168)
if (rbp_1 u< rax_11 - 1)
if (rax_11 == 0)
rbx_1 = *((&data_404160)[r9_2 * 3] + rbp_1) ^ 0x61
else
int64_t rax_12 = 0
int64_t rcx_8
do
char* rcx_7 = (&data_404160)[r9_2 * 3] + rax_12
rax_12 += 1
*rcx_7 ^= *(r9_2 * 0x18 + 0x404170)
rcx_8 = *(r9_2 * 0x18 + 0x404168)
while (rax_12 u< rcx_8)
rbx_1 = *((&data_404160)[r9_2 * 3] + rbp_1) ^ 0x61
if (rcx_8 != 0)
int64_t j = 0
do
char* rcx_10 = (&data_404160)[r9_2 * 3] + j
j += 1
*rcx_10 ^= *(r9_2 * 0x18 + 0x404170)
while (j u< *(r9_2 * 0x18 + 0x404168))
if (*i_1 != rbx_1)
goto label_401397
i_1 += 1
r10_1 += 2
while (&var_117 != i_1)
var_158 = 7
int64_t var_150_1 = 5
sub_4015e0(&var_158, 2, 1)
*(fsbase + 0x28)
if (rax == *(fsbase + 0x28))
return result
__stack_chk_fail()
noreturn
void sub_4015e0(int64_t* arg1, int64_t arg2, int32_t arg3)
int64_t* rbx = arg1
while (true)
int64_t rdi = *rbx
if (rdi u<= 0x2f)
sub_401550(rdi)
rbx = &rbx[1]
if (rbx == &arg1[arg2])
break
fputc(c: 0x20, fp: stdout)
if (arg3 != 0)
return fputc(c: 0xa, fp: stdout) __tailcall
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 flagWe already know
maincallssub_4015e0with small integer arrays. Each element is an ID into a string table.sub_4015e0prints 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
0x7b4f4e45in 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
0x2020as(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) <-- thatwwwis 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