HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2
НОВЫЕ ТОРГОВАЯ НОВОСТИ ЧАТ
loading...
Скрыть
Вернуться   ANTICHAT > БЕЗОПАСНОСТЬ И УЯЗВИМОСТИ > Этичный хакинг или пентестинг > Задания/Квесты/CTF/Конкурсы
   
Ответ
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 10.08.2023, 22:03
aeonworm
Новичок
Регистрация: 09.08.2023
Сообщений: 1
С нами: 1455623

Репутация: 0
По умолчанию

Всем привет!
Решил поделиться с вами подробным описанием решения довольно интересной задачи "random??" из раздела "Реверс-инжиниринг" на платформе Antichat.

Исходные данные:
- exe-файл encrypter.exe
- зашифрованный текст flag.crypt

Исходя из логики, нам необходимо разобрать, как же работает программа-шифратор, и эти знания применить для расшифровки шифротекста, в котором скорее всего и спрятан флаг (не просто так же нам его дали). Что ж, приступим.


Распаковка
Для анализа бинарных файлов я (как и многие) предпочитаю IDA Pro. Открываем наш бинарь
Код:
encrypter.exe
и видим какую-то хренотень, а не код программы:



Попробуем запустить наш бинарь - получаем ошибку. Мда, действительно хренотень…



Штош, попробуем посмотреть на данный бинарь в HEX-редакторе. Можно выбрать и скачать любой редактор, но зачем, если есть Sublime Text?



Первое, что бросается нам в глаза (красные области) – «битые» magic bytes DOS Executable –
Код:
6D 7A
(
Код:
mz
) вместо
Код:
4D 5A
(
Код:
MZ
).
Второе, что бросается в глаза – сигнатуры
Код:
UPX0
,
Код:
UPX1
и
Код:
UPX2
(желтая область). Глядя на них, понимаем, что белиберда в IDA – результат упаковки программы при помощи UPX.

Меняем magic bytes на правильные:



Пытаемся распаковать наш бинарь при помощи UPX и с удивлением обнаруживаем, что он вроде как и не запакован.



Учитывая битую сигнатуру DOS Executable, предположим, что и для UPX сигнатура могла быть тоже повреждена.
Внимательно смотрим на сигнатуры 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 =
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
}
Смотрим на функцию и первое, что нам бросается в глаза, - рандомы.

Вспомним, что передавалось в эту функцию:
  • Код:
    __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
;
Тут мы видим цикл, повторяющийся 100 раз (так как в переменной
Код:
a2
была передана константа
Код:
100
- длина исходных данных).

C:


Код:
11
dword_408970
=
time64
(
0
i64
)
^
0xCDBABC
;
12
srand
(
dword_408970
)
;
Тут у нас высчитывается текущее время в формате Unix Epoch, потом к нему применяется операция XOR со статическим значением
Код:
0xCDBABC
и получившимся в итоге значением инициализируется псевдорандом.

C:


Код:
13
*
(
_BYTE
*
)
(
a1
+
(
int
)
i
)
^=
rand
(
)
%
255
;
В левой части:
Тут происходит добавление к указателю на массив с исходными данными
Код:
a1
значения итератора
Код:
i
, преобразование его в указатель на тип BYTE и обращению к памяти по полученному адресу.
В правой части:
Тут вызовом функции
Код:
rand()
генерируется псевдослучайное значение, от которого которого оставляется только последний байт а остальные байты обнуляются путем [S]принятия поправок[/S] деления по модулю 255 (255 == 0xFF). Таким образом при применении операции
Код:
^=
операция XOR применяется только к одному выбранному байту, несмотря на то, что
Код:
rand()
выдает нам целых 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
;
Цитата:

Итого
Фнукция
Код:
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
(
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
}
Обращаем внимание, что одно из псевдослучайных значений -
Код:
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
(
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
;
Интересно, что кроме как в
Код:
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
.

Открываем наш шифратор в [S]hex-редакторе[/S] Sublime Text и ищем эти байты. С удивлением обнаруживаем не одно, а целых 4 вхождения. Возможно, что остальные 3 вхождения - это просто случайные совпадения, но что если создатели таска вкорячили сразу 4 блока кода с антиотладкой?

Возвращаемся в HEX-View в IDA Pro, ищем все последовательности байтов
Код:
83 E0 70
и находим, что это реально еще 3 блока антиотладки:
  1. Код:
    sub_401BD8
  2. Код:
    sub_401DC9
  3. Код:
    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
,
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!
 
Ответить с цитированием

  #2  
Старый 11.08.2023, 09:18
Kevgen
Новичок
Регистрация: 11.05.2023
Сообщений: 0
С нами: 1585741

Репутация: 0
По умолчанию

Спасибо за райтап!
Прям огромную работу проделал
 
Ответить с цитированием

  #3  
Старый 11.08.2023, 21:38
ROP
Новичок
Регистрация: 27.08.2019
Сообщений: 0
С нами: 3533622

Репутация: 0
По умолчанию

Огонь!
Спасибо за подробный райтап!

Месье, конечно, знает толк в извращениях...

Это точно

Цитата:

Kevgen сказал(а):

Спасибо за райтап!
Прям огромную работу проделал
100%
 
Ответить с цитированием

  #4  
Старый 14.08.2023, 20:51
yetiraki
Новичок
Регистрация: 07.02.2023
Сообщений: 0
С нами: 1719376

Репутация: 0
По умолчанию

Вот это работа!!!
Спасибо за райтап!
 
Ответить с цитированием

  #5  
Старый 17.08.2023, 18:51
GoBL1n
Новичок
Регистрация: 02.03.2017
Сообщений: 0
С нами: 4841507

Репутация: 0
По умолчанию

да уж, от души!
 
Ответить с цитированием
Ответ





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.