I'm not even sure if this is possible for Windows; I haven't seen a single person asking for something this general and finding a solution. It probably is possible, but there might not be APIs for handling it.
I have an automated testing module I'm working on for Windows that consumes a module to handle detected EXEs in a generalized fashion unless it detects that the binary is from a specific testing framework. So far, I've only been able to do this by querying the help and handling responses/string parsing. This can cause problems if I trigger a long test someone wrote outside of a framework that accepts command-line parameters for help but doesn't actually process those command-line parameters and just auto-runs. So instead of doing a lightning fast query, sometimes I can get stuck waiting for the test to finish. That's what I'm trying to avoid with this fancy new module. :)
The crux of this issue is that this consumed module for getting the list of DLLs will be distributed to non-development Windows systems, and that I can't say what it was built with(.NET, C++, etc). That prevents me from using dumpbin
and link
as Microsoft does not allow them to be distributed. For my own licensing requirements, this module of mine will not be sold; freeware forever.
I was advised to look into dumpbin
before I realized I couldn't distribute it. When I use that, this is what I'm getting:
c:\test_dir>dumpbin /dependents .\qt_unit_test.exe
Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file .\qt_test_unit_test.exe File Type: EXECUTABLE IMAGE Image has the following dependencies: Qt5Test.dll Qt5Core.dll KERNEL32.dll VCRUNTIME140.dll api-ms-win-crt-utility-l1-1-0.dll api-ms-win-crt-runtime-l1-1-0.dll api-ms-win-crt-math-l1-1-0.dll api-ms-win-crt-stdio-l1-1-0.dll api-ms-win-crt-locale-l1-1-0.dll api-ms-win-crt-heap-l1-1-0.dll Summary 1000 .data 1000 .gfids 1000 .pdata 2000 .rdata 1000 .reloc 1000 .rsrc 1000 .text
The only needed information is the list of DLL dependencies. Getting both dynamic and static libraries used is not required. Minimum needed is the DLLs, but if someone I can get static libraries used too, that would be cool; totally optional.
I was basically doing this to verify if it was a Qt Test binary:
c:\test_dir>dumpbin /dependents .\qt_unit_test.exe | call findstr /i qt5test 1>nul 2>nul
c:\test_dir>if "%errorlevel%"=="0" echo is Qt Test
Coding-wise, what I've tried so far is C#'s Assembly.GetReferencedAssemblies
, but that of course only gets assembly information, so regular old STL C++ apps generate exceptions.
Next I diff'd one of Ed Bayiates's answers to try to get something meaningful, but honestly I'm a fish out of water in WinAPI-land. There's whole concepts I'm probably misunderstanding... Anyway, you can check the current code here, but I don't understand how to translate from the returned IntPtr
to a string or list of strings that tell you what DLLs were used for it. It seems like it's working otherwise, but yeah, classic voodoo programming scenario here...
Btw, I looked into the possibility of just using the Visual C++ Build Tools standalone, but that thing ends up being over a gig after you get all the tools which defeats my purpose of a light-weight generalized automated testing module.
I'm willing to use any language to accomplish this; I started with C++ and C# as I figured having WinAPI access would probably get me out of the woods for something this OS-specific.
Update:
6.4. The .idata Section All image files that import symbols, including virtually all executable (EXE) files, have an .idata section. A typical file layout for the import information follows...
While working on getting the .idata
table to handle this, I ran across a nasty little gotcha. The "virtually all executables..." clause of the idata
section isn't so all-encompassing as the documentation would lead you to believe. Check out Windows 10's built in Calculator.exe app:
C:\WINDOWS\system32>dumpbin /summary "C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.1703.601.0_x64__8wekyb3d8bbwe\Calculator.exe" Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.1703.601.0_x64__8wekyb3d8bbwe\Calculator.exe File Type: EXECUTABLE IMAGE Summary 4C000 .data 1000 .gfids 1000 .giats 21000 .pdata 135000 .rdata A000 .reloc 1000 .rsrc 20D000 .text 1000 .tls 1000 minATL
No .idata
section. However, that doesn't stop dumpbin from finding those dependencies:
C:\WINDOWS\system32>dumpbin /dependents "C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.1703.601.0_x64__8wekyb3d8bbwe\Calculator.exe" Microsoft (R) COFF/PE Dumper Version 14.00.24215.1 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.1703.601.0_x64__8wekyb3d8bbwe\Calculator.exe File Type: EXECUTABLE IMAGE Image has the following dependencies: api-ms-win-core-localization-l1-2-1.dll api-ms-win-eventing-provider-l1-1-0.dll api-ms-win-core-com-l1-1-1.dll api-ms-win-core-sysinfo-l1-2-1.dll api-ms-win-core-processthreads-l1-1-2.dll api-ms-win-core-sysinfo-l1-2-3.dll vccorlib140_app.DLL MSVCP140_APP.dll CONCRT140_APP.dll VCRUNTIME140_APP.dll api-ms-win-crt-runtime-l1-1-0.dll api-ms-win-crt-convert-l1-1-0.dll api-ms-win-crt-string-l1-1-0.dll api-ms-win-crt-heap-l1-1-0.dll api-ms-win-crt-stdio-l1-1-0.dll api-ms-win-crt-math-l1-1-0.dll api-ms-win-crt-locale-l1-1-0.dll api-ms-win-core-util-l1-1-0.dll api-ms-win-core-synch-l1-2-0.dll api-ms-win-core-winrt-error-l1-1-1.dll api-ms-win-core-winrt-string-l1-1-0.dll api-ms-win-core-handle-l1-1-0.dll api-ms-win-core-winrt-l1-1-0.dll api-ms-win-core-profile-l1-1-0.dll api-ms-win-core-libraryloader-l1-2-0.dll api-ms-win-core-interlocked-l1-2-0.dll Summary 4C000 .data 1000 .gfids 1000 .giats 21000 .pdata 135000 .rdata A000 .reloc 1000 .rsrc 20D000 .text 1000 .tls 1000 minATL
Update #2:
Getting closer to accomplishing this one after getting some great advice and input from one of my mentors. I managed to remove all the windows internals that required debug library usage, or libs outside the GAC when using Visual C++ Runtime redists.
Current problem is going from taking the RVA of the import table to getting all the names in the import table. My first queried import gives a null name even though the rest of the data isn't null.
//******************************************************************************
// HEADERS
#include "Windows.h"
#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <vector>
#include <experimental\filesystem>
//******************************************************************************
// NAMESPACES
namespace fs = std::experimental::filesystem;
//******************************************************************************
//FUNCTION DECLARATIONS
bool verify_image_file(std::string);
std::vector<char> read_all_bytes(const char* file);
std::vector<std::string> parse_file(std::string file);
//******************************************************************************
// CONSTANTS
// LABEL HEX DEC
//
const WORD MAGIC_NUM_32BIT = static_cast<const WORD>(0x10b); // 267
const WORD MAGIC_NUM_64BIT = static_cast<const WORD>(0x20b); // 523
const int IMG_SIGNATURE_OFFSET = static_cast<const int>(0x3c); // 60
const int IMPORT_TABLE_OFFSET_32 = static_cast<const int>(0x68); // 104
const int IMPORT_TABLE_OFFSET_64 = static_cast<const int>(0x78); // 120
const int IMG_SIGNATURE_SIZE = static_cast<const int>(0x4); // 4
const int OPT_HEADER_OFFSET_32 = static_cast<const int>(0x1c); // 28
const int OPT_HEADER_OFFSET_64 = static_cast<const int>(0x18); // 24
const int DATA_DIR_OFFSET_32 = static_cast<const int>(0x60); // 96
const int DATA_DIR_OFFSET_64 = static_cast<const int>(0x70); // 112
const int DATA_IAT_OFFSET_64 = static_cast<const int>(0xD0); // 208
const int DATA_IAT_OFFSET_32 = static_cast<const int>(0xC0); // 192
const int SZ_OPT_HEADER_OFFSET = static_cast<const int>(0x10); // 16
const int RVA_AMOUNT_OFFSET_64 = static_cast<const int>(0x6c); // 108
const int RVA_AMOUNT_OFFSET_32 = static_cast<const int>(0x5c); // 92
const char * KNOWN_IMG_SIGNATURE = static_cast<const char*>("PE\0\0");
//******************************************************************************
// Globals
bool is64Bit = false;
bool is32Bit = false;
//******************************************************************************
// PLACEHOLDERS
const char* ph_file("C:\\Windows\\System32\\notepad.exe");
//******************************************************************************
// ENTRY
int main(int argc, char* argv[])
{
if (!verify_image_file(ph_file))return -1;
if (parse_file(ph_file).size() > 1)return 0;
else return -1;
return -1;
}
//******************************************************************************
// FILE PARSER
std::vector<std::string> parse_file(std::string file)
{
std::vector<char> bytes = read_all_bytes(file.c_str());
std::vector<std::string> dependencies;
DWORD * signature_offset_location = (DWORD*)&bytes[IMG_SIGNATURE_OFFSET];
char * signature = (char*)&bytes[*signature_offset_location];
if (*signature != *KNOWN_IMG_SIGNATURE)return dependencies;
DWORD coff_file_header_offset = *signature_offset_location + IMG_SIGNATURE_SIZE;
IMAGE_FILE_HEADER* coff_file_header = (IMAGE_FILE_HEADER*)&bytes[coff_file_header_offset];
DWORD optional_file_header_offset = coff_file_header_offset + sizeof(IMAGE_FILE_HEADER);
WORD size_of_optional_header_offset = coff_file_header_offset + SZ_OPT_HEADER_OFFSET;
WORD* size_of_optional_header = (WORD*)&bytes[size_of_optional_header_offset];
//Magic is a 2-Byte value at offset-zero of the optional file header regardless of 32/64 bit
WORD* magic_number = (WORD*)&bytes[optional_file_header_offset];
if (*magic_number == MAGIC_NUM_32BIT)is32Bit = true;
else if (*magic_number == MAGIC_NUM_64BIT)is64Bit = true;
else
{
std::cerr << "Could not parse magic number for 32 or 64-bit PE-format Image File." << std::endl;
return dependencies;
}
if (is64Bit)
{
IMAGE_OPTIONAL_HEADER64 * img_opt_header_64 = (IMAGE_OPTIONAL_HEADER64*)&bytes[optional_file_header_offset];
IMAGE_DATA_DIRECTORY* import_table_data_dir = (IMAGE_DATA_DIRECTORY*)&bytes[optional_file_header_offset + IMPORT_TABLE_OFFSET_64];
DWORD* import_table_address = (DWORD*)import_table_data_dir;
// To Get the import table, you need to check all the IMAGE_SECTION_HEADERs for the section that matches size of the direct-query.
// TO get those you can use normal offsets. To go further, we need to start using the RVA
// IMAGE_SECTION_HEADERS starts directly after the end of the optional file header for file_header->NumberOfSections
// Then, your RVA is if (ptr_to_raw_data >= va && ptr_to_raw_data < va + SizeOfData){//isSection}
// DWORD FileOffset = Ptr_To_Raw_Data - VA + PointerToRawData
DWORD image_section_header_offset = optional_file_header_offset;
for (int i = 0; i < coff_file_header->NumberOfSections; i++)
{
IMAGE_SECTION_HEADER* queried_section_header = (IMAGE_SECTION_HEADER*)&bytes[image_section_header_offset];
if (queried_section_header->PointerToRawData >= import_table_data_dir->VirtualAddress && (queried_section_header->PointerToRawData < (import_table_data_dir->VirtualAddress + queried_section_header->SizeOfRawData)))
{
DWORD import_table_offset = queried_section_header->PointerToRawData - import_table_data_dir->VirtualAddress + queried_section_header->PointerToRawData;
IMAGE_IMPORT_DESCRIPTOR* import_table_descriptor = (IMAGE_IMPORT_DESCRIPTOR*)&bytes[import_table_offset];
if (import_table_descriptor->Name==NULL &&
import_table_descriptor->Characteristics==NULL &&
import_table_descriptor->FirstThunk==NULL &&
import_table_descriptor->ForwarderChain==NULL &&
import_table_descriptor->OriginalFirstThunk==NULL &&
import_table_descriptor->TimeDateStamp==NULL)
{
break;//Signifies end of IMAGE_IMPORT_DESCRIPTORs
}
DWORD* dependency_name_address = (DWORD*)import_table_descriptor->Name;
char * dependency_name = (char *)&bytes[import_table_descriptor->Name];
dependencies.push_back((std::string)dependency_name);
int breakpoint = 0;
}
image_section_header_offset = image_section_header_offset + sizeof(IMAGE_SECTION_HEADER);
}
}
else//32-bit behavior
{
//todo
}
return dependencies;
}
//******************************************************************************
// FILE READER
std::vector<char> read_all_bytes(const char* filename)
{
std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
std::ifstream::pos_type pos = ifs.tellg();
std::vector<char> result(pos);
ifs.seekg(0, std::ios::beg);
ifs.read(&result[0], pos);
return result;
}
//******************************************************************************
// IMAGE-TYPE FILE VERIFIER
bool verify_image_file(std::string file_to_verify)
{
if (fs::exists(file_to_verify))
{
size_t extension_query = file_to_verify.find(".dll", 0);
if (extension_query == std::string::npos)
{
extension_query = file_to_verify.find(".DLL", 0);
if (extension_query == std::string::npos)
{
extension_query = file_to_verify.find(".exe", 0);
if (extension_query == std::string::npos)
{
extension_query = file_to_verify.find(".EXE", 0);
}
else { return true; }
if (extension_query != std::string::npos) { return true; }
}
else { return true; }
}
else { return true; }
}
return false;
}