Распаковка Perl2Exe

в 19:38, , рубрики: c++, detours, ollydbg, perl, windows, Программирование, метки: , , ,

Распаковка Perl2Exe

Одним из наиболее часто используемых продуктов для создания standalone-приложений из perl-скриптов и организации какой-никакой защиты является продукт IndigoStar Perl2Exe. Периодически возникают ситуации, когда исходный код скрипта потерян, а на руках имеется только полученный с помощью этой программы exe-файл, но всенепременно хочется добраться до сорцев. Разберемся, как это сделать.

Для начала скачаем сам продукт (дальнейшее описание приводится на основе Perl2Exe V11.00 for Windows) и воспользуемся им по назначению — превратим прилагающийся скрипт sample.pl в полноценный exe-файл. Для этого вводим в консоли незамысловатую команду perl2exe sample.pl или просто перетаскиваем sample.pl на perl2exe в проводнике.
Итак, у нас есть sample.exe, который необходимо изучить на предмет возможности извлечения исходного кода. Начнем с банального:
Возьмем любой hex-редактор и попробуем поискать элементы кода скрипта в теле файла. Тщетно, видимо, код хранится в упакованном/зашифрованном виде, что вполне логично.

Воспользуемся утилитой API Monitor и проанализируем обращения к файлам, которые совершает sample.exe после запуска (в качестве альтернативы можно воспользоваться Process Monitor за авторством Марка Руссиновича). Для этого отметим в списке перехватываемых функции CreateFileA, CreateFileW, нажмем Ctrl+H и укажем процесс, в контексте которого будет вестись наблюдение.

Распаковка Perl2Exe

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

Что ж, вооружимся отладчиком и приступим к беглому изучению внутреннего устройства программы. Цель — определить, появляется ли интересующий нас код в памяти процесса в открытом виде, и на каком этапе выполнения программы его будет проще всего перехватить и скопировать. В данном случае я предпочту воспользоваться OllyDbg, но в общем-то подошел бы практически любой отладчик, например, WinDbg, IDA или, скажем, Syser.
Загружаем программу в отладчик и наблюдаем стандартный CRT-шный пролог.

Распаковка Perl2Exe

Особо не задумываясь над этим нажимаем F5 — программа успешно отрабатывает, нас выкидывает в недра ntdll. Открываем память процесса (Alt+M) и ищем какой-нибудь кусок из скрипта, например, строку «This is sample». Вуаля, мы обнаруживаем исходный код в памяти. Стоит отметить, что данный подход ненадежен, так как к моменту завершения работы программы интересующие нас данные могли быть перетерты в памяти, что является правильным подходом с точки зрения безопасности (скажем, мастер-пароль в браузере Firefox через какое-то время после ввода невозможно найти в памяти).
Выясним, после какого этапа сей код появляется в памяти. Пропустим CRT-шное интро и перейдем к изучению первого значимого участка кода.

Распаковка Perl2Exe

Мы наблюдаем подгрузку файла динамической библиотеки p2x5142.dll, получение адреса экспортируемой функции RunPerl и её вызов. Проведя нехитрые манипуляции, обнаруживаем, что интересующее нас таинство происходит внутри RunPerl. Изучим её содержимое.

Распаковка Perl2Exe

Обращения к функциям WinAPI нас не интересуют, зато наблюдаются любопытная последовательность вызовов к функциям с префиксом perl (Perl_sys_init3, perl_alloc, perl_contruct, perl_parse, perl_run, ...), причем, проведя следственный эксперимент, выясняем, что код появляется в памяти в открытом виде после вызова perl_parse. Посмотрим, что находится внутри perl_parse. Отлично, опять куча вызовов функций с префиксом perl и прочая лабуда, изучению которой препятствует природная лень и отговорка «да я эту статью вообще пишу сидя в автобусе».
Пойдем другим путем. Пару раз запустим программу и убедимся, что память под исходный код скрипта аллоцируется в одном и том же месте (что опять-таки ненадежный подход, и логичнее было бы перехватить функции malloc, free и анализировать адресуемые области). Поставим на неё точку останова, чтобы найти код, отвечающий за запись данных по интересующему нас адресу.
Перезапускаем программу и выпадаем где-то тут:

Распаковка Perl2Exe

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

Распаковка Perl2Exe

А вот это нас более чем устраивает. В регистре ebx и стеке видим адреса, указывающие на интересующую нас область памяти с исходным кодом. Запишем в блокнот виртуальный адрес инструкции retn, посмотрим адрес, по которому загрузилась в память библиотека p2x5142.dll. Вычтем один из другого и получим смещение, которое пригодится в дальнейшем.

Теперь неплохо было бы автоматизировать данный процесс. Для этого напишем простенькую библиотеку, которую будем подгружать в память sample.exe. Сама же библиотека будет устанавливать обработчик векторных исключений, отслеживать момент, когда в память будет загружена библиотека p2x5142.dll, заменять по полученному ранее оффсету инструкцию retn на int3 и дампить содержимое памяти, которую адресует регистр, в файл.
Чтобы не писать код инжекта библиотеки по новой, воспользуемся классом, который я когда-то писал, осилив некоторую часть книги «С++ за 21 день». Вот он. Толщина его должна быть… примерно как толщина большого пальца. Также воспользуемся библиотекой Detours от Microsoft, чтобы перехватить вызов LoadLibrary. Можно, конечно, было бы обратиться к наработкам на MASM по данной тематике, но, к сожалению, пятница — это не день программирования на языках низкого уровня. Приступим к написанию. Начнем с программы-лаунчера, которая будет запускать sample.exe в приостановленном состоянии, инжектить библиотеку в созданный процесс и возобновлять его работу.

// Подключаем заголовочные файлы
#include <iostream>
#include <Windows.h>

#include "injector.hpp"

using namespace std;
// Говорящий за себя прототип вспомогательной функции
// Листинг приводить здесь не буду, его можно посмотреть, скачав исходный код в конце статьи
wstring str2wstr(const char * aIn);

int main(int argc, char *argv[])
{
    // Если количество переданных аргументов не равно двум, то выводим небольшой хелп
    if(argc != 3)
    {
        cout<<"Usage: launcher  sample.exe  inject.dll"<<endl;
        return 1;
    }
    
    // Необходимые структуры
    STARTUPINFO si = {0};
    PROCESS_INFORMATION pi = {0};
    
    // Запускаем приложение, переданное первым аргументом нашей программе
    // CREATE_SUSPENDED указывает на то, что процесс будет создан в "неактивном" состоянии
    if(CreateProcess(str2wstr(argv[1]).c_str(), NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi) == 0)
    {
        cout<<"Failed to create process"<<endl;
        return 1;
    }
    
    // Создаем объект класса injector
    injector a;
    // Устанавливаем неблокирующий режим работы (не ждем завершения выполнения удаленного потока)
    a.set_blocking(false);
    
    
    try
    {
        // Подгружаем библиотеку в созданный ранее процесс
        a.inject(pi.dwProcessId, str2wstr(argv[2]));
    }
    catch(const injector_exception &e)
    {
        // Если что-то пошло не так, то выводим информацию об ошибке и завершаем процесс
        e.show_error();
        
        CloseHandle(pi.hThread);
        CloseHandle(pi.hProcess);
        
        TerminateProcess(pi.hProcess, 1);

        return 1;
    }
    // Возобновляем выполнение процесса
    ResumeThread(pi.hThread);
    
    // Закрываем хендлы, они нам больше не нужны
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    
    return 0;
}

Перейдем к исходному коду библиотеки

// Заголовочные файлы
#include <iostream>
#include <sstream>
#include <fstream>

#include <Windows.h>

#include "detours.h"

#pragma comment(lib, "detours.lib")

using namespace std;

// Счетчик, служащий для формирования имени файла с дампом
static unsigned int i = 0;
// Относительное смещение инструкции, которая будет замещена на int3
static const DWORD retn_offset = 0xB40E9;
static const wstring dump_directory = L"dump";
static const wstring file_prefix = L"src_";
static const wstring perl_dll_name = L"p2x5142.dll";


// Почему ANSI версия? Потому что именно она вызывается (видно в листинге дизассемблера)
HMODULE (WINAPI * real_loadlibrary)(LPCSTR lpFileName) = LoadLibraryA;

// Вспомогательная функция, здесь приводить не буду
string wstr2str(const wchar_t * aIn);

// Функция установки перехвата
void hook()
{
    // Получаем адрес, по которому загружена интересующая нас библиотека
    void * base_address = GetModuleHandle(perl_dll_name.c_str());
    if(base_address == NULL)
        return;
    
    DWORD pr;
    // Прибавляем статичное смещение к адресу
    base_address = reinterpret_cast<void *>(reinterpret_cast<DWORD>(base_address) + retn_offset);
    // Меняем аттрибуты защиты памяти, записываем int3, восстанавливаем аттрибуты защиты
    VirtualProtect(base_address, 1, PAGE_READWRITE, &pr);
    CopyMemory(base_address, "xCC", 1);
    VirtualProtect(base_address, 1, pr, &pr);
}

// Функция, записывающая указанный буффер в файл
void dump_data(char * buffer, unsigned int size)
{
    DWORD pr;
    wstringstream ss;
    // Формируем относительный путь к файлу
    ss << dump_directory << L"\" << file_prefix << i++ << L".txt";
    
    ofstream file(ss.str(), ofstream::binary);
    file.exceptions(0);

    if(file.is_open())
    {
        // На всякий случай меняем аттрибуты защиты памяти
        VirtualProtect(buffer, size, PAGE_READONLY, &pr);
        file.write(buffer, size);
        VirtualProtect(buffer, size, pr, &pr);
        file.close();
    }
}

// Функция, выполняемая при обращении к LoadLibraryA
HMODULE WINAPI my_loadlibrary(LPCSTR lpFileName)
{
    HMODULE h = real_loadlibrary(lpFileName);
    
    // Если подгружена необходимая библиотека, то установим хук
    if
    (
        strstr(lpFileName, wstr2str(perl_dll_name.c_str()).c_str())
        &&
        i == 0
    )
    {
        i++;
        hook();
    }

    return h;
}

// Обработчик векторных исключений, который будет обрабатывать нашу int3
LONG CALLBACK VEH(PEXCEPTION_POINTERS ExceptionInfo)
{
    if
    (
        // Проведенные эксперименты показали, что в Eax хранится размер буфера, а в Ebx указатель на буфер
        ExceptionInfo->ContextRecord->Eax > 0
        &&
        ExceptionInfo->ContextRecord->Eax < 0xFFFFF
        &&
        // Также функция вызывается и для каких-то других целей, поэтому проводится такая вот "валидация"
        // дабы отсечь типичные вызовы с ненужными нам параметрами
        ExceptionInfo->ContextRecord->Ebx < 0x77000000
        &&
        ExceptionInfo->ContextRecord->Ebx > reinterpret_cast<DWORD>(GetProcessHeap())
    )
        dump_data(reinterpret_cast<char *>(ExceptionInfo->ContextRecord->Ebx), ExceptionInfo->ContextRecord->Eax);
    // Записываем в Eip адрес с верхушки стека и смещаем указатель на верхушку стека на 4 байта
    // Короче, выполняем действия, аналогичные инструкции retn
    ExceptionInfo->ContextRecord->Eip = *reinterpret_cast<DWORD *>(ExceptionInfo->ContextRecord->Esp);
    ExceptionInfo->ContextRecord->Esp += sizeof(DWORD);
    // Продолжаем выполнение программы как-будто ничего не произошло
    return EXCEPTION_CONTINUE_EXECUTION;
}

BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
    if(dwReason == DLL_PROCESS_ATTACH)
    {
        // Создаем директорию для хранения дампов памяти
        CreateDirectory(dump_directory.c_str(), NULL);
        AddVectoredExceptionHandler(1, VEH);
        // Устанавливаем хук на функцию в соответствии с документацией detours
        DetourRestoreAfterWith();
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourAttach(&reinterpret_cast<PVOID &>(real_loadlibrary), my_loadlibrary);
        DetourTransactionCommit();
    }
    else if(dwReason == DLL_PROCESS_DETACH)
    {
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(&reinterpret_cast<PVOID &>(real_loadlibrary), my_loadlibrary);
        DetourTransactionCommit();
    }

    return TRUE;
}

Осталось только протестировать получившееся решение.

Распаковка Perl2Exe

Как видно, всё отлично сдампилось. Мы достигли цели.
Интересующиеся могут также посмотреть похожий пример распаковки PerlApp.

Исходный код из статьи: скачать

Автор: kaimi_ru

  1. Артём:

    Здравствуйте. Достаточно интересная статья, но у меня имеется вопрос.

    У меня есть скрипт, упакованный утилитой PerlApp. (Он мой, но исходник был утерян когда сломалась виртуалка в которой происходила рахработка). Основной скрипт восстановил без труда – поставил брейкпоит в функции perl_get_context перед return и достал исходник из стека.

    Но ситуация осложнена тем, если к основному скрипту подключено ещё несколько через директиву require. Зная примерный код, можно найти их поиском в памяти. Но поскольку они записываются в одну область памяти, то они частично затирают друг друга. Адрес при каждом запуске изменяется, поэтому я не могу поставить на него брейкпоинт и отследить, какая функция производит запись. Собственно вопрос в том, как это ещё можно сделать?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js