Overview
With a suitable block of virtual memory allocated (Theory 3.2), the next critical phase is to populate this block with the contents of the DLL, effectively reconstructing the DLL’s layout as it would appear in memory if loaded conventionally. This process involves two main copying steps: transferring the PE headers and then mapping each individual section.
Allocate Memory (Recap)
As covered in the preceding section, the foundational step is allocating a contiguous block of virtual memory using VirtualAlloc
. The size requested is SizeOfImage
from the DLL’s Optional Header, and initial permissions are set to allow subsequent writing and eventual execution. Let’s call the base address returned by VirtualAlloc
the AllocatedBase
.
Copy PE Headers
The first thing we’ll do is copy the parts of the PE Headers that contain all the essential metadata: the DOS Header, NT Headers (including the File Header and Optional Header), and the array of Section Headers. It’s critical that its copied directly to the beginning of our newly allocated memory block.
In order to do this we’ll need to use the SizeOfHeaders
value obtained from the IMAGE_OPTIONAL_HEADER
since it tells us exactly how many bytes, starting from the beginning of our DLL memory buffer, constitute the complete set of headers.
So we copy SizeOfHeaders
bytes from the start of the source DLL buffer to the AllocatedBase
address. After this, the first SizeOfHeaders
bytes of our allocated block will identical to the first SizeOfHeaders
bytes of the original DLL file buffer.
Copy Sections
Our PE Headers provide the map, but the actual code and data reside in the various sections (.text
, .data
, .rdata
, etc.) located later in the DLL file buffer. Our goal now is to copy this raw data, for each section, from its position in the buffer to its correct virtual position within the allocated memory block.
We can do this by using the array of IMAGE_SECTION_HEADER
structures which we just copied in the previous step. The NumberOfSections
field from the IMAGE_FILE_HEADER
tells us how many section headers to process.
Once completed, we then iterate through each section header:
- Identify Source: Get the
PointerToRawData
andSizeOfRawData
from the current section header.PointerToRawData
is the file offset within our source DLL buffer where this section’s data begins.SizeOfRawData
is how many bytes to copy from that offset. - Identify Destination: Get the
VirtualAddress
(an RVA) from the current section header. Calculate the absolute destination address in our allocated block:DestinationVA = AllocatedBase + VirtualAddress
. - Action: Copy
SizeOfRawData
bytes from(SourceDllBufferBase + PointerToRawData)
toDestinationVA
. - Handle Empty Sections: If a section header has
SizeOfRawData
equal to zero, it means the section occupies space in memory but has no corresponding data in the file (this is typical for uninitialized data sections like.bss
). In this case, no copying is needed for this section, asVirtualAlloc
already initialized the committed memory pages to zero. - Iteration: Repeat this process for all sections specified by
NumberOfSections
.
I know this is quite a lot, but we’ll “map” each action here to its specific code and outcome in the next lab, that should help you form a much more concrete picture of what’s happening here.
Result of Mapping
After completing this final step the memory block starting at AllocatedBase
will now contain a complete, mapped image of the DLL. The headers are at the beginning, and each section’s data has been copied from its file offset in the source buffer to its correct relative virtual address within the allocated block.
This mapped image structurally resembles how the DLL would look in memory if loaded by LoadLibrary
, but it’s not quite ready for execution yet. The addresses within the code and data might still be pointing to locations relative to the DLL’s preferred ImageBase
, not the AllocatedBase
where we actually loaded it.
Furthermore, the DLL likely depends on functions from other system DLLs, and the pointers for these imports haven’t been resolved yet. These are the crucial tasks of fixing relocations and resolving imports, which we will cover in Module 4.
Checklist
Before we proceed with our lab, I thought it would be useful to distill what we’ve discussed here in a numbered checklist of sorts, which can then be referenced server as a roadmap to guide us.
- Allocate a contiguous block of virtual memory (result is
AllocatedBase
). - Obtain the
SizeOfHeaders
value from the PE Optional Header. - Copy
SizeOfHeaders
bytes from the start of the source DLL buffer toAllocatedBase
. - Obtain the
NumberOfSections
value from the PE File Header. - Calculate the starting address of the first
IMAGE_SECTION_HEADER
within the mapped headers (atAllocatedBase
). - Start a loop to iterate through each section (from 0 to
NumberOfSections - 1
). - Read the current
IMAGE_SECTION_HEADER
structure. - Get the
PointerToRawData
value from the current section header. - Get the
SizeOfRawData
value from the current section header. - Get the
VirtualAddress
(RVA) value from the current section header. - Check if
SizeOfRawData
is zero. - If
SizeOfRawData
is not zero, proceed to the next step; otherwise, skip to step 16 (continue loop). - Calculate the source address for copying (
SourceDllBufferBase + PointerToRawData
). - Calculate the destination address for copying (
AllocatedBase + VirtualAddress
). - Copy
SizeOfRawData
bytes from the calculated source address to the calculated destination address. - Continue the loop for the next section until all sections are processed.