
Одним из наиболее часто используемых продуктов для создания 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 день». Вот он.
// Подключаем заголовочные файлы #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.
Исходный код из статьи: скачать