Rechercher
Fermer ce champ de recherche.

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

04/12/2023

Blog technique

European Cyber Week 2023 : Challenges & Write ups

Team CESTI

Find here the crypto and reverse challenges that our teams created for the European Cyber Week pre-qualification and qualification tests of CTF, a recognized cybersecurity event that took place in Rennes from November 21 to 23, 2023.

The scenario? Designed to monitor and protect Europe’s critical information systems, an incredibly powerful artificial intelligence agent called ALICE has taken control of several critical information systems and has just launched massive cyberattacks on these systems, causing major damage…

Join the team of top experts from the European government to neutralize ALICE and restore these critical systems and try to solve the crypto and reverse challenges concocted by our teams.

Have you completed all the challenges?

Find below the challenges to replay as well as the write-ups.

Challenges

Random_key

Discover our pre-qualification crypto challenge here.

Go to the AMOSSYS Github!

StraightLine

Discover our pre-qualification crypto challenge here.

Go to the AMOSSYS Github!

Alice au captcha des merveilles

Discover our Final reverse challenge here.

Go to the AMOSSYS Github!

Write Ups

Random_key (crypto challenge)

In this challenge, an encrypted message sent by ALICE to its control center and code snippets used by ALICE to encrypt its message have been intercepted.

The goal was to retrieve the message in clear text

In the C function generate_256bits_encryption_key, the variable delta is always zero, and the return type is unsigned char *.

The null byte indicates the end of a string, then the key returned is not 32 bytes long (256 bits), but a truncated key of length 8 bytes.

This key is always the same and its value is the first half of the MD5 hash of the recipient_name parameter.
So it is predictable.

The following Python code finds the FLAG.

				
					from Cryptodome.Cipher import AES

enc = bytes.fromhex('<encrypted_message>')
key = b'134abb7bd9d248a9'
print(AES.new(key, AES.MODE_CBC, b'FEDCBA9876543210').decrypt(enc))
				
			

StraightLine (crypto challenge)

A user has access to case Wonderland Chat, which requires a token that is given after the username has been entered. After connection, a message indicates that the service is still a work in progress, and that an admin can help. Trying to get a token for the admin user is not possible:
				
					You cannot request a token for admin user.
				
			
Furthermore, the source code of the token generation is provided. All of the above leads to the goal of the challenge: we need to forge a token for the admin user.

Source code analysis

The token is generated by the gen_token() function:
				
					pub fn gen_token(&self, username: &str) -> Hash {
    // hash(key || message)
    let mut payload = vec![0u8; self.key.len() + username.as_bytes().len()];
    payload[..self.key.len()].copy_from_slice(&self.key);
    payload[self.key.len()..].copy_from_slice(username.as_bytes());
    alice_hash(&payload)
}
				
			

The token is the hash of the concatenation of:
– A 16-byte secret key (generated at initialization);
– The username.

The hash function is implemented in the remaining of the file.
A few key words such as iota, rho, pi and theta suggest that it is related to SHA-3.

If it is indeed SHA-3, then forging a token would necessitate to find a preimage to a valid token to retrieve the secret key.
This is not possible as SHA-3 is robust against preimage and collision attacks.

So there might be something wrong with the code.

Vulnerability

The hash function SHA-3 is composed of 5 permutations and an internal state of 200 bytes. A careful inspection reveals that one of these is missing: chi. This permutation brings non-linearity to the transformation of the internal state. Removing it means that the output of the hash function can be expressed as a linear combination of all the bits of the input. If the unknown part of the input is small enough, then the linear system that can be derived has a unique solution. In the current case, the unknown part of the input is the key of 128 bits, and the token is 256 bits:
				
					H(key || username) = token
				
			
Given a valid token and username, we solve a linear system and retrieve the secret key. Then a token can be forged for any user such as admin.

Exploitation

Using Sagemath, we can express the input as bits, with each key bit as a variable in the field GF(2): all operations will be made in a polynomial ring of 128 variables over the field GF(2). The following code generates the polynomial ring and gives names z0, z1, …, z127 for the secret key bits.
				
					F = GF(2)
K = PolynomialRing(F, 'z', 128)
Z = K.gens()
				
			
The XOR operation in SHA-3 can be converted as the addition in GF(2). Since all operations are linear, no multiplication is involved. SHA-3 can be rewritten in Sagemath by an adaptation of the Rust code of the challenge. The internal state is an array of 25 words of 64 bits which is accessed through two indexes x and y, each in the range [0,4]: an extra dimension z can be added that contains a list of 64 elements in the polynomial ring instead of a 64-bit word. To make it convenient, we keep it stored as a single array, using a conversion from [0,4] into the corresponding position (see the implementation of the Index trait in the Rust code):
				
					
def xyztoii(xx, yy, zz):
    xx = xx % 5
    yy = yy % 5
    zz = zz % 64
    return 64*(xx + 5*yy) + zz
				
			
An example of adaptation is given below for the iota permutation. This one uses an array RC of constants which is converted as lists of 64 bits each.
				
					# Permutation iota
def iota(s, ir):
    for zz in range(64):
        s[xyztoii(0, 0, zz)] += RC[ir][zz]
				
			

We’ll also need to convert a string (the username) into an array of bits.

				
					
def string_to_bits(s):
    m_bytes = s.encode('utf8')
    m_int = int.from_bytes(m_bytes, 'little')
    return [(m_int >> i) & 1 for i in range(len(m_bytes)*8)]
				
			
All other functions are given in the full script of the attack at the end of this write-up, including the compute_hash() function that works in the polynomial ring. Once it’s done, we hash the input that consists of the 128 key variables, and a list of bits that represent the username.
				
					output = compute_hash(list(Z) + string_to_bits(username))
				
			
The variable output is a list of 256 polynomials that depends of the 128 variables. Each coefficient in front of the variables of each polynomial are writen in a matrix M.
				
					M = Matrix(F, 256, 128)
for i in range(256):
    for j in range(128):
        M[i, j] = output[i].coefficient(Z[j])
				
			
We take the token, convert it as a list of bits and put them into a vector v (we add the constant coefficient of the polynomials we got earlier):
				
					a = int.from_bytes(token, 'little')
v = vector(F, 256)
for i in range(256):
    v[i] = ((a >> i) & 1) + output[i].constant_coefficient()
				
			
We are looking for a solution u such that Mu = v.
				
					key_bits = M.solve_right(target)
				
			
That’s it, key_bits contains the secret key, which can be used directly to compute a new token for admin:
				
					token_bits = compute_hash(list(key_bits) + string_to_bits('admin'))
token = b''
for i in range(0, 256, 8):
    token += bytes([sum(ZZ(k)*2**j for j, k in enumerate(token_bits[i:i + 8]))])
				
			
Use this token to get the flag on Wonderland Chat.

Script

Here is the whole Sage script which can be executed on the command line:
				
					sage ./solve_straight_line.sage USERNAME TOKEN
				
			
The arguments USERNAME and TOKEN must be replaced by the username and its corresponding token.
				
					#!/usr/bin/env sage

import os
import sys

# We work in polynomial ring over binary finite field GF(2)
# with 128 variables (each one correspond to one bit of the key).
# XOR operation is a simple addition in GF(2).
F = GF(2)
K = PolynomialRing(F, 'z', 128)
Z = K.gens()

BLOCK_SIZE = 136
BLOCK_SIZE_BITS = BLOCK_SIZE*8
HASH_SIZE = 32
HASH_SIZE_BITS = 32*8

def xyztoii(xx, yy, zz):
    '''
    Coordinate conversion for SHA-3:
    (x, y, z) converted as an index to a single array    
    '''
    xx = xx % 5
    yy = yy % 5
    zz = zz % 64
    return 64*(5*yy + xx) + zz

# SHA-3 round constants as bits (for iota permutation)
RC = [
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
]

# SHA-3 permutations
def iota(s, ir):
    for zz in range(64):
        s[xyztoii(0, 0, zz)] += RC[ir][zz]

def rho(s):
    s_copy = copy(s)
    xx, yy = 1, 0
    for t in range(24):
        for zz in range(64):
            s[xyztoii(xx, yy, zz)] = s_copy[xyztoii(xx, yy, (zz - (t + 1)*(t + 2)//2))]
        xx, yy = yy, (2*xx + 3*yy) % 5

def pi(s):
    s_copy = copy(s)
    for zz in range(64):
        for xx in range(5):
            for yy in range(5):
                s[xyztoii(xx, yy, zz)] = s_copy[xyztoii(xx + 3*yy, xx, zz)]

def theta(s):
    C = {}
    for xx in range(5):
        for zz in range(64):
            C[(xx, zz)] = F(0)
            for yy in range(5):
                C[(xx, zz)] = C[(xx, zz)] + s[xyztoii(xx, yy, zz)]
    D = {}
    for xx in range(5):
        for zz in range(64):
            D[(xx, zz)] = C[((xx - 1) % 5, zz)] + C[((xx + 1) % 5, (zz - 1) % 64)]

    for xx in range(5):
        for yy in range(5):
            for zz in range(64):
                s[xyztoii(xx, yy, zz)] += D[(xx, zz)]

def keccak_f(s):
    '''
    SHA-3 function (24 rounds without chi permutation)
    '''
    for ir in range(24):
        theta(s)
        rho(s)
        pi(s)
        iota(s, ir)

def xor(array1, array2):
    '''
    Useful function to "XOR" each new block (BLOCK_SIZE_BITS bits)
    into state array (1600 bits).
    '''
    assert len(array1) >= len(array2)
    n = len(array2)
    return [ a + b for a, b in zip(array1[:n], array2)] + array1[n:]

def compute_hash(data):
    '''
    Compute hash in polynomial ring of 128 variables in GF(2)
    '''
    pad_data = pad(data)
    state = [K(0)]*1600
    for i in range(0, len(pad_data), BLOCK_SIZE_BITS):
        chunk = pad_data[i:i + BLOCK_SIZE_BITS]
        state = xor(state, chunk)
        keccak_f(state)
    return state[:HASH_SIZE_BITS]

def pad(m):
    '''
    Pad the input to make full blocks of 576 bits.
    '''
    padsize = BLOCK_SIZE - (len(m)//8 % BLOCK_SIZE)
    
    if padsize == 1:
        padded_m = m + [0, 1, 1, 0, 0, 0, 0, 1]
    elif padsize == 2:
        padded_m = m + [0, 1, 1, 0, 0, 0, 0, 0] + [0, 0, 0, 0, 0, 0, 0, 1]
    else:
        padded_m = m + [0, 1, 1, 0, 0, 0, 0, 0] + [0]*8*(padsize - 2) + [0, 0, 0, 0, 0, 0, 0, 1]
    return padded_m

def string_to_bits(s):
    '''
    Conversion of string to an array of bits (length multiple of 8).
    '''
    m_bytes = s.encode('utf8')
    m_int = int.from_bytes(m_bytes, 'little')
    return [ (m_int >> i) & 1 for i in range(len(m_bytes)*8)]

def solve_challenge(token, username):
    '''
    token: as bytes
    username: as string (which is converted to bytes assuming UTF-8 encoding)
    '''

    # We compute H(key||username) with 128 variables for each bit of the key.
    # Output is a list of HASH_SIZE_BITS linear equations with 128 variables.
    output = compute_hash(list(Z) + string_to_bits(username))
    
    # We want to solve the system
    #     M*u = v
    # where v is the target vector (i.e., the token), and u the secret key.
    M = Matrix(F, HASH_SIZE_BITS, 128)
    for i in range(HASH_SIZE_BITS):
        for j in range(128):
            M[i, j] = output[i].coefficient(Z[j])

    # The token is converted as an array of length HASH_SIZE_BITS
    # and we add the constant coefficients of the output.
    a = int.from_bytes(token, 'little')
    v = vector(F, HASH_SIZE_BITS)
    for i in range(HASH_SIZE_BITS):
        v[i] = ((a >> i) & 1) + output[i].constant_coefficient()

    # We solve to get the secret key, and use it to compute the admin token.
    key_bits = M.solve_right(v)
    token_bits = compute_hash(list(key_bits) + string_to_bits('admin'))

    # We put the bits together to form the admin token.
    # Beware that `token_bits` are elements in GF(2),
    # so we cast into integers with ZZ.
    token = b''
    for i in range(0, HASH_SIZE_BITS, 8):
        token += bytes([sum(ZZ(k)*2**j for j, k in enumerate(token_bits[i:i + 8]))])
    print(f'Admin token: {token.hex()}')

if __name__ == "__main__":
    argc = len(sys.argv) - 1
    if argc&nbsp;!= 2:
        print("Usage: sage ./solve_straight_line.sage USERNAME TOKEN")
        sys.exit()

    username = sys.argv[1]
    token = sys.argv[2]
    solve_challenge(bytes.fromhex(token), username)
				
			

Alice au captcha des merveilles (reverse challenge)

Alice au captcha des merveilles, is a reverse challenge on Windows events. It does not contain anti-debug methods, but offuscation methods with on-the-fly execution of opcode. There is also an integrity check performed on a function and the code loaded using a TLSCallback to prevent these functions from being patched. There are several possible solutions to solve this challenge.

Automatic script

Once the reverse has been performed, you may want to solve the challenge by triggering the events to actually perform the captcha. This can be done using the script solve_auto.ahk (given below).
				
					Run "notepad.exe"
Run "notepad.exe"
Sleep 2000
Loop 135 {
    Send "!{Tab Down}"
    Send "!{Tab Up}"
    Sleep 150
    Send "!{Tab Down}"
    Send "!{Tab Up}"

    Sleep 50

    Send "#"
    Sleep 50
    Send "{Esc}"
    Sleep 50
    Send "#"
    Sleep 50
    Send "{Esc}"
    Sleep 50

    Sleep 100

    Run "notepad.exe"
    Sleep 750

    Send "!{Space}"
    Sleep 400
    Send "{Down 3}"
    Sleep 100
    Send "{Enter}"

    Sleep 50 

    Send "!{Tab down}"
    Send "!{Tab up}"
    Sleep 150
    Send "!{Tab down}"
    Send "!{Tab up}"

    Sleep 100

    Send "#"
    Sleep 50
    Send "{Esc}"
    Sleep 50
    Send "#"
    Sleep 50
    Send "{Esc}"
    Sleep 50

    Run "taskkill.exe /im notepad.exe"

}
				
			
However, a AutoHotKeys interpreter is necessary on your machine. The script launch.ps1 (given below) can be used to test the solution.
				
					Start-Process "C:\Users\debug\AppData\Local\Programs\AutoHotkey\v2\AutoHotkey64.exe" -ArgumentList "Z:\solve_auto.ahk"
Sleep 2
Start-Process -FilePath "Z:\alice_au_le_captcha_des_merveilles.exe"
				
			
The file path must be adapted in the powershell script. To test this solution: 1. In a powershell, run .\server.exe. 2. In another powershell, run .\launch.ps1. After the execution of the challenge, something like this should appear:

Patch

Patch 1

It is possible to patch the binary in several ways, although this can be complicated. Firstly, you can patch the shellcode that generates the key at the level of various parameters. The code of the function is as follows:
				
					push   rbp
push   rbx
sub    rsp,0x18
lea    rbp,[rsp+0x10]
mov    rbx,rcx
mov    QWORD PTR [rbp+0x28],rdx
mov    QWORD PTR [rbp+0x30],r8
mov    edx,r9d
mov    eax,DWORD PTR [rbp+0x40]
mov    BYTE PTR [rbp+0x38],dl
mov    BYTE PTR [rbp-0x4],al
movzx  edx,BYTE PTR [rbx]
mov    rax,QWORD PTR [rbp+0x28]
mov    BYTE PTR [rax],dl
mov    edx,DWORD PTR [rbx+0x4]
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x1
mov    BYTE PTR [rax],dl
cmp    QWORD PTR [rbp+0x30],0x0
je     0x56
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x2
mov    edx,DWORD PTR [rbx+0xd]
mov    DWORD PTR [rax],edx
movzx  edx,WORD PTR [rbx+0x11]
mov    WORD PTR [rax+0x4],dx
jmp    0x6b
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x2
mov    edx,DWORD PTR [rbx+0x13]
mov    DWORD PTR [rax],edx
movzx  edx,WORD PTR [rbx+0x17]
mov    WORD PTR [rax+0x4],dx
cmp    BYTE PTR [rbp+0x38],0x0
je     0x88
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x8
mov    edx,DWORD PTR [rbx+0x19]
mov    DWORD PTR [rax],edx
movzx  edx,WORD PTR [rbx+0x1d]
mov    WORD PTR [rax+0x4],dx
jmp    0x9d
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x8
mov    edx,DWORD PTR [rbx+0x27]
mov    DWORD PTR [rax],edx
movzx  edx,WORD PTR [rbx+0x2b]
mov    WORD PTR [rax+0x4],dx
cmp    BYTE PTR [rbp-0x4],0x0
je     0xb2
mov    rax,QWORD PTR [rbp+0x28]
lea    rdx,[rax+0xe]
mov    eax,DWORD PTR [rbx+0x2d]
mov    DWORD PTR [rdx],eax
jmp    0xbf
mov    rax,QWORD PTR [rbp+0x28]
lea    rdx,[rax+0x8]
mov    eax,DWORD PTR [rbx+0x31]
mov    DWORD PTR [rdx],eax
mov    rax,QWORD PTR [rbp+0x28]
add    rax,0x12
mov    rdx,QWORD PTR [rbx+0x37]
mov    QWORD PTR [rax],rdx
mov    rdx,QWORD PTR [rbx+0x3d]
mov    QWORD PTR [rax+0x6],rdx
nop
add    rsp,0x18
pop    rbx
pop    rbp
ret 
				
			
It is possible to patch two conditional jumps related to the booleans of the local timer and the triggered event. Indeed, the global timer would not be a problem here. This would make it possible to always generate valid keys. Keep in mind that the shellcode is XORed, so the opcodes would have to be patched in an XORed manner. In addition, the TLS Callback executed at each Thread attach or Process attach will check the integrity of the shellcode. If the shellcode does not have the expected hash, the binary will stop executing. You will then need to patch the TLS Callback either with a 0xC3 at the start of the function, or by patching the calls to ExitProcess().

Patch 2

A simpler patch this time would be to patch the function that links event values to their type. This function is as follows:
				
					...
mov     rax, [rbp+arg_8]&nbsp;; jumptable 0000000140009520 case 0
mov     dword ptr [rax], 0
mov     rax, [rbp+arg_10]
mov     dword ptr [rax], 80FFh
jmp     loc_14000963B
mov     rax, [rbp+arg_8]&nbsp;; jumptable 0000000140009520 case 1
mov     dword ptr [rax], 9
mov     rax, [rbp+arg_10]
mov     dword ptr [rax], 9
jmp     loc_14000963B
...
				
			
The first case of this switch case is never used, but if it is the hit case every time, this will allow all Windows events to be captured. To do this, you can either write the value to the address of rax with => dword ptr [rax], 0 and dword ptr [rax], 0x80FF, or change it to switch case by setting 0 to always reach case 0. If you launch the challenge like this, the user will just have to wait for the captcha to complete itself. Because at least one event will be captured by the binary, validating each step bit by bit. Once again, this function is protected by TLS Callback. So you’ll also need to think about patching the callback.

Step by step

The automatic script can be complex to complete, especially in the time available, so it may be worthwhile to only complete certain steps in the captcha. After a while, all the images can be retrieved and the client’s final steps completed. With the hash calculation, global hash calculation and sending it to the server for decryption (or retrieving the data directly from the server’s .data). Alternatively, the files could be made non-writable by the client, so there’s nothing more to develop; all you have to do is wait for the captcha to finish.

Voir les derniers articles de notre Blog technique

11 avril 2024
M&NTIS Platform est une solution SaaS destinée au test d'efficacité de produits de défense (AV, EDR, sondes réseau/NDR, SIEM, XDR, ...) et d'architectures de supervision. Une nouvelle version majeure de la plateforme vient de sortir.
25 mars 2024
Through this article, we propose a way to find LPE in Windows applications, by using SysInternals tools. What and how to look at? How to exploit in an easy and quick way?
31 janvier 2024
During the inter-CESTI challenge organized by ANSSI, many vulnerabilities were included to test our abilities to find them and, if possible, to exploit them. In this article, we explore one vulnerability that we found during the original challenge, and explainhow we exploited it: can we find a secret key from a singlesignature?
29 mars 2023
On the 24th of January, AMOSSYS and Malizen put together a Blue Team CTF, for the SupSec seminar organized by Inria. In this blog post, we explain how we, at AMOSSYS, generated the dataset used in this challenge.
20 décembre 2022
Find here the crypto and web challenges that our teams created for the European Cyber Week pre-qualification tests of CTF, a recognized cybersecurity event that took place in Rennes from November 15 to 17, 2022.
22 novembre 2022
Find here the write-ups of the crypto and web challenges that our teams created for the European Cyber Week pre-qualification tests of CTF
14 septembre 2022
This article starts with a quick overview on NIDS (Network Intrusion Detection System) evasions to remind what it is and why it could happen.
31 mars 2021
Essor du numérique, diversification des surfaces d’exposition, multiplication des cyberattaques… Depuis plusieurs années, la sécurité informatique est devenue une composante essentielle de l'administration d'un Système d'Information (SI).
18 septembre 2020
Depuis plusieurs années, l'écosystème informatique a dû faire face à une recrudescence de compromissions de systèmes d'informations par des rançongiciels, ou cryptolockers, qui s'introduisent principalement par des méthodes automatiques (_spear phishing_, etc.).
12 août 2020
We will discuss the feasibility in real world of the Spectre V1 flaw from a cross-process, userland perspective.