02.23
Today we finish our AlexProtector unpacker. We started creating it last week with file format analysis. We initially intended to create a dynamic unpacker for this protection, but since it is just as "easy" to create a static one, we went for that option. We are a day late with our blog as a result, and we are glad we are, since we noticed some bugs in the Importer module that we have since resolved. But we did more then just bug fixing - we made some tweaks to the existing functions, improving import elimination protection support.
Since we already did the analysis, let's go straight to coding an unpacker and describe everything that needs to be done to complete it. We'll start by reserving enough space for the section data that needs to be decompressed. This can be a complex task if we try to manually re-size the file and move all data to the appropriate locations. However TitanEngine comes with two functions which can do the hard work for us, even though they are not meant to do this exact task.
But before we go into that let's explain what is really needed. The problem: some sections are compressed by aPLib which means that the protected file size is most likely smaller than the original file size. To ensure there is enough extra space to hold the decompressed data, we have to re-size the sections so they can hold the physical data. But how much space is needed? As it turns out, that part is simple, because AlexProtector stores compressed section data inside its original sections. This means that if the first PE section has been compressed, its compressed data will still be located in the first section. After decompression that data will still need to be written in the first section, but will require more physical space, since the compressed data is up to ~70% smaller than the original data. However, since the original data is still in the section, we know that the maximum physical size of that data is equal to the virtual size of that section. As a result, we can simply re-size all sections so that their physical size is equal to their rounded up virtual size.
Doing this is just like dumping the memory of a running process with the TitanEngine's DumpProcess function, but since we're making a static unpacker, we don't want to start the process, so we will do it a little differently. We will use StaticFileLoad to simulate the file load, which has the same effect. Then we will dump that memory to disk with DumpMemory. But that doesn't complete the task - we need to correct the PE section values of the dumped image to make the file a valid PE image. To do this, we set the raw size of each section to the rounded up virtual size, and set the raw offset of each section to its virtual offset. Once that is done, all virtual locations in the file will be equal to their physical counterparts. For example, the virtual address 0x1000 will be on the 0x1000 byte from the start of the file.
Once we have the file re-sized to its approximate original size, we can start decompressing data. However we don't want to guess which section is compressed and which isn't, so we will directly read the protector data to reverse the compression. To find out where this data is stored, we monitor calls to the aPLib decompression code and find a call to it from the second protection layer, which is exactly what we need:
;Layer base: 0x00220000 ;-------------------------------------------------------------------- /*2202D1*/ LEA ESI,DWORD PTR SS:[EBP+4023C1] ;Internal section data /*2202D7*/ MOV EAX,DWORD PTR DS:[ESI+4] /*2202DA*/ PUSH 4 /*2202DC*/ PUSH 1000 /*2202E1*/ PUSH EAX ... Junk removed with DeJunk ... /*220387*/ PUSH 0 /*220389*/ CALL NEAR DWORD PTR SS:[EBP+402411] ;VirtualAlloc /*22038F*/ MOV DWORD PTR SS:[EBP+40239D],EAX /*220395*/ PUSH ESI /*220396*/ MOV EBX,DWORD PTR DS:[ESI] /*220398*/ ADD EBX,DWORD PTR SS:[EBP+402399] /*22039E*/ PUSH EAX ... Junk removed with DeJunk ... /*220444*/ PUSH EBX /*220445*/ LEA ECX,DWORD PTR SS:[EBP+401108] /*22044B*/ CALL NEAR ECX ;aPLib decompress /*22044D*/ ADD ESP,8 /*220450*/ MOV ECX,EAX /*220452*/ MOV EDI,DWORD PTR DS:[ESI] ... Junk removed with DeJunk ... /*2204F9*/ ADD EDI,DWORD PTR SS:[EBP+402399] /*2204FF*/ MOV ESI,DWORD PTR SS:[EBP+40239D] /*220505*/ REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI] /*220507*/ POP ESI /*220508*/ MOV EAX,DWORD PTR SS:[EBP+40239D] /*22050E*/ PUSH 8000 /*220513*/ PUSH 0 /*220515*/ PUSH EAX /*220516*/ CALL NEAR DWORD PTR SS:[EBP+402415] ;VirtualFree ... Junk removed with DeJunk ... /*2205C1*/ ADD ESI,8 /*2205C4*/ CMP DWORD PTR DS:[ESI],0 /*2205C7*/ JNZ 002202D7
As we can see from this code snippet, the protector is reading data located at EBP+0x004023c1. Here both numbers are constants with EBP being calculated at the start of the protector code and referred to as "protector delta." It is determined here:
PUSHAD CALL L002 L002: POP EBP SUB EBP,00401006
Most protections use this kind of coding which is popularly called offset independent coding. Protection authors who use this method don't need to know the exact offset at which their protection will be written. Since we are only coding an unpacker for this one protection version, we will use the same internal constants that the protector uses. So, at the location Delta+0x004023c1 we will find and read the internal AlexProtector data about the compressed sections. That data is a two dimensional array with the following structure:
typedef struct AlexProt_SectionData{ DWORD SectionVirtualOffset; DWORD SectionVirtualSize; }AlexProt_SectionData, *PAlexProt_SectionData;
After we decompress that data, all the sections are restored to their original state - with a couple of small exceptions: we need to identify the original entry point for the code section, and we need to remove import elimination protection. Of those two things, we will fix the import protection first, because removing that protection will correct the code section data.
When we were analyzing the file format, we determined the internal format that AlexProtector uses for storing import data. Now that we are creating an unpacker, we need that data, so we need to determine where it is stored and if it's encrypted at any point. With some tracing we determine that this code does the decryption of the compressed import data:
;Layer base: 0x00220000 ;-------------------------------------------------------------------- /*22080F*/ LEA EDI,DWORD PTR SS:[EBP+402531] ;Pointer to import data /*220815*/ ADD EDI,DWORD PTR SS:[EBP+4023B5] /*22081B*/ XOR EDX,EDX /*22081D*/ MOV ECX,100 /*220822*/ DIV ECX /*220824*/ MOV EBX,EDX /*220826*/ DIV ECX /*220828*/ ADD EBX,EDX /*22082A*/ DIV ECX /*22082C*/ ADD EBX,EDX /*22082E*/ DIV ECX /*220830*/ ADD EBX,EDX /*220832*/ POP ECX /*220833*/ MOV EAX,EBX /*220835*/ MOV ECX,DWORD PTR SS:[EBP+4023B9] ;Size of the import data /*22083B*/ XOR BYTE PTR DS:[EDI],AL ;Decryption loop /*22083D*/ INC EDI /*22083E*/ DEC ECX /*22083F*/ JNZ 0022083B
Internal import data is compressed and then encrypted. In order to use it, we must reverse this process by decrypting the memory content, then decompressing it. To decrypt it, we need the decryption key and the decryption algorithm. Since the algorithm is known - it is a simple XOR - we simply need the decryption key in order to continue. Here is where the protection author made a mistake: he used a decryption key which is a CRC hash of the selected memory part. Now this is a only partly a "mistake," since it does prevent us from using software breakpoints in some areas, but it makes the decryption key calculation unnecessary because that key can't change for this particular protector version. Since the EAX value at the start of this code snippet is 0xD0340178 we can calculate that the decryption key is 0x7D.
Once this memory is decrypted, it can be decompressed and then processed in order to correct both the import table and the code section. If we remember our analysis from the last time, here is how the internal import data is packed:
typedef struct ALEX_IAT_DLL{ BYTE DLLSignature; //0xC3 BYTE DLLNameLength; // DLLName[DLLNameLength] followed by 0x00 }ALEX_IAT_DLL, *PALEX_IAT_DLL; typedef struct ALEX_IAT_APIENTRY{ // 0xC4 indicates Ordinal import BYTE APINameLength; // APIName[APINameLength] followed by 0x00 BYTE RedirectionNumber; //Number of redirections DWORD RedirectionAddress[RedirectionNumber]; }ALEX_IAT_APIENTRY, *PALEX_IAT_APIENTRY;
Above is the pseudo C code that describes the internal import data structure. Here, "redirection data" refers to the addresses in the code section that need to be corrected to point to the correct API pointers. We can consider this table as a sort of relocation table, because the data that needs to be written at that location corresponds to a random location in memory at which the import redirection is allocated. Since that memory allocation is outside the PE image file memory, we call that kind of import protection "import eliminations." To repair it, we must estimate the location at which to reconstruct the import table, and create a table aligned to that location on the fly. There are two ways to do this and we decided to go with the more complex one. Its logic goes like this:
- Initialize the importer and say that the import table will be moved
- Create a relative virtual import table with no point of reference
- Relocate the virtual import table to the reserved spot
- Write the new import table
While the term "relative virtual import table" sounds confusing, it can be described simply as an import table whose references start with zero and increment by four. That would look something like this:
- New DLL: Kernel32.dll
- TrunkValue: 0x00000000; GetProcAddress (e.g.)
- TrunkValue: 0x00000004; LoadLibraryA (e.g.)
- TrunkValue: 0x00000008; FreeLibrary (e.g.); Remember its +8 for the last item
- New DLL: User32.dll
- TrunkValue: 0x00000010; MessageBoxA (e.g.)
- TrunkValue: 0x00000014; MessageBoxW (e.g.)
Once we compile the table, we simply relocate it by adding the correct value to its trunk relative address. That value is the virtual address of the section we will add to the file to hold the import table. During unpacking, that address will be a SectionAlignment aligned value of NtSizeOfImage. We could have built our table like this from the start, without relocating it, but we wanted to show a more general approach. Now we just need to write the correct pointers to the redirection addresses found in the AlexProtector internal data to connect the code section with the new import table.
With imports sorted, we move on the last item on our list, the entry point protection. It is processed by this code here:
;Layer base: 0x00220000 ;-------------------------------------------------------------------- /*220D0B*/ LEA EBX,DWORD PTR SS:[EBP+402531] ;Pointer to EP data ... Junk removed with DeJunk ... /*220DB6*/ ADD EBX,DWORD PTR SS:[EBP+4023B5] /*220DBC*/ ADD EBX,DWORD PTR SS:[EBP+4023B9] ;Pointer correction ... Junk removed with DeJunk ... /*220E67*/ MOV ECX,DWORD PTR SS:[EBP+402395] ... Junk removed with DeJunk ... /*220F12*/ MOV EAX,DWORD PTR SS:[EBP+4023AD] /*220F18*/ PUSH EAX /*220F19*/ PUSH EBX /*220F1A*/ LEA EDX,DWORD PTR SS:[EBP+401108] /*220F20*/ CALL NEAR EDX ;aPLib ... Junk removed with DeJunk ... /*220FC7*/ ADD ESP,8 /*220FCA*/ MOV EAX,DWORD PTR SS:[EBP+4023AD] /*220FD0*/ MOV EBX,DWORD PTR SS:[EBP+402391] ... Junk removed with DeJunk ... /*22107B*/ MOV ESI,DWORD PTR SS:[EBP+402385] ;EP resumes here /*221081*/ ADD EAX,EBX /*221083*/ MOV BYTE PTR DS:[EAX],0E9 ;Write OEP jump /*221086*/ INC EAX /*221087*/ MOV ECX,ESI ... Junk removed with DeJunk ... /*22112E*/ SUB ECX,EAX /*221130*/ SUB ECX,4 /*221133*/ MOV DWORD PTR DS:[EAX],ECX ... Junk removed with DeJunk ... /*2211DA*/ MOV EAX,DWORD PTR SS:[EBP+4023AD] /*2211E0*/ MOV DWORD PTR SS:[ESP+1C],EAX /*2211E4*/ POPAD /*2211E5*/ JMP NEAR EAX ;Jump to stolen EP
As we can see, the stolen entry point data is decompressed and a new jump to the first non-stolen instruction is written. To reverse this we must decompress the buffer with EP jump correction, after which we need to write it to a new section. It is possible to extract the correct the entry point to its original state, but that would mean that we would have to disassemble and analyze the decompressed buffer which contains those original functions riddled with junk code. Such disasesmbly and analysis is an error-prone process, however, so keeping the junk is the simplest and safest option.
Writing an unpacker for AlexProtector is a nice exercise for any reverser. We have shown in detail how it can be done statically. If you have any questions about any of the steps in writing this unpacker feel free to contact us. Until next week....
TitanEngine![]() ReversingLabs Corporation |
Samples and Protector / RL!deAlexProtector |
