Code Packing

Table of Contents

1. Lecture 11

The code packing is a technique used to hide the original cod eof a program through one or more layer of compression and/or encryption to make it harder to detect for antiviruses and malware analysts. In depth analysis of packed malware starts in a debugger.

For packed program the Windows Loader loads their unpacking stub that may load the program in a custom fashion. in the unpacking terminology the entry point of the original application is known as the original entry point, the unpacking stub is the entry point of the packed program. We want to intercept the moment in which the unpacking stub will unpack the packed code.

2. Features of a Packer

Packing can be nested, sophisticated packers have evolved into executable protectors, they can also perform anti-debugging/sandbox/tampering/disassembling actions. Encryption packers are popular and their purpose is to obtain a large amount of entropy to avoid statistical correlation.

2.1. Role of Unpacking Stub

It is the only part of the code exposed to the analylist, It preforms three steps:

  1. Unpack the original executable into memory
  2. Resolve all the imports of the original executable
  3. Transfer execution to OEP.

Our purpose is to obtain a reconstructed unpacked version of the sample. The unpacking stub reconstruct on the fly he import table and the PE header (which is different from the original one)

code-packing-1.png

Problem is that Windows in unaware of the imports of the packed program, so the unpacking stub has to fill the import address table. For this reason the unpack extraction has to be done between points (2) and (3).

The PE header includes a section to tell the loader whch functions it should import DLLs, and a section to store the addressess of imported (by name or ordinal) functions. The second section is called IAT where the first one is called Import Name Table, the unpacking stub will fill the IAT.

Generally speaking there are other strategies that can be used to recover those APIs, let’s say that we have the ANT, we have the option to parse the AMT and call loadLibrary and getProcAddress and manually solve each import. Another strategy can be represented by the fact that windows will try to resolve the addresses, so just keep the IAT intact and rely on windows. Another strategy is to import one function for each DLL and then you let windows do the rest of the work, thats because numerous calls to loadLibrary will trigger antivirus. The foruth strategy consists to remove all the imports and manually locate LoadLibrary and GetProcAddress. Of course these four strategies can be tweaked.

2.2. Strategy One

Use LoadLibrary and GetProcAddress to solve each import. This is the most common strategy. It consists of four steps. After unpacking read the original INT, and load each DLL using LoadLibrary and for each DLL solve imported funcions using GetProcAddress and then patch the IAT with the addresses resolved at step 3.

code-packing-3.png

2.3. Strategy Two

Unpacking stub will not solve anything, let windows do the dirty work. The problem with this approach is that there is a lack of stealth. Static analysis reveal all the original imports, and its possible to fingerprint malware from their import table. Of course a skilled malware writer can use this aspect to be subtle.

2.4. Strategy Three

In this strategy static analysis sees only one funciton per DLL, and the unpacking stub is simpler because there is no LoadLibrary activity,

2.5. Strategy Four

Remove all the imports, and find LoadLibrary and GetProcAddress elsewhere, it is a very stealth approach, and it works by look for kernel32.dll in memory an parse its exports table, or it works by manually looking for those functions among imports of other modules. This approach is very complex but loading many libraries at run time can trigger an allert from AV because LoadLibrary must be used.

3. Tail Jump

Once the stub ends it must transfer execution to the original entry point. This transfer is called Tail Jump; usually simple packers will perform just one jump, complex ones will perform more jumps, but the code executed in those cases is just part of the packer. A jmp instruction is the most common case, but packers can use call modifying the return address, or use OS control transfer functions (NtContinue, or ZwContinue). A more complex case consits in using exception handling.

4. Indicators of Packed Code

Some simple indicators are high entropy, few imports with LoadLibrary and getProcAddress are there; another good indicator is a big executable with very little code (the unpacking stub is small and is the only part recognized by the Disassembler). Commercial packers will use custom section names like UPX0, and abnormal section sizes like raw data size is 0 but virtual size is greater than zero.

None of those indicators should be trusted alone, if two or more indicators match, then likely the executable is packed.

5. Approaches to unpacking (for analysits)

There are automatic static unpackers tools that works for specific commercial packers, such as UPX. Automatic dynamic unpackers wil run the executable to try to intercept when the unpacking stub and decompressing code. Manual unpacking is the only way in many cases, sometimes can be done quickly.

Most reliable Automatic Dynamic Unpacking focus on what the application need: if the api called is inside a IAT populated by the unpakcer it means that likely the unpacking phase is done.

5.1. Manual Unpacking

Chi fa da se fa per tre. Two approaches are possible, we can figure out how the unpacking alorithm works, and implement it by wiriting an automatic static unpacker (MEH). Or just let the unpacking stub do the work, then dump the memory, and if you do it right, you only have to fix the PE header from the dump.

There are also plugins that helps performing the second approach like Scyllaand OllyDumpEx. A Section Hop approach can be used to trace all the operation performed by a malware (very expensive), normally the unpacked code will go from a different code section and intercept all the jmp that ges outside that section (tail jump); Section Hop could be ineffective on some packers, so looking for tail jump is a better strategy ignoring function calls that will surely be on another section.

Manual OEP identification is possible, there are some heristics: last valid instruciton on a sequence, or a better heuristic is a jump to a section full of garbage (packed code: jackpot 💰); normally a tail jump point to a far point in memory and look for sections that are both writable and executable o look for VirtualProtect calls that enables executable/writable at a specific destination.

Another tip consist of using breakpoints, memory breakpoints on the stack entry touched by the first push operation in the unpacking stub can be used to identify the OEP: the unpacker will eventually clear the stack to leave the execution at the payload. Breakpoint on conditionals can be useful in case of partial unpacking. Track access of GetProcAddress can be used to check when the last API has been loaded. The final resource is represented by looking at the use of common functions that can be used at the start of unpacked programs and then go backwards.

5.2. Rebuilding IAT

There are tools that can be used to recontruct the PE header after a memory dump. Import Recontructor can be used, it takes the RVA of an OPE and the tool will infer all the other data and will try to autolocate a IAT and update the memory dump rebuilding the PE Header.

Scylla is a stet of the art tool for import reconstruciton, it can be attached to a running process or as an IDA plugin.

The main pitfalls in IAT recontruction are represented by invalid entries, or the fact that the adversary will erase the INT after the unpacking.

Obfuscation and Packing are used togheter but their are orthogonal techniques.

Author: Andrea Ercolino

Created: 2022-12-12 lun 12:09