Одним из наиболее часто используемых продуктов для создания 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.
Исходный код из статьи: скачать
Автор: kaimi_ru
Здравствуйте. Достаточно интересная статья, но у меня имеется вопрос.
У меня есть скрипт, упакованный утилитой PerlApp. (Он мой, но исходник был утерян когда сломалась виртуалка в которой происходила рахработка). Основной скрипт восстановил без труда – поставил брейкпоит в функции perl_get_context перед return и достал исходник из стека.
Но ситуация осложнена тем, если к основному скрипту подключено ещё несколько через директиву require. Зная примерный код, можно найти их поиском в памяти. Но поскольку они записываются в одну область памяти, то они частично затирают друг друга. Адрес при каждом запуске изменяется, поэтому я не могу поставить на него брейкпоинт и отследить, какая функция производит запись. Собственно вопрос в том, как это ещё можно сделать?