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.
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}