[ << ALL_FEED ]

What is wrong with this AES?

More in General

What exactly is wrong with this AES? ❔

Attackers often use encryption to obfuscate parts of malware samples that may be of greatest interest during research. When an analyst discovers such code sections, they study the encryption algorithm, and sometimes, having quickly identified the algorithm, they see no point in studying it fully.

Using one of the malware families spotted in attacks on Latin America as an example, let’s examine a case where an advanced encryption technique is used, but it hides inside what seems to be a classic implementation.

👾 Grandoreiro — a Brazilian banking trojan that has been active from 2016 to the present day. It is delivered to the system via a loader that receives a download link for the payload from a C2 server.

Previous string decryption schemes in Grandoreiro (particularly in the loader) involved custom decoding of encrypted strings, the AES key, and IV via simple substitution. These components were decrypted using XOR, and then the ciphertext was finally decrypted using the AES key and IV in AES CBC 256-bit mode.

🧐 During analysis of recent Grandoreiro samples, an encryption technique new to this family was noticed, used to complicate analysis — Ciphertext Stealing (CTS). This is an encryption mode used when the plaintext is not a multiple of the block size.

For example, one of the most well-known padding schemes (PKCS #7) pads the last block with bytes to ensure it matches the size of a full block. CTS works without padding. We will notice its operation on the last incomplete data block (which is not a multiple of the block size).

What exactly happens:

1️⃣ The last complete block is encrypted.

2️⃣ The encrypted last complete block is XORed with the partial (incomplete) block.

This method provides the ability to encrypt plaintext of arbitrary length without adding padding, preserving the original data size.

To understand how this works in practice, let’s look at the implementation of the algorithm:


cipher = AES.new(aes_key, AES.MODE_ECB)
    encr = b"\x00"*16
    initial_key = cipher.encrypt(encr) # your value here
 
    block_size = 16
    total_blocks = len(ciphered_data) // block_size
    decrypted_data = bytearray()
    previous_block = initial_key
 
    if total_blocks:
        # Decrypt all complete blocks
        for i in range(total_blocks):
            current_block = ciphered_data[i * block_size:(i + 1) * block_size]
            key = previous_block
            decr_block = cipher.decrypt(current_block)
            decrypted_data.extend(bytes(x ^ y for x, y in zip(decr_block, key)))
            previous_block = current_block
 
        # Handle the last block with ciphertext stealing
    if len(ciphered_data) % block_size > 0:
        last_full_block = previous_block
        if total_blocks:
            last_full_block = cipher.encrypt(last_full_block)[:len(ciphered_data) - (i + 1) * block_size] #take as much bytes off of the last full block (encrypted) as remained yet to decrypt (ciphertext)
            partial_block = ciphered_data[(i + 1) * block_size:len(ciphered_data)] #bytes left to decrypt, not a multiple of the block size
        else:
            last_full_block = cipher.encrypt(last_full_block)[:len(ciphered_data)]
            partial_block = ciphered_data[:len(ciphered_data)]
         
        stolen_block = bytes(x ^ y for x, y in zip(partial_block, last_full_block))
        decrypted_data.extend(stolen_block)
Code language: plaintext (plaintext)


The method is quite original, and in Grandoreiro it hides inside functions that implement what appears at first glance to be ordinary AES.

Therefore, before starting to write decryption scripts upon seeing the cherished abbreviation, one should look at the code carefully — make sure that a truly classic version of the algorithm is being used, and not waste hours searching for an answer to the question “why won’t it decrypt?”.

#TI #tip #malware
@ptescalator

More from ti_author

More from ti_author

More in General