🇫🇷 MIDNIGHT BACKSLASH Qualif | [Hard]
Table of Contents
Deck Decoder
J’ai pu participer à la création de challenges lors de l’édition 2024 du Midnight Flag CTF.
Voici le writeup “untended” du challenge.
Initialisation
Ce challenge est un binaire ELF x86_64 strippé. Toutes les addresses mentionnées dans ce WU seront à partir de la base: 0x100000
On retrouve l’utilisation de srand
dans le main qui utilise en 1er argument l’argument pin
de notre programme.
On trouve arapidement un array de chaines d’octets qui semble subir plusieurs transformations, notement la fonction 0x10e5ef
, appelons la randomize
.
Cette fonction randomize
sers à initialiser un tableau qui servira juste après au “mélange” des pointeurs de notre chaine d’array.
Défaire le mélange
Le 1er objectif sera de reproduire tous les schémas de mélange possibles (entre 0 et 2000). (Félicitation à tous ceux qui ont identifié le nom de l’algorithme de mélange).
On peut comparer dynamiquement la 1ere sortie de rand() dans notre programme avec 123 en argument et la sortie de ce script, si les deux sorties sont simillaires, on peut continuer.
from ctypes import CDLL
libc = CDLL("libc.so.6")
libc.srand(123)
print(hex(libc.rand()))
Cependant attention le srand
et le rand
dépendent uniquement de la libc, il est nécéssaire de garder la même libc référencée dans le binaire.
Maintenant il faut comprendre et reverse l’algorithme de mélange de l’array qui consiste uniquement a assigner un nouvel array la correspondance de notre tableau d’entiers avec l’indice de l’élement
Chaque element de l’array est composé de la structure:
- 1 octet qui correspond à la taille du buffer
- le buffer de la taille énnoncée
L’array est en plus chiffré en utilisant du RC4. Le RC4 a une propriété de “roulement”, c’est à dire qu’initialiser une clée puis chiffrer “hello” puis “world” donne un résultat différent que chiffrer “world” puis “hello”, l’ordre initial de l’array a donc une grande importance, le résultat sera correct uniquement si l’array est dans le bon ordre.
from ctypes import CDLL
from Crypto.Cipher.ARC4 import ARC4Cipher
from capstone import *
import math
from collections import Counter
import matplotlib.pyplot as plt
array = [b"\x05\x6E\xF2\x67\xFE\x82", b"\x01\x2B", b"\x03\xED\x9C\x1B"]#... big array
libc = CDLL("libc.so.6")
n = 0x8e7
def calculate_entropy(data):
if not data:
return 0
frequency = Counter(data)
data_len = len(data)
probabilities = [freq / data_len for freq in frequency.values()]
entropy = -sum(prob * math.log2(prob) for prob in probabilities if prob > 0)
return entropy
def check_instructions(byte_code):
"""
Autre possibilité: True uniquement si seed == 1203
"""
md = Cs(CS_ARCH_X86, CS_MODE_64)
instructions = md.disasm(byte_code, 0x1000)
return len([x for x in instructions]) == n:
def randomize (arr, n):
for i in range(n-1, 0, -1):
j = libc.rand() % (i+1)
arr[i], arr[j] = arr[j], arr[i]
return arr
entropy_list = []
for k in range(2000):
libc.srand(k)
init = randomize([i for i in range(n)],n)
new_array = [0]*n
for i in range(n):
new_array[init[i]] = array[i]
c = ARC4Cipher(bytes.fromhex("e1a4646ee8acd20f4578dbea4a79380a"))
for i in range(len(new_array)):
new_array[i] = c.decrypt(new_array[i][1:])
# if check_instructions(b"".join(new_array)):
# print(f"[+] Found seed: {k}")
# exit(1)
entropy_list.append(calculate_entropy(b"".join(new_array)))
plt.figure(figsize=(10, 5))
plt.plot(entropy_list, marker='o', linestyle='-', color='b')
plt.title('Result')
plt.xlabel('PIN')
plt.ylabel('Entropie')
plt.grid(True)
plt.show()
On trouve la seed 1203
Cette seed nous donne donc un shellcode correct, on va pouvoir l’émuler avec miasm.
Résolution du crackme
Il faut être asticieux pour avoir a bruteforce que (100) possibilitées à chaque fois et non 2 octets en s’aidant du 1er caractère du flag connu.
shellcode = bytes.fromhex("666748b8560c47c8215c3817674831f8fff348bbda3f33b05daaca314a31fb67fff16648b95b7a470a7c1170bf664a31f967526648ba7a8f6775979f273e66483")#... big shellcode
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.arch.x86.jit import jitter_x86_64
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE,PAGE_EXEC
from string import printable
search_values = [0x751E22F6AFDC1D1A, 0x25E2D8737E1EE778, 0x5FA5062A2A463E93, 0x31E402F96E8BDF97, 0x835BE6989055C8F7, 0x5D96E686BBBB53FA, 0xECE6FF58CDD9D31A, 0xC21A2CBC37DBC25F, 0x504E496158267321, 0x1E7F1D0186F1EC8C, 0x177A0713EE72684C, 0x701183528F5AC6CF, 0xB732A1AFDC2F5679, 0x76C06994CE3A61BC, 0xF5F06AB9884DE54F, 0x701183528F5AC6CF, 0x5A37ADF157A8C201, 0xC094D5287D091C4F, 0xF5F06AB9884DE54F, 0x3DAA050C3B50450E, 0xA93C0AF6A0E9C41C, 0xF5D4CBAB3B2C88A3, 0xA27A01B298EC17A8, 0x5FCEFDDD776A26F0, 0xDEBDE5DBB1C1C4B3, 0xE08C572F24190AB, 0xD512D32829E4869, 0xFFBD122F4A434777]
result = 0
def finish(jitter: jitter_x86_64):
global result
result = jitter.cpu.RAX
jitter.running = False
return False
loc_db = LocationDB()
cont = Container.from_string(shellcode, loc_db)
machine = Machine("x86_64")
mdis = machine.dis_engine(cont.bin_stream, loc_db=loc_db)
myjit = machine.jitter(loc_db)
run_addr = 0
myjit.vm.add_memory_page(run_addr, PAGE_READ | PAGE_WRITE | PAGE_EXEC, shellcode)
myjit.init_stack()
myjit.push_uint64_t(0xdead0000)
myjit.add_breakpoint(0xdead0000, finish)
def test_combinaison(value1, value2):
t1 = 0
myjit.push_uint64_t(0xdead0000)
for i in range(0,8,2):
t1 |= (value1)<<(8*(i))
t1 |= (value2)<<(8*(i+1))
myjit.cpu.RDI = t1
myjit.run(run_addr)
return result
def find_carac(start, index):
for elm in printable:
a = test_combinaison(start,ord(elm))
if(a == search_values[index]):
return elm
start = 'M'
passwd = ""+start
for i in range(len(search_values)):
start = find_carac(ord(start), i)
passwd +=start
print(passwd)
Flag: MCTF{ShUff13_th3_sh3LLc0de!!}
Conclusion
Merci à l’organisation et félicitation aux flaggers du challenge ❤