Всем привет!
Решил поделиться с вами подробным описанием решения довольно интересной задачи "random??" из раздела "Реверс-инжиниринг" на платформе Antichat.
Исходные данные:
- exe-файл encrypter.exe
- зашифрованный текст flag.crypt
Исходя из логики, нам необходимо разобрать, как же работает программа-шифратор, и эти знания применить для расшифровки шифротекста, в котором скорее всего и спрятан флаг (не просто так же нам его дали). Что ж, приступим.
Распаковка
Для анализа бинарных файлов я (как и многие) предпочитаю IDA Pro. Открываем наш бинарь
и видим какую-то хренотень, а не код программы:
Попробуем запустить наш бинарь - получаем ошибку. Мда, действительно хренотень…
Штош, попробуем посмотреть на данный бинарь в HEX-редакторе. Можно выбрать и скачать любой редактор, но зачем, если есть Sublime Text?
Первое, что бросается нам в глаза (красные области) – «битые» magic bytes DOS Executable –
(
) вместо
(
).
Второе, что бросается в глаза – сигнатуры
,
и
(желтая область). Глядя на них, понимаем, что белиберда в IDA – результат упаковки программы при помощи UPX.
Меняем magic bytes на правильные:
Пытаемся распаковать наш бинарь при помощи UPX и с удивлением обнаруживаем, что он вроде как и не запакован.
Учитывая битую сигнатуру DOS Executable, предположим, что и для UPX сигнатура могла быть тоже повреждена.
Внимательно смотрим на сигнатуры UPX и видим, что одна из них -
(т.н.
).
Видим, что через 36 байтов после
идет версия упаковщика (4.01) и байты
(
). Меняем их на
(
)
Вроде, все поправили. Распаковываем бинарь через 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 =
a2
)
10
break
;
11
dword_408970
=
time64
(
0
i64
)
^
0xCDBABC
;
12
srand
(
dword_408970
)
;
13
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
^=
rand
(
)
%
255
;
14
dword_40897C
=
time64
(
0
i64
)
-
4919
;
15
srand
(
dword_40897C
)
;
16
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
+=
dword_40897C
^
0x15
;
17
dword_408978
=
time64
(
0
i64
)
+
48879
;
18
srand
(
dword_408978
)
;
19
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
-=
rand
(
)
%
14
;
20
dword_408974
=
time64
(
0
i64
)
/
2
;
21
srand
(
dword_408974
)
;
22
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
+=
rand
(
)
%
100
;
23
}
24
return
result
;
25
}
Смотрим на функцию и первое, что нам бросается в глаза, - рандомы.
Вспомним, что передавалось в эту функцию:
- - указатель на массив char'ов
с исходными данными
- - количество данных в массиве
- 100.
Разбираем по порядку:
C:
Код:
6
for
(
i
=
0
;
;
++
i
)
7
{
8
result
=
i
;
9
if
(
(
int
)
i
>=
a2
)
10
break
;
Тут мы видим цикл, повторяющийся 100 раз (так как в переменной
была передана константа
- длина исходных данных).
C:
Код:
11
dword_408970
=
time64
(
0
i64
)
^
0xCDBABC
;
12
srand
(
dword_408970
)
;
Тут у нас высчитывается текущее время в формате Unix Epoch, потом к нему применяется операция XOR со статическим значением
и получившимся в итоге значением инициализируется псевдорандом.
C:
Код:
13
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
^=
rand
(
)
%
255
;
В левой части:
Тут происходит добавление к указателю на массив с исходными данными
значения итератора
, преобразование его в указатель на тип BYTE и обращению к памяти по полученному адресу.
В правой части:
Тут вызовом функции
генерируется псевдослучайное значение, от которого которого оставляется только последний байт а остальные байты обнуляются путем [S]принятия поправок[/S] деления по модулю 255 (255 == 0xFF). Таким образом при применении операции
операция XOR применяется только к одному выбранному байту, несмотря на то, что
выдает нам целых 4 байта.
Фактически эта строчка означает следующее:
C:
Код:
13
Buffer
[
i
]
=
Buffer
[
i
]
^
(
rand
(
)
%
255
)
;
В остальных участках повторно три раза происходят аналогичные действия - генерация инцициализирующего значения, инцициализация псевдорандома и изменение текущего байта.
C:
Код:
14
dword_40897C
=
time64
(
0
i64
)
-
4919
;
15
srand
(
dword_40897C
)
;
16
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
+=
dword_40897C
^
0x15
;
17
dword_408978
=
time64
(
0
i64
)
+
48879
;
18
srand
(
dword_408978
)
;
19
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
-=
rand
(
)
%
14
;
20
dword_408974
=
time64
(
0
i64
)
/
2
;
21
srand
(
dword_408974
)
;
22
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
+=
rand
(
)
%
100
;
Итого
Фнукция
принимает на вход массив с исходными данными и к каждому значению четыре раза применяет операцию XOR с четырьмя разными значениями (три из которых псевдослучайные).
Тут у нас сразу возникает вопрос - если мы будем писать дешифратор, то откуда мы возьмем инициализирующие значения для наших рандомов -
,
,
,
? Пока что не ясно. Оставим этот вопрос на потом, а пока продолжим дальше разбираться с логикой работы шифратора.
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
(
0
i64
)
^
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
(
0
i64
)
^
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
}
Обращаем внимание, что одно из псевдослучайных значений -
- не используется непосредственно в преобразовании, а передается в функцию
.
Функция в функции в функции.
Тут опять же возникает вопрос о том, что нам делать с неизвестным сидом для рандома. Опять отложим этот вопрос на потом.
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
(
2
i64
)
;
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
;
Интересно, что кроме как в
входные данные никак не используются. Нафига тогда весь остальной код нужен? Можно, конечно, забить на него, но мы любопытные. Пробуем загуглить
Код:
NtGlobalFlag = v3->NtGlobalFlag
и получаем первой же ссылкой статью на xakep.ru об антиотладке.
Вот же хитрюги!
Внимательно читаем раздел про
и понимаем, что в строчке
C:
Код:
16
if
(
(
NtGlobalFlag
&
0x70
)
!=
0
)
Происходит проверка на отладку.
Что происходит, когда проверка на отладку срабатывает, вы можете узнать самостоятельно, засунув программу в дебагер и протыкав несколько раз F8 .
Настоятельно не рекомендую осуществлять эту проверку эпилептикам!
Пока нам отладка кода не потребовалась, но на всякий случай лучше сразу грохнуть эту антиотладку.
Что нужно чтобы условие
никогда не сработало? Правильно, заменить
на
, тогда что бы ни было в
, результат операции
Код:
(NtGlobalFlag & 0x00)
всегда будет равен 0.
Смотрим, как выглядят эти строчки кода в ассемблерных инструкциях и в hex:
Код:
Код:
.text:00000000004018EA and eax, 70h
.text:00000000004018ED test eax, eax
Инструкции
соответствуют байты
, в которых нам нужно поменять
на
.
Открываем наш шифратор в [S]hex-редакторе[/S] Sublime Text и ищем эти байты. С удивлением обнаруживаем не одно, а целых 4 вхождения. Возможно, что остальные 3 вхождения - это просто случайные совпадения, но что если создатели таска вкорячили сразу 4 блока кода с антиотладкой?
Возвращаемся в HEX-View в IDA Pro, ищем все последовательности байтов
и находим, что это реально еще 3 блока антиотладки:
Код:
Код:
.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 проверки на отладку, заменяя
на
Переоткрываем в IDA измененный файл, заново открываем функцию
и с радостью обнаруживаем, что умная IDA заботливо убрала из отображаемого кода все, что было связано с блоком антиотладки. Резонно, ведь этот код стал недостижим для выполнения, так как блок
никогда выполнится.
Теперь эта функция просто-напросто применяет к передаваемому в нее значению операцию XOR с
.
C:
Код:
1
__int64 __fastcall
sub_4018AB
(
int
a1
)
2
{
3
return
a1
^
0xA3u
;
4
}
Итого
Функция
принимает на вход массив с видоизмененными предыдущей функцией исходными данными и к каждому байту этих данных добавляет число, формируемое на основе двух псевдослучайных байтов -
и
, к последнему из которых применяется операция XOR со значением
.
С функцией
разобрались, идем дальше.
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
,
4u
i64
)
;
12
for
(
i
=
0
;
i
#include
#include
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
,
4u
i64
)
;
void
*
SourceBuffer
;
// И копируем туда соответствующие данные
for
(
i
=
0
;
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
;
}
Теперь собираем все это воедино, компилируем, запускаем и смотрим флаг.
Как-то так
Спасибо за прочтение! Успехов в решении следующих тасков на Antichat!