Introduction
Recently, I’ve come across a Golang malware sample which have been packed by UPX. The sample was made to be cannot be unpack using UPX tool. So, I thinks it’s good to write a blog/note to explain how the UPX manually unpacking works.
UPX tool doesn’t work
In the figure below, there are certain situation when the malware author makes their packed malware cannot be unpack by the UPX tool to slow down the analyst. Therefore, we need to do the manually unpacking to unpack the malware.
As described in the figure above, when we strings
the binary, we can see a lot of “UPX” string indicate the file packed with UPX. But, when we try to unpack it with -d
using UPX tool, the tool saying the file is not packed with UPX.
In another case, in figure below, the windows executable has been packed and cannot be unpack by the UPX tool automatically due to some alteration. In this case, if we know how to alter back to become the original will be good. But, somehow not everyone knows which bytes to be change or alter.
So, to solve this problems, manual unpacking come to the rescue!
Tools required
The required tools to unpack the ELF will be:
- IDA Pro
- Linux VM.
- OllydumpEx plugin for IDA Pro
While, unpacking EXE we going to use:
- x32Dbg
- OllydumpEx
- Scylla
ELF unpacking
Setup OllyDumpEx for IDA Pro
- Download the plugin here
- Copy the files into IDA Pro directory plugins.
Set-up IDA Pro debugger
In order to dynamic analysis using IDA for ELF, we need to copy IDA linux debug server file into our Linux VM and run it. The file location is at %IDA installation directory%/dbgsrv/linux_server64
. After you did run the ./linux_server64
and it will start listening for the connection from the IDA Pro in your host.
While in IDA, open the packed sample and we can see there are 6 functions include start
function which are very few functions indicate the binary is packed. Select the last function from the 6 function.
This function is actually being the first callee in the start
function.
This function will invoke few functions like sysmap, sysmmap and sysmprotect to initialize the environment to unpack the code. Scrolling down through the function and find jmp r13
instruction then put the breakpoint at that line.
Basically, the code jmp r13
will transfer the RIP to the address contain in r13.
We now can start debug the sample by choose the debugger. Select Remote Linux debugger
.
In the options, make sure you put the path based on your Linux VM, not your Windows. And for the IP address, fill in with your Linux VM IP address which I suggest to use Host-only network adapter.
After done setup the configuration, click OK and then run the debugger. It then will break at the breakpoint we setup previously. If a warning pop-up, just click OK.
Manual unpacking
Soon we run the debugger. It will stop at the breakpoint jmp r13
.
Step into
and it will transfer the RIP into a new code region which the first instruction is call function. Step into
the call function.
Now, we arrived at another region of code.
Scroll down until you see the following assembly instruction:
pop rax
jmp qword ptr [r14-8]
Breakpoint at the line jmp qword ptr [r14-8]
and continue run. Step into the JMP
instruction then step over
until retn
instruction.
Step over
the retn will transfer our RIP to another region of code. At this part, breakpoint at the end of the line of the assembly code which is jmp r12
.
In the below figure, you will see IDA prompts that the RIP has been changed, which means that we have to jump back to the real start function which are OEP of the unpacked sample.
Then you will see real start function like figure below. This indicate we have successfully arrived at OEP of the unpacked sample.
At this point, open OllyDumpEx plugin at Edit > Plugins > OllyDumpEx
. Just click Dump
button and save the unpack file in your Windows. And click Finish
.
Now, try to run the binary. If it can be run without any error like the packed version, it’s indicate that our unpacked binary successfully unpacked. Another indicator is, to compared functions detected in IDA between the packed and unpacked version of the sample. The unpacked version will have a lot of functions like below:
EXE unpacking
Manually unpacking Windows executable packed with UPX is easy. Just find the “tail jump” instruction and we can start dump the executable.
Finding the tail jump
First, load the binary in the x32dbg. And the x32dbg will pause in the module ntdll.dll. At here, just click run and the debugger will automatically stop at entry point of the sample. The first instruction for UPX packed sample should be pushad
instruction. This instruction will pushes the contents of the general-purpose registers onto the stack.
Normally, pushad
instruction will be pairing with popad
at the end of the function. So, for UPX packed sample, the tail jump can be identify by looking forward the popad
instruction which usually the tail jump is made after the popad
instruction.
Or another alternative to find tail jump, just looks for a lot of add byte ptr ds:[eax], al
and the one jmp
before the first add byte ptr ds:[eax], al
is the tail jump.
Breakpoint on the tail jump, run and step into the address. Now, we arrived into the Original Entry Point.
Dump process
The packed sample now has been unpacked into the memory and we can start dump from here using OllyDumpEx Plugins > OllyDumpEx > Dump Process
.
Following the number in the figure above, first we will get the current EIP as our OEP because the current EIP is the OEP. Click the Get EIP as OEP
button. In our case, the OEP is 0x401190
. And then click Dump
button. Save into the disk and click Finish.
Before the program (dump) can be work and running smoothly, we need to build the Import Address Table (IAT).
Now open up Scylla by cliciking the Scylla icon at the above of the x32dbg interface.
Now click on 1IAT Autosearch1 button so the Scylla will automatically find the Import Address Table (IAT) of our sample. After that, click on Get Imports
button to get a list of imports that the executable already has. Then, click Fix dump
and choose the dumped binary file to fix the dumped binary we dumped using the OllyDumpEx. We do this step because the previous dump binary doesn’t have IAT.
Now the fixed dump will be save with the SCY
append at the back of the dumped file.
To verify whether we successfully unpacked the file or not. Load the binary into dissassembler or debugger, and now the program have a lot of functions compared to the packed version.