Lab 8 - Anti-RE Techniques

In this lab, you will explore anti-reverse-engineering tricks that malware authors employ so that their malware will evade detection and slow down analysts.

Deliverables

Upload the following items to the Lab 8 Assignment on Canvas when finished.

  • The PDF file containing the required documentation
  • Source code for your obfuscated fake-malware program

Part 1 - UPX Packer - Exploration

Launch your Windows virtual machine. A copy of UPX for Windows should already be pre-installed. To test, launch the command prompt and type upx -h to display the documentation.

Obtain a copy of your fake-malware.exe from Lab 4. Use the Windows command line or the GUI to make a copy of the executable called fake-malware-original.exe and set it aside.

Use the open-source UPX packer to pack your fake malware. Be warned that UPX will overwrite the original file with its output file! Thus, you should:

  1. Copy fake-malware.exe to fake-malware-packed.exe, and then
  2. Pack it via upx fake-malware-packed.exe

Use some well-known tools to detect if your fake-malware-packed.exe file is packed. Note that UPX is a very common packer. You won't see detection results this good with malware using custom packers.

Question Answer
Does the Detect It Easy tool in Windows detect the file is packed? If so, what does it report about it?
Note: DIE works best without resolution scaling in Windows. Sorry
Tip: Drag and drop the malware onto the utility desktop shortcut
Does the Exeinfo PE tool in Windows detect the file is packed? If so, what does it report about it?
Tip: Drag and drop the malware onto the utility desktop shortcut
Does the DIEC tool in Linux detect the file is packed? If so, what does it report about it?
(This is the command-line version of Detect It Easy for Linux, so results should be similar to Windows).
Tip: Use command diec fake-malware-packed.exe.
Does the TrID tool in Linux detect the file is packed? If so, what does it report about it?
Tip: Use command trid fake-malware-packed.exe
Does the PEPack tool in Linux detect the file is packed? If so, what does it report about it?
Tip: Use command pepack fake-malware-packed.exe
Does the PackerID tool in Linux detect the file is packed? If so, what does it report about it?
Tip: Use command packerid fake-malware-packed.exe
Does the PEScan tool in Linux detect the file is packed? If so, what does it report about it?
Tip: Use command pescan fake-malware-packed.exe

Compare and contrast the unpacked and packed files using information obtained from PeStudio and IDA.

Question fake-malware-original.exe fake-malware-packed.exe
What is the address of the program entry point, and what code section is it within? (Look under the optional-headers category).
What libraries are imported? (DLLs)
How many API calls are used? (Imported functions)
How many of those functions does PEStudio flag on its blacklist?
What interesting strings do you see? (Don't list them all for the original file)
What is the name of each section, its virtual-size, its raw-size, and its entropy?
From that observation alone, what section of the packed file likely has a payload? N/A
From that observation alone, where do you think the unpacked payload will go? (Not every packer will have such an obvious location). N/A
How many subroutines (functions) exist in the program code? (In IDA, View->Open Subviews->Functions)

Use the open-source UPX packer to unpack your fake malware. Note that UPX will overwrite the original file with its output file! Thus, you should:

  1. Copy fake-malware-packed.exe to fake-malware-unpacked.exe, and then
  2. Unpack it via upx -d fake-malware-unpacked.exe

Note that real malware packers will not have convenient command-line extraction capabilities. Also, it is unlikely that you will have access to the same packer program that the malware author used. It's only possible here because UPX is designed for good and not evil.

Question Answer
Is the unpacked .exe bit for bit identical to the original .exe?
How did you tell if the files were identical?
If the file is not identical, why not? (Google for how UPX works)

Part 2 - UPX Packer - Manual Unpacking

What if the packer didn't come with a handy pre-built unpacking command you could run? (Like any serious malware ever produced). In this part of the lab, you are going to steal the unpacked malware code from memory after the decompression stub finishes but before the malware runs, thus allowing you to inspect it in code analysis tools like IDA.

Installation Steps
The Windows VM contains a plugin for x64dbg: OllyDumpEx, but it was improperly installed - sorry! You should correctly install it now:
Open the plugin folder: C:\Users\cyberlab\Desktop\Standalone Programs\OllyDumpEx_v1.62
Copy the 64-bit plugin OllyDumpEx_X64Dbg.dp64 to C:\Users\cyberlab\Desktop\Standalone Programs\x64dbg\release\x64\plugins
Copy the 32-bit plugin OllyDumpEx_X64Dbg.dp32 to C:\Users\cyberlab\Desktop\Standalone Programs\x64dbg\release\x32\plugins

If you were to run your packed fake-malware, UPX would take the following steps to unpack it:

  1. The UPX decompression stub begins running at the Original Entry Point (OEP) specified in the PE file header. Note that this is not the OER of your original fake-malware, but rather UPX changes the OEP to point to its decompression stub which needs to run first.
  2. The current set of registers are saved via PUSHAD instruction
  3. All packed sections are unpacked in memory
  4. The import address table of the original file is resolved
  5. The original registers are restored via POPAD instruction
  6. The decompression stub jumps to a new Original Entry Point to begin executing the packed payload (your fake-malware)

Note that the packed payload is never written to disk. To recover the code, let's steal it out of memory after unpacking.

Step 1: Disable Address Space Layout Randomization (ASLR) - This security feature of the operating system randomizes the memory location and internal layout of system files and processes. It can complicate unpacking and code-level analysis, however, and is best to disable on a per-file basis. You can use a PE file editor like CFF Explorer to accomplish this. Drag and drop your fake-malware-packed.exe executable onto the CFF Explorer desktop shortcut. You can inspect a variety of file properties here, but we are interested in Nt Headers->Optional Header->DllCharacteristics/"Click here". Then, uncheck the "DLL can move" box. Close CFF Explorer and allow it to overwrite the executable with this modified version.

Step 2: Load the fake-malware-packed.exe file (with ASLR disabled) into x64dbg (either 32 or 64 bit depending on how you compiled your file). Confirm, via the Breakpoints panel, that the automatically set "EntryPoint" breakpoint is still valid. Press the "Run" button to run to this breakpoint, which skips over the loader code and takes you straight to the program entry point.

Step 3: Speculate (wildly!) on where the malware unpacker code might finish its unpacking work and jump into the malware for execution. Given that this file is packed with UPX, things are simple. Scroll to the very bottom of the file, where you should see page after page of empty space with opcode 00 00. Scroll up to the last line of "real code" before all the zeroed-out memory (ignore a few random instructions in the middle of the zeros). In the case of UPX the last line should be a jump instruction, e.g. jmp fake-malware-packed.######. Observe that the jump target memory address (######) is significantly different from the current location in the program. Note that for other packers, you might see that the code manipulates the stack and then does a return, or perhaps does a call to a memory address stored in a register. Other packers might also hide this important code in the middle of the unpacker and not at the very last line.

Step 4: View the jump target memory address in the Dump. Click on the instruction to highlight the line, right-click, and choose Follow in Dump->######. The dump should show a large region of all zeros. Maybe something interesting will go here?

Step 5: We don't yet know if that JMP is actually significant, or completely unrelated to our search for the end of the unpacking process. Use a trick involving the stack to skip through the unpacking code without having to trace it in detail. The hypothesis is that the unpacker will call a bunch of functions (using the stack), but when the unpacker has returned from all the functions (and the stack is empty), then that should be very close to the end of the unpacking. Recall that the program is still paused at the very first line of the program entry point, way above. Hit the Step Into button once, to advance to the second line (and scroll back up to the right place).

Step 6: You should be at the second line of code after the program entry point (beyond the PUSHAD function that pushes a bunch of registers onto the stack). Set a hardware breakpoint on any accesses to the first element of the stack. The stack is the bottom-right panel of the debugger. Right-click on the very first line (memory address) and choose Breakpoint->Hardware, Access->Word. If you are not sure if you have the right spot, look at the hardware ESP/RSP register, which stores the stack pointer. That value should be correct on the second line of the program (after the PUSHAD instruction). Make sure this memory address is the one you are setting the breakpoint on. Any time this location in the stack is accessed (read, written), the debugger will stop.

Step 7: Hit the run button!

Step 8: Observe where the program has stopped. It should be only a few lines above the location we manually scrolled to previously, at the end of the file, confirming our earlier hypothesis that the unpacker code finishes at the end of the file.

Step 9: Observe the Dump - Is it still empty? It should be full of data now. If you want to see if these bytes mean anything, and not just gibberish, right-click on one byte and choose Follow In Disassembler. The CPU panel will scroll to that memory address and show you that these bytes can be decoded as x86/x64 assembly code. Hopefully this is where the unpacked program resides!

Step 10: Step Into (single step) once, just to get the view to scroll back down to the end.

Step 11: Highlight the final JMP instruction, and choose Debug->Run until Selection. You might need to repeat this multiple times if the hardware breakpoint causes the debugger to stop before reaching the desired location. You are now at the very last instruction of the unpacker stub.

Step 12: Step Into (single step) once. Notice your memory location in the file changes significantly. You are now at the first instruction of the unpacked malware.

Step 13: Steal the unpacked malware out of memory! There are two plugins that will simplify this process. First, go to Plugins->OllyDumpEx->Dump Process.

  1. Click "Get EIP as OEP" - This takes the current Instruction Pointer (EIP or RIP) and tells the plugin to use this address as the Original Entry Point (OEP) for the program you are about to dump. This button only works correctly because the debugger is currently paused at what we believe is the first instruction of the unpacked program.
  2. Double-click each of the UPX sections in the bottom panel (expand the columns to view the section names). Enable the checkbox for MEM_WRITE for each one to avoid a potential memory permission error later when executing the malware.
  3. Click Dump and save the file as fake-malware-packed_dump.exe
  4. Click Finish

Step 14: The dumped malware is not runnable (yet) - The Import Address Table (IAT) is broken. Fix it with Scylla. Go to Plugins->Scylla

  1. Click "IAT Autosearch" - Magic happens
  2. Click "Get Imports" - Magic happens
  3. Right click and choose Delete tree node on any Imports that show the error icon in the preview panel
  4. Expand the preview panel - Do the Imports shown correspond to the functions you used in the original C++ code?
  5. Click "Fix Dump"
  6. Locate and Open the fake-malware-packed_dump.exe file produced by the OllyDumpEx plugin
  7. Scylla will produce a new file with _SCY appended to the filename, i.e. fake-malware-packed_dump_SCY.exe
  8. Close Scylla and x64dbg before the malware has a chance to execute

Step 15: Profit! Run the final output file, which is unpacked malware that you dumped directly from memory without any need to understand exactly how the unpacker algorithm works. Does your fake-malware program still function? (enumerate processes, connect to a webserver if the Internet is enabled, dump a test file to disk, etc...)

Reporting Requirements
Provide a screenshot in x64dbg showing the OllyDumpEx plugin when you dumped the file from memory.
Provide a screenshot in x64dbg showing the Scylla plugin when you fixed the IAT.
Provide a screenshot showing your unpacked program running successfully.

Part 3 - Fun with Obfuscation

Obfuscate your fake-malware program in the following ways:

API Obfuscation: Currently, you call WriteFile() directly, but that makes it immediately obvious in a program like PEStudio that your program writes to files. Instead, call it via ordinal number by the following process:

  1. Obtain a handle to the library containing WriteFile() by using LoadLibrary(). You will want to look at the MSDN documentation for WriteFile() to determine which DLL stores the implementation code.
  2. Obtain a pointer to the WriteFile() code inside the library by using GetProcAddress() with the ordinal number of the desired function, not its name.
    • Tip: To obtain the ordinal number using the DumpBin tool provided with Visual Studio, launch the "Developer Command Prompt for VS 2017" in the Start menu. Then navigate to the C:\Windows\SysWOW64 folder (assuming you're building a 32-bit binary on 64-bit Windows) or C:\Windows\system32. Then enter dumpbin /exports TheDLLName.dll. Or, you can open the DLL using IDA and view the ordinal numbers in the Exports panel. Or use another PE viewer.
    • Warning: Microsoft changes its DLLs with each Windows release, and there is no guarantee that the ordinal numbers will stay the same! Such is the life of malware authors. If you are building on one machine and testing on another machine, be sure to use the ordinal numbers on your desired test computer.
  3. Call the function by using a pointer to it, instead of its name. Pass arguments to the function as you would normally.
  4. Confirm that your program still executes correctly and drops a file (with data inside!) to disk.

String Obfuscation: The API obfuscation works, but it still has a string containing the DLL name buried somewhere in the program, waiting for a malware analyst to find it. Obfuscate the string via a custom method of your own creation. You might XOR the stored string against a special value to transform it from gibberish into a usable value. Or compose the string character by character in an out of order sequence. Or do ASCII character math to generate it.

Debugger Resistance: The malware analyst is still able to attach a debugger to your fake-malware and step through it line by line. This is unacceptable! Detect if a debugger is attached to your program at the very beginning of main(), and if a debugger is present, immediately call _exit().

Using x64dbg, patch your fake-malware-obfuscated assembly code to bypass the if-debugger/then-exit check.

Reporting Requirements
Provide a screenshot in IDA showing the assembly code starting at CreateFile() (where you create the file for the dropped malware on disk), the call to LoadLibrary() and GetProcAddress(), and ending at the call by ordinal number to write the file to disk.
Provide a screenshot in IDA showing the assembly code for your string obfuscation. Draw a box around the code from beginning to end.
Submit your final C++ code with your assignment.
Describe how you bypassed the debugger/exit check in x64dbg at runtime.