📜 ⬆️ ⬇️

Unpacking Perl2Exe



One of the most commonly used products for creating standalone applications from perl scripts and organizing some kind of protection is IndigoStar Perl2Exe . Periodically, there are situations when the source code of the script is lost, and in the hands there is only an exe-file obtained using this program, but without fail I want to get to the samples. We will understand how to do it.

To begin with, we will download the product itself (the further description is based on Perl2Exe V11.00 for Windows ) and use it for its intended purpose - we will turn the attached script sample.pl into a full-fledged exe-file. To do this, enter the simple perl2exe command sample.pl into the console, or simply drag sample.pl to perl2exe in the explorer.
So, we have sample.exe, which needs to be examined for the possibility of extracting source code. Let's start with the banal:
Take any hex editor and try to search for script code elements in the body of the file. In vain, apparently, the code is stored in a packed / encrypted form, which is quite logical.

We use the API Monitor utility and analyze the accesses to files that sample.exe makes after launching (as an alternative, you can use Process Monitor by Mark Russinovich). To do this, we note in the list of intercepted functions CreateFileA, CreateFileW, press Ctrl + H and specify the process in the context of which the observation will be conducted.
')


Again miss, intermediate files for storing the source code of the script in a readable form are not used.

Well, we will arm ourselves with a debugger and proceed to a cursory study of the internal structure of the program. The goal is to determine whether the code of interest appears in the memory of the process in open form, and at what stage of the program it will be easiest to intercept and copy it. In this case, I would prefer to use OllyDbg , but in general, almost any debugger would work, for example, WinDbg, IDA or, say, Syser.
We load the program into the debugger and observe the standard CRT shny prolog.



Without really thinking about it, we press F5 - the program successfully works, we are thrown into the depths of ntdll. Open the process memory (Alt + M) and look for some piece of the script, for example, the string "This is sample". Voila, we find the source code in memory. It is worth noting that this approach is unreliable, because by the time the program is finished, the data of interest to us could have been erased in memory, which is the right approach from a security point of view (say, the master password in the Firefox browser cannot be found after some time in mind).
Find out after what stage this code appears in memory. Let's skip the CRT shno intro and move on to the study of the first significant part of the code.



We observe the loading of the p2x5142.dll dynamic library file, obtaining the address of the exported RunPerl function and its call. Having carried out simple manipulations, we find that the sacrament of interest to us occurs inside RunPerl. We examine its contents.



Appeals to WinAPI functions do not interest us, but there is a curious sequence of calls to functions with the perl prefix (Perl_sys_init3, perl_alloc, perl_contruct, perl_parse, perl_run, ...), and, having conducted an investigative experiment, we find out that the code appears in memory in open form after calling perl_parse. Let's see what is inside perl_parse. Great, again a bunch of function calls with the perl prefix and other crap, the study of which is impeded by natural laziness and the excuse "yes, I write this article in general while sitting on the bus."
Let's go the other way. We run the program a couple of times and make sure that the memory for the source code of the script is allocated in the same place (which is again an unreliable approach, and it would be more logical to intercept the functions malloc, free and analyze the addressable areas). Put a breakpoint on it to find the code responsible for recording data at the address of interest to us.
Restart the program and drop out somewhere here:



But intercepting data in the middle of a cycle is probably not the best choice. Let's see what we have before returning from a function.



But this more than suits us. In the ebx register and the stack, we see the addresses pointing to the memory region with the source code of interest. Let's write down the virtual address of the instruction retn in a notebook, look at the address where the p2x5142.dll was loaded into the memory. We subtract one from the other and get an offset, which is useful later.

Now it would be nice to automate this process. To do this, we will write a simple library, which we will load into the sample.exe memory. The library itself will install a vector exception handler, track the moment when the library p2x5142.dll is loaded into memory, replace the retn instruction with int3 using the offset received earlier and dump the contents of the memory addressed by the register to a file.
In order not to write the code of the library's injecting in a new way, we will use the class that I once wrote, having mastered some part of the “C ++ for 21 Day” book. Here it is . Its size should be ... about the thickness of the thumb. That's about this. We will also use Microsoft's Detours library to intercept the LoadLibrary call. Of course, it would be possible to refer to the developments in MASM on this topic, but, unfortunately, Friday is not a day of programming in low-level languages. Let's start writing. Let's start with the launcher program, which will run sample.exe in the suspended state, inject the library into the created process and resume its work.

//    #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; } 


Let's go to the source code of the library.

 //   #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; } 


It remains only to test the resulting solution.



As you can see, everything went great. We reached the goal.
Those interested can also see a similar example of unpacking PerlApp .

Source code from the article: download

Source: https://habr.com/ru/post/149015/


All Articles