Insomni-hack Teaser-Unstringify

On nous fournit un fichier : unstringify

unstringify: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cd0484e35a7fab1db8a1834a3a3a52574c089c6a, for GNU/Linux 4.4.0, not stripped

Analyse du main

un premier coup d’oeil révele que le binaire utilise beaucoup de l’inline ASM pour utiliser les registres AVX (principalement YMM0, YMM1) qui sont des registres 256bits (32octets) ainsi que des registres XMM (128bit).

"Main"

La fonction generate_key() est une fonction qui genère une clé de 32 octets à l’aide de permutation et se base sur des flags (rabbit hole) hardcodé dans la mémoire.

"Main"

Cette fonction retourne toujours une clé fixe de 32 octets

Analyse du round AES

Juste après generatekey, un code asm inline est executé.

vmovdqa ymm1, ymm0
vpermq  ymm0, ymmword ptr [rsp+60h+r1], 1Bh
vaesenc ymm0, ymm0, ymm1
vpcmpeqq ymm0, ymm0, ymmword ptr cs:stored_flag
vpmovmskb eax, ymm0

vmovdqa ymm1, ymm0 copie les 32octets de YMM0 dans YMM1 (la clé retourné par generatekey).
vpermq ymm0, ymmword ptr [rsp+60h+r1], 1Bh (permute 32 octets de r1 avec 0x1B dans YMM0, r1 est l’input de l’utilisateur)
vaesenc ymm0, ymm0, ymm1 fait un round AES avec l’instruction vaesenc et met le résultat dans YMM0
YMM0 étant l’input user (input AES, YMM1 la clé AES)

le détail de l’instruction :

(KL,VL) = (1,128), (2,256)
FOR I := 0 to KL-1:
    STATE := SRC1.xmm[i]
    RoundKey := SRC2.xmm[i]
    STATE := ShiftRows( STATE )
    STATE := SubBytes( STATE )
    STATE := MixColumns( STATE )
    DEST.xmm[i] := STATE XOR RoundKey
DEST[MAXVL-1:VL] := 0

S’en suit un check entre YMM0 (résultat du round aes et cs:stored_flag qui est le ciphertext chiffré en mémoire).

Resolution

je me suis trompé de direction au début, je pensais que vaesdec était l’instruction inverse de vaesenc. Mais non.

Un shellcode aurait pu être utiliser :

section .data
    aes_key dq 1.0767242e16, 1.5942744e-37, 2.736913e-38, 2.324516e18, 1.1416161e-25, 5.978845e-31, 0.00074127543, 1.04546e-29
    stored_flag dq -0.14042784, 8.5904241e-27, 1.0001436e-9, 6.5840891e-22, 1.1724008e35, 7.5752593e-35, -0.12882388, -1942.2239

section .text
global _start

_start:

    vmovdqa ymm1, [stored_flag] ; y1 = ciphertext
    vmovdqa ymm0,[aes_key] ; Y0 = aeskey
    vaesdec ymm1, ymm1, ymm0
    int3

    ;exit
    mov rax, 60            
    xor rdi, rdi
    syscall

(J’ai mit les valeurs en float, mais un db aurait suffit avec v2_int128 dans gdb via info register ymm0)

Ce script gdb permet de break et de récuperer automatiquement la clé également (pour le debug)

import gdb
import binascii

bp = "*0x0000555555555104"
bp_aesencrypted = "*0x555555555114"

class BP128(gdb.Breakpoint):
    def __init__(self, bp):
        super().__init__(bp)

    def stop(self):
            ymm0_value = gdb.parse_and_eval('$ymm0')
            raw_bytes = ymm0_value["v32_int8"]
            print(raw_bytes) #dump de la clé AES
            return True
    

class BPAES(gdb.Breakpoint):
    def __init__(self, bp):
        super().__init__(bp)

    def stop(self):
            ymm0_value = gdb.parse_and_eval('$ymm0')
            ymm0_128bitcast_1 = ymm0_value["v2_int128"][0]
            ymm0_128bitcast_2 = ymm0_value["v2_int128"][1]

            hex_ymm0 =f"{ymm0_128bitcast_1}"[2:]
            hex_ymm0_2 =f"{ymm0_128bitcast_2}"[2:]

            if len(hex_ymm0) % 2 != 0:
                 hex_ymm0 = "0" + hex_ymm0
            if len(hex_ymm0_2) % 2 != 0:
                 hex_ymm0_2 = "0" + hex_ymm0_2
                            
            print("encrypted user_input with aes key:", hex_ymm0 + hex_ymm0_2) #dump de l'user data chiffré après AES
            return False


testflag = "A"*32
gdb.execute("d") #remove old bp
bp128 = BP128(bp)
bp_aes = BPAES(bp_aesencrypted)
gdb.execute(f"r <<< {testflag}")

Pour résoudre le chall, on peut prendre une implementation AES (Merci Mister7f pour le lien) sur internet et faire l’opération inverse de aesenc, c’est à dire

ciphertext ^ RoundKey
inv_mix_columns(plain_state)
inv_sub_bytes(plain_state)
inv_shift_rows(plain_state)

ce qui donne:

#!/usr/bin/env python3
import binascii

s_box = (
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)

inv_s_box = (
    0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
    0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
    0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
    0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
    0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
    0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
    0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
    0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
    0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
    0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
    0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
    0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
    0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
    0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
    0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
    0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)


def sub_bytes(s):
    for i in range(4):
        for j in range(4):
            s[i][j] = s_box[s[i][j]]


def inv_sub_bytes(s):
    for i in range(4):
        for j in range(4):
            s[i][j] = inv_s_box[s[i][j]]


def shift_rows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
    s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
    s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]


def inv_shift_rows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
    s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
    s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]

def add_round_key(s, k):
    for i in range(4):
        for j in range(4):
            s[i][j] ^= k[i][j]


# learned from https://web.archive.org/web/20100626212235/http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)


def mix_single_column(a):
    # see Sec 4.1.2 in The Design of Rijndael
    t = a[0] ^ a[1] ^ a[2] ^ a[3]
    u = a[0]
    a[0] ^= t ^ xtime(a[0] ^ a[1])
    a[1] ^= t ^ xtime(a[1] ^ a[2])
    a[2] ^= t ^ xtime(a[2] ^ a[3])
    a[3] ^= t ^ xtime(a[3] ^ u)


def mix_columns(s):
    for i in range(4):
        mix_single_column(s[i])


def inv_mix_columns(s):
    # see Sec 4.1.3 in The Design of Rijndael
    for i in range(4):
        u = xtime(xtime(s[i][0] ^ s[i][2]))
        v = xtime(xtime(s[i][1] ^ s[i][3]))
        s[i][0] ^= u
        s[i][1] ^= v
        s[i][2] ^= u
        s[i][3] ^= v

    mix_columns(s)


r_con = (
    0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
    0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
    0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
    0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
)


def bytes2matrix(text):
    """ Converts a 16-byte array into a 4x4 matrix.  """
    return [list(text[i:i+4]) for i in range(0, len(text), 4)]

def matrix2bytes(matrix):
    """ Converts a 4x4 matrix into a 16-byte array.  """
    return bytes(sum(matrix, []))

def xor_bytes(a, b):
    """ Returns a new byte array with the elements xor'ed. """
    return bytes(i^j for i, j in zip(a, b))

def inc_bytes(a):
    """ Returns a new byte array with the value increment by 1 """
    out = list(a)
    for i in reversed(range(len(out))):
        if out[i] == 0xFF:
            out[i] = 0
        else:
            out[i] += 1
            break
    return bytes(out)

def pad(plaintext):
    """
    Pads the given plaintext with PKCS#7 padding to a multiple of 16 bytes.
    Note that if the plaintext size is a multiple of 16,
    a whole block will be added.
    """
    padding_len = 16 - (len(plaintext) % 16)
    padding = bytes([padding_len] * padding_len)
    return plaintext + padding

def unpad(plaintext):
    """
    Removes a PKCS#7 padding, returning the unpadded text and ensuring the
    padding was correct.
    """
    padding_len = plaintext[-1]
    assert padding_len > 0
    message, padding = plaintext[:-padding_len], plaintext[-padding_len:]
    assert all(p == padding_len for p in padding)
    return message

def split_blocks(message, block_size=16, require_padding=True):
        assert len(message) % block_size == 0 or not require_padding
        return [message[i:i+16] for i in range(0, len(message), block_size)]


class AES:
    """
    Class for AES-128 encryption with CBC mode and PKCS#7.

    This is a raw implementation of AES, without key stretching or IV
    management. Unless you need that, please use `encrypt` and `decrypt`.
    """
    rounds_by_key_size = {16: 10, 24: 12, 32: 14}
    def __init__(self, master_key):
        """
        Initializes the object with a given key.
        """
        assert len(master_key) in AES.rounds_by_key_size
        #self.n_rounds = AES.rounds_by_key_size[len(master_key)]
        self.n_rounds = 1
        self._key_matrices = self._expand_key(master_key)

    def _expand_key(self, master_key):
        """
        Expands and returns a list of key matrices for the given master_key.
        """
        # Initialize round keys with raw key material.
        key_columns = bytes2matrix(master_key)
        iteration_size = len(master_key) // 4

        i = 1
        while len(key_columns) < (self.n_rounds + 1) * 4:
            # Copy previous word.
            word = list(key_columns[-1])

            # Perform schedule_core once every "row".
            if len(key_columns) % iteration_size == 0:
                # Circular shift.
                word.append(word.pop(0))
                # Map to S-BOX.
                word = [s_box[b] for b in word]
                # XOR with first byte of R-CON, since the others bytes of R-CON are 0.
                word[0] ^= r_con[i]
                i += 1
            elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
                # Run word through S-box in the fourth iteration when using a
                # 256-bit key.
                word = [s_box[b] for b in word]

            # XOR with equivalent word from previous iteration.
            word = xor_bytes(word, key_columns[-iteration_size])
            key_columns.append(word)

        # Group key words in 4x4 byte matrices.
        return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]

import os
from hashlib import pbkdf2_hmac
from hmac import new as new_hmac, compare_digest

AES_KEY_SIZE = 16
HMAC_KEY_SIZE = 16
IV_SIZE = 16

if __name__ == '__main__':
    
    ###TEST
    aeskey = [0x0, 0x3, 0x19, 0x5a, 0x5b, 0x0, 0x59, 0x2, 0x0, 0x3, 0x15, 0x1, 0x5c, 0x9, 0x1, 0x5e, 0x42, 0x53, 0xd, 0x16, 0x4d, 0x6, 0x42, 0xd, 0x27, 0x52, 0x42, 0x3a, 0x63, 0xb, 0x54, 0xf]
    plaintext = [0x41]*32
    expected = [0x83, 0x80, 0x9a, 0xd9, 0xd8, 0x83, 0xda, 0x81, 0x83, 0x80, 0x96, 0x82, 0xdf, 0x8a, 0x82, 0xdd, 0xc1, 0xd0, 0x8e, 0x95, 0xce, 0x85, 0xc1, 0x8e, 0xa4, 0xd1, 0xc1, 0xb9, 0xe0, 0x88, 0xd7, 0x8c]
    ###TEST

    p1= plaintext[0:16]
    k1 = aeskey[0:16]

    p2= plaintext[16:32]
    k2 = aeskey[16:32]

    plain_state = bytes2matrix(p1)
    shift_rows(plain_state)
    sub_bytes(plain_state)
    mix_columns(plain_state)
    r1 = matrix2bytes(plain_state)
    finalblock = xor_bytes(r1, k1)

    plain_state = bytes2matrix(p2)
    shift_rows(plain_state)
    sub_bytes(plain_state)
    mix_columns(plain_state)
    r2 = matrix2bytes(plain_state)
    finalblock2 = xor_bytes(r2, k2)

    expected_computed = []
    for byte in finalblock+finalblock2:
        expected_computed.append(byte)

    # OK ENCRYPT MATCH
    print("result encrypt", expected_computed)
    print("expected      ", expected)

    ciphertext =[0x51, 0xCC, 0x0F, 0xBE, 0xA0, 0x26, 0x2A, 0x14, 0x6D, 0x75, 
                0x89, 0x30, 0xEC, 0xFD, 0x46, 0x1C, 0x0F, 0xA3, 0xB4, 0x79, 
                0x8B, 0x62, 0xC9, 0x06, 0x68, 0xEA, 0x03, 0xBE, 0x2A, 0xC7, 
                0xF2, 0xC4
                ]
    
    c1 = ciphertext[0:16]
    c2 = ciphertext[16:32]

    c1 = xor_bytes(c1, k1)
    c2 = xor_bytes(c2, k2)

    plain_state = bytes2matrix(c1)
    inv_mix_columns(plain_state)
    inv_sub_bytes(plain_state)
    inv_shift_rows(plain_state)
    r1 = matrix2bytes(plain_state)

    plain_state = bytes2matrix(c2)
    inv_mix_columns(plain_state)
    inv_sub_bytes(plain_state)
    inv_shift_rows(plain_state)
    r2 = matrix2bytes(plain_state)

    #INS{T45k_Sp0n$0r3d_By_G4l4h4d<3}