owned this note
owned this note
Published
Linked with GitHub
# Intro
At 6-8 January 2024, i'm playing IrisCTF with Social Engineering Expert, and solved several Reverse Engineering challenges. Here are my writeup for challenge The Johnson's, Secure Computing, The Maze and CloudVM.
# Writeup
## The Johnson's
![image](https://hackmd.io/_uploads/HyvCxcFua.png)
To solve this challenge we need to guess each person's favorite color and food
### Reversing
Decompile the binary using IDA
#### Main
![image](https://hackmd.io/_uploads/HkVCEcKOT.png)
![image](https://hackmd.io/_uploads/BkFyHqKOa.png)
In main function, the program will ask the user to input each person's favorite color and food. each person has unique favorite color and food, and there are only 4 options to choose from per category. Then Our string will be mapped into a number (red=>1 , blue=>2 , green=>3 , yellow=>4 , pizza=>1 , pasta=>2 , steak=>3 , chicken=>4) After that, check() function is called.
#### Check
![image](https://hackmd.io/_uploads/BywqU5td6.png)
To get the correct value, we need to make the if statement return false. that means:
- v2 = True
- color[0] != 3
- color[1] != 3
- v1 = True
- v0 = True
- food[2] != 2
- food[3] != 2
- color[1] != 1
- food[0] = 4
- food[3] != 3
- color[2] != 4
- color[3] = 2
### Getting Flag
After process of elimination, we can determine the correct order of food and color, which is
food = [(~~1~~,~~2~~,~~3~~,4),(~~1~~,2,~~3~~,~~4~~),(~~1~~,~~2~~,3,~~4~~),(1,~~2~~,~~3~~,~~4~~)] = [4,2,3,1] = [chicken,pasta,steak,pizza]
color = [(1,~~2~~,~~3~~,~~4~~),(~~1~~,~~2~~,~~3~~,4),(~~1~~,~~2~~,3,~~4~~),(~~1~~,2,~~3~~,~~4~~)] = [1,4,3,2] = [red,yellow,green,blue]
Sending this to the server will give the flag
![image](https://hackmd.io/_uploads/r1T_o9Y_6.png)
``FLAG : irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3}``
## Secure Computing
![image](https://hackmd.io/_uploads/rkljnqFuT.png)
We are given a challenge binary and the code snippet. from the code snippet, we know that the flag is irisctf{<48-bytes flag>}
### Reversing
Decompile the binary in IDA
Main function is the same as the code snippet.
![image](https://hackmd.io/_uploads/rJrwa9Kda.png)
At the start function, we discover that there is an additional function call in init array
![image](https://hackmd.io/_uploads/S1fZA5KOp.png)
![image](https://hackmd.io/_uploads/r1cXC9FO6.png)
At this function, seccomp syscall was called with SECCOMP_SET_MODE_FILTER flag. [More information about seccomp](https://www.kernel.org/doc/html/v4.19/userspace-api/seccomp_filter.html)
![image](https://hackmd.io/_uploads/BJFIkiKOp.png)
Basically after adding a filter using seccomp, every syscall that we want to execute will be checked using this filter. because the while loop is executed 8 (64/8) times, that means there are 8 sets of filter being applied. Lets extract those filters.
![image](https://hackmd.io/_uploads/HkjD0ojuT.png)
![image](https://hackmd.io/_uploads/ByVORojuT.png)
After extracting all of the filters, we can disassemble the filters by using [seccomp-tools](https://github.com/david942j/seccomp-tools)
```seccomp-tools disasm --no-bpf export_results1.txt > chunk1```
![image](https://hackmd.io/_uploads/SJyhgiYdp.png)
![image](https://hackmd.io/_uploads/HJ5AAoj_p.png)
There are 3 types of return value, but after reading the disassembly a little bit, we can assume that we need the filter to do return ERRNO(0).
### Gettting Flag
Looks like inserting these equations into Z3 is possible, but we need to know several static values
- sys_number = 0x1337
- X = 0
- arch = 0xc000003e ([const for x86_64](https://github.com/david942j/seccomp-tools/blob/2c51ad9a1be7b6c00675289cd9868b088b81d362/lib/seccomp-tools/const.rb#L153))
Solve script
```python=
from z3 import *
import re
s=Solver()
for numfile in range(1,9):
#seccomp-tools disasm --no-bpf export_results{i}.txt > chunk{i}
x=open(f'chunk{numfile}').readlines()
stop_line=len(x)
mem=[BitVecVal(0,32) for i in range(16)]
X=BitVecVal(0,32)
A=BitVecVal(0x1337,32)
arch=BitVecVal(0xc000003e,32)
sys_number=BitVecVal(0x1337,32)
reg=r'([0-9]+?)\)'
reg_hex=r'(0x[0-9a-f]+?)\)'
reg_first_part='A = (args\[.+?\]) >> 32'
reg_second_part='A = (args\[.+?\])'
args=[BitVec(f"x{i}",64) for i in range(6)]
for i in args:
for j in range(0,64,8):
s.add(Extract(j+7,j,i)>=0x20)
s.add(Extract(j+7,j,i)<0x7f)
for i in range(3,stop_line):
num,cmd=x[i].strip().split(': ')
if("KILL" in cmd or "ERRNO(0)" in cmd):
continue
if("A = args" in cmd):
cmd=re.sub(reg_first_part,"A = Extract(63,32,\\1)",cmd)
cmd=re.sub(reg_second_part,"A = Extract(31,0,\\1)",cmd)
if("goto" in cmd):
try:
val=eval(re.findall(reg,cmd)[0])
except Exception as e :
val=eval(re.findall(reg_hex,cmd)[0])
s.add(A==val)
continue
exec(cmd)
#simplifying the equation
if(type(A)==BitVecRef):
A=simplify(A)
else:
A=A%2**32
if(type(X)==BitVecRef):
X=simplify(X)
else:
X=X%2**32
print(s.check())
m=s.model()
for i in args:
print(int.to_bytes(m[i].as_long(),8,"little").decode(),end='')
```
![image](https://hackmd.io/_uploads/SJ9NzstOT.png)
``FLAG : irisctf{1f_0nly_s3cc0mp_c0ulD_us3_4ll_eBPF_1nstruct10ns!}``
## The Maze
![image](https://hackmd.io/_uploads/rJboXoKOT.png)
We need to somehow get flag from this maze game.
After viewing the html source, we can see that one .js file is used to run the game.
![image](https://hackmd.io/_uploads/ByPeVsFda.png)
![image](https://hackmd.io/_uploads/Sk-84sKup.png)
The code snippet looks intimidating, but the most interesting part is in function y(), because it looks like the function is doing a decryption process. Its possible that we can get the flag by decrypting some data through this function. Our analysis will be focusing on this function.
To analyze it, we can debug the javascript by adding a breakpoint in the javascript code then observe the variable value, or try to call some functions manually.
![image](https://hackmd.io/_uploads/HkjUNhF_6.png)
![image](https://hackmd.io/_uploads/ryW2dhtda.png)
### Reversing
#### global variable init
These are the variable that is initialized at the start of the game
![image](https://hackmd.io/_uploads/rk-Er2td6.png)
#### y()
If we put a breakpoint on the first line after the if statement, it will be called everytime we try to move a step in the maze.
![image](https://hackmd.io/_uploads/HkjUNhF_6.png)
Lets see the most interesting part first, the decryption process
![image](https://hackmd.io/_uploads/SJ1qwjtd6.png)
This code snippet is doing a one-liner to process data and call a function to decrypt something. To understand this code, we need to understand how the one-liner code works.
```
<operation-1> && <operation-2> -> only execute operation-2 if operation-1 is true
<operation-1> || <operation-2> -> only execute operation-2 if operation-1 is false
<operation-1> , <operation-2> -> execute operation-2 after operation-1
```
![image](https://hackmd.io/_uploads/HyaXznF_T.png)
After understanding how the one-liner works, this is the deobfuscated version.
![image](https://hackmd.io/_uploads/r1P-EnF_a.png)
After debugging the code, this is what we found.
```
i.g -> (x,y) position of player
i.f -> maze metadata (maze size, grid information )
i.b -> maze level
f(N) -> function to generate level N maze
n (local var) -> iv
e (local var) -> base64 encoded secret
t, r, c (local var) -> key
i.e -> unknown (needed to generate key)
a (local var) -> base64 decoded secret
i.l -> string that will be displayed to the screen
```
To successfully call the decrypt function, we must:
- finish 4 levels of maze by reaching the bottom right of the maze
- i.e size is 4 and all value in i.e is not -1
The only unknown value is in **i.e**. Lets check how the array is generated.
![image](https://hackmd.io/_uploads/HJPKtnFda.png)
**i.e** is generated by using another one-liner code. Lets make this more readable.
![image](https://hackmd.io/_uploads/HJ-26hYOT.png)
Looks like for every step we take, the program will update **i.k** value using the player's current (x,y) coordinate, and if **i.i** length is 16 and **i.j** is 1, some kind of process is done to an array in **t**. the processed data is used to determine what value to push into **i.e**, **i.k** if succesful and -1 if not. Lets find out what process is happening once the if statement is true
![image](https://hackmd.io/_uploads/rJOeGTtOT.png)
Turns out that series of operations were a matrix multiplication of **i.i** with a matrix in **a**[**i.b**]. To return true, the multiplication result must result in identity matrix, which means we just need to inverse the matrix in **a**[**i.b**] to get the correct value for **i.i** for each level.
### Getting Flag
we will use sage to do the matrix operations
![image](https://hackmd.io/_uploads/BkJh7aYdT.png)
![image](https://hackmd.io/_uploads/rJ1CQaYup.png)
We now have the value of the correct **i.i** for each level, now we just need to translate this into a maze movement to generate **i.k**
![image](https://hackmd.io/_uploads/BJL6E6K_a.png)
**i.d[i.c.findIndex((e => 1 === e))]** is our step direction in the maze translated into a value. we can see what value is each direction by adding a code to console.log() the value.
![image](https://hackmd.io/_uploads/Hyz6U6tua.png)
![image](https://hackmd.io/_uploads/S1I2HaYd6.png)
```
down -> 40 & 3 = 00
right -> 39 & 3 = 11
left -> 37 & 3 = 01
up -> 38 & 3 = 10
```
The program will take the first 2 bits of each direction value and append it to **i.j**. if 4 moves are completed (8 bits), it will be pushed into **i.i** after subtracting it with 32.
So we just need to convert the matrix we inversed before into binary representation, split it into 2 bits, then translate it into movement direction.
From the movement direction, we can generate each coordinate that the player visited, which results in the correct **i.k** value for each level. We now have the complete **i.e**
```python=
from sage.all import *
keys=[
[[-0.035712653158569425, -0.03287257405769594, 0.02334270751557671, -0.02540569009549741],
[0.006411543162643718, 0.02983176158440242, 0.02174280291361509, 0.0067315240830360425],
[-0.010545458159016606, -0.00814858244477348, -0.042581311921773606, 0.020546352515626396],
[-0.033596009182061196, -0.000705547992169411, 0.02680088640677326, -0.051391718257793324]],
[[0.17638922081966382, 0.09011377379943657, -0.2507513705723452, -0.14779730737755375],
[0.055901970582484195, 0.015708171290764118, -0.08718795489603928, -0.034523214634888215],
[-0.08558020292437048, -0.07923610054967736, 0.17700480152953701, 0.08977339387750669],
[-0.1294747286013282, -0.05801667137404857, 0.20491595512778732, 0.13903433491935893]],
[[-1.4436222005842259, 0.33729308666017527, 0.5668938656280429, -1.2835443037974683],
[5.040019474196689, -1.136222005842259, -1.895520934761441, 4.410126582278481],
[2.833203505355404, -0.6585199610516066, -1.0631937682570594, 2.4658227848101264],
[-1.0833495618305744, 0.23018500486854918, 0.4296007789678676, -0.9417721518987342]],
[[-0.30391992395012024, 0.37225297768830734, 0.10770005032712632, 0.18833529049935693],
[0.2496784655818375, -0.27545713806408323, -0.055359838953195774, -0.15288262595761337],
[-0.027456243359615277, 0.10879047139741654, 0.055359838953195774, 0.09732707040205782],
[0.20058155790415477, -0.2713470894145278, -0.11726220432813286, -0.1713079460940558]]
]
def hass(movement):
start=[-7,-7]
ik=0
for move in movement:
if(move=="down"):
start[1]+=1
elif(move=="up"):
start[1]-=1
elif(move=="right"):
start[0]+=1
elif(move=="left"):
start[0]-=1
ik = ik + 211 * (start[0] + 9) * (start[1] + 9) * 239 & 4294967295
return ik
for r in keys:
key=Matrix(r)
ans=key.inverse()
ans=list(ans.apply_map(lambda x: int(bin(round(x)+32)[2:])))
ans=''.join(list(map(lambda x: ''.join([str(i).zfill(6) for i in x]) ,ans)))
ans=[['down','left','up','right'][int(ans[i:i+2],2)] for i in range(0,len(ans),2)]
print(hass(ans),end=',')
```
use this to decrypt the secret
```js=
async function aesGcmDecrypt(key, ciphertext, nonce) {
const algorithm = { name: 'AES-GCM', iv: nonce};
const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']);
const decryptedData = await crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext);
return new Uint8Array(decryptedData);
}
cipher="Dugd8DbBCXnrEF1kKd2Hg4lsRQ1eV/6gQ+NfwsVhtr4UgeXQFq1m6WctmIljEG7PZg==";
aa = new Uint8Array(atob(cipher).split("").map((e => e.charCodeAt(0))));
nn = Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
w=[40645774,130661539,116339703,150379278].map((e => e.toString(16).padStart(8, "0"))).join("")
key=(new TextEncoder).encode(w)
aesGcmDecrypt(key,aa,nn).then(e => console.log(new TextDecoder().decode(e)))
```
`FLAG: irisctf{thankfully_no_ghost_girl}`
## CloudVM
We are only given a VM bytecode and a service that executes those bytecodes.
![image](https://hackmd.io/_uploads/Syq3satdT.png)
![image](https://hackmd.io/_uploads/Hye-J0F_6.png)
### Discovering Opcode
Thankfully, we can debug the opcodes because the service will dump the register and memory content, and also the reason why the code fails, such as invalid opcode or memory access error if an error in emulation occurs.
![image](https://hackmd.io/_uploads/HJ-u2Tt_p.png)
After some trial and errors while finding out how the opcode works, most of the opcode used in michaelpaint.bin has been disassembled using the following code
```python=
x=open('michaelpaint.bin','rb').read()
idx=0
header=x[:idx+4];idx+=4
function_num=int.from_bytes(x[idx:idx+4],"little");idx+=4
addr2fun={}
fun2addr={}
for i in range(function_num):
addr=int.from_bytes(x[idx:idx+4],"little");idx+=4
fun_name_len=int.from_bytes(x[idx:idx+2],"little");idx+=2
fun_name=x[idx:idx+fun_name_len];idx+=fun_name_len
addr2fun[addr]=fun_name.decode()
fun2addr[fun_name.decode()]=addr
#read_strings
addr2string={}
string2addr={}
while(idx!=fun2addr["main"]):
addr=idx
str_len=int.from_bytes(x[idx:idx+2],"little");idx+=2
str_val=x[idx:idx+str_len];idx+=str_len
addr2string[addr]=str_val.decode()
string2addr[str_val.decode()]=addr
while idx!=len(x):
if(idx in addr2fun.keys()):
print("=== "+addr2fun[idx]+" ===")
print(str(idx).ljust(6)+":",end='')
if(x[idx]==0xc0):
r_x=x[idx+1]
r_y=x[idx+2]
print(f"reg[{r_x}]=reg[{r_y}]")
idx+=3
elif(x[idx]==0xc1):
r_x=x[idx+1]
val=int.from_bytes(x[idx+2:idx+2+4],'little')
print(f"reg[{r_x}]={val}")
idx+=7-1
elif(x[idx]==0xc2):
r_x=x[idx+1]
print(f"reg[8] += reg[{r_x}]")
idx+=2
elif(x[idx]==0xc3):
r_x=x[idx+1]
print(f"reg[8] -= reg[{r_x}]")
idx+=2
elif(x[idx]==0xc4):
r_x=x[idx+1]
print(f"reg[8] *= reg[{r_x}]")
idx+=2
elif(x[idx]==0xc5):
r_x=x[idx+1]
print(f"reg[7] = reg[8] % reg[{r_x}]")
idx+=2
elif(x[idx]==0xc6):
r_x=x[idx+1]
print(f"reg[8] &= reg[{r_x}]")
idx+=2
elif(x[idx]==0xc7):
r_x=x[idx+1]
print(f"reg[8] |= reg[{r_x}]")
idx+=2
elif(x[idx]==0xc8):
r_x=x[idx+1]
print(f"reg[8] ^= reg[{r_x}]")
idx+=2
elif(x[idx]==0xc9):
r_x=x[idx+1]
print(f"reg[8] <<= reg[{r_x}]")
idx+=2
elif(x[idx]==0xca):
r_x=x[idx+1]
print(f"reg[8] >>= reg[{r_x}]")
idx+=2
elif(x[idx]==0xD0):
addr=int.from_bytes(x[idx+1:idx+1+4],'little')
print(f"print({repr(addr2string[addr])})")
idx+=5
elif(x[idx]==0xD3):
r_x=x[idx+1]
r_y=x[idx+2]
print(f"mem[reg[{r_x}]:reg[{r_x}]+reg[{r_y}]]] = bytearray(input().strip()[:reg[{r_y}]].encode())")
idx+=3
elif(x[idx]==0xD4):
r_x=x[idx+1]
print(f"A = readint(mem[{r_x}])")
idx+=2
elif(x[idx]==0xD5):
r_x=x[idx+1]
print(f"unknown {hex(x[idx])} {hex(x[idx+1])}")
idx+=2
elif(x[idx]==0xE2):
addr=int.from_bytes(x[idx+1:idx+1+4],'little')
r_x=x[idx+1+4]
print(f"cmp reg[{r_x}],reg[8] ; je {addr}")
idx+=6
elif(x[idx]==0xE0):
addr=int.from_bytes(x[idx+1:idx+1+4],'little')
print(f"jmp {addr}")
idx+=5
elif(x[idx]==0xE1):
addr=int.from_bytes(x[idx+1:idx+1+4],'little')
r_x=x[idx+1+4]
print(f"cmp reg[{r_x}],reg[8] ; jne {addr}")
idx+=6
elif(x[idx]==0xE5):
addr=int.from_bytes(x[idx+1:idx+1+4],'little')
r_x=x[idx+1+4]
print(f"cmp reg[{r_x}],reg[8] ; jle {addr}")
idx+=6
elif(x[idx]==0xF0):
straddr=int.from_bytes(x[idx+1:idx+1+4],'little')
fun_name=addr2string[straddr]
print(f"call {fun_name}")
idx+=5
elif(x[idx]==0xF2 or x[idx]==0xF1):
print(f"return {hex(x[idx])}")
idx+=1
else:
print(x[idx:idx+6].hex().upper())
break
```
one of the string said that the flag content is the name of the object painted in the canvas
![image](https://hackmd.io/_uploads/r1JV0pYu6.png)
### Breaking Down Functions
#### main
The main function
![image](https://hackmd.io/_uploads/SJrjxAFdT.png)
#### paint
Function to do painting activity
![image](https://hackmd.io/_uploads/SJKTe0Yua.png)
#### render
displaying color to the screen based on value in memory
![image](https://hackmd.io/_uploads/H1DQZCFdp.png)
#### suSsY
Interesting function that is used to determine if the image is correct or not.
![image](https://hackmd.io/_uploads/S1wSbCY_p.png)
![image](https://hackmd.io/_uploads/B1cDbRK_p.png)
#### SUssY
contains operation that calculates a value using reg[0] and reg[1]
![image](https://hackmd.io/_uploads/rJV0WRKua.png)
### Reversing
we need to know how the memory is translated into color. For that we need to look at **render** function
![image](https://hackmd.io/_uploads/H1vXrRt_p.png)
there are 8 colors to choose from, and each color is mapped to number 0-7. that means the only possible byte value in the memory is from 0b000 to 0b111.
### Getting Flag
to get the correct value, we need to satisfy all conditions in suSsY function. we can use z3 to solve this.
```python=
from z3 import *
def get_val(x,res):
reg=[0 for i in range(8)]
reg[1]=x
A = BitVec("A",32)
reg[2]=A
reg[3]=8
reg[4]=A
reg[5]=A
A=reg[2]
reg[3]=12
A >>= reg[3]
reg[6]=A
reg[7]=A
reg[3]=255
A=reg[4]
A &= reg[3]
reg[4]=A
A=reg[6]
A &= reg[3]
reg[6]=A
reg[3]=65280
A=reg[5]
A &= reg[3]
reg[5]=A
A=reg[7]
A &= reg[3]
reg[7]=A
A=0
A |= reg[4]
A |= reg[5]
A |= reg[6]
A |= reg[7]
A ^= reg[1]
s=Solver()
s.add(A==res)
s.add(Extract(7,0,BitVec("A",32))<0b1000)
s.add(Extract(15,8,BitVec("A",32))<0b1000)
s.add(Extract(23,16,BitVec("A",32))<0b1000)
s.add(Extract(31,24,BitVec("A",32))<0b1000)
s.check()
return bytearray(int.to_bytes(s.model()[BitVec("A",32)].as_long(),4,"little"))
image=[0 for i in range(512)]
image=bytearray(image)
iddx=35
key=34216
res=38072
image[iddx:iddx+4]=get_val(key,res)
iddx=39
key=57981
res=62316
image[iddx:iddx+4]=get_val(key,res)
iddx=43
key=22478
res=22223
image[iddx:iddx+4]=get_val(key,res)
iddx=51
key=6088
res=1753
image[iddx:iddx+4]=get_val(key,res)
iddx=55
key=5918
res=1551
image[iddx:iddx+4]=get_val(key,res)
.
.
.
<SNIP>
.
.
.
iddx=243
key=62403
res=62403
image[iddx:iddx+4]=get_val(key,res)
iddx=247
key=43710
res=43710
image[iddx:iddx+4]=get_val(key,res)
iddx=251
key=46028
res=46028
image[iddx:iddx+4]=get_val(key,res)
iddx=259
key=9729
res=9729
image[iddx:iddx+4]=get_val(key,res)
iddx=263
key=49732
res=49732
image[iddx:iddx+4]=get_val(key,res)
iddx=267
key=15739
res=15739
image[iddx:iddx+4]=get_val(key,res)
color=[
'\x1b[40m ',
'\x1b[41m ',
'\x1b[42m ',
'\x1b[43m ',
'\x1b[44m ',
'\x1b[45m ',
'\x1b[46m ',
'\x1b[47m '
]
res=['\x1b[40m ' for i in range(512)]
for i in range(len(image)):
res[i]=color[image[i]]
for i,data in enumerate(res[:256]):
if(i%16==0):
print('\x1b[0m ')
print(data,end='')
```
![image](https://hackmd.io/_uploads/HJb8DAY_6.png)
`FLAG : irisctf{gameboy}`