HTB Partial Encryption

Kaddate |

Difficulty : Easy
Category : Reversing


Inspecting the given file indicates that this is a PE program :

➜  rev_partialencryption git:(main) file partialencryption.exe 
partialencryption.exe: PE32+ executable for MS Windows 6.00 (console), x86-64, 5 sections

Ghidra FTW

Opening the binary in ghidra, shows an easy logic

64f5873ea1b681a977ed431407c42cca.png

The main function doesn't have many calls, so this will probably be easy to understand the logic of the code. The 2 first functions just checks for the number of arguments and for the size of the user input (passed as CLI argument).

The real interesting thing is that, the code uses a 'loader' to start functions :

d96aa225f22983b764efba4ac53e324e.png

Here is how the loader looks like :

3d3a25683caa77f66a6ea570418c9067.png

It takes 2 arguments, a memory area and a size, and then iterates over it 16 bytes per 16 bytes. It then calls a function at each iteration. Let's check what it does :

954528163fc404d8b445a3ce0861f06d.png

887f4cbb8474c382b549aa8a4cf7e51d.png

Looking at the assembly, it is very easy to understand that it does some kind of decrypting using Intel's AES extension. We can see that the keys and algorithm have no seeds nor anything random so it would be easy get the keys and decrypt the functions manually using a custom script :

#include <emmintrin.h>
#include <wmmintrin.h>
#include <stdio.h>

__m128i decrypt_block(__m128i block, __m128i nonce)
{
  __m128i key = _mm_aeskeygenassist_si128(nonce, 0);
  __m128i xor = _mm_aeskeygenassist_si128(nonce, 0x10);
  __m128i decrypted = _mm_aesdeclast_si128(block ^ xor, key);
  return decrypted;
}

void unpack(unsigned char *encryptedCode, int size)
{
  for (int i = 0; i < size/16; i++) {
    __m128i nonce = _mm_cvtsi32_si128(i);
    nonce = _mm_unpacklo_epi8(nonce, nonce);
    nonce = _mm_unpacklo_epi16(nonce, nonce);
    nonce = _mm_shuffle_epi32(nonce, 0);

    __m128i block = _mm_loadu_si128((__m128i*)(encryptedCode+i*16));
    __m128i cBlock = decrypt_block(block, nonce);

    _mm_storeu_si128((__m128i*)(encryptedCode+i*16), cBlock);
  }
}

int main() { // gcc -Wall -o decrypt decrypt.c -maes -msse2
  #define codeSize 480
  unsigned char encryptedCode[codeSize] = { ...SNIP... };
  unpack(encryptedCode, codeSize);
  for (int i = 0; i < codeSize; i++) {
    printf("%02x", encryptedCode[i]);
  }
}

Now I can just pass the encrypted function to my script and paste their decrypted versions into Ghidra.

ad845df7cd021d7ac8896ef208dddb01.png

We can easily see the flag unravel with each functions, and we can easily get the flag now. Since the comparisons to verify the flag are very simple, I guess it would've also be possible to just hook each test instructions and retrieve the flag like this.