Codeby Games [WriteUp] Крестики | Реверс-инжиниринг

d15f87a56c149adbf73bbfda5bfa486d.png


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

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

9c53b359cf40167fdffd6847a3f32f01.png


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

План статьи​

  1. Статический анализ файла
    1. Detect It Easy
      • Карта памяти
      • Энтропия
      • Строки
      • Импорты
      • Анализ заголовков
      • Виртуализация
      • Ресурсы
    2. Resource Hacker
    3. Hex-редактор
  2. Динамический анализ
  3. Анализ в IDA
    1. Поиск сообщения о неправильном флаге
    2. Анализ функции и начало сбора флага
    3. Составляем "тело флага"

5140f84e4b34b1ca3beb796533f2c138.png




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

1.1 Detect It Easy

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

caae8c4e54f327f442dcb33c077f31a2.png
d95281e983712e0d169a19ef64306dd2.png


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

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

Карта памяти

09449bf484828098c271fdac5896b2a7.png


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

Энтропия

f8cef5dc8fb8a76a544c608008d3ae82.png


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

Строки

aca7b6a2fa0009fd8fa3d958c3a178e8.png


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

Импорты

d72740a677939d9cec2a96990c679239.png


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

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

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

c4a9fb5aeb2bea6933a4d43967a5f3c9.png
5531cafe8e0d9192a0aa009ac227b0b7.png


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

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

949e2a06621d417afd1cc3d3c1929f3a.png


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

Ресурсы

c4d272536fab13d8646926c5821067e1.png


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

1.2 Resource Hacker

3e47cd32f88f9439e4273c507e5b2f8f.png


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

1.3 Hex-редактор

ffe26e64e6aafeaba5ab5e10f2f87017.png


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

959b5f462ea4958ba3d51085f7df55b1.png




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

cee7ceb451fe70ab1a6a0037dec0da74.png
15ece79fd53aa463d7165cd945ba693c.png


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



3. Анализ в IDA

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

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

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

08f3a0a38405b8c857031f6c2507755b.png


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

dba7562f863a40f1b3994a8a3392bb07.png


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

d71ad4959fb93a4c5f91451a3f19a53f.png
6255ad7c5a2cbce677b6de5276768ec1.png


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

849d5e45ef19a8e8921f9daf7d5f392c.png
43b58c23917ceb1ddce60f3464cf0354.png


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

89c90a2b78232b5ec9da41ea05985a74.png
f03c2aab0a6feaf7f2ba12901d0ed364.png


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

da4c80a52048586bfcce9c5589844d90.png
64723ad4da9ee55db51570c448175150.png


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

8c0d86d41a70a84797aa7350245b85cb.png
8a7beff781bf67136e6cb84dcadcbdc2.png


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

ce122e549bf0e13ac97d984463f684b8.png


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

e89c448d8830f5f03394e2b7f13cfd23.png


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

ff81c082c99193a56a6aaec4a94b6880.png
36c3b15fec426053a76fdc8b015c5709.png


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

fed4a87646ea10f30c8ff9cbf0271a40.png
dc45f2c5d9cacfe5b76308baa85dcb7c.png

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

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

fb641234937882a848547fb2728e6ef7.png


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

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

f69fcc702d3a6d6cc105b0909fddcff0.png
9105b9dbd3dfe2e8a153fda6355e364c.png
87c5fbca4d292a256ecf04cf1701851d.png


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

2502f0aa68b853284b2f7c9d2fce0dc5.png


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

C++:
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);
}

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

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

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

66df9b874e6396cde36c17a4b27c405e.png


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

72446e76c7c8ad93f076d2c8ba841f02.png


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

fa9a5460f6997444da2ff46f4b244c6b.png


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

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

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

1409b82d6b9907bfe8f9c0971fa6b8af.png


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

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

14f3c157f418d1b42854fdcd8cb42a6e.png


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

fc5f1a7b61e62783b5a23e7f2190438b.png


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

b79433761f733acfa124aecd46b31755.png


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

ef9cf2c5e3a7273d3161fca1a1c1f544.png


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

0e4987abe28afdee9bf3b5eb308c7460.png


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

efe18e7035c53cb6aebb1e2ca551c96a.png


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

Код:
-XXXX-XXXX-XXXX

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

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

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

9cad3ddf389e9b67f43c6ccd68ecce21.png


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

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

be6b6f21cd206c2ddb79ba2200654346.png


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

65b5e56e1b204a6eef129ee8e4d14394.png
1d12baf08bb42fd603bbf1640081398e.png


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

706a95c785d394bb2cc1f707f62c30b2.png

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

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

59ec3767180b85c8b7f0c8f5e84f203d.png


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

0d30fc0cf39a83cfd565cf500e256129.png


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

f0124e1c0286213a4912fea803256ff4.png


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

C++:
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.
C++:
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-байтовых регистрах.

7ebb9218c01b6fe32ac871ad451a26cc.png


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

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

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

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

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

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

c594a4c4fe4362a1c1d8a879d9dcdab1.png


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

abstract abstract

Green Team
12.07.2023
15
3
BIT
189
О, классно!
Не знал как русские символы отобразить и поэтому не мог найти шифрование флага, а дальше сам всё решил не читая дальше статью :)
Но за раскрытие темы спасибо!
 
  • Нравится
Реакции: ROP

abstract abstract

Green Team
12.07.2023
15
3
BIT
189
На счёт кстати третьего ключа, решил брутфорсом. Метод глупый конечно, но рабочий

Python:
>>> for i in range(0, 9999):
...     if hex((0x100000000 + i - 9283) ^ 0xa808) == '0xffff4681':
...             print(i)
...
 
  • Нравится
Реакции: ROP

Niapoll

New member
14.11.2023
1
0
BIT
182
Перепишем это в более понятном виде
Пользователей дизассемблера Ghidra будет ждать приятный сюрприз (в данном случае):
C:
if ((((iVar1 != 0x2460) || (iVar2 != 0x227f)) || (iVar3 != 0x12cc)) || (iVar4 != 0x4c5))
P. S. Поэтому я даже сначала и не понял о каком "шифровании" вообще идет речь в статье :)
 

ROP

Red Team
27.08.2019
320
636
BIT
102
Пользователей дизассемблера Ghidra будет ждать приятный сюрприз (в данном случае):
C:
if ((((iVar1 != 0x2460) || (iVar2 != 0x227f)) || (iVar3 != 0x12cc)) || (iVar4 != 0x4c5))
P. S. Поэтому я даже сначала и не понял о каком "шифровании" вообще идет речь в статье :)
Ого! Но такое и в IDA бывает, к сожалению. Декомпилятор не идеален и нужно в дизассемблере проверять :)
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!