Vous êtes victime d’un incident de sécurité ? Contactez notre CERT

12/07/2024

Blog technique

Breizh CTF 2024 – write ups | Part 2

Equipe SEAL

Amossys et Almond ont participé à la 8ème édition du Breizh CTF qui s’est déroulée du 17 au 18 mai 2024 à Rennes. A travers ce deuxième article, plusieurs write ups détaillant notre méthode de résolution de challenges et la façon dont nous les avons abordés sont présentés.

1. Crypto_first_chall

Description du challenge

Cette première épreuve de cryptographie est constituée d’un simple fichier python :

				
					#!/usr/bin/env python3

flag = "REDACTED"

res = []

ALPHABET="ABCDEFGHIJKLMNOPQRSTUVWXYZ{}0123456789abcdefghijklmnopqrstuvwxyz"


for i in range(len(flag)):
    if i ==0:
        res.append(ord(flag[i]) + i)
    else:

        newValue = (ord(ALPHABET[ALPHABET.index(flag[i])]) + i) *i 
        res.append(newValue)

print(res) 
# [66, 91, 148, 210, 352, 375, 774, 406, 784, 711, 590, 1276, 1512, 1664, 1820, 1230, 1040, 2193, 2196, 1330, 2680, 2268, 1562, 3197, 1920, 2700, 3198, 3807, 3556, 3770, 4650]
				
			

L’objectif est de retrouver le flag ayant donné la sortie placée en commentaire.

Solution

Les opérations appliquées à la variable flag sont toutes inversibles.
Il suffit donc d’appliquer les opérations inverses pour obtenir la solution.

Plus précisément, le premier caractère est juste transformé en son code ASCII (puisque i est nul). Il suffit donc d’appliquer chr au premier entier.
Pour les caractères suivants, comme

				
					newValue = (ord(ALPHABET[ALPHABET.index(flag[i])]) + i) *i 
				
			
				
					newValue / i == ord(ALPHABET[ALPHABET.index(flag[i])] + i) 
				
			

puis

				
					chr(newValue/i) == ALPHABET[ALPHABET.index(flag[i])] + i
				
			

Or ALPHABET.index(flag[i]) est l’indice de flag[i] dans la suite de caractères ALPHABET, donc ALPHABET[ALPHABET.index(flag[i])] == flag[i] et finalement

				
					flag[i] == chr(newValue/i) - i
				
			

Le flag peut donc être obtenu ainsi :

				
					ciphered = [66, 91, 148, 210, 352, 375, 774, 406, 784, 711, 590, 1276,
            1512, 1664, 1820, 1230, 1040, 2193, 2196, 1330, 2680, 2268,
            1562, 3197, 1920, 2700, 3198, 3807, 3556, 3770, 4650]

flag_chars = [chr(newValue) if i == 0 else chr(int(newValue/i - i))
              for i, newValue in enumerate(ciphered)]
print("".join(flag_chars))
# BZHCTF{3ZF1irstC1ph3rW1t8Sarce}
				
			

2. Crypto_mines

Description du challenge

Pour le deuxième challenge de cryptographie, un script python reproduit ci-dessous était fourni. Ce script était exécuté sur un serveur TCP auquel il fallait se connecter pour obtenir le flag.

Le code contient l’implémentation du générateur d’aléa Wichmann-Hill qui ajoute les sorties de trois générateurs congruents linéaires. Ce générateur est utilisé pour générer les coordonnées de mines. L’objectif est de deviner ces coordonnées.

Une même graine est utilisée à chaque exécution du processus, et un nombre aléatoire de sorties du générateur est ignoré.

L’utilisateur doit envoyer des coordonnées de mines au format JSON. S’il devine correctement leurs positions, il remporte le tour. S’il remporte au moins trois tours sur les cinq, il récupère le flag.

Exemple d’échange avec le serveur :

				
					Hello, can you find all mines ?
Round number : 1
{"x":1,"y":2}
{"x":4,"y":5}
{"x":1,"y":3}

The map was :
Mine 1 : 21 12
Mine 2 : 39 939
Mine 3 : 40 331
				
			

Script exécuté sur le serveur : (ce script peut être testé localement, par exemple en exécutant nc -l -p <port> -e <script>)

				
					#!/usr/bin/env python3

import random
import json

class WichmannHill:

    def __init__(self,s1,s2,s3):
        self.s1 = s1
        self.s2 = s2
        self.s3 = s3

    def generate_number(self):
        self.s1 = (self.s1 * 171 ) % 30269
        self.s2 = (self.s2 * 172 ) % 30307
        self.s3 = (self.s3 * 170 ) % 30323

        return  self.s1 + self.s2 + self.s3


FLAG = "REDACTED"


def create_map(prng):
    carte = []

    rng1 = prng.generate_number()
    rng2 = prng.generate_number()
    rng3 = prng.generate_number()

    mine1 = (rng1 // 1000,rng1 % 1000)
    mine2 = (rng2 // 1000,rng2 % 1000)
    mine3 = (rng3 // 1000,rng3 % 1000)

    carte.append(mine1)
    carte.append(mine2)
    carte.append(mine3)

    return carte


def play_round(prng):
    carte = create_map(prng)
    result = False
    nb_mine_found = 0

    copy_carte = []
    for mine in carte:
        copy_carte.append((mine[0], mine[1]))

    for _ in range(3):
        user_input = json.loads(input())

        for mine in list(copy_carte):
            if user_input["x"] == mine[0] and user_input["y"] == mine[1]:
                print("You found a mine&nbsp;!")
                nb_mine_found += 1
                copy_carte.remove(mine)


    print("The map was&nbsp;: ")
    print(f"Mine 1&nbsp;: {carte[0][0]} {carte[0][1]}")
    print(f"Mine 2&nbsp;: {carte[1][0]} {carte[1][1]}")
    print(f"Mine 3&nbsp;: {carte[2][0]} {carte[2][1]}")

    if nb_mine_found == 3:
        result = True

    return result

def main():
    s1=4598
    s2=19635
    s3=5236

    prng = WichmannHill(s1,s2,s3)

    for _ in range(random.randrange(20_000, 30_000)):
        prng.generate_number()

    print("Hello, can you find all mines&nbsp;? ")

    nb_victory = 0

    for i in range(5):
        print(f"Round number&nbsp;: {i+1}")

        try:
            result = play_round(prng)
        except Exception:
            print("Input error&nbsp;!")
            i -= 1
            result = False

        if result:
            nb_victory += 1

        if nb_victory == 3:
            print(f"Good job, here is your flag&nbsp;: {FLAG}")


if __name__ == "__main__":
    main()
				
			

Solution

La graine étant connue, il suffit de savoir combien de nombres aléatoires ont été ignorés avant la génération des mines. Pour cela, l’utilisateur peut
commencer par envoyer une tentative quelconque, dans le seul but d’obtenir les positions des premières mines.

				
					def send_bad_try(s: socket):
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")

def main():
    s = socket()
    s.connect(("challenge.ctf.bzh", 30012))

    send_bad_try(s)
				
			

Celles-ci permettent de reconstituer l’état du générateur pseudo-aléatoire et donc de prédire les positions suivantes des mines.

Comme les coordonnées x et y des mines sont données par la formule suivante :

				
					mine1 = (rng1 // 1000,rng1 % 1000)
				
			

rng1 est un entier généré par le générateur Wichmann-Hill, cet entier peut être récupéré ainsi :

				
					def get_first_mine(response: bytes) -> Optional[int]:
    lines = response.split(b"\n")
    for line in lines:
        print(line.decode())
    for line in lines:
        if line.startswith(b"Mine"):
            words = line.split()
            return int(words[-1])+1000*int(words[-2])
				
			

Une fois la valeur de la première mine obtenue, il est possible de générer des nombres aléatoires à partir de la graine et de trouver la valeur provenant de la mine au-delà de la 20 000ième itération, puis les valeurs des mines suivantes (9 mines sont nécessaires pour obtenir le flag) :

				
					def compute_following_mines(mine_value: int) -> List[int]:
    s1=4598
    s2=19635
    s3=5236
    prng = WichmannHill(s1,s2,s3)

    for i in range(30_000):
        guess = prng.generate_number()
        if i < 20_000:
            continue
        if guess&nbsp;!= mine_value:
            continue
        print(f"[*] {i} pseudo-random numbers were skipped")
        print(f"[+] Found first mine {guess}")
        print(f"[*] Throwing away second mine {prng.generate_number()}")
        print(f"[*] Throwing third mine {prng.generate_number()}")
        return [prng.generate_number() for _ in range(9)]
    raise Exception("Should have found a match between 20_000 and 30_000")
				
			

Les valeurs peuvent alors être envoyées au format JSON pour obtenir le flag :

				
					following_mines = compute_following_mines(first_mine)
for mine in following_mines:
    s.send(json.dumps({"x": mine//1000, "y": mine%1000}).encode()+b"\n")
    print(s.recv(8000).decode())
s.send(b"\n")
print(s.recv(8000).decode())
				
			

La sortie du script complet, reproduit ci-dessous, est la suivante :

				
					Hello, can you find all mines&nbsp;? 
Round number&nbsp;: 1

The map was&nbsp;:
Mine 1&nbsp;: 39 204
Mine 2&nbsp;: 60 287
Mine 3&nbsp;: 43 47
Round number&nbsp;: 2

[*] First mine was 39204
29230 pseudo-random numbers were skipped
[+] Found first mine 39204
[*] Throwing second mine 60287
[*] Throwing third mine 43047
You found a mine&nbsp;!

You found a mine&nbsp;!

You found a mine&nbsp;!
The map was&nbsp;:
Mine 1&nbsp;: 37 90
Mine 2&nbsp;: 26 333
Mine 3&nbsp;: 49 487
Round number&nbsp;: 3

You found a mine&nbsp;!

You found a mine&nbsp;!

You found a mine&nbsp;!
The map was&nbsp;:
Mine 1&nbsp;: 73 346
Mine 2&nbsp;: 36 774
Mine 3&nbsp;: 60 2
Round number&nbsp;: 4

You found a mine&nbsp;!

You found a mine&nbsp;!

You found a mine&nbsp;!
The map was&nbsp;:
Mine 1&nbsp;: 30 753
Mine 2&nbsp;: 62 14
Mine 3&nbsp;: 75 559
Good job, here is your flag&nbsp;: REDACTED
				
			

Le script final est le suivant :

				
					from socket import socket
from typing import List, Optional
import json

class WichmannHill:
    def __init__(self,s1,s2,s3):
        self.s1 = s1
        self.s2 = s2
        self.s3 = s3

    def generate_number(self):
        self.s1 = (self.s1 * 171 ) % 30269
        self.s2 = (self.s2 * 172 ) % 30307
        self.s3 = (self.s3 * 170 ) % 30323

        return  self.s1 + self.s2 + self.s3

def compute_following_mines(mine_value: int) -> List[int]:
    s1=4598
    s2=19635
    s3=5236
    prng = WichmannHill(s1,s2,s3)

    for i in range(30_000):
        guess = prng.generate_number()
        if i < 20_000:
            continue
        if guess&nbsp;!= mine_value:
            continue
        print(f"[*] {i} pseudo-random numbers were skipped")
        print(f"[+] Found first mine {guess}")
        print(f"[*] Throwing second mine {prng.generate_number()}")
        print(f"[*] Throwing third mine {prng.generate_number()}")
        return [prng.generate_number() for _ in range(9)]
    raise Exception("Should have found a match between 20_000 and 30_000")


def get_first_mine(response: bytes) -> Optional[int]:
    lines = response.split(b"\n")
    for line in lines:
        print(line.decode())
    for line in lines:
        if line.startswith(b"Mine"):
            words = line.split()
            return int(words[-1])+1000*int(words[-2])

def send_bad_try(s: socket) -> None:
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")
    s.send(json.dumps({"x": 1, "y": 1}).encode()+b"\n")

def main():
    s = socket()
    # s.connect(("challenge.ctf.bzh", 30012))
    s.connect(("localhost", 1234))

    send_bad_try(s)
    first_mine = None
    while first_mine is None:
        first_mine = get_first_mine(s.recv(8000))

    print(f"[*] First mine was {first_mine}")
    following_mines = compute_following_mines(first_mine)
    for mine in following_mines:
        s.send(json.dumps({"x": mine//1000, "y": mine%1000}).encode()+b"\n")
        print(s.recv(8000).decode())
    s.send(b"\n")
    print(s.recv(8000).decode())

if __name__ == "__main__":
    main()
				
			

3. Misc_unicorn

Description du challenge

Un serveur TCP demande la valeur de trois registres d’une architecture (ARM32, AArch64, x86 ou x64) après l’exécution d’un shellcode qu’il fournit :

Pour valider l’épreuve, l’utilisateur doit renvoyer (rapidement !) 15 bonnes réponses successives au format demandé.

Solution

La licorne de l’image ci-dessus suggère fortement l’utilisation de l’émulateur unicorn. Le code fourni ci-dessous est celui utilisé lors du CTF. Il contient des éléments dupliqués et mériterait des refactorisations. Cependant, il n’est plus possible de vérifier si le code, une fois factorisé, fonctionnerait toujours. Il a donc été laissé tel qu’il fut écrit le soir même.

Extraction de l'information

La première étape à réaliser est la récupération des différents éléments de la question :

Les questions se situent sur les seules lignes commençant par un caractère '[', il est donc possible de les obtenir de la façon suivante :

				
					def get_question(buffer: bytes)->Optional[bytes]:
    lines = buffer.split(b'\n')
    for l in lines:
        if l.startswith(b'['):
            return l
				
			

Le shellcode est le dernier mot de la question, l’architecture le quatrième en partant de la fin, et les trois registres sont séparés par une virgule aux positions 7, 8 et 9. Ces éléments peuvent donc être récupérés ainsi :

				
					def parse(question: bytes) -> Tuple[bytes, bytes, list]:
    words = question.split()
    return words[-1], words[-4], [w.replace(b",", b"") for w in words[7:10]]
				
			

Le shellcode est constitué de caractères ASCII représentant un objet python de type bytes. Par exemple, l’octet nul est représenté par les quatre caractères \x00, qui seraient notés b'\\x00' en python. Il convient donc de convertir chacun de ces groupes de quatre caractères en un seul octet donné par le décodage hexadécimal des deux derniers caractères. Par exemple, transformer les
quatre octets \x00 (b'\\x00' en python) en l’octet nul (b'\x00').

C’est ce que réalise la fonction suivante :

				
					def to_bytes(bb: bytes)->bytes:
    res = []
    if len(bb) % 4&nbsp;!= 0:
        raise (Exception("invalid"))
    for _ in range(len(bb)//4):
        res.append(bytes.fromhex(bb[2:4].decode()))
        bb = bb[4:]
    return b"".join(res)
				
			

Conversion des registres

Les noms des registres transférés par le serveur doivent être traduits en constantes unicorn :

				
					def translate_regs_arm64(reg):
    if reg == b'x11':
        return UC_ARM64_REG_X11
    if reg == b'x9':
        return UC_ARM64_REG_X9
    if reg == b'x10':
        return UC_ARM64_REG_X10
    if reg == b'x13':
        return UC_ARM64_REG_X13
    if reg == b'x12':
        return UC_ARM64_REG_X12
    if reg == b'x14':
        return UC_ARM64_REG_X14
    if reg == b'x15':
        return UC_ARM64_REG_X15
    raise (Exception(reg))


def translate_regs_x64(reg):
    if reg == b'rbx':
        return UC_X86_REG_RBX
    if reg == b'rdx':
        return UC_X86_REG_RDX
    if reg == b'rcx':
        return UC_X86_REG_RCX
    if reg == b'rax':
        return UC_X86_REG_RAX
    raise (Exception(reg))


def translate_regs_arm32(reg):
    if reg == b'r2':
        return UC_ARM_REG_R2
    if reg == b'r5':
        return UC_ARM_REG_R5
    if reg == b'r6':
        return UC_ARM_REG_R6
    if reg == b'r1':
        return UC_ARM_REG_R1
    if reg == b'r3':
        return UC_ARM_REG_R3
    if reg == b'r4':
        return UC_ARM_REG_R4
    if reg == b'r0':
        return UC_ARM_REG_R0
    raise (Exception(reg))


def translate_regs_x86(reg):
    if reg == b'ebx':
        return UC_X86_REG_EBX
    if reg == b'eax':
        return UC_X86_REG_EAX
    if reg == b'ecx':
        return UC_X86_REG_ECX
    if reg == b'edx':
        return UC_X86_REG_EDX
    raise (Exception(reg))
				
			

Exécution du code

Différents exemples trouvés sur internet (par exemple dans la documentation) ont été adaptés pour l’exécution des shellcodes :

				
					def arm64_execute(code: bytes, regs: list):
    print("Emulate ARM64 Big-Endian code")
    ENTRY = 0x10000
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

        # map 2MB memory for this emulation
        MEM_SIZE = 2 * 1024 * 1024
        mu.mem_map(ENTRY, MEM_SIZE)
        mu.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
        # write machine code to be emulated to memory
        mu.mem_write(ENTRY, code)

        # emulate machine code in infinite time
        mu.reg_write(UC_ARM64_REG_X0, ENTRY)
        mu.emu_start(ENTRY, ENTRY + len(code))

        return [mu.reg_read(r) for r in regs]

    except UcError as e:
        print("ERROR: %s" % e)


def x64_execute(code: bytes, regs: list):
    print("x64_execute")
    mu = unicorn.Uc(UC_ARCH_X86, UC_MODE_64)
    ENTRY = 0x00000800000000000
    try:
        # map 2MB memory for this emulation
        MEM_SIZE = 2 * 1024 * 1024
        mu.mem_map(ENTRY, MEM_SIZE)
        mu.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
        # write machine code to be emulated to memory
        mu.mem_write(ENTRY, code)

        mu.reg_write(UC_X86_REG_RBP, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RSP, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RAX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RCX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RBX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RCX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RIP, ENTRY)

        # emulate machine code in infinite time
        mu.emu_start(ENTRY, ENTRY + len(code))

        return [mu.reg_read(r) for r in regs]

    except UcError as e:
        print("ERROR: %s" % e)
    pass


def arm32_execute(code: bytes, regs: list):
    uc = Uc(UC_ARCH_ARM, UC_MODE_LITTLE_ENDIAN)

    # Map 2MB memory for this emulation
    ENTRY = 0x10000
    MEM_SIZE = 2 * 1024 * 1024
    uc.mem_map(ENTRY, MEM_SIZE)

    # Set memory permissions
    uc.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
    uc.mem_write(ENTRY, code)
    uc.emu_start(ENTRY, ENTRY + len(code))

    return [uc.reg_read(r) for r in regs]
    pass


def x86_execute(code: bytes, regs: list):
    ADDRESS = 0x1000000

    print("Emulate i386 code")
    try:
        # Initialize emulator in X86-32bit mode
        mu = Uc(UC_ARCH_X86, UC_MODE_32)

        # map 2MB memory for this emulation
        mu.mem_map(ADDRESS, 2 * 1024 * 1024)

        # write machine code to be emulated to memory
        mu.mem_write(ADDRESS, code)

        # initialize machine registers
        mu.reg_write(UC_X86_REG_ECX, 0x1234)
        mu.reg_write(UC_X86_REG_EDX, 0x7890)
        mu.reg_write(UC_X86_REG_EBP, ADDRESS + 1024*1024)

        # emulate code in infinite time & unlimited instructions
        mu.emu_start(ADDRESS, ADDRESS + len(code))

        resp = [mu.reg_read(r) for r in regs]
        return resp

    except UcError as e:
        print("ERROR: %s" % e)
				
			

Orchestration

Les fonctions appropriées parmi les fonctions précédentes peuvent alors être appelées :

				
					def dispatch(code: bytes, arch: bytes, regs: list):
    if arch == b'AArch64':
        regs = [translate_regs_arm64(r) for r in regs]
        register_values = arm64_execute(code, regs)
        return register_values
    if arch == b'x64':
        regs = [translate_regs_x64(r) for r in regs]
        register_values = x64_execute(code, regs)
        return register_values
    if arch == b'x86':
        regs = [translate_regs_x86(r) for r in regs]
        register_values = x86_execute(code, regs)
        return register_values
    if arch == b'ARM32':
        regs = [translate_regs_arm32(r) for r in regs]
        register_values = arm32_execute(code, regs)
        return register_values
    raise (Exception(arch.decode()))
				
			

La fonction suivante combine les fonctions présentées jusqu’ici pour transformée les données reçues dans le socket en la liste des valeurs de registre à renvoyer.

				
					def handle_received(received):
    question = get_question(received)
    if question is not None:
        code, arch, regs = parse(question)
        code = to_bytes(code)
        register_values = dispatch(code, arch, regs)
        return register_values
				
			

Il ne reste alors plus qu’à renvoyer la réponse au format attendu :

				
					def reply(s: socket, reg_values: list[bytes]):
    print(reg_values[0])
    reply = f"0x{reg_values[0]:02x},0x{
        reg_values[1]:02x},0x{reg_values[2]:02x}"
    print(reply)
    s.send(reply.encode()+b"\n")
    pass
				
			

Script complet

				
					from unicorn import *
from unicorn.x86_const import *
from unicorn.arm64_const import *
from unicorn.arm_const import *
from socket import socket
from typing import Optional, Tuple

ADDRESS = ("challenge.ctf.bzh", 32169)


def init():
    s = socket()
    s.connect(ADDRESS)
    return s


def get_question(buffer: bytes)->Optional[bytes]:
    lines = buffer.split(b'\n')
    for l in lines:
        if l.startswith(b'['):
            return l


def parse(question: bytes) -> Tuple[bytes, bytes, list]:
    words = question.split()
    return words[-1], words[-4], [w.replace(b",", b"") for w in words[7:10]]


def arm64_execute(code: bytes, regs: list):
    print("Emulate ARM64 Big-Endian code")
    ENTRY = 0x10000
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

        # map 2MB memory for this emulation
        MEM_SIZE = 2 * 1024 * 1024
        mu.mem_map(ENTRY, MEM_SIZE)
        mu.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
        # write machine code to be emulated to memory
        mu.mem_write(ENTRY, code)

        # emulate machine code in infinite time
        mu.reg_write(UC_ARM64_REG_X0, ENTRY)
        mu.emu_start(ENTRY, ENTRY + len(code))

        return [mu.reg_read(r) for r in regs]

    except UcError as e:
        print("ERROR: %s" % e)


def x64_execute(code: bytes, regs: list):
    print("x64_execute")
    mu = unicorn.Uc(UC_ARCH_X86, UC_MODE_64)
    ENTRY = 0x00000800000000000
    try:

        # map 2MB memory for this emulation
        MEM_SIZE = 2 * 1024 * 1024
        mu.mem_map(ENTRY, MEM_SIZE)
        mu.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
        # write machine code to be emulated to memory
        mu.mem_write(ENTRY, code)

        mu.reg_write(UC_X86_REG_RBP, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RSP, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RAX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RCX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RBX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RCX, ENTRY + 1024*1024)
        mu.reg_write(UC_X86_REG_RIP, ENTRY)

        # emulate machine code in infinite time
        mu.emu_start(ENTRY, ENTRY + len(code))

        return [mu.reg_read(r) for r in regs]

    except UcError as e:
        print("ERROR: %s" % e)
    pass


def arm32_execute(code: bytes, regs: list):
    uc = Uc(UC_ARCH_ARM, UC_MODE_LITTLE_ENDIAN)

    # Map 2MB memory for this emulation
    ENTRY = 0x10000
    MEM_SIZE = 2 * 1024 * 1024
    uc.mem_map(ENTRY, MEM_SIZE)

    # Set memory permissions
    uc.mem_protect(ENTRY, MEM_SIZE, UC_PROT_ALL)
    uc.mem_write(ENTRY, code)
    uc.emu_start(ENTRY, ENTRY + len(code))

    return [uc.reg_read(r) for r in regs]
    pass


def x86_execute(code: bytes, regs: list):
    ADDRESS = 0x1000000

    print("Emulate i386 code")
    try:
        # Initialize emulator in X86-32bit mode
        mu = Uc(UC_ARCH_X86, UC_MODE_32)

        # map 2MB memory for this emulation
        mu.mem_map(ADDRESS, 2 * 1024 * 1024)

        # write machine code to be emulated to memory
        mu.mem_write(ADDRESS, code)

        # initialize machine registers
        mu.reg_write(UC_X86_REG_ECX, 0x1234)
        mu.reg_write(UC_X86_REG_EDX, 0x7890)
        mu.reg_write(UC_X86_REG_EBP, ADDRESS + 1024*1024)

        # emulate code in infinite time & unlimited instructions
        mu.emu_start(ADDRESS, ADDRESS + len(code))

        resp = [mu.reg_read(r) for r in regs]
        return resp

    except UcError as e:
        print("ERROR: %s" % e)


def test_arm64():
    code = b"\x09\xaf\x9d\xd2\xca\x61\x95\xd2\xab\xf6\x98\xd2\x4c\xee\x9c\xd2\x0d\xd0\x8e\xd2\x6e\x57\x84\xd2\x6f\x39\x8b\xd2\xca\x01\x0a\xaa\x4b\x01\x0a\x8b\xab\x01\x0d\xca\xcc\x01\x0f\xaa\xeb\x03\x0c\xaa\xec\x01\x0a\x8b\xa9\x01\x0a\x8a\xec\x03\x0e\xaa\x2d\x01\x0c\xaa\xe9\x03\x09\xaa\xce\x01\x09\x8b\xed\x03\x0e\xaa\xec\x01\x0d\x8b\xef\x03\x0a\xaa\x6f\x01\x0d\x8a"
    regs = [UC_ARM64_REG_X13, UC_ARM64_REG_X10, UC_ARM64_REG_X12]
    print(arm64_execute(code, regs))
    pass


def test_x86():
    code = b"\xb8\xb4\x61\x00\x00\xbb\x17\x8c\x00\x00\xb9\x00\x68\x00\x00\xba\xb8\xa7\x00\x00\x21\xd0\x09\xc3\x21\xc0\x01\xd9\x09\xd2\x09\xc9\x21\xc9\x21\xdb\x01\xc2\x09\xcb\x31\xc9\x89\xc3"
    regs = [UC_X86_REG_EDX, UC_X86_REG_ECX, UC_X86_REG_EBX]
    print(x86_execute(code, regs))


def test_x64():
    code = b"\x48\xc7\xc0\x38\x7d\x00\x00\x48\xc7\xc3\x7f\x0d\x00\x00\x48\xc7\xc1\x63\x37\x00\x00\x48\xc7\xc2\xce\x89\x00\x00\x48\xff\xc1\x48\x01\xda\x48\x89\xd2\x48\x89\xcb\x48\x09\xc9\x48\x21\xd2\x48\x31\xdb\x48\x01\xc1\x48\x89\xd8\x48\x89\xd0\x48\x31\xc9\x48\x01\xc1\x48\x89\xc2\x48\xff\xc2"
    regs = [UC_X86_REG_RDX, UC_X86_REG_RAX, UC_X86_REG_RCX]
    print(x64_execute(code, regs))


def test_x64_bis():
    code = b"\x69\x67\x9d\xd2\xca\x0e\x93\xd2\x2b\x65\x9c\xd2\x8c\xaa\x99\xd2\x6d\x2e\x8c\xd2\xae\xc0\x81\xd2\x0f\xa6\x88\xd2\xe9\x01\x09\xca\xce\x01\x0c\xca\xef\x01\x0d\x8a\x6e\x01\x0a\x8a\xe9\x01\x0e\x8b\xec\x03\x0e\xaa\xce\x01\x0c\xca\x6d\x01\x0e\x8b\xac\x01\x0f\xaa\xe9\x03\x0c\xaa\xec\x03\x09\xaa\xca\x01\x0b\x8b\x4e\x01\x0b\xca\xef\x01\x0f\xca"
    regs = [UC_X86_REG_RDX, UC_X86_REG_RAX, UC_X86_REG_RCX]
    print(x64_execute(code, regs))


def test_arm32():
    code = b"\xd3\x0b\x02\xe3\xd2\x1e\x00\xe3\x1e\x25\x01\xe3\xbf\x38\x01\xe3\x8a\x41\x08\xe3\x7d\x5a\x00\xe3\x0d\x69\x09\xe3\x05\x10\x81\xe1\x03\x60\x06\xe0\x04\x30\x83\xe1\x02\x10\x81\xe1\x01\x30\x23\xe0\x05\x40\x84\xe1\x02\x30\x03\xe0\x00\x50\x85\xe1\x01\x00\x80\xe0\x02\x40\x84\xe1\x04\x30\x03\xe0\x06\x30\x23\xe0"
    regs = [UC_ARM_REG_R2, UC_ARM_REG_R5, UC_ARM_REG_R1]
    print(arm32_execute(code, regs))


def test_arm32_bis():
    code = b"\xc3\x05\x08\xe3\xe0\x18\x0a\xe3\xa5\x24\x06\xe3\x09\x3c\x02\xe3\x23\x41\x04\xe3\x93\x53\x07\xe3\x0a\x67\x03\xe3\x01\x20\x22\xe0\x04\x20\x22\xe0\x01\x10\xa0\xe1\x00\x30\x83\xe1\x05\x10\x01\xe0\x05\x10\x01\xe0\x05\x20\x22\xe0\x01\x40\x84\xe0\x05\x50\x05\xe0\x00\x10\x01\xe0\x05\x20\xa0\xe1\x03\x00\x80\xe0\x06\x00\xa0\xe1\x06\x30\x03\xe0"
    regs = [UC_ARM_REG_R2, UC_ARM_REG_R5, UC_ARM_REG_R1]
    print(arm32_execute(code, regs))


def translate_regs_arm64(reg):
    if reg == b'x11':
        return UC_ARM64_REG_X11
    if reg == b'x9':
        return UC_ARM64_REG_X9
    if reg == b'x10':
        return UC_ARM64_REG_X10
    if reg == b'x13':
        return UC_ARM64_REG_X13
    if reg == b'x12':
        return UC_ARM64_REG_X12
    if reg == b'x14':
        return UC_ARM64_REG_X14
    if reg == b'x15':
        return UC_ARM64_REG_X15
    raise (Exception(reg))


def translate_regs_x64(reg):
    if reg == b'rbx':
        return UC_X86_REG_RBX
    if reg == b'rdx':
        return UC_X86_REG_RDX
    if reg == b'rcx':
        return UC_X86_REG_RCX
    if reg == b'rax':
        return UC_X86_REG_RAX
    raise (Exception(reg))


def translate_regs_arm32(reg):
    if reg == b'r2':
        return UC_ARM_REG_R2
    if reg == b'r5':
        return UC_ARM_REG_R5
    if reg == b'r6':
        return UC_ARM_REG_R6
    if reg == b'r1':
        return UC_ARM_REG_R1
    if reg == b'r3':
        return UC_ARM_REG_R3
    if reg == b'r4':
        return UC_ARM_REG_R4
    if reg == b'r0':
        return UC_ARM_REG_R0
    raise (Exception(reg))


def translate_regs_x86(reg):
    if reg == b'ebx':
        return UC_X86_REG_EBX
    if reg == b'eax':
        return UC_X86_REG_EAX
    if reg == b'ecx':
        return UC_X86_REG_ECX
    if reg == b'edx':
        return UC_X86_REG_EDX
    raise (Exception(reg))


def dispatch(code: bytes, arch: bytes, regs: list):
    if arch == b'AArch64':
        regs = [translate_regs_arm64(r) for r in regs]
        register_values = arm64_execute(code, regs)
        return register_values
    if arch == b'x64':
        regs = [translate_regs_x64(r) for r in regs]
        register_values = x64_execute(code, regs)
        return register_values
    if arch == b'x86':
        regs = [translate_regs_x86(r) for r in regs]
        register_values = x86_execute(code, regs)
        return register_values
    if arch == b'ARM32':
        regs = [translate_regs_arm32(r) for r in regs]
        register_values = arm32_execute(code, regs)
        return register_values
    raise (Exception(arch.decode()))


def handle_received(received):
    question = get_question(received)
    if question is not None:
        code, arch, regs = parse(question)
        code = to_bytes(code)
        register_values = dispatch(code, arch, regs)
        return register_values


def to_bytes(bb: bytes)->bytes:
    res = []
    if len(bb) % 4&nbsp;!= 0:
        raise (Exception("invalid"))
    for _ in range(len(bb)//4):
        res.append(bytes.fromhex(bb[2:4].decode()))
        bb = bb[4:]
    return b"".join(res)


def reply(s: socket, reg_values: list[bytes]):
    print(reg_values[0])
    reply = f"0x{reg_values[0]:02x},0x{
        reg_values[1]:02x},0x{reg_values[2]:02x}"
    print(reply)
    s.send(reply.encode()+b"\n")
    pass


def main():
    s = init()
    received = s.recv(20_000)
    response = handle_received(received)
    while True:
        received = s.recv(20_000)
        if received == b'':
            return
        print(received)
        response = handle_received(received)
        print(response)
        if response is None:
            continue
        reply(s, response)


# test_x64_bis()
# test_arm64()
# test_arm32()
# test_arm32_bis()
# test_x86()
# test_x64()

if __name__ == '__main__':
    main()
				
			

Voir les derniers articles de notre Blog technique

9 janvier 2025
Contrairement à une évaluation de sécurité réalisée dans un objectif de certification (CSPN ou Critères Communs), la recherche de vulnérabilités […]
20 décembre 2024
La sécurité informatique peut paraître, pour beaucoup, comme un centre de coût et de complexité : plan d’audits à mettre en […]
16 décembre 2024
Après avoir exploré les vulnérabilités inhérentes aux modèles de langage à grande échelle (LLM) dans notre série d'articles, il est […]
28 novembre 2024
L'exfiltration de modèles LLM (Model Theft in english) par des acteurs malveillants ou des groupes de cyberespionnage avancés est une […]
26 novembre 2024
La surconfiance (Overreliance en anglais) peut survenir lorsqu'un LLM produit des informations erronées et les présente de manière autoritaire [...]
25 novembre 2024
Avec une souche éprouvée, des outils bien choisis et des cibles stratégiques, 8Base se distingue comme une menace particulièrement redoutable. […]
13 novembre 2024
Un système basé sur les LLM (Large Language Models) est souvent doté d'un certain degré d'autonomie par son développeur, [...]
12 novembre 2024
Les plugins pour LLM sont des extensions qui, lorsqu'ils sont activés, sont automatiquement appelés par le modèle pendant les interactions […]
7 novembre 2024
Les LLM ont le potentiel de révéler des informations sensibles (Sensitive Information Disclosure en anglais), des algorithmes propriétaires ou d'autres […]
6 novembre 2024
Le machine learning étend les vulnérabilités aux modèles pré-entraînés et aux données d'entraînement fournis par des tiers, qui sont susceptibles […]