The very first section of the PE file is the DOS header. The DOS header is a 64-byte structure, described by the _IMAGE_DOS_HEADER structure definition in winnt.h. Let's try and read this structure from the file.
We declare a function read_dos_header which takes pointer to a FILE, and returns an _IMAGE_DOS_HEADER structure. The function:
- seeks to the beginning of the file;
- reads a number of bytes equal to the size of
_IMAGE_DOS_HEADERdirectly into a buffer.
To allow us to seek, we create a slightly more robust wrapper for fseek, utils::safe_seek. A problem with fseek is that it does not fail if attempting to seek beyond the end of the file; safe_seek guards against this and throws an exception if attempted.
We read the bytes with fread_s, which is a more secure version of fread. If it fails, read_dos_header throws.
(Note, it would be more modern practice to do this using a filestream rather than using the older C-style methods for file handling, but it's OK for our purposes).
The function explore::explore is where we will open the ExampleDll.dll file and then call some helper functions to explore each part in turn. Opening the file actually presents a little challenge, as we need to pass the path to this file as an argument to fopen_s (which is a secure version of fopen).
The ExampleDll appears in the same build directory as the application executable:
This gives us a strategy for getting the full path of ExampleDll:
- have the executable get the full path of itself, i.e. the exectuable file of the current process;
- strip the filename from the path;
- append with the name of another file (e.g. ExampleDll.dll).
To do this, we create a little utility function app_utils::_get_abs_path_from_filename to do this for any given file name. This achieves the steps above using three Windows functions:
GetModuleFileNameretrieves the fully qualified path for the file that contains the specified module. If the first parameter is NULL, it retrieves the path of the executable file of the current process, which is what we want.PathCchRemoveFileSpecremoves the last element in a path string, whether that element is a file name or a directory name. The element's leading backslash is also removed.PathCchAppendappends one path to the end of another.
Note that the last two functions are defined in the static library pathcch.lib, so we must add this as a linker input to PeClient.
Phew!
The function explore::explore opens the file before calling explore::explore_dos_header. There is one wrinkle to opening the file:get_abs_path_from_filename puts the path into a WCHAR buffer, but f_opens expects the path in a char buffer; to handle this, we define a little conversion function app_utils::wchar_t_buffer_to_string. This function follows the process for converting between string types outlined on MS learn.
Finally, explore::explore_dos_header calls read_dos_header, and prints the only two values that are of real interest: the magic number and the offset to the start of the Windows NT headers.