My Profile Photo

Lefevre Sylvain


Follow my super adventures in an insecure world.


GACTF 2020

Crypto - ezAES

For this first challenge we are given the following python file

from Crypto.Cipher import AES
import binascii, sys
import hashlib

key = b'T0EyZaLRzQmNe2**'
KEYSIZE = len(key)
assert(KEYSIZE==16)


def pad(message):
    p = bytes((KEYSIZE - len(message) % KEYSIZE) * chr(KEYSIZE - len(message) % KEYSIZE),encoding='utf-8')
    return message + p


def encrypt(message,passphrase,iv):
	aes = AES.new(passphrase, AES.MODE_CBC, iv)
	return aes.encrypt(message)


h = hashlib.md5(key).hexdigest()
SECRET = binascii.unhexlify(h)[:10]


with open('flag','rb') as f:
	IV = f.read().strip(b'gactf{').strip(b'}')	

message = b'AES CBC Mode is commonly used in data encryption. What do you know about it?'+SECRET

print("Encrypted data: ", binascii.hexlify(encrypt(pad(message),key,IV)))

'''
Encrypted data: b'a8**************************b1a923**************************011147**************************6e094e**************************cdb1c7**********a32c412a3e7474e584cd72481dab9dd83141706925d92bdd39e4'
'''

Let’s have a look at what we have. at the top of the file there is a key T0EyZaLRzQmNe2**, the * seems to indicate missing characters so we have a key with 2 missing characters.

Next we hafve a classic padding function, followed by an AES-CBC encrypt. The flag is used as the IV of our encrypted message, and an hash of the key is appended to the message we are encrypting.

At the end of the file is the output of the encryption, with some missing characters.

First we need to create a function to decrypt a message, given a passphrase and an IV.

def decrypt(message, passphrase, iv):
    aes = AES.new(passphrase, AES.MODE_CBC, iv)
    return aes.decrypt(message)

The next step is to determine the key used to encrypt our message. Luckily we have the last block intact 72481dab9dd83141706925d92bdd39e4. During the encryption the previous block was used to encrypt this one, as the encryption is in CBC mode, the previous block is c7**********a32c412a3e7474e584cd. As the length of the encrypted string doesn’t change, we now that the last 10 characters are padding, and are 0xA. They are the only characters we need to be able to guess the key

I then tried all combinations of printable characters to find the correct key

import string

def find_key():
    keytmp = 'T0EyZaLRzQmNe2{}{}'
    for c1 in string.printable:
        for c2 in string.printable:
            tmp = decrypt(binascii.unhexlify('72481dab9dd83141706925d92bdd39e4'), keytmp.format(c1, c2).encode('utf8'), binascii.unhexlify('0' * 12 + 'a32c412a3e7474e584cd'))
            if int(tmp[-2]) < 0x10 and tmp[-1] == tmp[-2] and tmp[-2] == tmp[-3]:
                return keytmp.format(c1, c2).encode('utf8')

print(find_key().decode('utf8'))

which output T0EyZaLRzQmNe2pd. Now that we now the key, we can have the encrypted message:

b'AES CBC Mode is commonly used in data encryption. What do you know about it?\xfc\x89\xb4\xd5\xe2\x0b\xd2\xc6U\xae'

Using an arbitrary IV, we can encrypt the message, then use it to get the original encrypted message, and finally the flag

IV = b'yellow_submarine'

arbitrary = binascii.hexlify(encrypt(pad(message), key, IV))
encrypted = 'a8**************************b1a923**************************011147**************************6e094e**************************cdb1c7**********a32c412a3e7474e584cd72481dab9dd83141706925d92bdd39e4'.replace('*', '0')
encrypted = [encrypted[i:i+32] for i in range(0, len(encrypted), 32)]
arbitrary = [arbitrary[i:i+32] for i in range(0, len(arbitrary), 32)]

def guess_block(flag_block, correct_block, correct_iv):
    c1 = decrypt(binascii.unhexlify(flag_block), key, binascii.unhexlify(b'0' * 32))
    c2 = decrypt(binascii.unhexlify(correct_block), key, binascii.unhexlify(correct_iv))
    result = b''
    for i in range(16):
        result += bytes([c1[i] ^ c2[i]])
    return binascii.hexlify(result)

for i in range(1, len(arbitrary) + 1):
    if i == len(arbitrary):
        e2 = binascii.hexlify(IV)
    else:
        e2 = arbitrary[-i - 1]
    tmp = guess_block(encrypted[-i], arbitrary[-i], e2)
    if i == len(arbitrary):
        flag = binascii.unhexlify(tmp)
    else:
        encrypted[-i - 1] = tmp

print(flag.decode('utf8'))
9j_for_aes_cbc!!

Reverse - Checkin

Opening the binary in IDA, and looking at the imports, we can see GetCommandLineA, which is unusual. I jumped to the first XRef to it, and putted a breakpoint. I then started the debugger, and obtained the following. IDA

It is running the following ruby script

require 'openssl'
require 'base64'

def aes_encrypt(key,encrypted_string)
	aes = OpenSSL::Cipher.new("AES-128-ECB")
	aes.encrypt
	aes.key = key
	cipher = aes.update(encrypted_string) << aes.final
	return Base64.encode64(cipher) 
end

print "Enter flag: "
flag = gets.chomp

key = "Welcome_To_GACTF"
cipher = "4KeC/Oj1McI4TDIM2c9Y6ahahc6uhpPbpSgPWktXFLM=\n"

text = aes_encrypt(key,flag)
if cipher == text
	puts "good!"
else
	puts "no!"
end

I modified it for the following

require 'openssl'
require 'base64'

def aes_decrypt(key,encrypted_string)
	aes = OpenSSL::Cipher.new("AES-128-ECB")
	aes.decrypt
	aes.key = key
	cipher = aes.update(encrypted_string) << aes.final
        return cipher
end

key = "Welcome_To_GACTF"
cipher = "4KeC/Oj1McI4TDIM2c9Y6ahahc6uhpPbpSgPWktXFLM=\n"

puts aes_decrypt(key, Base64.decode64(cipher))

Which outputs

GACTF{Have_a_wonderful_time!}

Reverse - Wannaflag

I opened the binary in IDA, and looked at the import table. I looked at the XRef of CryptDecrypt. The function calling it didn’t seem to have much of a verification, so I jump to the XRef of this function.

The interesting part of the function is

v35 = GetDlgItem(hWndParent, 4);
sub_406920(&v58, 0, 50);
GetWindowTextA(v35, (LPSTR)&Paint, 32);
v36 = strlen((const char *)&Paint);
if ( v36 >= 6 )
{
  v37 = SLOBYTE(Paint.fErase) % 7;
  v38 = 1;
  for ( i = 2; i < v37; ++i )
    v38 *= (_BYTE)i;
  v40 = 0;
  if ( (unsigned int)v36 >= 0x40 )
  {
    v41 = _mm_cvtsi32_si128(v38);
    v42 = _mm_unpacklo_epi8(v41, v41);
    v43 = _mm_shuffle_epi32(_mm_unpacklo_epi16(v42, v42), 0);
    do
    {
      *(__int128 *)((char *)&v58 + v40) = (__int128)_mm_xor_si128(*(__m128i *)((char *)&Paint.hdc + v40), v43);
      *(__int128 *)((char *)&v59 + v40) = (__int128)_mm_xor_si128(
                                                      *(__m128i *)((char *)&Paint.rcPaint.right + v40),
                                                      v43);
      *(__int128 *)((char *)&v60 + v40) = (__int128)_mm_xor_si128(*(__m128i *)&Paint.rgbReserved[v40], v43);
      *(__m128i *)((char *)&v61 + v40) = _mm_xor_si128(*(__m128i *)&Paint.rgbReserved[v40 + 16], v43);
      v40 += 64;
    }
    while ( v40 < v36 - v36 % 64 );
  }
  for ( ; v40 < v36; ++v40 )
    *((_BYTE *)&v58 + v40) = v38 ^ *((_BYTE *)&Paint.hdc + v40);
  *((_BYTE *)&v58 + v40) = 0;
  v44 = 0;
  do
  {
    *((_BYTE *)&v58 + v44) = __ROL1__(*((_BYTE *)&v58 + v44) ^ byte_420AE0[v44], v44);
    ++v44;
  }
  while ( v44 < v36 );
  v45 = &v58;
  v46 = (char *)&unk_4278D8;
  v47 = 27;
  while ( *(_DWORD *)v45 == *(_DWORD *)v46 )
  {
    v45 = (__int128 *)((char *)v45 + 4);
    v46 += 4;
    v48 = v47 < 4;
    v47 -= 4;
    if ( v48 )
    {
      if ( *(_WORD *)v45 == *(_WORD *)v46 && *((_BYTE *)v45 + 2) == v46[2] )
      {
        MessageBoxA(hWndParent, "orz", "orz", 0);
        function_calling_decrypt((BYTE *)&Paint);
      }
      return 0;
    }
  }
}

There still is much to look at. v58 is the user input, and v36 is its length. By trying some things here and there, I found out that the user input can’t be more than 0x40. Reducing the code to

v35 = GetDlgItem(hWndParent, 4);
sub_406920(&v58, 0, 50);
GetWindowTextA(v35, (LPSTR)&Paint, 32);
v36 = strlen((const char *)&Paint);
if ( v36 >= 6 )
{
  v37 = SLOBYTE(Paint.fErase) % 7;
  v38 = 1;
  for ( i = 2; i < v37; ++i )
    v38 *= (_BYTE)i;
  v40 = 0;
  for ( ; v40 < v36; ++v40 )
    *((_BYTE *)&v58 + v40) = v38 ^ *((_BYTE *)&Paint.hdc + v40);
  *((_BYTE *)&v58 + v40) = 0;
  v44 = 0;
  do
  {
    *((_BYTE *)&v58 + v44) = __ROL1__(*((_BYTE *)&v58 + v44) ^ byte_420AE0[v44], v44);
    ++v44;
  }
  while ( v44 < v36 );
  v45 = &v58;
  v46 = (char *)&unk_4278D8;
  v47 = 27;
  while ( *(_DWORD *)v45 == *(_DWORD *)v46 )
  {
    v45 = (__int128 *)((char *)v45 + 4);
    v46 += 4;
    v48 = v47 < 4;
    v47 -= 4;
    if ( v48 )
    {
      if ( *(_WORD *)v45 == *(_WORD *)v46 && *((_BYTE *)v45 + 2) == v46[2] )
      {
        MessageBoxA(hWndParent, "orz", "orz", 0);
        function_calling_decrypt((BYTE *)&Paint);
      }
      return 0;
    }
  }
}

SLOBYTE(Paint.fErase) is the 7th character of the user input. v38 value depends then on this character value modulo 7. The whole user input is then xored with v38. Then it is xored again, with ANNAWGALFYBKVIAHMXTFCAACLAAAAYK.

The result is then compared with a fixed value.

I made the following script from these observations

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>

uint8_t rotr32 (uint8_t value, unsigned int count) {
    // From wikipedia
    const unsigned int mask = CHAR_BIT * sizeof(value) - 1;
    count &= mask;
    return (value >> count) | (value << (-count & mask));
}

const unsigned char correct[] = {0x4E, 0xAE, 0x61, 0x0BA, 0x0E4, 0x2B, 0x55, 0x0AA, 0x59, 0x0FC, 0x4D, 0x2, 0x17, 0x6B, 0x13, 0x0A1, 0x41, 0x0FE, 0x35, 0x0B, 0x0B4, 0x0B, 0x52, 0x2F, 0x46, 0x0CC, 0x35, 0x82, 0x0E5, 0x88, 0x50};
const unsigned char modif[] = "ANNAWGALFYBKVIAHMXTFCAACLAAAAYK";

int main() {
  char final[32] = {0};
  for (unsigned char i = 0; i < 32; i++) {
    unsigned char tmp = rotr32(correct[i], i) ^ 0x78 ^ modif[i]; // After a few attemps v38 was 0x78
    final[i] = tmp;
  }
  printf("%s\n", final);
  return 0;
}

which output wannaflag_is_just_a_paper_tigerx Entering this into the binary give you the flag

GACTF{WannaFlag_is_just_a_easy_re_with_a_beautiful_appearance}