Skip to content

Instantly share code, notes, and snippets.

@vient
Last active August 13, 2022 10:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vient/b8aad76a28d80532c5235c190060a39e to your computer and use it in GitHub Desktop.
Save vient/b8aad76a28d80532c5235c190060a39e to your computer and use it in GitHub Desktop.
Kaspersky Crackme 2016

Нам дан бинарный исполняемый файл PE под x86. Открываем его в IDA, переходим в main и видим такой код:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax@2
  const char *v4; // ecx@3

  printf("Welcome to Kaspersky CrackMe 2016!\n");
  if ( argc == 3 )
  {
    v4 = argv[2];
    if ( sub_401A40((int)argv[1]) )
    {
      printf("Good work!\n");
      result = 0;
    }
    else
    {
      printf("Wrong!\n");
      result = 0;
    }
  }
  else
  {
    printf("Usage: CrackMe.exe <E-mail> <Code>\n");
    result = 0;
  }
  return result;
}

Из строки "Usage: ..." ясно, что argv[1] это e-mail, а argv[2] это ключ. Один из них передаётся в sub_401A40, посмотрим, что там внутри.

Рассмотрим первые 10 строк функции:

int __cdecl sub_401A40(int a1)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v2 = v1;
  v3 = 0;
  while ( 1 )
  {
    v4 = *(_BYTE *)(v2 + v3);
    if ( (v4 < 48 || v4 > 57) && (v4 < 65 || v4 > 70) && (v4 < 97 || v4 > 102) )
      return 0;

Видно строку v4 = *(_BYTE *)(v2 + v3), значит, или v2 или v3 является указателем на BYTE. v3 изначально устанавливается в 0, значит, указатель это v2. Однако самая первая строка функции говорит, что v2 = v1;. Это бессмысленно, потому что v1 ещё не инициализирован... Или нет? На самом деле в данном месте проявляется ошибка декомпиляции. v1 хранится в регистре ECX, если мы укажем, что в функцию передаётся ещё один параметр в ECX, то всё станет выглядеть нормально:

int __thiscall sub_401A40(_BYTE *a1, int a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v2 = a1;
  v3 = 0;
  while ( 1 )
  {
    v4 = v2[v3];
    ...

Посмотрим, как изменился вызов в main: if ( sub_401A40(argv[2], (int)argv[1]) ). Таким образом, эта функция принимает на вход email и ключ.

Продолжим исследование:

  while ( 1 )
  {
    pass_i = pass_1[i_1];
    if ( (pass_i < '0' || pass_i > '9') && (pass_i < 'A' || pass_i > 'F') && (pass_i < 'a' || pass_i > 'f') )
      return 0;

Здесь берётся очередной символ ключа и проверяется, что это либо цифра, либо символ из строки "abcdefABCDEF".

Затем можно встретить такой кусок:

    if ( ++v3 >= 32 )
    {
      v5 = sub_401960((int)v2) & 0xFF0000;
      v6 = (((unsigned int)sub_401960((int)v2) >> 16) | v5) >> 8;
      v7 = sub_401960((int)v2) << 16;
      v40 = ((sub_401960((int)v2) & 0xFF00 | v7) << 8) | v6;

Много раз вызывается функция sub_401960. Внутри много кода, который на самом деле делает простую вещь: принимая на вход hex-строку длины 8, переводит её в число.

Разберём вызывающий её код подробнее. Пусть у нас v2 указывает на строку 'AABBCCDD'. Тогда sub_401960(v2) возвращает число 0xAABBCCDD. Далее, есть строка v5 = sub_401960(v2) & 0xFF0000;, она значит v5 = 0xAABBCCDD & 0xFF0000 = 0xBB0000. Следующая строка v6 = ((sub_401960(v2) >> 16) | v5) >> 8 = ((0xAABBCCDD >> 16) | 0xBB0000) >> 8 = (0xAABB | 0xBB0000) >> 8 = 0xBBAABB >> 8 = 0xBBAA. Затем v7 = sub_401960(v2) << 16 = 0xAABBCCDD << 16 = 0xCCDD0000. Последняя строка подсчитывает итоговый результат, v40 = ((sub_401960(v2) & 0xFF00 | v7) << 8) | v6 = ((0xAABBCCDD & 0xFF00 | 0xCCDD0000) << 8) | 0xBBAA = (0xCCDDCC00 << 8) | 0xBBAA = 0xDDCC0000 | 0xBBAA = 0xDDCCBBAA.

Таким образом, на самом деле вся эта куча кода делает простую вещь: декодирует строку как hex-запись числа в little-endian формате. На питоне это записывается в одну строку: v40 = struct.unpack('<I', 'AABBCCDD')[0]. Работаем дальше.

Далее идёт такой интересный код:

      v36 = 1732584193;
      v37 = -271733879;
      v38 = -1732584194;
      v39 = 271733878;
      sub_401700(a2);
      sub_401830(&v34);

Может быть, эти числа ни о чём нам не говорят. Переведём их в hex:

      v36 = 0x67452301;
      v37 = 0xEFCDAB89;
      v38 = 0x98BADCFE;
      v39 = 0x10325476;

Тут уже видна какая-то закономерность. В целом, если есть какое-то интересное число, то есть смысл его загуглить. В данном случае поиск "0x67452301 0xEFCDAB89" выдаёт нам результаты по хеш-функциям MD5 и SHA-1. В них действительно используются данные числа. Как понять, какой это хеш? Можно посмотреть, как именно происходит его расчёт. В данном случае функция sub_401700 приводит нас в функцию sub_401000, где есть очень много интересных чисел, которые можно поискать. Посмотрим на самые первые строки:

int __usercall sub_401000@<eax>(int a1@<eax>, int a2)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v2 = *(_DWORD *)(a2 + 12);
  v3 = *(_DWORD *)a1;
  v4 = *(_DWORD *)(a2 + 4);
  v5 = *(_DWORD *)(a2 + 8);
  v6 = __ROL4__(*(_DWORD *)a1 + (v4 & *(_DWORD *)(a2 + 8) | v2 & ~*(_DWORD *)(a2 + 4)) + *(_DWORD *)a2 - 680876936, 7);

Берём первое же интересное число 680876936, гуглим его, и результаты выдаются только по MD5. Успех.

В итоге, у нас есть буфер из декодированного ключа, и он ксорится с MD5(email). Длина буфера равна 16. Далее вот такой интересный код:

      v32 = 0;
      buf_1 = buf;
      v33 = (char *)dword_40CDC0 - buf_md5;
LABEL_12:
      v35 = 0;
      v34 = 0;
      v36 = 0x67452301;
      v37 = 0xEFCDAB89;
      v38 = 0x98BADCFE;
      v39 = 0x10325476;
      MD5(3u, (int)&v34, buf_1);
      MD5_finalize((int)&v34, (int)buf_md5);
      v22 = 16;
      buf_md5_1 = buf_md5;
      while ( *(_DWORD *)&buf_md5_1[v33] == *(_DWORD *)buf_md5_1 )
      {
        v22 -= 4;
        buf_md5_1 += 4;
        if ( v22 < 4 )
        {
          v33 += 16;
          buf_1 += 3;
          if ( ++v32 < 5 )
            goto LABEL_12;

Трудно разобраться с ходу, но на самом деле тут берётся md5 3 байтов из буфера и сравнивается с захардкоженным хешом, и так происходит 5 раз.

После этого остаётся один байт в буфере (так как длинга буфера равна 16), и для него происходит следующая проверка:

          v25 = 0;
          v26 = 0;
          v30 = 0;
          for ( i = 0; ; i = v31 )
          {
            v25 += email_md5[k];
            v26 += email_md5[k + 3];
            v31 = email_md5[k + 1] + i;
            v29 = email_md5[k + 2] + v30;
            k += 4;
            v30 = v29;
            if ( k >= 16 )
              break;
          }
          return v31 + v29 + v26 + v25 == buf[15];

Последний байт должен быть равен сумме байтов в хеше email по модулю 256.

В итоге, для нахождения ответа нужно сбрутить заданные 5 хешей, получить последний байт как сумму хеша, и мы получим правильное значение проксоренного буфера. Дальше мы его опять ксорим с MD5(email), немного преобразуем и получаем правильный код для нашей почты. Ниже приведён готовый вариант кейгена (нужно учесть, что для его работы требуется ~2.2 ГБ оперативной памяти из-за предподсчёта MD5 для всех последовательностей из 3 байт).

#!/usr/bin/env python3
import struct
from hashlib import md5
mail = 'ul7r4_70p_h4ck3r@1337.com'
h = md5()
h.update(mail.encode())
mail_hash = h.digest()
# brute
m = {}
for i in range(0x1000000):
s = struct.pack('<I', i)[:3]
h = md5()
h.update(s)
m[h.digest()] = s
md5s = ['CD64341F68CEDE586C9693EDC505F378', '05D34B65E96F1D39CCB584C00372D8A3', 'A15B7EBAAD915326241DFB5B1BBC546F', '3BA00294E441051554FD64A729114FC9', '8D0AA0050DC9D654200A3D1649DE9D36']
md5s = list(map(bytes.fromhex, md5s))
xored = b''.join(m[x] for x in md5s) + struct.pack('<B', sum(mail_hash) % 256)
key = bytes([x ^ y for x, y in zip(xored, mail_hash)])
print('mail:', mail)
print('key:', ''.join('%08X' % x for x in struct.unpack('>IIII', key)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment