top of page

Manually unpacking a UPX packed binary

Hey, it's been a while indeed! A quick update on what I've been up to; I recently just passed the eCMAP exam and am now a Certified Malware Analysis Professional! :) I am also currently taking Sektor7's Malware Development Essentials course, so look out for some Malware Development blogs in the future.


I'm writing this blog with the intention to explain in very simple terms what it means to have a packed binary and also how to manually unpack it. When studying for the eCMAP exam, I was desperately looking for a resource that would walk me through this so I decided to write it myself.



What is a packed binary?

Before understanding what a packed binary is, let's first understand what a packer is. A Packer is a type of software that is commonly used by both malware authors and by those wanting to protect their intellectual property. It works by obfuscating its code and compressing the binary.


Usually the the payload of the binary is either encrypted or encoded and is decoded or decrypted and then executed at run time. The packing process would usually follow the following oversimplified steps:

  1. The packer will compress the contents or payload of the binary.

  2. The packer will then Insert an unpacking stub which will be called to unpack the binary and return execution to the OEP (Original Entry Point).

  3. The packer will then modify the entry point of the binary to point to unpacking stub's location.

  4. Finally, the packer will produce a packed binary.

The unpacking stub

When the binary is executed, the Operating System will load the unpacking stub which in turn will unpack and load the original executable. Easy right? Let's dive in a bit deeper.


The OEP (Original Entry Point) code will point to the location where the unpacking stub is stored. This means when the OEP is executed, it will jump to another region of code which will unpack the original executable and then resume execution of it.


Manual Unpacking

When manually unpacking the binary, we will run the packed binary to let the unpacking stub unpack the binary for us and then dump the process to disc and manually fix the PE header. The general steps are:

  1. Determine the OEP which is the first instruction before packing. We need to find the instruction that will jump to the OEP from within the packed binary.

  2. Execute the program until we reach the OEP and then let the malware unpack itself in memory and pause execution at the OEP. This will land us right before the unpacked malicious code.

  3. Next we will be able to dump the unpacked process in memory and save it to disc.

  4. Lastly, we can fix the IAT table of the dumped file so that we can then resolve the imports.



How to determine if the binary is packed?

When conducting your initial assessment there are some indicators to look out for which will indicate that the binary is packed.


The first of which is if the binary has an overall high entropy. This is a measure of randomness and the higher the entropy the more random the data is, usually indicating that it is encoded or encrypted. The rule of thumb is that, if the entropy is 6.5 and above this is an indicator that the sample may be packed.


Another easy to spot indicator is the signature, which in this case is of UPX which hints that it may be UPX packed.

Looking at the entry point of the sample we can see that it is pointing to the section named UPX1 at address 0x00017B30. Usually the entry point of the binary would be the text section where the code of the program is stored, however in this case it is pointing to the unpacking stub which will unpack the binary and then execute the unpacked code.

Looking at the sections, I can see there is also a UPX0 section. What is interesting, is that this section has a raw size of 0 bytes however has a virtual size of 49152 bytes which is very strange. It also is executable, writeable, self-modifying and virtualized which indicates that some data will be written here. The section UPX1 (which is the entry point) will unpack the binary and write the unpacked code to the UPX0 section and will then transfer execution to the UPX0 section.


Method 1 unpacking using pushad and popad instructions

The first method of manually unpacking this binary is to look for a pushad instruction and let the binary unpack itself. First I needed to identify the memory address of the UPX0 section which is where the unpacking stub will unpack to which is address 00401000.

The next step is to locate the pushad instruction which is the binaries OEP (Original Entry Point) and will be used to push all of the 32-bit registers to the stack. This is done so that when the unpacking stub unpacks the binary it can pop these registers again using the popad instruction and return to this location. I then hit F7 to execute this instruction.

After executing this instruction, I went to the registers window and right clicked on the value in the ESP register (0019FF54) and followed it in dump. This is the stack pointer and points to the next item on the stack.

In the dump window, I right clicked on the address in the dump that was in the ESP register (0019FF54) and set a hardware breakpoint when the DWORD is accessed. This is the address that will be accessed immediately after the popad instruction is executed and the register values are restored. This occurs when the unpacking stub has finished unpacking the binary and execution is transferred back to the OEP to execute the unpacked code.

At this point I have a breakpoint on the next memory address to be accessed after the binary has been unpacked. At this point I can hit F9 and stop just before the unpacked code.

After hitting F9, I hit a breakpoint on the next instruction after the popad instruction. From this point onwards, the binary has unpacked itself and written the unpacked code to the UPX0 section. The next step is to look for a tailjump, which is just a jump instruction going to an address within the UPX0 section.


I recall that the UPX0 section's memory address was 00401000 and a few instructions below the popad instruction is a jump going to the memory address 401c50 which is in the range of the UPX0 section. This means that this jump instruction will take us to the unpacked code. At this point I placed a breakpoint on this instruction.


The next step would be to take this jump to land in unpacked code and then dump the unpacked process. However, before we do this I want to explain another method to find this exact jump instruction and then I will walkthrough how to dump the unpacked process.


Method 2 unpacking using a tailjump

The second method to manually unpack this binary is to simply search for a tailjump directly. The first step is to find the pushad instruction which is the entrypoint to this program.

I then, the same as before checked what the memory address of the UPX0 section was which is 00401000. This will be where the unpacked code is written to.

Next using CTRL+F I located the popad instruction which is used to restore the register values after the binary has unpacked itself to return to the OEP.

Just under the popad instruction, using the same method as before I was able to locate the tailjump which will jump to the memory address 401C50 within the UPX0 section.

Similarly to before I set a breakpoint on this instruction and now Hit F7 to step into this jump instruction. Now, we have landed in unpacked code and can begin dumping the processes.


Using Scylla to dump the unpacked process

In x64dbg, there will be a plugin called Scylla and I used this to dump the unpacked process. When opening the plugin you should see a screen similar to the below.



The first step to dumping the unpacked process is to find and then rebuild the import table. This will allow us to identify all of the imports, their addresses and fix the virtual addresses. To first search for an IAT table, press the IAT Autosearch button.

Once the IAT table has been found, I then clicked the Get Imports table to resolve the imports that are used in the binary. below we can see a list of libraries that have been imported and within them will contain the imported functions.

Now the IAT table has been found and the imports resolved, I then clicked the Dump button to dump the unpacked binary to file.

At this point, if we tried to run the binary, it would return an error because the addresses in memory (Virtual addresses) are different from the addresses on disk (Raw addresses). To fix this, I clicked the Fix Dump button which fixes the miss match of these addresses.

After clicking the Fix Dump button, Scylla will dump a new binary with the postfix _SCY with all new fixed addresses and IAT table.

Analysing the unpacked binary

Now the binary has been unpacked, when analysing it in PeStudio I could see that the binary entropy had dropped to 5.6 which is considerably lower than before. This indicates that the binary is no longer packed. In addition to this the UPX signature is no longer there.

Looking at the entry point of the binary, it has now changed to the address in the UPX0 region. This is because the unpacking stub was able to unpack the binary to this location where the OEP is.

Having a quick look at the sections UPX0 no longer has a raw size of 0 bytes, in fact the raw and virtual sizes match up very closely. Another noticeable difference is the addition of the .SCY section that is created by Scylla when dumping the binary from x64dbg.

Before unpacking the binary, there were very little references to libraries or functions imported by the sample which makes analysis increasingly harder. After unpacking however, all of the imports are listed in PeStudio with a total of 90 listed.

Hopefully this article gave you the tools you need to now go and confidently unpack a UPX packed binary and understand the process of how it was packed and unpacked. If you have any suggestions or improvements i could make to this article please let me know!






1,189 views

Recent Posts

See All

Comments


bottom of page