Хотели порешать эти ваши реверс-инжиниринги на CTF? Да ещё и на C++ с Windows Forms? К тому же чтобы он был не сложным и в райтапе были объяснения? Тогда вам сюда ?

Ссылка на задание (файлы): нажми на меня :)

В архиве будет лежать cross_cr.exe. Флаг должен быть формата CODEBY{FLAG}.

План статьи

  1. Статический анализ файла

    1. Detect It Easy

      • Карта памяти

      • Энтропия

      • Строки

      • Импорты

      • Анализ заголовков

      • Виртуализация

      • Ресурсы

    2. Resource Hacker

    3. Hex-редактор

  2. Динамический анализ

  3. Анализ в IDA

    1. Поиск сообщения о неправильном флаге

    2. Анализ функции и начало сбора флага

    3. Составляем "тело флага"


1. Статический анализ файла

1.1 Detect It Easy

Начнём анализ исполняемого файла с помощью утилиты Detect It Easy (далее DIE).

DIE не обнаружил никаких упаковщиков/протекторов и прочих средств защиты. Сам таск свежий, если верить отметки времени. Кроме того он с графическим интерфейсом.

Посмотрим информацию о файле через продвинутый анализ.

Карта памяти

В карте памяти аномалий не выявлено.

Энтропия

Файл кажется сжатым, но если смотреть по секциям, то наиболее сжатый по энтропии - это секция ресурсов (.rsrc). Пока что просто запомним это. Возможно, далее нам это пригодится.

Строки

Поиск по строкам дал нам возможные ориентиры, на места в коде с выводом сообщение о флаге и о проигрыше. Также названия некоторых функций и пока что непонятное MASTER_OF_CODEBY.

Импорты

Из импортов чего-то "слишком криминального" не выявлено. Но есть IsDebuggerPresent.

Примечание: Хоть функция IsDebuggerPresent и есть в программе, но она не используется в основном коде для изменения поведения таска во время отладки. Поэтому нам будет проще.

Анализ заголовков

По заголовкам всё выглядит нормально для EXE-файла.

Виртуализация

Бывает полезно посмотреть таким образом на файл. По графическому представлению видно, что ресурсы занимают больше всего места в этом исполняемом файле.

Ресурсы

Теперь понятно, откуда такой размер. Это PNG-файл. Посмотрим более детально в Resource Hacker.

1.2 Resource Hacker

Это иконка. Если смотреть на неё в Hexdump'е (Binary View) ничего странного не видно.

1.3 Hex-редактор

Иногда в конце исполняемых файлов некоторых тасков что-то можно найти. Но тут ничего странного нет.


2. Динамический анализ

Динамический анализ в Autoruns, Process Monitor, Process Hacker и Wireshark не показал ничего необычного.


3. Анализ в IDA

Примечание: далее будет использована IDA Freeware 8.4. Её хватит с головой :)
Но вы также можете использовать Ghidra, Cutter (Radare2) или любой другой отладчик, который вам нравится. Код в декомпиляторах будет +- похожим.

3.1 Поиск сообщения о неправильном флаге

Попробовав запустить файл и ввести что-то в поля для ввода, мы наблюдаем вызов функции MessageBox с текстом о неправильном флаге.

Запустим IDA и попробуем найти код, что вызывает этот MessageBox. Перейдём в окно Strings (Shift + F12).

Русские символы по умолчанию не отображаются. Для этого нужно нажать правую кнопку мыши (далее ПКМ), затем Setup и выбрать такие пункты меню.

Опция Unicode нужна, чтобы нормально отображались символы из 16-битных кодировок в стандарте Unicode. Строки в 16-битной кодировке активно используются в WinAPI.
Галочку со Strict ASCII мы убираем для поиска русских символов (в том числе для опции Unicode). Но для нормального отображения нам нужно зайти ещё сюда.

Затем тут выставляем cp1251. Это стандартная 8-битная кодировка для русских символов в Windows.
Далее нажимаем Rebuild и видим русские символы.

Первых символов не видно, но понять можно. Жмём на "хх..." (от Эхх...).

Нужно представить эти байты как строку. Жмём ALT+A, затем сюда.

Байты представились как текст в кодировке UTF-16LE (UTF-16 Little Endian). Наводим курсор на название строки (aE) И нажимаем X, чтобы IDA показала нам, где используется эта строка. Жмём левой кнопкой мыши (далее ЛКМ) на пункт меню со скрина.

Видим 2 блока, ведущих к MessageBoxW. Обычно это блоки о неправильном/правильном флаге. Переменные unk на скриншотах - это русские строки. Их тоже можно представить в нормальном виде.

Через клавишу N можно поменять название ноды графа, наведя курсор на его первую инструкцию. Кроме этого можно задать цвет через ПКМ или специальную иконку.

Более того мы сразу попали на функцию проверки ввода.

3.2 Анализ функции и начало сбора флага

Составим краткий "скелет" функции через декомпилятор - клавиша F5. В IDA Freeware онлайн-декомпилятор, поэтому иногда он может быть недоступен. Учтите это, если будете пробовать сами.

Через клавишу N можно менять названия объектов.

Убедиться, что эта функция используется для проверки нашего ввода можно, если навести курсор на её название и нажать X. А далее посмотреть, где вызывается.

Эту функцию можно назвать так. Она обрабатывает сообщения от GUI-окна. По коду 0x111 (WM_COMMAND) вызывается наша функция проверки ввода.

На check_input и сосредоточим внимание. Вывод декомпилятора:

int check_input()
{
  __int64 v0; // rdi
  __int64 v1; // rax
  __int64 v2; // rsi
  WCHAR *v3; // rbx
  int v4; // esi
  int v5; // ebx
  int v6; // ebp
  int result; // eax
  int v8; // edx
  UINT v9; // r9d
  const WCHAR *v10; // r8
  const WCHAR *v11; // rdx
  WCHAR String1[256]; // [rsp+20h] [rbp-418h] BYREF
  WCHAR String[256]; // [rsp+220h] [rbp-218h] BYREF

  memset(String, 0, sizeof(String));
  GetWindowTextW(hWnd, String, 256);
  memset(String1, 0, sizeof(String1));
  GetWindowTextW(qword_140005670, String1, 256);
  v0 = -1LL;
  v1 = -1LL;
  do
    ++v1;
  while ( String1[v1] );
  if ( v1 == 27 && !wcsncmp(String1, L"CODEBY{", 7uLL) && String1[26] == 125 )
  {
    v2 = 11LL;
    v3 = &String1[13];
    while ( *(v3 - 2) == 45 && iswdigit(*(v3 - 1)) && iswdigit(*v3) && iswdigit(v3[1]) && iswdigit(v3[2]) )
    {
      v2 += 5LL;
      v3 += 5;
      if ( v2 > 24 )
      {
        v4 = wtoi(&String1[7]);
        v5 = wtoi(&String1[12]);
        v6 = wtoi(&String1[17]);
        result = wtoi(&String1[22]);
        v8 = result;
        while ( String[v0 + 1] == aMasterOfCodeby[v0 + 1] )
        {
          v0 += 2LL;
          if ( v0 == 17 )
          {
            if ( (((v4 ^ 0xDFAF7) + 22098798) ^ 0x23B97B) == 24947582 && (((v5 ^ 0x378) + 1361) ^ 0xB84C) == 40468 )
            {
              result = (v6 - 9283) ^ 0xA808;
              if ( result == -47487 && (((v8 ^ 0xFD836) - 13112) ^ 0xBC3F) == 988548 )
              {
                v9 = 0;
                v10 = L"Флаг!!!";
                v11 = L"Флаг, поздравляю! :)";
                return MessageBoxW(0LL, v11, v10, v9);
              }
            }
            return result;
          }
          result = String[v0];
          if ( result != aMasterOfCodeby[v0] )
            goto fail;
        }
        break;
      }
    }
  }
fail:
  v9 = 16;
  v10 = L"Эхх...";
  v11 = L"Ну, почти...";
  return MessageBoxW(0LL, v11, v10, v9);
}

Самые первые инструкции получают данные из полей с вводом имени и флага:

  memset(String, 0, sizeof(String));
  GetWindowTextW(hWnd, String, 256);
  memset(String1, 0, sizeof(String1));
  GetWindowTextW(qword_140005670, String1, 256);

В дизассемблированном виде код выглядит так (вывод декомпилятора полезно проверять на листинге дизассемблера):

Сразу дадим нормальные имена, чтобы повысить читаемость. Через отладку можно понять, где будет что.

В данном случае в поле имени было введено "qwe".

Следующая часть кода выполняет подсчёт длины строки с флагом:

  v0 = -1LL;
  v1 = -1LL;
  do
    ++v1;
  while ( flag[v1] );
  if ( v1 == 27...

Листинг дизассемблера:

Флаг должен быть длиною в 27 символов.

Следующий кусок кода:

Первое условие из выделения со скрина проверяет, что начальные 7 символов флага - это CODEBY{. Второе условие из выделения проверяет, что код символа с индексом 26 (по счёту 27-ой) равен 125. В таблице ASCII это символ }.

Листинг дизассемблера:

Таким образом сейчас у нас есть следующее представление флага:

Следующий блок кода:

Листинг дизассемблера:

Данный блок кода проверяет часть флага между фигурными скобками {}. Она должна быть такого вида:

-XXXX-XXXX-XXXX

Где X - это цифра в 10-ой системе.

Немного странно, что в этой проверке упущена часть перед первым -, это мы поймём далее.

Затем идёт такой блок:

Он преобразует строку с числом между символами - в число.

Листинг дизассемблера:

Следующий блок кода сравнивает введённое имя со строкой MASTER_OF_CODEBY. Это будет правильным именем для дальнейшего решения таска.

Листинг дизассемблера:

3.3 Составляем "тело флага"

Посмотрим на следующий блок:

Он выполняет шифрование чисел через простые операции и сравнивает их с нужными.

Листинг дизассемблера:

Перепишем это в более понятном виде:

first_decimal ^= 0xDFAF7;
first_decimal += 0x151336E;
first_decimal ^= 0x23B97B;
// first_decimal должен быть 0x17CAB7E

second_decimal ^= 0x378;
second_decimal += 0x551;
second_decimal ^= 0xB84C;
// second_decimal должен быть 0x9E14

third_decimal -= 0x2443;
third_decimal ^= 0xA808;
// third_decimal должен быть -47487 (0xFFFF4681)

fourth_decimal ^= 0xFD836;
fourth_decimal -= 0x3338;
fourth_decimal ^= 0xBC3F;
// fourth_decimal должен быть 0xF1584

Для удобства я не использовал копии чисел, как показал декомпилятор.

Теперь останется просто применить противоположные операции в обратном порядке к нужным значениям.

  • Для операции + обратная операция -.

  • Для операции - обратная операция +.

  • Для операции xor обратная операция xor.

first_decimal = 0x17CAB7E;
first_decimal ^= 0x23B97B;
first_decimal -= 0x151336E;
first_decimal ^= 0xDFAF7;
// (((0x17CAB7E ^ 0x23B97B) - 0x151336E) ^ 0xDFAF7) = 9312

second_decimal = 0x9E14;
second_decimal ^= 0xB84C;
second_decimal -= 0x551;
second_decimal ^= 0x378;
// (((0x9E14 ^ 0xB84C ) - 0x551 ) ^ 0x378) = 8831

third_decimal = 0xFFFF4681;
third_decimal ^= 0xA808;
third_decimal += 0x2443;
// ((0xFFFF4681 ^ 0xA808) + 0x2443) = 4294972108

fourth_decimal = 0xF1584;
fourth_decimal ^= 0xBC3F;
fourth_decimal += 0x3338;
fourth_decimal ^= 0xFD836;
// (((0xF1584 ^ 0xBC3F) + 0x3338) ^ 0xFD836) = 1221

Итого наши части флага:

9312-8831-4294972108-1221

Общий флаг:

CODEBY{9312-8831-4294972108-1221}

Третья часть очень выделяется. И проверку не пройдёт. Если посмотреть листинг дизассемблера для функции шифрования, то данные там хранятся в 4-байтовых регистрах.

Представим число 4294972108 в 16-ом виде:

0x1000012CC
0x01 00 00 12 CC (по байтам)

Это 5-байтовое число. А регистры 4-байтовые. 5-байтовое число в 4-байтовый регистр никак не влезает. Поэтому в процессе шифрования первые лишние байты просто отсекаются. В данном случае это байт 0x01 в самом начале:

0x00 00 12 CC (по байтам)

Получили 0x000012CC. Незначащие байты (0x0000) можно убрать и получить 16-ое число. Это 16-ое число переводим в 10-е. Далее оформляем флаг и сдаём.

Делиться открытым флагом не буду, дальше вы сами добьёте таск! :)

Спасибо за прочтение статьи! ?

Комментарии (0)