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 :)

challenge

  • 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 helper sub_4015e0 and the leaf sub_401550. All three touched data_404160, data_404168, and data_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, prints length-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).

Field (per id)
Address pattern
Meaning

(&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 calls sub_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.

And with all that information, the solve script follow these steps:

  • First -- open the string table in .data (starting at 0x4160), 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 add www) <-- that www 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