Challenge
Ce challenge provient du BuckeyeCTF 2023. ENCORE une fois, nous allons affronter une VM (stop it) écrite en Rust (Yes)
Analyse du binaire
Nous avons dans belt::main() un dispatcher d’opcode. (tips : utiliser IDARustDemangler pour simplifier le rev)
Nous trouvons rapidement 15 opcodes dont les fonctions sont plus au moins approximatives (certain évident : printf, exit, assign to mem, get from mem, …) Nous constatons également l’utilisation d’une mémoire dans la VM ainsi que des registres virtuels
Voici un début de parser du fichier fourni contenant les opcodes/valeurs du challenges :
opcode = {
0 : "mem[index] = r1",
1 : "testSize",
2 : "2?",
0x10 : "incrementIndex",
0x12: "0x12?",
0x20 : "0X20? assign to queue?",
0x21 : "0X21? assign to queue?",
0x22 : "0X22? assign to queue?",
0x23 : "0X23? assign to queue?",
0x24 : "0X24? assign to queue?",
0x40 : "print()",
0x41 : "print(lowHex())",
0x42 : "readline()",
0x43 : "readlineAsHex()",
0x50 : "exit()",
}
fd = open("flag_checker","rb")
data= fd.read()
rdata= ""
cpt = 0
while cpt < len(data):
value = None
byte = data[cpt]
if byte == 0:
value = data[cpt+1]
rdata += chr(value)
cpt += 1
print(byte , opcode[byte], value)
cpt += 1
print(rdata)
Je ne souhaite pas perdre 3h à analyser les opcodes et ayant fait 4 challenges de VM les 3 dernières semaines, j’ai vite décidé de skip cette partie. Bien, il n’y a pas de strcmp ou autre opcode qui pourrait faire un test du mot de passe. Cependant, l’op code 0x10 est particulierement intéréssant car il va tester une valeur de la mémoire et incrémenter l’index de la position du fichier en fonction C’est très intéréssant car c’estr un moyen de tester un mot de passe, si le pointeur va dans une zone du fichier au lieu d’une autre, il éxecute différents opcodes.
Je vais donc immédiatement sur gdb pour tester et voir si les bytes du mot de passe sont utilisé pour ce test la.
Voici la template utilisé dans python :
opcode = {
0 : "mem[index] = r1",
1 : "testSize",
2 : "2?",
0x10 : "TestFlag => incrementIndex",
0x12: "0x12?",
0x20 : "0X20? assign to queue?",
0x21 : "0X21? assign to queue?",
0x22 : "0X22? assign to queue?",
0x23 : "0X23? assign to queue?",
0x24 : "0X24? assign to queue?",
0x40 : "print()",
0x41 : "print(lowHex())",
0x42 : "readline()",
0x43 : "readlineAsHex()",
0x50 : "exit()",
}
import gdb
cpt=0
bl = None
index = None
gdb.execute("file ./belt")
gdb.execute("d")
data = ""
class VMDecoder(gdb.Breakpoint):
def __init__(self, bp, opcode):
super().__init__(bp)
self.opcode = opcode
def stop(self):
dp = [0, 1, 2, 36, 0x21, 0x42]
global fd
global cpt
global index_
global index
if self.opcode == 1:
index = int(gdb.parse_and_eval("$rax"))
bl = int(gdb.parse_and_eval("(unsigned char)$bl"))
if self.opcode == 0:
index = gdb.parse_and_eval("$rcx")
bl = gdb.parse_and_eval("(unsigned char)$bl")
if int(bl) != 0:
print(f"[0] Set queue[{int(index)}] = {hex(bl)} ({chr(int(bl))})")
else:
print(f"[0] Set queue[{int(index)}] = 0")
if self.opcode == 36:
index = gdb.parse_and_eval("$rax")
bl = gdb.parse_and_eval("(unsigned char)$bl")
if int(bl) != 0:
print(f"[0x24] (~&) Set queue[{int(index)}] = {hex(bl)} ({chr(int(bl))})")
else:
print(f"[0x24] (~&) Set queue[{int(index)}] = 0")
if self.opcode == 0x21:
index = gdb.parse_and_eval("$rax")
bl = gdb.parse_and_eval("(unsigned char)$bl")
if int(bl) != 0:
print(f"[0x21] (-) Set queue[{int(index)}] = {hex(bl)} ({chr(int(bl))})")
else:
print(f"[0x21] (-) Set queue[{int(index)}] = 0")
return False #gdb.continue()
gdb.execute("file belt")
gdb.execute("d")
readline = VMDecoder("*0x000055555555E0F2", 66)
oP2 = VMDecoder("*0x000055555555E2D3", 2)
testflag = VMDecoder("*0x55555555ecd6", 0x10)
exit =VMDecoder("*0x000055555555EEE6", 80)
oP3 = VMDecoder("*0x000055555555E09E", 65)
oP4 = VMDecoder("*0x000055555555E177", 64)
oP5 = VMDecoder("*0x000055555555E1A1", 35)
oP6 = VMDecoder("*0x000055555555EE87", 36)
oP7 = VMDecoder("*0x000055555555E2A9", 32)
oP8 = VMDecoder("*0x000055555555E321", 34)
oP9 = VMDecoder("*0x000055555555E1CB", 67)
oP10 = VMDecoder("*0x000055555555E27F", 18)
oP11 = VMDecoder("*0x000055555555EE87",33)
oP13 = VMDecoder("*0x000055555555EE87", 1)
op0 = VMDecoder("*0x000055555555E93F", 0)
En effet, en suivant chaque opcode sur gdb, on retrouve plusieurs opérations arithmétiques basiques par octets du mot de passe suivi de ce test. Il n’est pas nécéssaire de dumper chaque opération de la VM mais cela donne un apercu On peut donc essayer de break sur cette opcode, et bruteforcer octet par octet.
ce qui donne comme script de résolution :
opcode = {
0 : "mem[index] = r1",
1 : "testSize",
2 : "2?",
0x10 : "TestFlag => incrementIndex",
0x12: "0x12?",
0x20 : "0X20? assign to queue?",
0x21 : "0X21? assign to queue?",
0x22 : "0X22? assign to queue?",
0x23 : "0X23? assign to queue?",
0x24 : "0X24? assign to queue?",
0x40 : "print()",
0x41 : "print(lowHex())",
0x42 : "readline()",
0x43 : "readlineAsHex()",
0x50 : "exit()",
}
import gdb
cpt=0
bl = None
index = None
gdb.execute("file ./belt")
gdb.execute("d")
data = ""
class VMDecoder(gdb.Breakpoint):
def __init__(self, bp, opcode):
super().__init__(bp)
self.opcode = opcode
def stop(self):
dp = [0, 1, 2, 36, 0x21, 0x42]
global fd
global cpt
global index_
global bl
global index
global found
if self.opcode == 0x10:
index = int(gdb.parse_and_eval("$rax"))
bl = int(gdb.parse_and_eval("(unsigned char)$bl"))
cpt += 1
if (cpt == index_ +1 ):
print("should be 0 for win => bl= ", bl)
print(cpt, index_ + 1)
if bl == 0:
found=True
return False
return False
gdb.execute("file belt")
gdb.execute("d")
readline = VMDecoder("*0x000055555555E0F2", 66)
oP2 = VMDecoder("*0x000055555555E2D3", 2)
testflag = VMDecoder("*0x55555555ecd6", 0x10)
exit =VMDecoder("*0x000055555555EEE6", 80)
oP3 = VMDecoder("*0x000055555555E09E", 65)
oP4 = VMDecoder("*0x000055555555E177", 64)
oP5 = VMDecoder("*0x000055555555E1A1", 35)
oP6 = VMDecoder("*0x000055555555EE87", 36)
oP7 = VMDecoder("*0x000055555555E2A9", 32)
oP8 = VMDecoder("*0x000055555555E321", 34)
oP9 = VMDecoder("*0x000055555555E1CB", 67)
oP10 = VMDecoder("*0x000055555555E27F", 18)
oP11 = VMDecoder("*0x000055555555EE87",33)
oP13 = VMDecoder("*0x000055555555EE87", 1)
op0 = VMDecoder("*0x000055555555E93F", 0)
flag_ = ['A']*29
count = 0
flag_[0] = "b"
flag_[1] = "c"
flag_[2] = "t"
flag_[3] = "f"
flag_[4] = "{"
import string
ptr = string.printable[0:len(string.printable)-5] + '0_'
# got some bugs with caracters here , not really good script
for i in range(23,len(flag_)):
index_= i
for char in ptr:
found=False
flag_[len(flag_)-index_ - 1] = char
flag = ''.join(flag_)
print(f'Trying {flag}')
args = ""
for i in range(len(flag)):
args += flag[i] + "\n"
fd = open("input","wb")
fd.write(args.encode())
fd.close()
cmd = f"r flag_checker < input"
gdb.execute(cmd)
if found:
print("found a valid char : " + char)
flag_[len(flag_)-index_ - 1] = char
print(''.join(flag_))
break
cpt = 0
print(''.join(flag_))
On obtient donc le flag de validation (après quelque temps, le script a pas mal buggé et gdb est très lent) :