Форум программистов, компьютерный форум, киберфорум
Наши страницы
_lunar_
Войти
Регистрация
Восстановить пароль
Рейтинг: 5.00. Голосов: 1.

Чтение данных PE заголовка

Запись от _lunar_ размещена 27.03.2019 в 13:13
Обновил(-а) _lunar_ 28.03.2019 в 09:43

Несмотря на то, что исполняемые файлы имеют чёткую структуру и документированное описание, найти полезную информацию в интернете о том как читать PE заголовок достаточно сложно (если вообще возможно).

Переписывать документацию наверное не имеет смысла. Вместо этого я покажу примеры кода как читать конкретные секции (заодно, поверхностно, рассмотрим защиту одной игры от взлома).
Код я буду писать как говорится "чистый", без всяких дефайнов, обёрток и прочей ерунды, которая только путает и забивает понимание того, что происходит.

Сначала инициализируем DOS и NT заголовки (и сразу заголовок секций, т.к. в NT заголовке этой структуры нет)
C++
1
2
3
IMAGE_DOS_HEADER * __ptr64 pDos = (IMAGE_DOS_HEADER * __ptr64)ImageBaseAddr;
IMAGE_NT_HEADERS64 * __ptr64 pNt = (IMAGE_NT_HEADERS64 * __ptr64)((unsigned __int64)ImageBaseAddr + pDos->e_lfanew);
IMAGE_SECTION_HEADER * __ptr64 pSec = (IMAGE_SECTION_HEADER * __ptr64)((unsigned __int64)ImageBaseAddr + pDos->e_lfanew + sizeof(IMAGE_NT_HEADERS64));
ImageBaseAddr получается стандартно - поочерёдным вызовом трех функций:
C++
1
2
3
HANDLE hFile = CreateFile("PathToFile", ...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
unsigned __int64* __ptr64 ImageBaseAddr = MapViewOfFile(hFileMapping, ...);
IMAGE_SECTION_HEADER
Начнём с IMAGE_SECTION_HEADER. Заголовок содержит данные, которые размещаются в адресном пространстве процесса во время загрузки исполняемого файла в память.
Здесь располагаются секции кода, ресурсов, импорта, экспорта, и прочее.
Именно эти секции пытаются защитить от реверса, обфусцируя их виртуальными машинами, добавляя мусор и мутации кода и многое другое.
Чтобы осуществить взлом необходимо найти в секции кода так называемую OEP (original entry point), или базовую точку входа. OEP часто путают с EP (entry point). EP прописывается в PE заголовке и не является базовой точкой входа. Найдя OEP можно спокойно отсоединять отладчиком защиту от исполняемого файла.

Информацию о секциях можно добыть следующим кодом:
C++
1
2
3
4
5
6
wprintf(L"****************************************************************\n                      IMAGE_SECTION_HEADER\n****************************************************************");
 
wprintf(L"   Name  ---     VA     ---  pRawData  ---    VS    --- sRawData\n");
for (unsigned __int16 i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
    printf("%-8s --- 0x%08X --- 0x%08X --- %08X --- %08X\n", pSec[i].Name, pSec[i].VirtualAddress, pSec[i].PointerToRawData, pSec[i].Misc.VirtualSize, pSec[i].SizeOfRawData);
}
Пример вывода данных в консоль
Нажмите на изображение для увеличения
Название: 1.png
Просмотров: 157
Размер:	25.8 Кб
ID:	5313

После исполнения кода получаем информацию об именах, виртуальных адресах, смещениях и прочее.
Опытный реверсер сразу обратит внимание на такие секции как .xcode, .xdata, .vmp0 - это и есть обфусцированные виртуальной машиной секции.
Приставка .x говорит о том, что секция зашифрована средствами защиты Denuvo, а .vmp - это виртуальная машина VMProtect.
Их расшифровка отдельная тема, и достаточно сложная, поэтому оставим её и перейдем к секции экспорта.

IMAGE_EXPORT_DIRECTORY
Исполняемые файлы (будь то exe или dll) могут иметь (или не иметь) секции экспорта и импорта.
Экспорт для dll файла означает, что созданные в его коде функции могут использоваться путём их импорта в exe файле.

Таблицу экспорта получаем следующим кодом:
C++
1
2
3
4
5
6
7
8
9
10
11
12
wprintf(L"****************************************************************\n                    IMAGE_EXPORT_DIRECTORY\n****************************************************************");
 
IMAGE_EXPORT_DIRECTORY* __ptr64 pExpDir = (IMAGE_EXPORT_DIRECTORY * __ptr64)((unsigned __int64)ImageBaseAddr + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
if (pExpDir->NumberOfNames != 0) {
    unsigned __int32* __ptr64 Name = (unsigned __int32* __ptr64)((unsigned __int64)ImageBaseAddr + pExpDir->AddressOfNames);
    unsigned __int32* __ptr64 Func = (unsigned __int32* __ptr64)((unsigned __int64)ImageBaseAddr + pExpDir->AddressOfFunctions);
    for (unsigned __int32 i = 0; i < pExpDir->NumberOfNames; i++) {
        printf("%-51s --- ", (unsigned __int64)ImageBaseAddr + Name[i]);
        wprintf(L"0x%08X\n", Func[i]);
    }
}
else wprintf(L"\nExport directory not found!\n\n");
Пример вывода данных в консоль
Нажмите на изображение для увеличения
Название: 2.png
Просмотров: 147
Размер:	19.0 Кб
ID:	5314

IMAGE_IMPORT_DESCRIPTOR
Аналогично получаем информацию о таблице импорта:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
wprintf(L"****************************************************************\n                   IMAGE_IMPORT_DESCRIPTOR\n****************************************************************");
 
IMAGE_IMPORT_DESCRIPTOR* __ptr64 pImpDir = (IMAGE_IMPORT_DESCRIPTOR * __ptr64)((unsigned __int64)ImageBaseAddr + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
if (pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size != 0) {
    while (pImpDir->Name) {
        IMAGE_THUNK_DATA64* __ptr64 OriginalFirstThunk = (IMAGE_THUNK_DATA64 * __ptr64)((unsigned __int64)ImageBaseAddr + pImpDir->OriginalFirstThunk);
        IMAGE_THUNK_DATA64 * __ptr64 FirstThunk = (IMAGE_THUNK_DATA64 * __ptr64)((unsigned __int64)ImageBaseAddr + pImpDir->FirstThunk);
        IMAGE_THUNK_DATA64 * __ptr64 ThunkData = OriginalFirstThunk;
        if (!ThunkData) ThunkData = FirstThunk;
        printf("\n%s (OFT: 0x%08X, FT: 0x%08X)\n", (unsigned __int64)ImageBaseAddr + pImpDir->Name, pImpDir->OriginalFirstThunk, pImpDir->FirstThunk);
        while (ThunkData->u1.AddressOfData) {
            if (!(ThunkData->u1.Ordinal & IMAGE_ORDINAL_FLAG64)) {
                IMAGE_IMPORT_BY_NAME* __ptr64 ImportFunctionName = (IMAGE_IMPORT_BY_NAME * __ptr64)((unsigned __int64)ImageBaseAddr + ThunkData->u1.AddressOfData);
                printf(" %-50s --- ", ImportFunctionName->Name);
                wprintf(L"0x%016llX\n", ThunkData->u1.AddressOfData);
            }
            ThunkData++;
        }
        pImpDir++;
    }
}
else wprintf(L"\nImport directory not found!\n\n");
Пример вывода данных в консоль
Нажмите на изображение для увеличения
Название: 3.png
Просмотров: 147
Размер:	31.2 Кб
ID:	5315

Здесь стоит сделать небольшое примечание - чем отличаются OriginalFirstThunk и FirstThunk.
Если работать с файлом как с набором байт на диске, а не как с загруженным образом в память, то эти поля будут идентичны.
В случае с загруженным образом эти поля будут отличаться: OriginalFirstThunk так и будет ссылаться на массив имён (import name table - INT), но FirstThunk будет ссылаться на таблицу прокси вызовов (import address table - IAT), где адрес нужного Thunk будет прокси вызовом call dword ptr[&Thunk] (FF 15 XX XX XX XX).

IMAGE_TLS_DIRECTORY64
Перейдем к наверно самой интересной с точки зрения реверса таблице - TlsCallback.
Немного расскажу о значимости этой таблицы в части защиты кода от взлома.
TLSCallBack вызывается из системы при создании потоков. Главная фишка этой таблицы – вызов потока до точки входа в приложение (секция CRT).
Если создать поток до точки входа и вызывать из него функцию антидебага (к примеру IsDebuggerPresent), то при попытке реверса такого файла (структура DEBUG_EVENT, WaitForDebugEvent/ContinueDebugEvent, etc.) мы попадем на IMAGE_TLS_CALLBACK с последующим TerminateProcess.

Небольшой пример:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <Windows.h>
 
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma section (".CRT$XLY", long, read)
 
void __stdcall TLSCallBack(void *, unsigned long, void *) {
    if (IsDebuggerPresent()) {
    MessageBox(nullptr, "Debugger is detected", "Test", MB_ERROR);
    TerminateProcess(hProcess, 0);
    }
}
__declspec(allocate(".CRT$XLY"))PIMAGE_TLS_CALLBACK pTLSCallBack = TLSCallBack;
 
int __stdcall WinMain(HINSTANCE, HINSTANCE, char *, int) {
    MessageBox(nullptr, "All ok", "Test", MB_OK);
    return 0;
}
При обычном исполнении такой программы будет выведено сообщение что всё хорошо.
Но при попытке отладить программу в отладчике, определенный в CRT поток IMAGE_TLS_CALLBACK, сработает быстрее чем точка входа WinMain и приложение завершится так и не добравшись до OEP.

Определить имеется ли TlsCallback в коде приложения можно следующим образом:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef void(__fastcall* __ptr64 PIMAGE_TLS_CALLBACK)(void* __ptr64 DllHandle, void* __ptr64 Reason, void* __ptr64 Reserved);
 
wprintf(L"****************************************************************\n                     IMAGE_TLS_DIRECTORY64\n****************************************************************");
 
IMAGE_TLS_DIRECTORY64* __ptr64 pTlsDir = (IMAGE_TLS_DIRECTORY64 * __ptr64)((unsigned __int64)ImageBaseAddr + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
PIMAGE_TLS_CALLBACK* __ptr64 TlsCallback = (PIMAGE_TLS_CALLBACK * __ptr64)pTlsDir->AddressOfCallBacks;
if (*(__int64* __ptr64)TlsCallback != 0) {
    wprintf(L"\n");
    __int32 i = 1;
    while (*TlsCallback) {
        wprintf(L"TlsCallback_%d: 0x%016llX\n", i, *(__int64* __ptr64)TlsCallback);
        TlsCallback++;
        i++;
    }
}
else wprintf(L"\nTLS directory not found!\n\n");
Пример вывода данных в консоль
Нажмите на изображение для увеличения
Название: 4.png
Просмотров: 175
Размер:	8.5 Кб
ID:	5316

В данной игре навесили аж 2 потока IMAGE_TLS_CALLBACK, которых при реверсе обойти будет не так то просто.

Но и это не предел возможностей данной таблицы. Анализируя защиту другой игры, я обнаружил так называемый вложенный TlsCallback
Assembler
1
mov rax, gs:[0x58] inputhost!TlsCallback_0/1
Отлаживая поток IMAGE_TLS_CALLBACK я понял, что код вызывает ещё 2 TlsCallback из inputhost.dll и причём так хитро, что самой библиотеки inputhost.dll изначально не было ни в таблице импорта, ни в таблице отложенного импорта.
TlsCallback из inputhost.dll работает следующим образом:
все необработанные исключения уходят в kernel32!UnhandledExceptionFilter
но в импорте игры в экспорте kernel32.dll не было UnhandledExceptionFilter
если SEH нет, то rip->UnhandledExceptionFilter(EXCEPTION_POINTERS) устанавливается через SetUnhandledExceptionFilter(0)
C++
1
2
3
4
5
6
7
RaiseSecurity(struct _EXCEPTION_POINTERS* __ptr64 ExceptionInfo) {
    SetUnhandledExceptionFilter(0);
    UnhandledExceptionFilter(ExceptionInfo);
    if (!IsDebuggerPresent())
        _crt_debugger_hook(1);
    return TerminateProcess(hProcess, 0xC0000409);
}
IMAGE_DELAYLOAD_DESCRIPTOR
Таблица отложенного импорта содержит информацию, необходимую для отложенной загрузки импортируемых библиотек.
Отложенная загрузка импорта означает, что dll присоединена к исполняемому файлу, но загружается в память не сразу, как при обычном импорте, а только при первом обращении программы к символу, импортируемому из этой dll.

Получение информации о таблице отложенного импорта:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
wprintf(L"****************************************************************\n                IMAGE_DELAYLOAD_DESCRIPTOR\n****************************************************************");
 
IMAGE_DELAYLOAD_DESCRIPTOR* __ptr64 pDelayDir = (IMAGE_DELAYLOAD_DESCRIPTOR * __ptr64)((unsigned __int64)ImageBaseAddr + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress);
if (pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].Size != 0) {
    while (pDelayDir->DllNameRVA) {
        IMAGE_THUNK_DATA64* __ptr64 ImportNameTable = (IMAGE_THUNK_DATA64 * __ptr64)((unsigned __int64)ImageBaseAddr + pDelayDir->ImportNameTableRVA);
        IMAGE_THUNK_DATA64 * __ptr64 ImportAddressTable = (IMAGE_THUNK_DATA64 * __ptr64)((unsigned __int64)ImageBaseAddr + pDelayDir->ImportAddressTableRVA);
        printf("\n%s (INT: 0x%016llX, IAT: 0x%016llX, BoundIAT: 0x%016llX)\n",
            (unsigned __int64)ImageBaseAddr + pDelayDir->DllNameRVA,
            (unsigned __int64)ImageBaseAddr + pDelayDir->ImportNameTableRVA,
            (unsigned __int64)ImageBaseAddr + pDelayDir->ImportAddressTableRVA,
            (unsigned __int64)ImageBaseAddr + pDelayDir->BoundImportAddressTableRVA);
        while (ImportNameTable->u1.AddressOfData) {
            if (!(ImportNameTable->u1.Ordinal & IMAGE_ORDINAL_FLAG64)) {
                IMAGE_IMPORT_BY_NAME* __ptr64 ImportFunctionName = (IMAGE_IMPORT_BY_NAME * __ptr64)((unsigned __int64)ImageBaseAddr + ImportNameTable->u1.AddressOfData);
                printf(" %-50s --- ", ImportFunctionName->Name);
                wprintf(L"0x%016llX\n", ImportAddressTable->u1.AddressOfData);
            }
            ImportNameTable++;
            ImportAddressTable++;
        }
        pDelayDir++;
    }
}
else wprintf(L"\nDelayload directory not found!\n\n");
Пример вывода данных в консоль
Нажмите на изображение для увеличения
Название: 5.png
Просмотров: 139
Размер:	21.0 Кб
ID:	5317
Здесь видно, что библиотека, необходимая для запуска клиента игры, находится в таблице отложенного импорта.
При взломе эту таблицу эмулируют, получая от неё данные в процессе отладки.
Но здесь поступили хитро, убрав библиотеку из основного импорта, поэтому отлаживать её возможно только после запуска игры.

Подведём итоги проанализированной защиты:
1. код программы обфусцирован двумя виртуальными машинами (Denuvo, VMProtrect)
2. имеется два TlsCallback (с возможно вложенными потоками)
3. основная библиотека клиента игры не доступна для отладки без запуска самой игры.

PS: это не все таблицы. По мере тестирования буду добавлять остальные.
Размещено в Без категории
Просмотров 399 Комментарии 1
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Аватар для MrOnlineCoder
    Не системный программист и не занимаюсь защитой/взломов программ - но информация интересная, наблюдать какие приемы используются для защиты занимательно.
    Запись от MrOnlineCoder размещена 27.03.2019 в 17:54 MrOnlineCoder вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.