In this blog post, we have a packed PE file. We will analyze it and unpack it with Qiling Framework. Once you understand how encryption works, I will explain more about Qiling. In this article we will use Cutter for analysis and Ghidra for decompile (included in Cutter).
Table Of Contents
1. Analysis Packed Sample 1.1 Detect It Easy Output 1.2 Packer Basics 1.3 Cutter - Ghidra (Disassemble & Decompile) 2. Automate Unpacking with Qiling 2.1 Qiling Framework Structure 2.2 Layle's Emu Analysis 2.3 Unpacking Script
Analysis Packed Sample
Detect It Easy Output
The first thing I would do for analysis is to scan the file into Detect It Easy. On the Detect It Easy screen, it says that it contains the usual protections. But we know that Sample doesn’t. To investigate why, I’ll look at how Detect It Easy does this.
PE: protector: PELock(-)[-]
PE: protector: Unopix(0.94)[-]
PE: linker: Microsoft Linker(14.33**)[EXE32,console]
In fact, it is not difficult to understand how he did it. When we switch to the Signatures tab, we see that all kinds of identifiable things have rules. Here we need to look at PELock. This signature name is PELock.2 and code is below:
function detect(bShowType,bShowVersion,bShowOptions)
{
if(PE.getNumberOfImports()==1)
{
if((PE.isLibraryFunctionPresent("KERNEL32.DLL", "LoadLibraryA"))!=-1&&
(PE.isLibraryFunctionPresent("KERNEL32.DLL", "VirtualAlloc"))!=-1)
{
if(PE.getNumberOfResources()>=1)
{
if(PE.getNumberOfSections()>=4)
{
if((PE.getSectionName(0)==PE.getSectionName(1))&&(PE.getSectionName(0)==PE.getSectionName(3)))
{
bDetected=1;
}
}
}
}
}
return result(bShowType,bShowVersion,bShowOptions);
}
The first thing it does is that there is “only” Kernel32.dll on the Import Table. Secondly, does it have LoadLibraryA and VirtualAlloc? Thirdly, it checks the number of Resources and Sections and checks if some of the section names are equal to each other.
The Unopix protection (first time I’ve seen it) works like this:
function detect(bShowType,bShowVersion,bShowOptions)
{
var nLastSection=PE.nLastSection;
if(nLastSection>=2)
{
var nVirtualSize=PE.section[nLastSection].VirtualSize;
if(nVirtualSize==0x1000)
{
var nRawSize=PE.section[nLastSection].FileSize;
if(nVirtualSize==nRawSize)
{
var nFlags=PE.section[nLastSection].Characteristics;
if((nFlags==0xe0000040)&&(PE.section[nLastSection].Name!=".!ep"))
{
sVersion="0.94";
bDetected=1;
}
}
}
}
return result(bShowType,bShowVersion,bShowOptions);
}
I will not explain what he did because it is very clear. Summary: PELock and Unopix(?) show undesirable results in Detect It Easy because they display a similar representation. What I mainly want to do here is to compare EntryPoint and Sections in PE Info by pressing the PE button. I will find the place in Section that matches the address we see in AddressOfEntryPoint (00022000).
If we look at the virtual address of the section named .shell, we will see that it matches the entrypoint.
Packer Basics
There are many sources written about this. But it would be wrong to start the analysis without writing about it. Most packers work the same way. To simplify our interpretation, I will first briefly explain how they work. Packer encrypts the .text section containing executable code. It then inserts a new section into the file and changes the Entry Point accordingly. The new section does the decryption of the encrypted section (unpacking). The decrypted codes are then executed in memory.
For example, the data encryption function of the packer we analyzed:
My goal in this article is not to dump in an executable way (i.e. I will not fix the IAT table after dumping). After Decryption with Qiling, we will take a look at the sections. For example, an image of a sample partition before it is unpacked:
After unpacking:
Cutter - Ghidra (Disassemble & Decompile)
We already knew that the file was encrypted by the deletion of the partition names. The entry point of the file we have is redirected to the address of the partition named .shell . After opening the Cutter, we already see one function in the functions section and we start to analyze it. This is what we will see on the graph when we open the file on Cutter (in read mode):
I will use Ghidra since it is more difficult to interpret such packing operations in assembly. Once we figure out how it works, we can use x32dbg and Scylla.
We will see while debugging, but there are some things I want to report first. Structures like [ebp + 24] that we see in assembly commands are local variables defined at the beginning of the program. Local variable definition:
;-- (0x0042202e) GetProcAddress:
0x0042202d add byte [ebx + 0x20], cl
0x00422030 .dword 0x205c0002
;-- GetModuleHandleA:
0x00422032 .dword 0x0002205c ; reloc.Kernel32.dll_GetModuleHandleA
;-- LoadLibraryA:
0x00422036 .dword 0x0002206f ; reloc.Kernel32.dll_LoadLibraryA
For example, we see that the packer initially uses the following commands to access some Windows APIs:
In Disassemble code:
0x004220c1 lea esi, [ebp + 0x9e]
0x004220c2 mov ch, 0x9e ; 158
0x004220c4 add byte [eax], al
0x004220c6 add byte [esi + 0x50], dl
0x004220c9 call dword [ebp + 0x2e] ; 46
0x004220cc mov dword [ebp + 0xab], eax
The code is missing, some parts are not interpreted as functions, so the decompiler cannot detect them. Junk code added in the packer also shows the decompiler as broken, for example the function “func_0x00422129”. So let’s try to go through disassembly.
Our goal here is to dump the software from memory after decrypting the sections. So we will examine the decrypt instructions on disassembly.
; DecryptData proc src: dword, dest: dword, count: dword
0x0042212c pushal
0x0042212d mov ecx, dword [ebp + 0x10] ;count
0x00422130 mov esi, dword [ebp + 8] ;src
0x00422133 mov edi, dword [ebp + 0xc] ;dest
0x00422136 jmp 0x42213d
0x00422138 lodsb al, byte [esi]
0x00422139 sub al, 0xcc ; 204
0x0042213b stosb byte es:[edi], al
0x0042213c dec ecx
0x0042213d or ecx, ecx
0x0042213f jne 0x422138
This part does the opposite of the EncryptData() function. The reason why this function is executed several times is that all data is encrypted with the same function.
Ok, our goal here is to dump the partitions from memory immediately after the decrypt procedure. Actually unpacking exactly is very easy with Qiling. But I won’t go into IAT build in this article. I want to talk about an example project that does this. The project is vacation3-emu, which emulates Layle’s vac3 modules.
And here is the function that decrypts the sections:
If we analyze it dynamically on x32dbg. We see that after decrypting the data, it jumps to [ebp+0xAF] which it stores locally. Here are the codes that decrypt the sections.
008F20E3 | 6A 00 | push 0x0
008F20E5 | FF95 AB000000 | call dword ptr ss:[ebp+0xAB]
008F20EB | 8985 AF000000 | mov dword ptr ss:[ebp+0xAF],eax
........
Automate Unpacking with Qiling
In this section we will see some of the functions in the Qiling Framework and then we will develop a software that automatically decodes and dump partitions. Since most API implementations of the Qiling Framework on Windows are missing, I will also show how to hook some functions (I may even post a PR for them later).
First, let’s take a look at what exactly the Qiling Framework is.
Qiling Framework Structure
Since the previous blog post in vx.zone went into historical details about qiling, I would like to be more specific. Unicorn is a CPU emulator. It is simply a framework that can only emulate processor instructions. Qiling is a high-level framework that covers that too and can even emulate operating system files. Based on this information, when we look at the structure of the Qiling project on GitHub, we see several features implemented in detail.
For example, most famous file structures are defined in the loader folder:
qiling\qiling\loader
qiling\qiling\loader\macho_parser
qiling\qiling\loader\__init__.py
qiling\qiling\loader\blob.py
qiling\qiling\loader\dos.py
qiling\qiling\loader\elf.py
qiling\qiling\loader\evm.py
qiling\qiling\loader\loader.py
qiling\qiling\loader\macho.py
qiling\qiling\loader\mcu.py
qiling\qiling\loader\pe_uefi.py
qiling\qiling\loader\pe.py
This folder is important. Because we are going to implement some unimplemented windows apis using the pe.py file here. I will give a small spoiler. For example, we will use the Process structure in pe.py to emulate the GetModuleHandleA function.
There is an os folder to emulate other specific features of operating systems. Within the folder, there are similar features between operating systems, as well as separate files containing specific features.
qiling\qiling\os\windows
qiling\qiling\os\windows\dlls
qiling\qiling\os\windows\__init__.py
qiling\qiling\os\windows\api.py
qiling\qiling\os\windows\clipboard.py
qiling\qiling\os\windows\const.py
qiling\qiling\os\windows\fiber.py
qiling\qiling\os\windows\fncc.py
qiling\qiling\os\windows\handle.py
qiling\qiling\os\windows\registry.py
qiling\qiling\os\windows\structs.py
qiling\qiling\os\windows\thread.py
qiling\qiling\os\windows\utils.py
qiling\qiling\os\windows\wdk_const.py
qiling\qiling\os\windows\windows.py
Of course, the processors have an arch folder to make the necessary adjustments before the emulation process. It contains implementations of many versions. For example x86.py:
Layle’s Emu Analysis (BONUS) <3
I got permission from layle for this. It’s a great example of using Qiling on Windows. So we will analyze this.
There is not much to look at in the structure of the project. For this, let’s go directly into the emu.py file. When we look at the beginning of the script, I see that two Windows APIs are defined. Layle did not have the implementation of these functions in Qiling when he wrote this script, so he wrote them himself.
While writing the functions here, the loader structure mentioned in the “Qiling Structure” section was used. For example, what the GetProcAddress function does is to return the address where the functions in the dlls on the Import Table are located.
After setting the classic ql variable, hooking is done using the set_api
function.
QL_INTERCEPT:
POSIX system calls may be hooked to allow the user to modify their parameters, alter the return value or replace their funcionality altogether. System calls may be hooked either by their name or number, and intercepted at one or more stages: - QL_INTERCEPT.CALL : when the specified system call is about to be called; may be used to replace the system call functionality altogether - QL_INTERCEPT.ENTER : before entering the system call; may be used to tamper with the system call parameters values - QL_INTERCEPT.EXIT : after exiting the system call; may be used to tamper with the return value
The other point that attracted my attention on this script is that the function execution process that we do by moving the eip register on the debugger can be done very simply here.
When we run the function after specifying the start and end address of the function, the function is executed in the ql sandbox. For example, Layle uses the function in the file to rebuild the IAT table in the file it emulates.
Unpacking Script
First, let us explain our purpose. We can’t read the chapters because of the encrypted data. For this, we will dump the partitions after they are decrypted. After running the function that decrypts the partitions, it is enough to run the dump_memory_region
function.
Since the starting addresses of the functions in the assembly are loaded into local variables to be executed, we must first analyze this. After initializing on x32dbg I find where the decrypt function is loaded.
Our target value is [EBP + 0xAF]
Only after running these parts (the addresses are known) I will change the EIP and redirect it to the address I want.
Python code:
Output:
utku%> python main.py
[=] Initiate stack address at 0xfffdd000
[=] Loading testFile-packed.exe to 0x400000
[=] PE entry point at 0x422000
[=] TEB is at 0x6000
[=] PEB is at 0x61b0
[=] LDR is at 0x6630
[=] Loading ntdll.dll ...
[=] Done loading ntdll.dll
[=] Loading kernel32.dll ...
[=] Loading kernelbase.dll ...
[=] Done loading kernelbase.dll
[=] Done loading kernel32.dll
[=] Loading ucrtbase.dll ...
[=] Calling ucrtbase.dll DllMain at 0x10298260
[=] GetSystemTimeAsFileTime(lpSystemTimeAsFileTime = 0xffffcfcc)
[x] Error encountered while running ucrtbase.dll DllMain, bailing
[=] Done loading ucrtbase.dll
[=] GetModuleHandleA(lpModuleName = "Kernel32.dll") = 0x6b800000
[=] GetProcAddress(hModule = 0x6b800000, lpProcName = "VirtualAlloc") = 0x6b8181b0
[=] VirtualAlloc(lpAddress = 0, dwSize = 0xe0d, flAllocationType = 0x3000, flProtect = 0x40) = 0x50006f8
[EBP + 0xAF]: 0x50006f8
[=] GetModuleHandleA(lpModuleName = "Kernel32.dll") = 0x6b800000
[=] GetProcAddress(hModule = 0x6b800000, lpProcName = "VirtualAlloc") = 0x6b8181b0
[=] VirtualAlloc(lpAddress = 0, dwSize = 0xe0d, flAllocationType = 0x3000, flProtect = 0x40) = 0x5001505
Dumped