Финансы Сайт на котором знают все про финансы

Распаковываем Perl-скрипты, обработанные PerlApp

Как известно, для Perl, впрочем, как и для других скриптовых языков, существуют утилиты, позволяющие создавать из скрипта полноценный exe-файл, который можно переносить на другие компьютеры и запускать, даже если интерпретатор языка на них не установлен. В случае с perl’ом наиболее популярными утилитами являются Perl2Exe и PerlApp.
Принцип работы этих утилит довольно прост и состоит в упаковке внутрь результирующего exe-файла библиотеки перла, основного скрипта и зависимых модулей. Содержимое, естественно, сжимается, шифруется (с помощью XOR) и не хранится в открытом виде внутри файла. Исследуем чуть подробнее внутреннее устройство результирующих exe-файлов, которые получаются с помощью PerlApp.
Для начала, определим с помощью чего сжимаются данные. Это сделать довольно просто, например, с помощью PeID (с плагином Krypto Analyzer) или какого-нибудь hex-редактора. В случае с PeID все тривиально: указываем путь к файлу, запускаем плагин и получаем список найденных крипто-сигнатур.

С hex-редактором тоже просто: открываем нужный файл, нажимаем Alt+F6 (справедливо для Hiew), получаем список строковых ресурсов, гуглим эти строки.

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

Итак, нам понадобится какой-нибудь дизассемблер, например, IDA или OllyDbg, а также пара подопытных exe-файлов, желательно, упакованных разными версиями PerlApp, чтобы однозначно определить сигнатуру функции распаковки. Функция распаковки элементарно ищется, если ориентироваться по строке с версией (1.1.4), но, как видно, функции довольно сильно могут отличаться от версии к версии:

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

Как видно из скриншотов, она может слегка различаться (всего 2 байта), но это не проблема, так как никто не мешает реализовать поиск по маске. Теперь нам надо как-то перехватить данные, которые помещаются в EAX в конце функции, чтобы затем записать их в файл. Один из вариантов — организовать в конце функции JMP в тело своей функции, в ней выполнить затертые прыжком инструкции, записать содержимое буфера, куда надо и вернуться назад, но опять же, мне захотелось пойти немного другим путем. Вместо создания «трамплина» я просто переписываю инструкцию RETN инструкцией INT3, которая передает управление в VEH, в нём содержимое буфера записывается в файл, изменяются регистры EIP и ESP (через структуру PEXCEPTION_POINTERS) и программа продолжает работать дальше, как будто ничего не произошло и вместо INT3 была выполнена инструкция RETN.

Теперь приведу код, которые реализует то, что я описал выше. В результате получится DLL, которую нужно прописать в импорты exe-файла со скриптом, либо подгрузить её каким-нибудь другим способом.
Для начала обозначим инклюды, пару констант и создадим пустую экспортируемую функцию:

#include 

#define DUMP_DIRECTORY TEXT("dump")
#define SEARCH_LIMIT 0xB000
#define RANGE_LIMIT 15
const unsigned char SIG[] = {0x80, 0x00, 0xEA, 0x00, 0x48, 0x75, 0xF9};
unsigned int i = 0;

void dummy()
{

}

DUMP_DIRECTORY — определяет имя папки, куда будут сохранены «перехваченные данные». SEARCH_LIMIT — максимальная дальность поиска сигнатуры функции от указанного начала (можно считать за размер секции кода, он вроде бы не меняется от версии к версии и как раз равен 0xB000). RANGE_LIMIT — максимальная дальность поиска инструкции RETN относительно найденной сигнатуры функции. SIG — сама сигнатура, i — счетчик, на основе которого формируется имя очередного файла для записи содержимого буфера. dummy — пустая функция, которая будет экспортироваться библиотекой.
Теперь нам понадобятся функции поиска по маске указанной последовательности байт. Функции я честно позаимствовал из какого-то сорца за авторством sn0w, вот они:

BOOL CompareData(const BYTE* pData, const BYTE* bMask, const char* pszMask)
{
	for(;*pszMask; ++pszMask, ++pData, ++bMask)
		if(*pszMask == 'x' && *pData !=* bMask) 
			return FALSE;
	return (*pszMask) == 0;
}

DWORD FindPattern(DWORD dwAddress, DWORD dwLen, BYTE *bMask, char * pszMask)
{
	for(DWORD i=0; i < dwLen; i++)
		if(CompareData((BYTE*)( dwAddress+i ), bMask, pszMask))
			return (DWORD)(dwAddress + i);
	return 0;
}

Настал черед функции, которая найдет сигнатуру конца функции и заменит RETN на INT3.

void Hook()
{
	DWORD pr, addr;
	void * module = GetModuleHandle(NULL);
	if(module != NULL)
	{
		addr = FindPattern(((DWORD)module + 0x1000), SEARCH_LIMIT, (BYTE*)SIG, "x?x?xxx");
		if(addr != 0)
		{
			addr += sizeof(SIG);
			addr = FindPattern(addr, RANGE_LIMIT, (BYTE *)"\xC3", "x");

			VirtualProtect((LPVOID)addr, 1, PAGE_EXECUTE_READWRITE, &pr);
			CopyMemory((void *)addr, "\xCC", 1);
			VirtualProtect((LPVOID)addr, 1, pr, &pr);
		}
	}
}

Функция довольно тривиальная. Сначала определяется адрес, по которому загрузился exe-файл, потом по сигнатуре ищется функция (смещаемся на 0х1000, чтобы пропустить заголовок PE), если сигнатура найдена, то ищется опкод инструкции RETN, меняется тип защиты региона и вместо RETN (0xC3) записывается INT3 (0xCC).
Теперь функция записи в файл и VEH:

void Write(char * buf)
{
	DWORD wr;
	wchar_t fname[256];

	wsprintf(fname, L"%ws/%u.txt", DUMP_DIRECTORY, i++);
	HANDLE file = CreateFile(fname, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if(file != INVALID_HANDLE_VALUE)
	{
		WriteFile(file, buf, lstrlenA(buf), &wr, NULL);
		CloseHandle(file);
	}
}

LONG CALLBACK VEH(PEXCEPTION_POINTERS ExceptionInfo)
{
	Write((char *)ExceptionInfo->ContextRecord->Eax);

	ExceptionInfo->ContextRecord->Eip = *(DWORD *)ExceptionInfo->ContextRecord->Esp;
	ExceptionInfo->ContextRecord->Esp += sizeof(DWORD);
	
	return EXCEPTION_CONTINUE_EXECUTION;
}

В функции Write формируется имя файла вида «имя_директории/число.txt», далее определяется размер буфера (вплоть до первого нулл-байта) и его содержимое записывается в файл. В VEH мы сначала вызываем функцию Write, передав ей в качестве аргумента адрес буфера, который хранится в EAX, далее меняем содержимое EIP, чтобы выполнение продолжилось с адреса, на который указывает верхушка стека, меняем ESP (указатель на верхушку стека), как это делает инструкция RETN, и, наконец, возвращаем EXCEPTION_CONTINUE_EXECUTION, чтобы нормально продолжить выполнение с места возникновения исключения.
Теперь осталась только функция DllMain:

BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved)
{
	if(dwReason == DLL_PROCESS_ATTACH)
	{
		CreateDirectory(DUMP_DIRECTORY, NULL);
		AddVectoredExceptionHandler(1, VEH);
		Hook();
    }

    return TRUE;
}

В ней мы создаем директорию для хранения текстовиков с дампами, устанавливаем обработчик исключений (Vectored Exception Handler) и вызываем функцию Hook. Вот и всё. Почему мы ставим именно обработчик векторных исключений, а не структурных (Structured Exception Handling, SEH)? Все просто — внутри exe-файла, созданного PerlApp, вполне могут использоваться собственные обработчики структурных исключений, которые перебьют установленный нами в DLL обработчик. А у VEH перед SEH всегда приоритет, так что SEH-обработчики внутри файла даже ничего не узнают о том, что возбуждалось исключение INT3.
Теперь компилируем этот код как DLL’ку, берем какой-нибудь скрипт, обработанный с помощью PerlApp, прописываем библиотеку в импорты, например, с помощью CFF Explorer:

И наслаждаемся результатом:

В файле 3.txt видим исходный скрипт, что нам и требовалось.
Исходный код и скомпилированная библиотека: скачать