What Every Malware Analyst Should Know About PE Relocations

The Portable Executable (PE) base relocation table is crucial in Windows executable files. It handles memory addresses for functions and data, making sure the program runs well no matter where it's loaded in memory.

For malware analysis, the PE base relocation table is vital. When we study suspicious files, malicious software often tries to hide by changing memory addresses. The base relocation table helps us catch these changes. By checking this table, we can find clues about tampering, helping us understand what the malware is up to. So, knowing the PE base relocation table is a big part of figuring out how malware works, and it helps us defend against it.

I this I will go thought the relocation table in depth and also provide with some tools to parse and manipulate PE files via relocation table

So what are PE relocations ?

In order to understand relocation, we need to grasp a basic memory management concept in Windows. The main executable in a program is a fragmented part of the whole program. It also includes other parts known as DLLs, which are loadable modules. For the program to work correctly, the main executable and the required DLLs must be loaded into virtual memory.

In a 64-bit Windows operating system, the user space virtual memory typically spans from 0x0000000000000000 to 0x00007FFFFFFFFFFF in hexadecimal notation. This is equivalent to a range of 0 to 140,737,488,355,327 in decimal notation, covering a vast address space of 128 terabytes (TB).

In a 32-bit Windows operating system, the user space virtual memory typically spans from 0x00000000 to 0x7FFFFFFF in hexadecimal notation. This is equivalent to a range of 0 to 2,147,483,647 in decimal notation, covering a total of 2 gigabytes (GB) of addressable memory space

The variable Relocation will always be hardcoded with the base address as ImageBase. So, if ImageBase is at 0x4000000, and the .CODE section happens to be loaded at 0x1000 relative to the base, then the variable will be located at 0x4001000 whenever it is used. This is in contrast to stack variables, which are always addressed using the relative offset of EBP/RBP stack pointer or RCX/RDX/R8/R9 (for 64-bit).

However, a crucial question arises: what happens if the ImageBase is not available or is already occupied by another image loaded at the same address?

In such a scenario, Windows provides a mechanism known as image base relocations. In this case, the .reloc section of the binary (which could possibly have any other name as well) contains the information necessary for relocation to occur. During relocation, all the hard-coded addresses used will be relocated to the new image base where the image was loaded.

This is particularly useful for malware, as with process injection techniques, malware binaries often get a randomly allocated memory region from which they have to relocate the binary further. So, this is a very important topic to cover for malware unpacking.

Let's see how the relocation table looks when compiled on a 32-bit Windows system.

A relocation entry has one entry with one block and a slot count of 4. Slots refer to the number of similar relocations on the same page.

According to online documentation, Base Image relocations are represented in a structure as follows:

  typedef struct _IMAGE_BASE_RELOCATION {
      DWORD VirtualAddress;
      DWORD SizeOfBlock;
      WORD TypeOffset[];
    } IMAGE_BASE_RELOCATION;
    typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;

TypeOffset is a variable size array base on the (SizeOfBlock - IMAGE_SIZEOF_BASE_RELOCATION )

looking at the .reloc binary we find the following hex data

As the program was compiled without any libc requirements, that's why the relocation section is so small. The /nodefaultlib parameter was used to keep the content concise.

00 10 00 00 10 00 00 00 10 30 1E 30 24 30 38 30

To put this binary block into perspective, we will map it with the structure above

Virtual Address Size of Block Type Offset
0x00001000 0x00000010 1E30 2430 3830

size of block is 0x10 bytes , this includes the structure itself

The absolute address where the relocation needs to happen can be obtained using the combination of VirtualAddress and extracting the offset from the TypeOffset field.

There isn't much documentation available about the actual process of parsing the relocation table. It is mostly an opaque structure with a plethora of relocation entry types.

#define IMAGE_REL_BASED_ABSOLUTE 0
#define IMAGE_REL_BASED_HIGH 1
#define IMAGE_REL_BASED_LOW 2
#define IMAGE_REL_BASED_HIGHLOW 3
#define IMAGE_REL_BASED_HIGHADJ 4
#define IMAGE_REL_BASED_MIPS_JMPADDR 5
#define IMAGE_REL_BASED_ARM_MOV32 5
#define IMAGE_REL_BASED_THUMB_MOV32 7
#define IMAGE_REL_BASED_MIPS_JMPADDR16 9
#define IMAGE_REL_BASED_IA64_IMM64 9
#define IMAGE_REL_BASED_DIR64 10

How each of them actually processes the relocation is not documented. Let's take the challenging path and reverse-engineer the kernel to figure out how these types of relocations are processed.

A text search for RelocateImage yields the following function where the image relocation is precisely processed.

So the function where this takes place in the kernel happens to be LdrRelocateImageWithBias() .

As we see in the LdrProcessRelocationBlockLongLong(), quite interestingly, the code also processes the ARM processor instruction set, i.e., ARM v7 and AArch32/64. I believe it is precisely there because of the support for x64 emulation on Windows ARM OS (https://blogs.windows.com/windows-insider/2020/12/10/introducing-x64-emulation-in-preview-for-Windows-10-on-ARM-PCs-to-the-Windows-Insider-program/).

It functions similarly to the WOW64 Layer, but it operates by translation or emulation.

Two functions responsible for this behavior are:

  • LdrpThumbProcessRelocation()
  • LdrpArmProcessRelocation()

For generic x86 and x64, LdrpGenericProcessRelocation() is used. To trigger the relocation block, LdrpThumbProcessRelocation() or LdrpArmProcessRelocation(), TypeOffset is checked against the following parameters:

NtHeader.Machine is compared against following values

  • IMAGE_FILE_MACHINE_ARM
  • IMAGE_FILE_MACHINE_ARMV7
  • IMAGE_FILE_MACHINE_ARM64

And for X86/X64 following reloc types are supported

  • IMAGE_REL_BASED_HIGH
  • IMAGE_REL_BASED_LOW
  • IMAGE_REL_BASED_HIGHLOW
  • IMAGE_REL_BASED_HIGHADJ
  • IMAGE_REL_BASED_DIR64

    IMAGE_REL_BASED_HIGH

IMAGE_REL_BASED_HIGH is an x86 relocation type. In IMAGE_REL_BASED_HIGH, the higher part, in terms of linear access, is modified by changing the lower two bytes of the address with the delta. The delta represents the scalar difference between the new base and the referenced base.

---

IMAGE_REL_BASED_LOW

IMAGE_REL_BASED_LOW is a relocation type for x86. In this type, the lower half of the delta is added to the location of the relocation.

mov     ax, word ptr [ebp+Delta] (delta Lower)
add     [esi], ax

For 64bit

add     [r9], r8w ; Delta (lower)
---

IMAGE_REL_BASED_HIGHLOW

IMAGE_REL_BASED_HIGHLOW is a relocation type used in both x86 and x64. In this type, a simple delta is added to the location. This addition may affect the higher half of a 64-bit address, which is why it is considered for both 64-bit and 32-bit platforms.

add     [r9], r8d
---

IMAGE_REL_BASED_HIGHADJ

IMAGE_REL_BASED_HIGHADJ is a more complex relocation type. This type occupies two slots, meaning that two TypeOffset elements are used to process it.

The process involves:

  1. Adding the lower bits at the relocation to Delta.
  2. Adding the lower half with sign extension from the second slot.
  3. Adding this value to Delta again.
  4. Finally, adjusting the lower half back to the relocation.

MalwareID