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.
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:
- Adding the lower bits at the relocation to
Delta
. - Adding the lower half with sign extension from the second slot.
- Adding this value to
Delta
again. - Finally, adjusting the lower half back to the relocation.