ctf: dice25q
name: oño
author: aplet123
description: How do you dice an oño?
category: rev
link: https://github.com/dicegang/dicectf-quals-2025-challenges/blob/main/rev/ono/ (hosted)
writeup: l4cr0ss
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The provided challenge is a 64-bit ELF compiled for x86-64. It is position-independent, has been dynamically linked, and specifies the standard interpreter. Symbols have been stripped. The file size is 57920 bytes.

A large region of high-entropy data occupies most of the binary.

This region begins at offset 0x4080.

The binary imports fexecve from libc.

The description and initial observations suggest this challenge involves unwrapping encrypted payloads. That is, the challenge is to peel back the layers of this oño.
The high entropy data at &data_4080 is referenced
directly by main.

main reads 0x40 bytes from
stdin. It counts bytes until the first \n. It
uses this count to set the null terminator. The program halts if the
count doesn’t equal 0x20.

Two pointers are declared, argv_1 and
envp_1. These will be passed to fexecve
later.

argv_1 is populated with the last 24 bytes of the flag.
The bytes are taken as qwords and packed into the vector as ASCII
decimal.

memfd_create is called to create the fd that will be
handed to fexecve.

The decryption loop is shown next. On the left is the binja HLIL. On the right is the equivalent python.
The key update function is highlighted.

envp_1 is populated with the ASCII decimal
representation of the first key update.

After 0x1535 rounds, the decrypted bytes are written
into the file descriptor and executed.

This means the first eight bytes of the payload
(0x7f861cd6d160ebc3) must decrypt to the first eight bytes
of a 64-bit ELF header.

Because each round’s key depends on the key from the previous round, the initial key is sufficient to decrypt the entire payload.

The second stage operates in much the same way as the first.
0x4010c6) It reads out the second chunk of eight bytes
from the flag passed in by the argv pointer.0x4010d7) It derives a key from this qword using the
value of the first envp string.0x401119) It requests a new file descriptor that will
be used as before.0x401143) It uses the bytes from step #1 to decrypt
the bytes at &data_404060.0x401152) The decrypted bytes are written into the
file descriptor.0x40118a) A new envp is prepared for the
next stage to use.0x4011a6) The next stage is executed.
Constraining the solver to find a value satisfying the condition at
40110a yields the second chunk of the flag.

Dumping the contents of the file descriptor yields the next stage.

The third stage follows a similar pattern as the previous two stages.
0x4010d6) The key forwarded from the previous stage is
read onto the stack.0x4010ef) The next eight bytes of the flag are read
onto the stack.0x40110d) A sanity check is made on the flag
bytes.0x401125) The third chunk of the flag is used as input
to the point-slope equation.0x401131) A check is made to ensure the result matches
the key from step #1.0x401162) A new envp is prepared for the
next stage to use.0x4011a1) The next stage is executed.
Constraining the solver appropriately yields the next chunk of the flag.

Dumping the contents of the file descriptor yields the final stage.

The final stage is different. The last eight bytes of the flag,
interpreted as an 8x8 grid, must generate a maze with seven
disjoint walls of set bits and a single continuous path of cleared
bits.
That is:


Flip the length and starting point of each path hardcoded in the data section to account for endianess, then solve.

This result and the key forwarded from the previous stage are sufficient to extract the final eight bytes of the flag.

At last, the flag may be assembled.
