Pareil pour l'input, on va passer le flag à tester directement lors de la création de la VM et remplacer la ligne de l'input par le flag en question :
20: class VM:
21:
22: def __init__(self, flag: str) -> None:
23: self.flag = flag
# [...] reste du code
185: elif syscall_id == 0x5:
186: # Read stdin to memory
187: self.add_pc(2)
188: # data = input()
189: data = self.flag
190: data_byte_array = bytearray(data.encode())
Maintenant, on est prêt...
Connaître la taille du flag
On va modifier le fichier vm.py. Pour ça, on ajoute dans run() la variable instruction_count au début, et dans sa boucle while, on va incrémenter cette variable. Enfin, on la retourne en fin de fonction.
Elle nous permet de compter le nombre d'instructions jouées, et donc de savoir si on a provoqué une modification en fonction de notre input.
def run(self, entry_point: int) -> None:
instructions_count = 0
self.jmp(entry_point)
while True:
instructions_count += 1
if self.registers[self.pc] == 0xBADC0DE:
# [...] Reste du code
On va lancer la vm avec plein de flags de tailles différentes :
def start():
program = open('program.bin', "rb").read()
for i in range(45):
vm = VM("a" * i)
entry_point = vm.load_code(program)
n = vm.run(entry_point)
print(i, n)
# 40 1032
# 41 1032
# 42 1044 <-- Lezgoooo
# 43 1032
# 44 1032
Avec un flag de 42 caractères, on fait plus d'instructions. Donc, on comprend qu'il y avait une condition sur la taille du flag et qu'on l'a validée avec 42 caractères.
Début du flag
On va réutiliser le même principe en espérant que le binaire vérifie caractère par caractère le flag. Donc, on va tester tous les caractères possibles à la première position et voir s'il y a quelque chose qui change :
def start():
program = open('program.bin', "rb").read()
CHARSET = string.ascii_letters + string.digits + string.punctuation
flag_size = 42
base_flag = ''
for c in CHARSET:
flag = base_flag + c + 'a' * (flag_size - len(base_flag) - 1)
vm = VM(flag)
entry_point = vm.load_code(program)
n = vm.run(entry_point)
print(c, n)
# a 1044
# b 1044
# c 1044
# d 1044
# e 1044
# f 1048
# g 1044
# h 1044
Avec f il y a plus d'instructions jouées, comme si on avait passé une condition de plus. Si on continue en gardant le f au début et en testant le 2nd caractère, on obtient
j 1048
k 1048
l 1056
m 1048
n 1048
Le second caractère est l. Si on continue comme ça, on obtient le début flag{. Malheureusement, plus rien ensuite. En réfléchissant un peu, on se doute que le dernier caractère est }. Et ça tombe bien, ça augmente beaucoup nos instructions :
def start():
program = open('program.bin', "rb").read()
CHARSET = string.ascii_letters + string.digits + string.punctuation
flag = 'flag{' + 'a' * 36 + '}'
vm = VM(flag)
entry_point = vm.load_code(program)
n = vm.run(entry_point)
print(n)
# 1354
Fonction de chiffrement
Ce qui se trouve à l'intérieur du flag est chiffré et vérifié. On peut le voir en ajoutant des lignes dans la fonction enc().
Un petit tour sur ChatGPT avec le bout de code en question nous apprend qu'il s'agit possiblement d'une variante de Tiny Encryption Algorithm et que c'est donc réversible !
Cette fonction est appelée une fois qu'on a trouvé le début et la fin du flag justement. On va donc afficher les variables qui la concerne et qui nous intéressent :
k : la clé de chiffrement
y et z : 4 caractères de notre input en clair au début et chiffrés à la fin
Ensuite, on sait qu'elles sont forcément comparés avec le flag, donc on va également afficher avec quoi elles le sont. Pour ça, direction la fonction cmp_regs() :
Maintenant, si on lance, on va avoir le résultat de notre input chiffré avec ce à quoi il devrait ressembler. Ici j'utilise le flag flag{abcdefghijklmnopqrstuvwxyzABCDEFGHIJ} :
2026883528 c'est le début du flag chiffré
943156865 c'est le résultat du chiffrement qu'il faudrait
On va donc mettre à jour manuellement y pour éviter que ça quitte et pouvoir voir la prochaine comparaison. De cette façon, on sera capable de récupérer tous les y et z attendus !
J'ajoute la variable encryption_counter dans VM :
class VM:
def __init__(self, flag: str) -> None:
self.flag = flag
self.encryption_counter = 0
Et ensuite, on va compléter notre tableau avec les valeurs attendues :
On a tout ce qu'il nous faut. De mon côté, j'ai demandé à ChatGPT de me faire la fonction de déchiffrement, mais elle est très largement faisable à la main étant donné que l'algo de chiffrement est simple.
from ctypes import c_uint32
def decrypt(y: int, z: int, k: tuple[int, int, int, int]):
y = c_uint32(y)
z = c_uint32(z)
n = 32
delta = 0x9e3779b9
sum = c_uint32(delta * n)
while n > 0:
z.value -= ((y.value << 4) + k[2]) ^ (y.value + sum.value) ^ ((y.value >> 5) + k[3])
y.value -= ((z.value << 4) + k[0]) ^ (z.value + sum.value) ^ ((z.value >> 5) + k[1])
sum.value -= delta
n -= 1
return y.value.to_bytes(4, 'little'), z.value.to_bytes(4, 'little')
k = [3385206627, 632509235, 1634953960, 3929412302]
compared_values = [
(943156865, 140430192),
(2530532487, 129893444),
(261106022, 2629986848),
(3679519246, 3532129935),
(794422906, 4144136265)
]
flag = b''
for values in compared_values:
d = decrypt(values[0], values[1], k)
flag += d[0] + d[1]
print(flag.decode())
# Th1s_Py7h0n_VM_h4s_n0_s3cr3t5_f0r_m3
Finalement, on peut relancer la vraie VM avec le programme en testant notre flag : flag{Th1s_Py7h0n_VM_h4s_n0_s3cr3t5_f0r_m3}
$ python3 vm.py program.bin
Ahoy ! Welcome aboard, matey! I've lost the password to the digital whisky reserve. Help me find it, and I'll be grateful!
Enter the password: flag{Th1s_Py7h0n_VM_h4s_n0_s3cr3t5_f0r_m3}
Good job !