Как известно, для 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-файла со скриптом, либо подгрузить её каким-нибудь другим способом.
Для начала обозначим инклюды, пару констант и создадим пустую экспортируемую функцию:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <Windows.h> #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, вот они:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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:
1 2 3 4 5 6 7 8 9 10 11 |
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 видим исходный скрипт, что нам и требовалось.
Исходный код и скомпилированная библиотека: скачать
GitHub: perlapp-unpacker
Про распаковку скриптов после Perl2Exe читайте здесь.
что ты наделал,теперь уже не как не скрыть исходники перла,удали быстра
ты слишком глубоко капаешь.
гениально
Шикарно. Как всегда интересный материал.
Спасибо. Случайно наткнулся на статью, но за то понял как все это работает.
invalid pe file. possible reason no export table present - что за хня?
Папка dump создалась, но в ней пусто. Как быть?
Править код под конкретную версию PerlApp