Распаковка 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 и укажем процесс, в контексте которого будет вестись наблюдение.



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

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



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



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



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



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



    А вот это нас более чем устраивает. В регистре 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;
    }
    
    


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



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

    Исходный код из статьи: скачать
    • +18
    • 5,5k
    • 3
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 3
    • +1
      Странно, я думал что perl2exe хранит скомпиленый байткод и уже только его отрабатывает…
      • –1
        Скрипты Denwer (Джентльменский Набор Web-Разработчика) пробовали таким образом распаковать?
        • 0
          Для бинарей под unix, полученных из перла компиляцией (perlcompile/perlcc), обычно помогает B::Deparse, если заинжектить его загрузку в любом внешнем .pm модуле, которые тянутся в исходном виде (в отличие от perl2exe — там все сложнее)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.