Всем привет!
Решил поделиться с вами подробным описанием решения довольно интересной задачи "random??" из раздела "Реверс-инжиниринг" на платформе Codeby Games.Исходные данные:
- exe-файл encrypter.exe
- зашифрованный текст flag.crypt
Исходя из логики, нам необходимо разобрать, как же работает программа-шифратор, и эти знания применить для расшифровки шифротекста, в котором скорее всего и спрятан флаг (не просто так же нам его дали). Что ж, приступим.
Распаковка
Для анализа бинарных файлов я (как и многие) предпочитаю IDA Pro. Открываем наш бинарьencrypter.exe
и видим какую-то хренотень, а не код программы:Попробуем запустить наш бинарь - получаем ошибку. Мда, действительно хренотень…
Штош, попробуем посмотреть на данный бинарь в HEX-редакторе. Можно выбрать и скачать любой редактор, но зачем, если есть
Ссылка скрыта от гостей
?Первое, что бросается нам в глаза (красные области) – «битые»
Ссылка скрыта от гостей
Ссылка скрыта от гостей
– 6D 7A
(mz
) вместо 4D 5A
(MZ
).Второе, что бросается в глаза – сигнатуры
UPX0
, UPX1
и UPX2
(желтая область). Глядя на них, понимаем, что белиберда в IDA – результат упаковки программы при помощи UPX.Меняем magic bytes на правильные:
Пытаемся распаковать наш бинарь при помощи UPX и с удивлением обнаруживаем, что он вроде как и не запакован.
Учитывая битую сигнатуру DOS Executable, предположим, что и для UPX сигнатура могла быть тоже повреждена.
Внимательно смотрим на
Ссылка скрыта от гостей
и видим, что одна из них - UPX!
(т.н. UPX_MAGIC_LE32
).Видим, что через 36 байтов после
UPX2
идет версия упаковщика (4.01) и байты 43 44 42 21
(CDB!
). Меняем их на 55 50 58 21
(UPX!
)Вроде, все поправили. Распаковываем бинарь через UPX:
Код:
> upx.exe -d encrypter.exe
PS C:\> C:\bin\upx-4.0.2-win64\upx.exe C:\temp\encrypter.exe -d
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2023
UPX 4.0.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 30th 2023
File size Ratio Format Name
-------------------- ------ ----------- -----------
22016 <- 10752 48.84% win64/pe encrypter.exe
Реверс
Снова открываем распакованный файл в IDA Pro, жмакаем на функциюmain
и открываем С-код по кнопке F5.main
Смотрим, что у нас тут есть:
Исходя из строк вывода (13, 14) понимаем, что указатель
Stream
- это объект шифруемого файла, который читается (23) и данные записываются из него в массив char Buffer
(29). Примечательно, что из шифруемого файла считываются только 100 символов == 100 байт, при этом наш файл с шифротекстом flag.encrypt
занимает аж 15 208 байт. Запомним это несоответствие.После того как шифратор считал данные в переменную
Buffer
(29), происходит вызов трех функций:sub_401BA6(Buffer, 100i64)
sub_4015EF(Buffer, 100i64)
sub_402314(Block, 100i64, argv[1])
Прежде чем идти дальше, мы можем обратить внимание на то, что размер переменной
Buffer
- 96 байт, а считываем в нее мы 100 байт. Это могло бы нас насторожить, но нет - после переменной Buffer
идет неиспользуемая переменная v5
размером 4 байта (такой размер имеет тип int). В сумме получается как раз 100 байт. Так что, все OK, это просто особенность IDA трансляции ассемблерных инструкций в код на C.sub_401BA6
Открываем функциюsub_401BA6
и видим внутри еще две
C:
1 __int64 __fastcall sub_401BA6(__int64 a1, unsigned int a2)
2 {
3 sub_4016AD(a1, a2);
4 return sub_401A37(a1, a2);
5 }
Опять начинам с верхней -
sub_4016AD
.sub_4016AD
C:
1 __int64 __fastcall sub_4016AD(__int64 a1, int a2)
2 {
3 __int64 result; // rax
4 unsigned int i; // [rsp+2Ch] [rbp-4h]
5
6 for ( i = 0; ; ++i )
7 {
8 result = i;
9 if ( (int)i >= a2 )
10 break;
11 dword_408970 = time64(0i64) ^ 0xCDBABC;
12 srand(dword_408970);
13 *(_BYTE *)(a1 + (int)i) ^= rand() % 255;
14 dword_40897C = time64(0i64) - 4919;
15 srand(dword_40897C);
16 *(_BYTE *)(a1 + (int)i) += dword_40897C ^ 0x15;
17 dword_408978 = time64(0i64) + 48879;
18 srand(dword_408978);
19 *(_BYTE *)(a1 + (int)i) -= rand() % 14;
20 dword_408974 = time64(0i64) / 2;
21 srand(dword_408974);
22 *(_BYTE *)(a1 + (int)i) += rand() % 100;
23 }
24 return result;
25 }
Смотрим на функцию и первое, что нам бросается в глаза, - рандомы.
Вспомним, что передавалось в эту функцию:
__int64 a1
- указатель на массив char'овBuffer
с исходными даннымиint a2
- количество данных в массивеBuffer
- 100.
Разбираем по порядку:
C:
6 for ( i = 0; ; ++i )
7 {
8 result = i;
9 if ( (int)i >= a2 )
10 break;
a2
была передана константа 100
- длина исходных данных).
C:
11 dword_408970 = time64(0i64) ^ 0xCDBABC;
12 srand(dword_408970);
0xCDBABC
и получившимся в итоге значением
Ссылка скрыта от гостей
псевдорандом.
C:
13 *(_BYTE *)(a1 + (int)i) ^= rand() % 255;
Тут происходит добавление к указателю на массив с исходными данными
a1
значения итератора i
, преобразование его в указатель на тип BYTE и обращению к памяти по полученному адресу.В правой части:
Тут вызовом функции
rand()
Ссылка скрыта от гостей
псевдослучайное значение, от которого которого оставляется только последний байт а остальные байты обнуляются путем ^=
операция XOR применяется только к одному выбранному байту, несмотря на то, что rand()
выдает нам целых 4 байта.В общем случае размер возвращаемого значения указывается в stdlib.h.
Фактически эта строчка означает следующее:
C:
13 Buffer[i] = Buffer[i] ^ (rand() % 255);
В остальных участках повторно три раза происходят аналогичные действия - генерация инцициализирующего значения, инцициализация псевдорандома и изменение текущего байта.
C:
14 dword_40897C = time64(0i64) - 4919;
15 srand(dword_40897C);
16 *(_BYTE *)(a1 + (int)i) += dword_40897C ^ 0x15;
17 dword_408978 = time64(0i64) + 48879;
18 srand(dword_408978);
19 *(_BYTE *)(a1 + (int)i) -= rand() % 14;
20 dword_408974 = time64(0i64) / 2;
21 srand(dword_408974);
22 *(_BYTE *)(a1 + (int)i) += rand() % 100;
Итого
Фнукцияsub_4016AD
принимает на вход массив с исходными данными и к каждому значению четыре раза применяет операцию XOR с четырьмя разными значениями (три из которых псевдослучайные).
Тут у нас сразу возникает вопрос - если мы будем писать дешифратор, то откуда мы возьмем инициализирующие значения для наших рандомов -
dword_408970
, dword_40897C
, dword_408978
, dword_408974
? Пока что не ясно. Оставим этот вопрос на потом, а пока продолжим дальше разбираться с логикой работы шифратора.sub_401A37
C:
1 __int64 __fastcall sub_401A37(__int64 a1, int a2)
2 {
3 __int64 result; // rax
4 int v3; // [rsp+24h] [rbp-Ch]
5 int v4; // [rsp+28h] [rbp-8h]
6 unsigned int i; // [rsp+2Ch] [rbp-4h]
7
8 Seed = time64(0i64) ^ 0xDEAD;
9 srand(Seed);
10 for ( i = 0; ; ++i )
11 {
12 result = i;
13 if ( (int)i >= a2 )
14 break;
15 v4 = rand() % 255;
16 v3 = rand() % 255;
17 *(_BYTE *)(a1 + (int)i) += sub_4018AB((unsigned int)(char)v4) - v3;
18 }
19 return result;
20 }
Становится понятно, почему таска так называется
Видим, что тут операции идентичны предыдущей функции - опять происходит вычисление инициализирующего значения на основе текущего времени,
C:
8 Seed = time64(0i64) ^ 0xDEAD;
9 srand(Seed);
C:
10 for ( i = 0; ; ++i )
11 {
12 result = (unsigned int)i;
13 if ( i >= a2 )
14 break;
15 v4 = rand() % 255;
16 v3 = rand() % 255;
17 *(_BYTE *)(a1 + i) += sub_4018AB((char)v4) - v3;
18 }
Обращаем внимание, что одно из псевдослучайных значений -
v4
- не используется непосредственно в преобразовании, а передается в функцию sub_4018AB
.Функция в функции в функции.
Тут опять же возникает вопрос о том, что нам делать с неизвестным сидом для рандома. Опять отложим этот вопрос на потом.
sub_4018AB
Штош, посмотрим, что у нее внутри.
C:
1 __int64 __fastcall sub_4018AB(int a1)
2 {
3 struct tagRECT Rect; // [rsp+60h] [rbp-40h] BYREF
4 struct _PEB *v3; // [rsp+70h] [rbp-30h]
5 int v4; // [rsp+78h] [rbp-28h]
6 HDC hdcDest; // [rsp+80h] [rbp-20h]
7 HWND hWnd; // [rsp+88h] [rbp-18h]
8 struct _PEB *v7; // [rsp+90h] [rbp-10h]
9 unsigned int NtGlobalFlag; // [rsp+9Ch] [rbp-4h]
10
11 NtGlobalFlag = 0;
12 v4 = 96;
13 v3 = NtCurrentPeb();
14 v7 = v3;
15 NtGlobalFlag = v3->NtGlobalFlag;
16 if ( (NtGlobalFlag & 0x70) != 0 )
17 {
18 sub_401550();
19 sub_403C10(2i64);
20 while ( 1 )
21 {
22 hWnd = GetDesktopWindow();
23 hdcDest = GetWindowDC(hWnd);
24 GetWindowRect(hWnd, &Rect);
25 StretchBlt(
26 hdcDest,
27 50,
28 50,
29 Rect.right - 100,
30 Rect.bottom - 100,
31 hdcDest,
32 0,
33 0,
34 Rect.right,
35 Rect.bottom,
36 0xCC0020u);
37 BitBlt(hdcDest, 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdcDest, 0, 0, 0x330008u);
38 ReleaseDC(hWnd, hdcDest);
39 }
40 }
41 return a1 ^ 0xA3u;
42 }
Непонятно тут все, кроме последней строчки:
C:
41 return a1 ^ 0xA3u;
return
входные данные никак не используются. Нафига тогда весь остальной код нужен? Можно, конечно, забить на него, но мы любопытные. Пробуем загуглить NtGlobalFlag = v3->NtGlobalFlag
и получаем первой же ссылкой
Ссылка скрыта от гостей
на xakep.ru об антиотладке.Вот же хитрюги!
Внимательно читаем
Ссылка скрыта от гостей
про NtGlobalFlag
и понимаем, что в строчке
C:
16 if ( (NtGlobalFlag & 0x70) != 0 )
Что происходит, когда проверка на отладку срабатывает, вы можете узнать самостоятельно, засунув программу в дебагер и протыкав несколько раз F8 .
Настоятельно не рекомендую осуществлять эту проверку эпилептикам!
Пока нам отладка кода не потребовалась, но на всякий случай лучше сразу грохнуть эту антиотладку.
Что нужно чтобы условие
if
никогда не сработало? Правильно, заменить 0x70
на 0x00
, тогда что бы ни было в NtGlobalFlag
, результат операции (NtGlobalFlag & 0x00)
всегда будет равен 0.Смотрим, как выглядят эти строчки кода в ассемблерных инструкциях и в hex:
Код:
.text:00000000004018EA and eax, 70h
.text:00000000004018ED test eax, eax
Инструкции
and eax, 70h
соответствуют байты 83 E0 70
, в которых нам нужно поменять 70
на 00
.Открываем наш шифратор в
Ссылка скрыта от гостей
и ищем эти байты. С удивлением обнаруживаем не одно, а целых 4 вхождения. Возможно, что остальные 3 вхождения - это просто случайные совпадения, но что если создатели таска вкорячили сразу 4 блока кода с антиотладкой?Возвращаемся в HEX-View в IDA Pro, ищем все последовательности байтов
83 E0 70
и находим, что это реально еще 3 блока антиотладки:sub_401BD8
sub_401DC9
sub_402016
Код:
.text:0000000000401C3C and eax, 70h
.text:0000000000401C3F test eax, eax
Код:
.text:0000000000401E93 and eax, 70h
.text:0000000000401E96 test eax, eax
Код:
.text:00000000004020E8 and eax, 70h
.text:00000000004020EB test eax, eax
Возвращаемся в Sublime и с чистой совестью гасим все 4 проверки на отладку, заменяя
83 E0 70
на 83 E0 00
Переоткрываем в IDA измененный файл, заново открываем функцию
sub_4018AB
и с радостью обнаруживаем, что умная IDA заботливо убрала из отображаемого кода все, что было связано с блоком антиотладки. Резонно, ведь этот код стал недостижим для выполнения, так как блок if
никогда выполнится.Теперь эта функция просто-напросто применяет к передаваемому в нее значению операцию XOR с
0xA3
.
C:
1 __int64 __fastcall sub_4018AB(int a1)
2 {
3 return a1 ^ 0xA3u;
4 }
С функциейИтого
Функцияsub_401A37
принимает на вход массив с видоизмененными предыдущей функцией исходными данными и к каждому байту этих данных добавляет число, формируемое на основе двух псевдослучайных байтов -v3
иv4
, к последнему из которых применяется операция XOR со значением0xA3
.
sub_401BA6
разобрались, идем дальше.sub_4015EF
Тут мы встречаем коротенькую функцию, в которой сходу не видим ничего криминального:
C:
1 char *__fastcall sub_4015EF(__int64 a1, int a2)
2 {
3 char Buffer[4]; // [rsp+27h] [rbp-19h] BYREF
4 char v4; // [rsp+2Bh] [rbp-15h]
5 int v5; // [rsp+2Ch] [rbp-14h]
6 char *Destination; // [rsp+30h] [rbp-10h]
7 int i; // [rsp+3Ch] [rbp-4h]
8
9 *(_DWORD *)Buffer = 0;
10 v4 = 0;
11 Destination = (char *)calloc(a2, 4ui64);
12 for ( i = 0; i < a2; ++i )
13 {
14 sprintf(Buffer, "%03hhu", *(unsigned __int8 *)(a1 + i));
15 v5 = strlen(Buffer);
16 strncat(Destination, Buffer, v5);
17 }
18 return Destination;
19 }
sprintf
. Прежде чем разбирать функцию посмотрим, что это за говно такое - "%03hhu"
.Лезем в
Ссылка скрыта от гостей
и в первой же ссылке видим
Ссылка скрыта от гостей
на Stack Overflow, где нам дают ссылку на
Ссылка скрыта от гостей
форматов printf
.Функция
Ссылка скрыта от гостей
идентична
Ссылка скрыта от гостей
, за исключением того, что вывод производится не в консоль, а в массив, указанный аргументом функции.Изучаем данную спецификацию и понимаем, что этот модификатор делает следующее:
Код:
0 → дополнить запись числа символом 0 слева (если будет нехватать значений)
3 → представить аргумент как минимум в 3-х позициях
hh → интерпретировать аргумент как символ
u → интерпретировать аргумент как беззнаковое целое
Примеры:
'A' → 0x41 → 65 → 065 → "065" → 0x30 0x36 0x35 0x00
'z' → 0x7A → 122 → "122" → 0x31 0x32 0x32 0x00
Последний байт
0x00
- конец строки.Вернемся к нашей функции и посмотрим, что же она делает.
Ссылка скрыта от гостей
размером в 4 раза больше, чем размер исходных данных. Указатель на эту память кладется в переменную Destination
:
C:
11 Destination = (char *)calloc(a2, 4ui64);
Каждый байт из исходного массива данных конвертируется в строку из 4 байт по указанному выше алгоритму:
C:
14 sprintf(Buffer, "%03hhu", *(unsigned __int8 *)(a1 + i));
Считается длина получившейся строки:
C:
15 v5 = strlen(Buffer);
Может показаться, что длина у строки Buffer будет равна 4, но нет. Длина будет равна 3, так как нулевой символ
Ссылка скрыта от гостей
Полученная строка
Buffer
(а точнее, только 3 символа из нее)
Ссылка скрыта от гостей
к той строке, которая уже лежит в памяти по указателю Destination
:
C:
16 strncat(Destination, Buffer, v5);
И все это повторяется в цикле для каждого байта данных, ссылка на массив которых была передана в функцию в аргументе
a1
.Итого
Функция берет входящие данные (если быть точным, то указатель на эти данные, но это не столь важно) и каждый байт данных преобразовывает в три байта по алгоритму:
В результате работы функции 100 байт данных превращаются в 300 байт преобразованных данных.
Ссылка скрыта от гостейзначение байта как десятичное беззнаковое число в десятичной системе счисления ("A" → 65)- дополнить числа с двумя разрядам символом 0 слева (65 → 065)
- представить получившееся число как строку (065 → "065")
sub_402314
Первое, что нам бросается в глаза - вызов данной функции вmain
:
C:
32 sub_402314(Block, 100i64, argv[1]);
sub_4015EF
, и константа 100
- размер исходных данных. Странно, как мы только что выяснили, в предыдущей функции должно было сгенерироваться 300 байт данных.Третьим аргументом передается имя файла с исходными данными -
argv[1]
.Смотрим код самой функции:
C:
1 int __fastcall sub_402314(__int64 a1, int a2, const char *a3)
2 {
3 char Buffer[2000]; // [rsp+20h] [rbp-60h] BYREF
4 int v5; // [rsp+7F0h] [rbp+770h]
5 _DWORD v6[501]; // [rsp+7F4h] [rbp+774h] BYREF
6 _DWORD v7[1834]; // [rsp+FC8h] [rbp+F48h] BYREF
7 _DWORD v8[490]; // [rsp+2C70h] [rbp+2BF0h] BYREF
8 _DWORD v9[140]; // [rsp+3418h] [rbp+3398h] BYREF
9 _DWORD v10[336]; // [rsp+3648h] [rbp+35C8h] BYREF
10 FILE *Stream; // [rsp+3B88h] [rbp+3B08h]
11 char *Destination; // [rsp+3B90h] [rbp+3B10h]
12 int v13; // [rsp+3B98h] [rbp+3B18h]
13 int i; // [rsp+3B9Ch] [rbp+3B1Ch]
14 int v17; // [rsp+3BC8h] [rbp+3B48h]
15
16 v13 = strlen(a3) + 2;
17 Destination = (char *)malloc(v13 + 10i64);
18 strncpy(Destination, a3, v13);
19 strcat(Destination, ".encrypt");
20 Stream = fopen(Destination, "wb");
21 if ( !Stream )
22 {
23 puts("Error opening the file!");
24 sub_40168E();
25 }
26 ((void (__fastcall *)(char *, __int64))sub_401DC9)(Buffer, 2000i64);
27 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v6, 2000i64);
28 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v8, 1550i64);
29 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v9, 555i64);
30 ((void (__fastcall *)(_DWORD *, __int64))sub_402016)(v7, 7331i64);
31 ((void (__fastcall *)(_DWORD *, __int64))sub_402016)(v10, 1337i64);
32 v5 = dword_408970;
33 v6[500] = dword_40897C;
34 v7[1833] = dword_408978;
35 v8[388] = dword_408974;
36 v8[489] = unk_408980;
37 v9[139] = Seed;
38 v10[335] = dword_408034;
39 v17 = 4 * a2;
40 for ( i = 0; i < v17; ++i )
41 *((_BYTE *)&v8[389] + i) = *(_BYTE *)(a1 + i);
42 sub_40226B();
43 fwrite(Buffer, 0x3B68ui64, 1ui64, Stream);
44 return fclose(Stream);
45 }
В самом начале функции мы видим объявление кучи больших массивов данных
Buffer[2000]
, v6[501]
, v7[1834]
, v8[490]
, v9[140]
, v10[336]
. Вспоминаем то, что мы подметили в самом начале - из исходного файла с данными считываются всего 100 символов, а наш шифротекст имеет размер 15 208 байт.Посчитаем суммарный объем всех объявленных в данной функции массивов, учитывая, что размер
DWORD
- 4 байта, а размер char
- 1 байт:2000*1 + 1*4 + 501*4 + 1834*4 + 490*4 + 140*4 + 336*4 = 15208
Совпадает. Значит, скорее всего отсюда и взялся наш размер итогового шифротекста.
Теперь внимательно смотрим, на то, что происходит с данными, передаваемыми в функцию и с удивлением обнаруживаем, что они используются всего в двух местах:
1. Добавление ".encrypt" к названию итогового файла:
C:
17 v13 = strlen(a3) + 2;
18 Destination = (char *)malloc(v13 + 10i64);
19 strncpy(Destination, a3, v13);
20 strcat(Destination, ".encrypt");
2. Копирование входных данных (указатель на которые лежит в
a1
) в переменную v8
:
C:
41 for ( i = 0; i < v17; ++i )
42 *((_BYTE *)&v8[389] + i) = *(_BYTE *)(a1 + i);
Возникает вопрос - а что же тогда делает весь остальной код в функции? Посмотрим по порядку:
C:
27 ((void (__fastcall *)(char *, __int64))sub_401DC9)(Buffer, 2000i64);
28 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v6, 2000i64);
29 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v8, 1550i64);
30 ((void (__fastcall *)(_DWORD *, __int64))sub_401DC9)(v9, 555i64);
31 ((void (__fastcall *)(_DWORD *, __int64))sub_402016)(v7, 7331i64);
32 ((void (__fastcall *)(_DWORD *, __int64))sub_402016)(v10, 1337i64);
Buffer
, v6
, v7
, v8
, v9
, v10
. Приводить их все нет смысла, но вот первая из них:
C:
1 void __fastcall sub_401DC9(__int64 a1, signed int a2)
2 {
3 _BYTE *v2; // [rsp+88h] [rbp-38h]
4 _BYTE *Block; // [rsp+B0h] [rbp-10h]
5 int i; // [rsp+BCh] [rbp-4h]
6 int j; // [rsp+BCh] [rbp-4h]
7
8 Block = malloc(a2);
9 dword_408034 = time64(0i64) - 74565;
10 srand(dword_408034);
11 for ( i = 0; i < a2; ++i )
12 Block[i] = rand() % 255;
13 v2 = (_BYTE *)sub_401BD8(Block, (unsigned int)a2);
14 free(Block);
15 for ( j = 0; j < a2; ++j )
16 *(_BYTE *)(a1 + j) = v2[j];
17 free(v2);
18 }
Смотрим дальше
C:
35 v5 = dword_408970;
36 v6[500] = dword_40897C;
37 v7[1833] = dword_408978;
38 v8[388] = dword_408974;
39 v8[489] = unk_408980;
40 v9[139] = Seed;
41 v10[335] = dword_408034;
Видим, что на эти переменные ведут сразу по несколько линков. Посмотрим, куда они ведут (ПКМ → Jump to xref to operand):
Идем с конца:
- sub_402314 - это наша текущая функция. Листаем дальше
- sub_40226B - тут наша переменная печатается в консоль. понятнее не стало
- sub_4016AD - Опачке! Да это же наша самая первая функция по преобразованию исходных данных! А наша переменная - это тот самый сид, который мы не знали откуда брать.
C:
11 dword_408970 = time64(0i64) ^ 0xCDBABC;
12 srand(dword_408970);
Смотрим остальные сиды - все на месте:
sub_4016AD
v5 = dword_408970;
v6[500] = dword_40897C;
v7[1833] = dword_408978;
v8[388] = dword_408974;
sub_401A37
v9[139] = Seed;
Ну и остается последняя строчка кода, которую нам надо разобрать:
C:
44 fwrite(Buffer, 0x3B68ui64, 1ui64, Stream);
Stream
0x3B68 байтов данных из переменной Buffer
. Как-то многовато для переменной Buffer
, там ведь всего 2000 байтов!Открываем калькулятор и переводим 0x3B68 в десятичную систему - получаем 15208.
Теперь все сходится. Ведь
Buffer
- это всего лишь указатель на начало данных. После того, как закончатся 2000 байтов Buffer
записываться пойдут остальные данные.Итого
Функция генерирует 14808 байтов мусорных данных и куда-то в середочку между ними кладет 300 байтов входных данных. Начало полезных данных надо искать вот тут -v8[389]
.
После этого функция берет значения сидов для рандомов, используемые в предыдущих функциях и записывает их поверх пяти мусорных байтов чтобы они там "затерялись":
C:v5 = dword_408970; // сид из sub_4016AD v6[500] = dword_40897C; // сид из sub_4016AD v7[1833] = dword_408978; // сид из sub_4016AD v8[388] = dword_408974; // сид из sub_4016AD v9[139] = Seed; // сид из sub_401A37
Пишем дешифратор
Теперь, когда логика работы шифратора нам полностью понятна, мы можем написать дешифратор. Для того чтобы не создавать костыли и велосипеды, мы будем писать дешифратор на C/C++. Большая часть кода для нашего дешифратора будет составляться просто копипастом из IDA. Только в местах преобразований данных мы будем менять порядок действий - начинать с нижних действий и заканчивать верхними, а все операции заменять на обратные (сложение на вычитание, вычитание на сложение).Соответственно, порядок преобразований по исходным функциям будет следующий:
1) sub_4015EF
2) sub_401BA6
2.1) sub_401A37
2.2) sub_4016AD
main
Начнем с того, что считаем наш шифротекст и раскидаем его содержимое по тем же переменным, из которых эти данные были записаны в файл.Подгружаем библиотечки и объявляем глобальные переменные с сидами чтобы не заморачиваться с передачей их в функции:
C++:
#include <iostream>
#include <string.h>
#include <windows.h>
DWORD dword_408970 = 0x00000000;
DWORD dword_40897C = 0x00000000;
DWORD dword_408978 = 0x00000000;
DWORD dword_408974 = 0x00000000;
DWORD Seed = 0x00000000;
DWORD unk_408980 = 0x00000000;
DWORD dword_408034 = 0x00000000;
Делаем функцию дешифровки
C++:
int main() {
// Читаем содержимое файла с шифротекстом и складываем считанные данные в массив encryptedData
const char* encryptedFile = "C:/temp/flag.encrypt";
FILE* encryptedFileStream = fopen(encryptedFile, "rb");
char encryptedData[15210];
memset(encryptedData, 0, sizeof(encryptedData));
fgets(encryptedData, sizeof(encryptedData), encryptedFileStream);
fclose(encryptedFileStream);
// Чтобы удобно оперировать данными в encryptedData создаем указатель
DWORD* ptr = (DWORD*)encryptedData;
// Объявляем все те же переменные, что были в функции sub_402314 шифратора
char Buffer[2000];
unsigned int v5 = 0;
DWORD v6[501];
DWORD v7[1834];
DWORD v8[490];
DWORD v9[140];
DWORD v10[336];
// Раскидываем байты из шифротекста по тем же переменным, в которых они хранились в шифраторе
memcpy(Buffer, ptr, 2000); ptr += sizeof(Buffer)/4;
memcpy(&v5, ptr, 4); ptr += sizeof(v5)/4;
memcpy(v6, ptr, sizeof(v6)); ptr += sizeof(v6)/4;
memcpy(v7, ptr, sizeof(v7)); ptr += sizeof(v7)/4;
memcpy(v8, ptr, sizeof(v8)); ptr += sizeof(v8)/4;
memcpy(v9, ptr, sizeof(v9)); ptr += sizeof(v9)/4;
memcpy(v10, ptr, sizeof(v10)); ptr += sizeof(v10)/4;
// Раскидываем значения сидов для рандомов по переменным
dword_408970 = v5;
dword_408974 = v8[388];
unk_408980 = v8[489];
Seed = v9[139];
dword_40897C = v6[500];
dword_408978 = v7[1833];
dword_408034 = v10[335];
// Подготавливаем память для немусорных байтов шифротекста
unsigned int dataBlockLength = 100;
int v17 = 4 * dataBlockLength;
int i;
char* dataBlock = (char*)calloc(dataBlockLength, 4ui64);
void* SourceBuffer;
// И копируем туда соответствующие данные
for (i = 0; i < v17; ++i)
*(BYTE*)(dataBlock + i) = *((BYTE*)&v8[389] + i);
// Выполняем первый этап преобразований (последний в шифраторе)
SourceBuffer = sub_4015EF_decrypt((__int64)dataBlock, 100);
// Выполняем последний этап преобразований (первый в шифраторе)
sub_401BA6_decrypt((__int64)SourceBuffer, 100);
// Так как в исходных данных всего 100 байт, то нет смысла писать их в файл и мы просто выводим их на экран
for (i = 0; i < 100; ++i){
std::cout << *(unsigned __int8*)((__int64)SourceBuffer + (int)i);
}
return 0;
}
sub_4015EF_decrypt
Пишем функцию обратную кsub_4015EF
.Ничего примечательного, просто меняем функцию
sprintf()
на обратную ей - sscanf()
:
C++:
char* __fastcall sub_4015EF_decrypt(__int64 dataPointer, int dataLength)
{
char Buffer[3];
char Ch[1];
*(char*)Buffer = 0;
*(char*)Ch = 0;
char* Destination;
int i;
Destination = (char*)calloc(dataLength, 4ui64);
for (i = 0; i < dataLength; ++i) {
memcpy(Buffer, (unsigned __int8*)dataPointer + i*3, 3);
sscanf(Buffer, "%03hhu", Ch);
strncat(Destination, Ch, 1);
}
return Destination;
}
sub_401BA6_decrypt
Пишем функцию обратную кsub_401BA6
.Это самая простая часть Просто меняем местами вызовы функций:
C++:
__int64 __fastcall sub_401BA6_decrypt(__int64 dataPointer, int dataLength)
{
sub_401A37_decrypt(dataPointer, dataLength);
return sub_4016AD_decrypt(dataPointer, dataLength);
}
sub_401A37_decrypt
Меняем порядок действий на обратный, а также меняем операции на противоположные (сложение меняем на вычитание и наоборот)
C++:
__int64 __fastcall sub_401A37_decrypt(__int64 dataPointer, int dataLength) {
__int64 result;
int v3;
int v4;
unsigned int i;
srand(Seed);
for (i = 0; ; ++i){
result = i;
if ((int)i >= dataLength)
break;
v4 = rand() % 255;
v3 = rand() % 255;
*(BYTE*)(dataPointer + (int)i) -= ((char)v4 ^ 0xA3u) - v3;
}
return result;
}
sub_4016AD_decrypt
Аналогично предыдущим действиям, меняем порядок преобразований данных снизу вверх, а также меняем операции на противоположные:
C++:
__int64 __fastcall sub_4016AD_decrypt(__int64 dataPointer, int dataLength)
{
__int64 result;
unsigned int i;
for (i = 0; ; ++i) {
result = i;
if ((int)i >= dataLength)
break;
srand(dword_408974);
*(BYTE*)(dataPointer + (int)i) -= rand() % 100;
srand(dword_408978);
*(BYTE*)(dataPointer + (int)i) += rand() % 14;
srand(dword_40897C);
*(BYTE*)(dataPointer + (int)i) -= dword_40897C ^ 0x15;
srand(dword_408970);
*(BYTE*)(dataPointer + (int)i) ^= rand() % 255;
}
return result;
}
Теперь собираем все это воедино, компилируем, запускаем и смотрим флаг.
C++:
#include <iostream>
#include <string.h>
#include <windows.h>
DWORD dword_408970 = 0x00000000;
DWORD dword_40897C = 0x00000000;
DWORD dword_408978 = 0x00000000;
DWORD dword_408974 = 0x00000000;
DWORD Seed = 0x00000000;
DWORD unk_408980 = 0x00000000;
DWORD dword_408034 = 0x00000000;
__int64 __fastcall sub_4016AD_decrypt(__int64 dataPointer, int dataLength)
{
__int64 result;
unsigned int i;
for (i = 0; ; ++i) {
result = i;
if ((int)i >= dataLength)
break;
srand(dword_408974);
*(BYTE*)(dataPointer + (int)i) -= rand() % 100;
srand(dword_408978);
*(BYTE*)(dataPointer + (int)i) += rand() % 14;
srand(dword_40897C);
*(BYTE*)(dataPointer + (int)i) -= dword_40897C ^ 0x15;
srand(dword_408970);
*(BYTE*)(dataPointer + (int)i) ^= rand() % 255;
}
return result;
}
__int64 __fastcall sub_401A37_decrypt(__int64 dataPointer, int dataLength) {
__int64 result;
int v3;
int v4;
unsigned int i;
srand(Seed);
for (i = 0; ; ++i) {
result = i;
if ((int)i >= dataLength)
break;
v4 = rand() % 255;
v3 = rand() % 255;
*(BYTE*)(dataPointer + (int)i) -= ((char)v4 ^ 0xA3u) - v3;
}
return result;
}
__int64 __fastcall sub_401BA6_decrypt(__int64 dataPointer, int dataLength)
{
sub_401A37_decrypt(dataPointer, dataLength);
return sub_4016AD_decrypt(dataPointer, dataLength);
}
char* __fastcall sub_4015EF_decrypt(__int64 dataPointer, int dataLength)
{
char Buffer[3];
char Ch[1];
*(char*)Buffer = 0;
*(char*)Ch = 0;
char* Destination;
int i;
Destination = (char*)calloc(dataLength, 4ui64);
for (i = 0; i < dataLength; ++i) {
memcpy(Buffer, (unsigned __int8*)dataPointer + i*3, 3);
sscanf(Buffer, "%03hhu", Ch);
strncat(Destination, Ch, 1);
}
return Destination;
}
int main() {
// Читаем содержимое файла с шифротекстом и складываем считанные данные в массив encryptedData
const char* encryptedFile = "C:/temp/flag.encrypt";
FILE* encryptedFileStream = fopen(encryptedFile, "rb");
char encryptedData[15210];
memset(encryptedData, 0, sizeof(encryptedData));
fgets(encryptedData, sizeof(encryptedData), encryptedFileStream);
fclose(encryptedFileStream);
// Чтобы удобно оперировать данными в encryptedData создаем указатель
DWORD* ptr = (DWORD*)encryptedData;
// Объявляем все те же переменные, что были в функции sub_402314 шифратора
char Buffer[2000];
unsigned int v5 = 0;
DWORD v6[501];
DWORD v7[1834];
DWORD v8[490];
DWORD v9[140];
DWORD v10[336];
// Раскидываем байты из шифротекста по тем же переменным, в которых они хранились в шифраторе
memcpy(Buffer, ptr, 2000); ptr += sizeof(Buffer)/4;
memcpy(&v5, ptr, 4); ptr += sizeof(v5)/4;
memcpy(v6, ptr, sizeof(v6)); ptr += sizeof(v6)/4;
memcpy(v7, ptr, sizeof(v7)); ptr += sizeof(v7)/4;
memcpy(v8, ptr, sizeof(v8)); ptr += sizeof(v8)/4;
memcpy(v9, ptr, sizeof(v9)); ptr += sizeof(v9)/4;
memcpy(v10, ptr, sizeof(v10)); ptr += sizeof(v10)/4;
// Раскидываем значения сидов для рандомов по переменным
dword_408970 = v5;
dword_408974 = v8[388];
unk_408980 = v8[489];
Seed = v9[139];
dword_40897C = v6[500];
dword_408978 = v7[1833];
dword_408034 = v10[335];
// Подготавливаем память для немусорных байтов шифротекста
unsigned int dataBlockLength = 100;
int v17 = 4 * dataBlockLength;
int i;
char* dataBlock = (char*)calloc(dataBlockLength, 4ui64);
void* SourceBuffer;
// И копируем туда соответствующие данные
for (i = 0; i < v17; ++i)
*(BYTE*)(dataBlock + i) = *((BYTE*)&v8[389] + i);
// Выполняем первый этап преобразований (последний в шифраторе)
SourceBuffer = sub_4015EF_decrypt((__int64)dataBlock, 100);
// Выполняем последний этап преобразований (первый в шифраторе)
sub_401BA6_decrypt((__int64)SourceBuffer, 100);
// Так как в исходных данных всего 100 байт, то нет смысла писать их в файл и мы просто выводим их на экран
for (i = 0; i < 100; ++i){
std::cout << *(unsigned __int8*)((__int64)SourceBuffer + (int)i);
}
return 0;
}
Как-то так
Спасибо за прочтение! Успехов в решении следующих тасков на Codeby Games!
Последнее редактирование модератором: