-
-
Save gavz/78fad525892152192dbd4f46125ec5ce to your computer and use it in GitHub Desktop.
A simple utility for modifying/adding exports to a PE file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <windows.h> | |
#include <iostream> | |
#include <fstream> | |
#include <string> | |
#include <vector> | |
#include <ctime> | |
#include <memory> | |
#include <optional> | |
#include <random> | |
#include <string_view> | |
#include <unordered_map> | |
#include <charconv> | |
#define DEBUG_LOGGING 0 | |
#if DEBUG_LOGGING | |
#define log_debug(msg) std::cout << "[DEBUG] " << msg << std::endl | |
#else | |
#define log_debug(msg) | |
#endif | |
#define log_info(msg) std::cout << "[INFO] " << msg << std::endl | |
#define log_error(msg) std::cerr << "[ERROR] " << msg << std::endl | |
struct exp_entry | |
{ | |
std::string name; | |
DWORD rva; | |
}; | |
struct expmod | |
{ | |
struct mmf | |
{ | |
HANDLE map = nullptr; | |
void* data = nullptr; | |
SIZE_T size = 0; | |
~mmf() | |
{ | |
if ( data ) | |
{ | |
FlushViewOfFile( data, 0 ); | |
UnmapViewOfFile( data ); | |
} | |
if ( map ) | |
{ | |
CloseHandle( map ); | |
} | |
} | |
}; | |
std::unique_ptr<mmf> mem; | |
std::string path; | |
IMAGE_DOS_HEADER* dos = nullptr; | |
IMAGE_NT_HEADERS64* nt = nullptr; | |
IMAGE_SECTION_HEADER* sec = nullptr; | |
IMAGE_EXPORT_DIRECTORY* exp_dir = nullptr; | |
DWORD exp_rva = 0; | |
DWORD exp_size = 0; | |
DWORD* func_addr = nullptr; | |
DWORD* name_addr = nullptr; | |
WORD* name_ord = nullptr; | |
std::vector<exp_entry> old_exp; | |
std::vector<exp_entry> new_exp; | |
bool load_file( const std::string& p ) | |
{ | |
log_debug( "loading file: " << p ); | |
path = p; | |
mem = std::make_unique<mmf>(); | |
HANDLE file = CreateFileA( p.c_str(), GENERIC_READ | GENERIC_WRITE, | |
FILE_SHARE_READ, nullptr, OPEN_EXISTING, | |
FILE_ATTRIBUTE_NORMAL, nullptr ); | |
if ( file == INVALID_HANDLE_VALUE ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to open file: " << p << ", Error: " << err ); | |
return false; | |
} | |
DWORD size_high; | |
DWORD size_low = GetFileSize( file, &size_high ); | |
mem->size = ( ( SIZE_T ) size_high << 32 ) | size_low; | |
log_debug( "file size: " << mem->size << " bytes" ); | |
mem->map = CreateFileMappingA( file, nullptr, PAGE_READWRITE, | |
size_high, size_low, nullptr ); | |
CloseHandle( file ); | |
if ( !mem->map ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to create file mapping, Error: " << err ); | |
return false; | |
} | |
log_debug( "file mapping created successfully" ); | |
mem->data = MapViewOfFile( mem->map, FILE_MAP_ALL_ACCESS, 0, 0, 0 ); | |
if ( !mem->data ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to map view of file, Error: " << err ); | |
return false; | |
} | |
log_debug( "file mapped into memory at address: " << std::hex << mem->data << std::dec ); | |
return init_headers(); | |
} | |
bool init_headers() | |
{ | |
log_debug( "initializing PE headers" ); | |
if ( !mem->data ) | |
{ | |
return false; | |
} | |
dos = static_cast< IMAGE_DOS_HEADER* >( mem->data ); | |
if ( dos->e_magic != IMAGE_DOS_SIGNATURE ) | |
{ | |
log_error( "invalid DOS signature" ); | |
return false; | |
} | |
nt = reinterpret_cast< IMAGE_NT_HEADERS64* >( ( BYTE* ) ( mem->data ) + dos->e_lfanew ); | |
if ( nt->Signature != IMAGE_NT_SIGNATURE ) | |
{ | |
log_error( "invalid NT signature" ); | |
return false; | |
} | |
if ( nt->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 ) | |
{ | |
log_error( "not a 64b PE" ); | |
return false; | |
} | |
log_debug( "number of sections: " << nt->FileHeader.NumberOfSections ); | |
log_debug( "size of optional header: " << nt->FileHeader.SizeOfOptionalHeader ); | |
sec = reinterpret_cast< IMAGE_SECTION_HEADER* >( | |
reinterpret_cast< BYTE* >( &nt->OptionalHeader ) + | |
nt->FileHeader.SizeOfOptionalHeader ); | |
log_debug( "section headers at: " << std::hex << sec << std::dec ); | |
log_debug( "checking export directory" ); | |
log_debug( "export directory RVA: 0x" << std::hex | |
<< nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress | |
<< std::dec ); | |
log_debug( "export directory Size: 0x" << std::hex | |
<< nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].Size | |
<< std::dec ); | |
if ( nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress == 0 ) | |
{ | |
log_info( "no export directory found. Will create one." ); | |
return true; | |
} | |
exp_rva = nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress; | |
exp_size = nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].Size; | |
exp_dir = ( IMAGE_EXPORT_DIRECTORY* ) ( rva_to_va( exp_rva ) ); | |
log_debug( "export directory at VA: " << std::hex << exp_dir << std::dec ); | |
if ( !exp_dir ) | |
{ | |
log_error( "failed to get export directory" ); | |
return false; | |
} | |
if ( DEBUG_LOGGING ) | |
{ | |
log_debug( "export directory details:" ); | |
log_debug( " Characteristics: 0x" << std::hex << exp_dir->Characteristics << std::dec ); | |
log_debug( " TimeDateStamp: 0x" << std::hex << exp_dir->TimeDateStamp << std::dec ); | |
log_debug( " MajorVersion: " << exp_dir->MajorVersion ); | |
log_debug( " MinorVersion: " << exp_dir->MinorVersion ); | |
log_debug( " Name RVA: 0x" << std::hex << exp_dir->Name << std::dec ); | |
char* name = ( char* ) ( rva_to_va( exp_dir->Name ) ); | |
if ( name ) | |
{ | |
log_debug( " Name: " << name ); | |
} | |
else | |
{ | |
log_debug( " Name: <invalid>" ); | |
} | |
log_debug( " Base: " << exp_dir->Base ); | |
log_debug( " NumberOfFunctions: " << exp_dir->NumberOfFunctions ); | |
log_debug( " NumberOfNames: " << exp_dir->NumberOfNames ); | |
log_debug( " AddressOfFunctions RVA: 0x" << std::hex << exp_dir->AddressOfFunctions << std::dec ); | |
log_debug( " AddressOfNames RVA: 0x" << std::hex << exp_dir->AddressOfNames << std::dec ); | |
log_debug( " AddressOfNameOrdinals RVA: 0x" << std::hex << exp_dir->AddressOfNameOrdinals << std::dec ); | |
} | |
func_addr = ( DWORD* ) ( rva_to_va( exp_dir->AddressOfFunctions ) ); | |
name_addr = ( DWORD* ) ( rva_to_va( exp_dir->AddressOfNames ) ); | |
name_ord = ( WORD* ) ( rva_to_va( exp_dir->AddressOfNameOrdinals ) ); | |
log_debug( "AddressOfFunctions VA: " << std::hex << func_addr << std::dec ); | |
log_debug( "AddressOfNames VA: " << std::hex << name_addr << std::dec ); | |
log_debug( "AddressOfNameOrdinals VA: " << std::hex << name_ord << std::dec ); | |
if ( !func_addr || !name_addr || !name_ord ) | |
{ | |
log_error( "failed to get export tables" ); | |
return false; | |
} | |
load_exports(); | |
return true; | |
} | |
void load_exports() | |
{ | |
log_debug( "loading existing exports" ); | |
if ( !exp_dir ) | |
{ | |
log_debug( "no export directory, nothing to load" ); | |
return; | |
} | |
old_exp.clear(); | |
for ( DWORD i = 0; i < exp_dir->NumberOfNames; i++ ) | |
{ | |
char* name = ( char* ) ( rva_to_va( name_addr[ i ] ) ); | |
if ( !name ) | |
{ | |
log_debug( "export " << i << ": Name pointer is invalid" ); | |
continue; | |
} | |
WORD ord = name_ord[ i ]; | |
if ( ord >= exp_dir->NumberOfFunctions ) | |
{ | |
log_debug( "export " << i << ": ordinal " << ord << " is out of range" ); | |
continue; | |
} | |
DWORD rva = func_addr[ ord ]; | |
log_debug( "export " << i << ": name=" << name << ", ordinal=" << ord << ", RVA=0x" << std::hex << rva << std::dec ); | |
exp_entry e{ std::string( name ), rva }; | |
old_exp.push_back( e ); | |
} | |
log_debug( "loaded " << old_exp.size() << " existing exports" ); | |
} | |
void* rva_to_va( DWORD rva ) const | |
{ | |
if ( !mem->data || rva == 0 ) | |
{ | |
return nullptr; | |
} | |
DWORD off = rva_to_off( rva ); | |
if ( off == 0 && rva != 0 ) | |
{ | |
return nullptr; | |
} | |
if ( off >= mem->size ) | |
{ | |
return nullptr; | |
} | |
void* va = ( BYTE* ) ( mem->data ) + off; | |
return va; | |
} | |
DWORD rva_to_off( DWORD rva ) const | |
{ | |
if ( !mem->data || rva == 0 ) | |
{ | |
return 0; | |
} | |
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) | |
{ | |
if ( rva >= sec[ i ].VirtualAddress && | |
rva < sec[ i ].VirtualAddress + sec[ i ].SizeOfRawData ) | |
{ | |
DWORD off = rva - sec[ i ].VirtualAddress + sec[ i ].PointerToRawData; | |
return off; | |
} | |
} | |
return rva; | |
} | |
DWORD off_to_rva( DWORD off ) const | |
{ | |
if ( !mem->data || off == 0 ) | |
{ | |
return 0; | |
} | |
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) | |
{ | |
if ( off >= sec[ i ].PointerToRawData && | |
off < sec[ i ].PointerToRawData + sec[ i ].SizeOfRawData ) | |
{ | |
DWORD rva = off - sec[ i ].PointerToRawData + sec[ i ].VirtualAddress; | |
return rva; | |
} | |
} | |
return off; | |
} | |
bool add_export( const std::string& name, DWORD64 va ) | |
{ | |
log_debug( "adding export: " << name << " -> 0x" << std::hex << va << std::dec ); | |
if ( !mem->data ) | |
{ | |
log_error( "file data is NULL" ); | |
return false; | |
} | |
DWORD rva = static_cast< DWORD >( va - nt->OptionalHeader.ImageBase ); | |
log_debug( "converted VA 0x" << std::hex << va << " to RVA 0x" << rva << std::dec ); | |
exp_entry e{ name, rva }; | |
new_exp.push_back( e ); | |
log_debug( "added export to list, total new exports: " << new_exp.size() ); | |
return true; | |
} | |
bool update_exports() | |
{ | |
log_debug( "adding to existing export directory" ); | |
if ( !mem->data ) | |
{ | |
return false; | |
} | |
if ( new_exp.empty() ) | |
{ | |
log_debug( "no exports to add" ); | |
return true; | |
} | |
if ( !exp_dir ) | |
{ | |
log_debug( "no existing export directory, will create new one" ); | |
return create_exp_dir(); | |
} | |
log_debug( "found existing export directory" ); | |
int sec_idx = -1; | |
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) | |
{ | |
if ( exp_rva >= sec[ i ].VirtualAddress && | |
exp_rva < sec[ i ].VirtualAddress + sec[ i ].Misc.VirtualSize ) | |
{ | |
sec_idx = i; | |
break; | |
} | |
} | |
if ( sec_idx == -1 ) | |
{ | |
log_error( "could not find section containing export directory" ); | |
return false; | |
} | |
log_debug( "export directory is in section " << sec_idx ); | |
char name[ 9 ] = { 0 }; | |
memcpy( name, sec[ sec_idx ].Name, 8 ); | |
log_debug( "export section name: " << name ); | |
std::vector<exp_entry> back = old_exp; | |
if ( create_exp_dir() ) | |
{ | |
log_debug( "successfully created new export directory with all exports" ); | |
return true; | |
} | |
log_error( "failed to create new export directory" ); | |
return false; | |
} | |
bool create_new_sec( DWORD need_size, const std::vector<exp_entry>& all_exp ) | |
{ | |
log_debug( "creating new section for export directory" ); | |
if ( nt->FileHeader.NumberOfSections >= 96 ) | |
{ | |
log_error( "maximum number of sections reached" ); | |
return false; | |
} | |
DWORD align_size = ( need_size + 0xFFF ) & ~0xFFF; | |
log_debug( "aligned section size: 0x" << std::hex << align_size << std::dec ); | |
IMAGE_SECTION_HEADER* last = &sec[ nt->FileHeader.NumberOfSections - 1 ]; | |
char lname[ 9 ] = { 0 }; | |
memcpy( lname, last->Name, 8 ); | |
log_debug( "prev section: " << lname ); | |
log_debug( "prev section VirtualAddress: 0x" << std::hex << last->VirtualAddress << std::dec ); | |
log_debug( "prev section VirtualSize: 0x" << std::hex << last->Misc.VirtualSize << std::dec ); | |
log_debug( "prev section PointerToRawData: 0x" << std::hex << last->PointerToRawData << std::dec ); | |
log_debug( "prev section SizeOfRawData: 0x" << std::hex << last->SizeOfRawData << std::dec ); | |
DWORD new_rva = ( last->VirtualAddress + last->Misc.VirtualSize + 0xFFF ) & ~0xFFF; | |
log_debug( "new section RVA: 0x" << std::hex << new_rva << std::dec ); | |
DWORD align = nt->OptionalHeader.FileAlignment; | |
DWORD new_off = ( last->PointerToRawData + last->SizeOfRawData + align - 1 ) & ~( align - 1 ); | |
log_debug( "new section raw offset: 0x" << std::hex << new_off << std::dec ); | |
DWORD new_size = new_off + align_size; | |
log_debug( "current file size: 0x" << std::hex << mem->size << std::dec ); | |
log_debug( "new file size: 0x" << std::hex << new_size << std::dec ); | |
if ( new_size > mem->size ) | |
{ | |
log_debug( "need to expand file from 0x" << std::hex << mem->size << " to 0x" << new_size << std::dec ); | |
UnmapViewOfFile( mem->data ); | |
CloseHandle( mem->map ); | |
HANDLE file = CreateFileA( path.c_str(), GENERIC_READ | GENERIC_WRITE, | |
FILE_SHARE_READ, nullptr, OPEN_EXISTING, | |
FILE_ATTRIBUTE_NORMAL, nullptr ); | |
if ( file == INVALID_HANDLE_VALUE ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to reopen file for resizing: " << path << ", Error: " << err ); | |
return false; | |
} | |
LARGE_INTEGER dist; | |
dist.QuadPart = new_size; | |
if ( !SetFilePointerEx( file, dist, nullptr, FILE_BEGIN ) ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "Failed to set file pointer: " << err ); | |
CloseHandle( file ); | |
return false; | |
} | |
if ( !SetEndOfFile( file ) ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to set end of file: " << err ); | |
CloseHandle( file ); | |
return false; | |
} | |
mem->map = CreateFileMappingA( file, nullptr, PAGE_READWRITE, 0, 0, nullptr ); | |
CloseHandle( file ); | |
if ( !mem->map ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to create new file mapping: " << err ); | |
return false; | |
} | |
mem->data = MapViewOfFile( mem->map, FILE_MAP_ALL_ACCESS, 0, 0, 0 ); | |
if ( !mem->data ) | |
{ | |
DWORD err = GetLastError(); | |
log_error( "failed to map view of expanded file: " << err ); | |
return false; | |
} | |
mem->size = new_size; | |
log_debug( "file expanded successfully to 0x" << std::hex << mem->size << std::dec ); | |
dos = static_cast< IMAGE_DOS_HEADER* >( mem->data ); | |
nt = reinterpret_cast< IMAGE_NT_HEADERS64* >( ( BYTE* ) ( mem->data ) + dos->e_lfanew ); | |
sec = reinterpret_cast< IMAGE_SECTION_HEADER* >( | |
reinterpret_cast< BYTE* >( &nt->OptionalHeader ) + | |
nt->FileHeader.SizeOfOptionalHeader ); | |
last = &sec[ nt->FileHeader.NumberOfSections - 1 ]; | |
} | |
IMAGE_SECTION_HEADER new_sec; | |
memset( &new_sec, 0, sizeof( IMAGE_SECTION_HEADER ) ); | |
memcpy( new_sec.Name, ".edata", 6 ); | |
new_sec.Misc.VirtualSize = need_size; | |
new_sec.VirtualAddress = new_rva; | |
new_sec.SizeOfRawData = align_size; | |
new_sec.PointerToRawData = new_off; | |
new_sec.PointerToRelocations = 0; | |
new_sec.PointerToLinenumbers = 0; | |
new_sec.NumberOfRelocations = 0; | |
new_sec.NumberOfLinenumbers = 0; | |
new_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ; | |
memcpy( &sec[ nt->FileHeader.NumberOfSections ], &new_sec, sizeof( IMAGE_SECTION_HEADER ) ); | |
nt->FileHeader.NumberOfSections++; | |
log_debug( "added new section '.edata' at index " << ( nt->FileHeader.NumberOfSections - 1 ) ); | |
DWORD new_img_size = new_rva + align_size; | |
if ( new_img_size > nt->OptionalHeader.SizeOfImage ) | |
{ | |
nt->OptionalHeader.SizeOfImage = new_img_size; | |
log_debug( "updated SizeOfImage to 0x" << std::hex << new_img_size << std::dec ); | |
} | |
memset( ( BYTE* ) ( mem->data ) + new_off, 0, align_size ); | |
DWORD exp_off = new_off; | |
DWORD exp_rva = new_rva; | |
if ( exp_rva % 16 != 0 ) | |
{ | |
DWORD a = 16 - ( exp_rva % 16 ); | |
exp_rva += a; | |
exp_off += a; | |
} | |
log_debug( "placing export directory in new section at RVA 0x" << std::hex << exp_rva | |
<< " (offset 0x" << exp_off << ")" << std::dec ); | |
void* exp_va = ( BYTE* ) ( mem->data ) + exp_off; | |
log_debug( "new export directory VA: " << std::hex << exp_va << std::dec ); | |
auto* new_dir = ( IMAGE_EXPORT_DIRECTORY* ) ( exp_va ); | |
new_dir->Characteristics = 0; | |
new_dir->TimeDateStamp = static_cast< DWORD >( time( nullptr ) ); | |
new_dir->MajorVersion = 0; | |
new_dir->MinorVersion = 0; | |
new_dir->Base = 1; | |
new_dir->NumberOfFunctions = all_exp.size(); | |
new_dir->NumberOfNames = all_exp.size(); | |
DWORD cur_off = exp_off + sizeof( IMAGE_EXPORT_DIRECTORY ); | |
DWORD cur_rva = exp_rva + sizeof( IMAGE_EXPORT_DIRECTORY ); | |
std::string dname = path.substr( path.find_last_of( "/\\" ) + 1 ); | |
log_debug( "setting up DLL name at offset 0x" << std::hex << cur_off << std::dec ); | |
char* nptr = ( char* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
strcpy_s( nptr, dname.length() + 1, dname.c_str() ); | |
new_dir->Name = cur_rva; | |
cur_off += dname.length() + 1; | |
cur_rva += dname.length() + 1; | |
if ( cur_off % 4 != 0 ) | |
{ | |
DWORD a = 4 - ( cur_off % 4 ); | |
cur_off += a; | |
cur_rva += a; | |
} | |
log_debug( "setting up functions table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfFunctions = cur_rva; | |
DWORD* ftbl = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += all_exp.size() * sizeof( DWORD ); | |
cur_rva += all_exp.size() * sizeof( DWORD ); | |
log_debug( "setting up names table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfNames = cur_rva; | |
DWORD* ntbl = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += all_exp.size() * sizeof( DWORD ); | |
cur_rva += all_exp.size() * sizeof( DWORD ); | |
log_debug( "setting up ordinals table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfNameOrdinals = cur_rva; | |
WORD* otbl = ( WORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += all_exp.size() * sizeof( WORD ); | |
cur_rva += all_exp.size() * sizeof( WORD ); | |
for ( size_t i = 0; i < all_exp.size(); i++ ) | |
{ | |
log_debug( "processing export " << i << ": " << all_exp[ i ].name | |
<< " -> 0x" << std::hex << all_exp[ i ].rva << std::dec ); | |
ftbl[ i ] = all_exp[ i ].rva; | |
otbl[ i ] = static_cast< WORD >( i ); | |
log_debug( "setting up name string at offset 0x" << std::hex << cur_off << std::dec ); | |
char* nstr = ( char* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
strcpy_s( nstr, all_exp[ i ].name.length() + 1, all_exp[ i ].name.c_str() ); | |
ntbl[ i ] = cur_rva; | |
cur_off += all_exp[ i ].name.length() + 1; | |
cur_rva += all_exp[ i ].name.length() + 1; | |
} | |
DWORD dir_size = cur_rva - exp_rva; | |
log_debug( "new export directory size: 0x" << std::hex << dir_size << std::dec ); | |
log_debug( "updating data directory" ); | |
nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress = exp_rva; | |
nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].Size = dir_size; | |
log_debug( "export directory created in new section successfully" ); | |
return true; | |
} | |
bool create_exp_dir() | |
{ | |
log_debug( "creating new export directory" ); | |
if ( !mem->data ) | |
{ | |
log_error( "file data is null" ); | |
return false; | |
} | |
std::vector<exp_entry> all = old_exp; | |
all.insert( all.end(), new_exp.begin(), new_exp.end() ); | |
DWORD n = all.size(); | |
log_debug( "total exports will be: " << n ); | |
DWORD need = sizeof( IMAGE_EXPORT_DIRECTORY ); | |
need += sizeof( DWORD ) * n; | |
need += sizeof( DWORD ) * n; | |
need += sizeof( WORD ) * n; | |
std::string dname = path.substr( path.find_last_of( "/\\" ) + 1 ); | |
need += dname.length() + 1; | |
log_debug( "size after adding DLL name (" << dname << "): " << need << " bytes" ); | |
for ( const auto& e : all ) | |
{ | |
need += e.name.length() + 1; | |
log_debug( "adding size for export name '" << e.name << "': " << e.name.length() + 1 << " bytes" ); | |
} | |
need = ( need + 15 ) & ~15; | |
log_debug( "total needed size (aligned): " << need << " bytes" ); | |
int rdata_idx = find_rdata_sec(); | |
if ( rdata_idx == -1 ) | |
{ | |
log_error( "could not find .rdata section or suitable section for exports" ); | |
return false; | |
} | |
log_debug( "using section index " << rdata_idx << " for exports" ); | |
IMAGE_SECTION_HEADER& rsec = sec[ rdata_idx ]; | |
char sname[ 9 ] = { 0 }; | |
memcpy( sname, rsec.Name, 8 ); | |
log_debug( "export section name: " << sname ); | |
log_debug( "export section VirtualAddress: 0x" << std::hex << rsec.VirtualAddress << std::dec ); | |
log_debug( "export section VirtualSize: 0x" << std::hex << rsec.Misc.VirtualSize << std::dec ); | |
log_debug( "export section PointerToRawData: 0x" << std::hex << rsec.PointerToRawData << std::dec ); | |
log_debug( "export section SizeOfRawData: 0x" << std::hex << rsec.SizeOfRawData << std::dec ); | |
DWORD avail = rsec.SizeOfRawData - rsec.Misc.VirtualSize; | |
if ( avail >= need ) | |
{ | |
DWORD exp_off = rsec.PointerToRawData + rsec.Misc.VirtualSize; | |
DWORD exp_rva = rsec.VirtualAddress + rsec.Misc.VirtualSize; | |
if ( exp_rva % 16 != 0 ) | |
{ | |
DWORD a = 16 - ( exp_rva % 16 ); | |
exp_rva += a; | |
exp_off += a; | |
} | |
log_debug( "placing export directory at RVA 0x" << std::hex << exp_rva | |
<< " (offset 0x" << exp_off << ")" << std::dec ); | |
if ( exp_off + need > rsec.PointerToRawData + rsec.SizeOfRawData ) | |
{ | |
log_error( "export directory would extend beyond section raw data" ); | |
return create_new_sec( need, all ); | |
} | |
DWORD v_size = exp_rva + need - rsec.VirtualAddress; | |
log_debug( "new virtual size for section: 0x" << std::hex << v_size << std::dec ); | |
if ( v_size > rsec.SizeOfRawData ) | |
{ | |
log_error( "new virtual size would exceed raw data size" ); | |
return create_new_sec( need, all ); | |
} | |
void* exp_va = ( BYTE* ) ( mem->data ) + exp_off; | |
log_debug( "new export directory VA: " << std::hex << exp_va << std::dec ); | |
memset( exp_va, 0, need ); | |
auto* new_dir = ( IMAGE_EXPORT_DIRECTORY* ) ( exp_va ); | |
new_dir->Characteristics = 0; | |
new_dir->TimeDateStamp = static_cast< DWORD >( time( nullptr ) ); | |
new_dir->MajorVersion = 0; | |
new_dir->MinorVersion = 0; | |
new_dir->Base = 1; | |
new_dir->NumberOfFunctions = n; | |
new_dir->NumberOfNames = n; | |
DWORD cur_off = exp_off + sizeof( IMAGE_EXPORT_DIRECTORY ); | |
DWORD cur_rva = exp_rva + sizeof( IMAGE_EXPORT_DIRECTORY ); | |
log_debug( "setting up DLL name at offset 0x" << std::hex << cur_off << std::dec ); | |
char* nptr = ( char* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
strcpy_s( nptr, dname.length() + 1, dname.c_str() ); | |
new_dir->Name = cur_rva; | |
cur_off += dname.length() + 1; | |
cur_rva += dname.length() + 1; | |
if ( cur_off % 4 != 0 ) | |
{ | |
DWORD a = 4 - ( cur_off % 4 ); | |
cur_off += a; | |
cur_rva += a; | |
} | |
log_debug( "setting up functions table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfFunctions = cur_rva; | |
DWORD* ftbl = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += n * sizeof( DWORD ); | |
cur_rva += n * sizeof( DWORD ); | |
log_debug( "setting up names table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfNames = cur_rva; | |
DWORD* ntbl = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += n * sizeof( DWORD ); | |
cur_rva += n * sizeof( DWORD ); | |
log_debug( "setting up ordinals table at offset 0x" << std::hex << cur_off << std::dec ); | |
new_dir->AddressOfNameOrdinals = cur_rva; | |
WORD* otbl = ( WORD* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
cur_off += n * sizeof( WORD ); | |
cur_rva += n * sizeof( WORD ); | |
for ( size_t i = 0; i < n; i++ ) | |
{ | |
log_debug( "processing export " << i << ": " << all[ i ].name | |
<< " -> 0x" << std::hex << all[ i ].rva << std::dec ); | |
ftbl[ i ] = all[ i ].rva; | |
otbl[ i ] = static_cast< WORD >( i ); | |
log_debug( "setting up name string at offset 0x" << std::hex << cur_off << std::dec ); | |
char* nstr = ( char* ) ( ( BYTE* ) ( mem->data ) + cur_off ); | |
strcpy_s( nstr, all[ i ].name.length() + 1, all[ i ].name.c_str() ); | |
ntbl[ i ] = cur_rva; | |
cur_off += all[ i ].name.length() + 1; | |
cur_rva += all[ i ].name.length() + 1; | |
} | |
DWORD dir_size = cur_rva - exp_rva; | |
log_debug( "new export directory size: 0x" << std::hex << dir_size << std::dec ); | |
log_debug( "updating data directory" ); | |
nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress = exp_rva; | |
nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].Size = dir_size; | |
if ( rsec.Misc.VirtualSize < v_size ) | |
{ | |
log_debug( "increasing section virtual size from 0x" << std::hex | |
<< rsec.Misc.VirtualSize << " to 0x" << v_size << std::dec ); | |
rsec.Misc.VirtualSize = v_size; | |
} | |
log_debug( "export directory created successfully in existing section" ); | |
return true; | |
} | |
log_debug( "not enough space in existing section. Creating new section." ); | |
return create_new_sec( need, all ); | |
} | |
int find_rdata_sec() const | |
{ | |
log_debug( "finding .rdata section or suitable section for exports" ); | |
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) | |
{ | |
char name[ 9 ] = { 0 }; | |
memcpy( name, sec[ i ].Name, 8 ); | |
log_debug( "checking section " << i << ": " << name ); | |
if ( _stricmp( name, ".rdata" ) == 0 ) | |
{ | |
log_debug( "found .rdata section at index " << i ); | |
return i; | |
} | |
} | |
for ( int i = 0; i < nt->FileHeader.NumberOfSections; i++ ) | |
{ | |
if ( ( sec[ i ].Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA ) && | |
( sec[ i ].Characteristics & IMAGE_SCN_MEM_READ ) && | |
!( sec[ i ].Characteristics & IMAGE_SCN_MEM_WRITE ) ) | |
{ | |
log_debug( "found suitable read-only data section at index " << i ); | |
return i; | |
} | |
} | |
log_debug( "no suitable section found" ); | |
return -1; | |
} | |
void print_exports() const | |
{ | |
log_debug( "printing exports" ); | |
if ( !mem->data ) | |
{ | |
log_debug( "file data is null, cannot show exports" ); | |
return; | |
} | |
DWORD exp_rva = nt->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress; | |
log_debug( "current export directory RVA: 0x" << std::hex << exp_rva << std::dec ); | |
if ( exp_rva == 0 ) | |
{ | |
log_info( "no export directory found." ); | |
return; | |
} | |
DWORD exp_off = rva_to_off( exp_rva ); | |
if ( exp_off == 0 && exp_rva != 0 ) | |
{ | |
log_error( "could not convert export directory RVA to file offset" ); | |
return; | |
} | |
auto* dir = ( IMAGE_EXPORT_DIRECTORY* ) ( ( BYTE* ) ( mem->data ) + exp_off ); | |
log_debug( "export directory VA: " << std::hex << dir << std::dec ); | |
if ( ( BYTE* ) ( dir ) < ( BYTE* ) ( mem->data ) || | |
( BYTE* ) ( dir ) >= ( BYTE* ) ( mem->data ) + mem->size || | |
exp_off + sizeof( IMAGE_EXPORT_DIRECTORY ) > mem->size ) | |
{ | |
log_error( "export directory is out of bounds" ); | |
return; | |
} | |
DWORD f_off = rva_to_off( dir->AddressOfFunctions ); | |
DWORD n_off = rva_to_off( dir->AddressOfNames ); | |
DWORD o_off = rva_to_off( dir->AddressOfNameOrdinals ); | |
if ( f_off == 0 || n_off == 0 || o_off == 0 ) | |
{ | |
log_error( "failed to get offsets for export tables" ); | |
return; | |
} | |
DWORD* funcs = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + f_off ); | |
DWORD* names = ( DWORD* ) ( ( BYTE* ) ( mem->data ) + n_off ); | |
WORD* ords = ( WORD* ) ( ( BYTE* ) ( mem->data ) + o_off ); | |
log_debug( "functions table VA: " << std::hex << funcs << std::dec ); | |
log_debug( "names table VA: " << std::hex << names << std::dec ); | |
log_debug( "ordinals table VA: " << std::hex << ords << std::dec ); | |
if ( ( BYTE* ) ( funcs ) < ( BYTE* ) ( mem->data ) || | |
( BYTE* ) ( funcs ) >= ( BYTE* ) ( mem->data ) + mem->size || | |
( BYTE* ) ( names ) < ( BYTE* ) ( mem->data ) || | |
( BYTE* ) ( names ) >= ( BYTE* ) ( mem->data ) + mem->size || | |
( BYTE* ) ( ords ) < ( BYTE* ) ( mem->data ) || | |
( BYTE* ) ( ords ) >= ( BYTE* ) ( mem->data ) + mem->size ) | |
{ | |
log_error( "export tables are out of bounds" ); | |
return; | |
} | |
DWORD name_off = rva_to_off( dir->Name ); | |
char* dname = ( char* ) ( ( BYTE* ) ( mem->data ) + name_off ); | |
std::cout << "Export Directory Information:\n"; | |
std::cout << " Characteristics: 0x" << std::hex << dir->Characteristics << '\n'; | |
std::cout << " TimeDateStamp: 0x" << dir->TimeDateStamp << '\n'; | |
std::cout << " MajorVersion: " << std::dec << dir->MajorVersion << '\n'; | |
std::cout << " MinorVersion: " << dir->MinorVersion << '\n'; | |
if ( dname && ( BYTE* ) ( dname ) >= ( BYTE* ) ( mem->data ) && | |
( BYTE* ) ( dname ) < ( BYTE* ) ( mem->data ) + mem->size ) | |
{ | |
std::cout << " Name: " << dname << '\n'; | |
} | |
else | |
{ | |
std::cout << " Name: <invalid>\n"; | |
} | |
std::cout << " Base: " << dir->Base << '\n'; | |
std::cout << " NumberOfFunctions: " << dir->NumberOfFunctions << '\n'; | |
std::cout << " NumberOfNames: " << dir->NumberOfNames << '\n'; | |
std::cout << " Exported Functions:\n"; | |
for ( DWORD i = 0; i < dir->NumberOfNames; i++ ) | |
{ | |
if ( i >= dir->NumberOfFunctions ) | |
{ | |
log_debug( "export index " << i << " exceeds function count" ); | |
break; | |
} | |
DWORD name_rva = names[ i ]; | |
WORD ord = ords[ i ]; | |
if ( ord >= dir->NumberOfFunctions ) | |
{ | |
log_debug( "export " << i << ": ordinal " << ord << " is out of range" ); | |
continue; | |
} | |
DWORD func_rva = funcs[ ord ]; | |
DWORD name_off = rva_to_off( name_rva ); | |
if ( name_off == 0 && name_rva != 0 ) | |
{ | |
log_debug( "export " << i << ": failed to get name offset for RVA 0x" | |
<< std::hex << name_rva << std::dec ); | |
continue; | |
} | |
char* exp_name = ( char* ) ( ( BYTE* ) ( mem->data ) + name_off ); | |
if ( ( BYTE* ) ( exp_name ) < ( BYTE* ) ( mem->data ) || | |
( BYTE* ) ( exp_name ) >= ( BYTE* ) ( mem->data ) + mem->size ) | |
{ | |
log_debug( "export " << i << ": name pointer is out of bounds" ); | |
continue; | |
} | |
std::cout << " " << exp_name << " -> " | |
<< "0x" << std::hex << func_rva << std::dec << '\n'; | |
} | |
log_debug( "finished printing exports" ); | |
} | |
DWORD64 get_image_base() const | |
{ | |
if ( !nt ) | |
{ | |
log_error( "nt headers not set" ); | |
return 0; | |
} | |
return nt->OptionalHeader.ImageBase; | |
} | |
}; | |
std::string gen_rand_name( int min_len, int max_len ) | |
{ | |
static const char chars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; | |
std::random_device rd; | |
std::mt19937 gen( rd() ); | |
std::uniform_int_distribution len_dist( min_len, max_len ); | |
std::uniform_int_distribution<> char_dist( 0, sizeof( chars ) - 2 ); | |
int len = len_dist( gen ); | |
std::string result; | |
result.reserve( len ); | |
for ( int i = 0; i < len; ++i ) | |
result += chars[ char_dist( gen ) ]; | |
return result; | |
} | |
DWORD64 gen_rand_va( DWORD64 min_va, DWORD64 max_va ) | |
{ | |
std::random_device rd; | |
std::mt19937_64 gen( rd() ); | |
std::uniform_int_distribution dist( min_va, max_va ); | |
return dist( gen ); | |
} | |
struct args_parser | |
{ | |
std::unordered_map<std::string_view, std::string_view> option_map; | |
std::vector<std::string_view> positional_args; | |
args_parser( int argc, char* argv[] ) | |
{ | |
for ( int it = 1; it < argc; it++ ) | |
{ | |
std::string_view arg = argv[ it ]; | |
if ( arg.starts_with( "--" ) ) | |
{ | |
auto pos = arg.find( '=' ); | |
if ( pos != std::string_view::npos ) | |
{ | |
std::string_view key = arg.substr( 0, pos ); | |
std::string_view value = arg.substr( pos + 1 ); | |
option_map[ key ] = value; | |
} | |
else if ( it + 1 < argc && !std::string_view( argv[ it + 1 ] ).starts_with( "-" ) ) | |
{ | |
option_map[ arg ] = argv[ ++it ]; | |
} | |
else | |
{ | |
option_map[ arg ] = ""; | |
} | |
} | |
else if ( arg.starts_with( "-" ) && arg.size() > 1 ) | |
{ | |
if ( it + 1 < argc && !std::string_view( argv[ it + 1 ] ).starts_with( "-" ) ) | |
{ | |
option_map[ arg ] = argv[ ++it ]; | |
} | |
else | |
{ | |
option_map[ arg ] = ""; | |
} | |
} | |
else | |
{ | |
positional_args.push_back( arg ); | |
} | |
} | |
} | |
bool has_flag( std::string_view flag ) const | |
{ | |
return option_map.contains( flag ); | |
} | |
std::optional<std::string_view> get_option( std::string_view name ) const | |
{ | |
auto it = option_map.find( name ); | |
if ( it != option_map.end() ) | |
{ | |
return it->second; | |
} | |
return std::nullopt; | |
} | |
std::optional<std::string_view> get_arg( size_t index ) const | |
{ | |
if ( index < positional_args.size() ) | |
{ | |
return positional_args[ index ]; | |
} | |
return std::nullopt; | |
} | |
std::optional<uint64_t> get_address( std::string_view name ) const | |
{ | |
auto value = get_option( name ); | |
if ( !value || value->empty() ) | |
{ | |
return std::nullopt; | |
} | |
uint64_t result = 0; | |
std::string_view str = *value; | |
if ( str.size() > 2 && str.substr( 0, 2 ) == "0x" ) | |
{ | |
str.remove_prefix( 2 ); | |
auto [ptr, ec] = std::from_chars( str.data(), str.data() + str.size(), result, 16 ); | |
if ( ec != std::errc() ) | |
{ | |
return std::nullopt; | |
} | |
} | |
else | |
{ | |
auto [ptr, ec] = std::from_chars( str.data(), str.data() + str.size(), result, 10 ); | |
if ( ec != std::errc() ) | |
{ | |
return std::nullopt; | |
} | |
} | |
return result; | |
} | |
std::optional<int> get_int( std::string_view name ) const | |
{ | |
const auto value = get_option( name ); | |
if ( !value || value->empty() ) | |
{ | |
return std::nullopt; | |
} | |
int result = 0; | |
auto [ptr, ec] = std::from_chars( value->data(), value->data() + value->size(), result, 10 ); | |
if ( ec != std::errc() ) | |
{ | |
return std::nullopt; | |
} | |
return result; | |
} | |
static uint64_t parse_address( std::string_view str ) | |
{ | |
uint64_t result = 0; | |
if ( str.size() > 2 && str.substr( 0, 2 ) == "0x" ) | |
{ | |
str.remove_prefix( 2 ); | |
auto [ptr, ec] = std::from_chars( str.data(), str.data() + str.size(), result, 16 ); | |
if ( ec != std::errc() ) | |
{ | |
return 0; | |
} | |
} | |
else | |
{ | |
auto [ptr, ec] = std::from_chars( str.data(), str.data() + str.size(), result, 10 ); | |
if ( ec != std::errc() ) | |
{ | |
return 0; | |
} | |
} | |
return result; | |
} | |
}; | |
void print_usage() | |
{ | |
std::cout << R"( | |
EXPMOD - just adds exports to 64-bit PE's | |
Usage: | |
exp <file_path> <export_name> <export_address> [--va | --rva] | |
exp <file_path> --random [count] | |
Options: | |
<file_path> Path to the 64-bit PE file to modify | |
<export_name> Name of the export to add | |
<export_address> Address of the export (hex with 0x prefix or decimal) | |
--va Specify that export_address is a Virtual Address (default) | |
--rva Specify that export_address is a Relative Virtual Address | |
--random, -r Add random exports | |
[count] Number of random exports to add (optional, default: 512) | |
--min-va=<addr> Minimum virtual address for random exports (hex or decimal) | |
--max-va=<addr> Maximum virtual address for random exports (hex or decimal) | |
--min-rva=<addr> Minimum RVA for random exports (hex or decimal) | |
--max-rva=<addr> Maximum RVA for random exports (hex or decimal) | |
Example: | |
exp mydll.dll MyExport 0x140001000 | |
exp mydll.dll MyExport 0x1000 --rva | |
exp mydll.dll --random 1000 --min-va=0x140010000 --max-va=0x140020000 | |
exp mydll.dll --random 100 --min-rva=0x1000 --max-rva=0x2000 | |
)"; | |
} | |
int main( int argc, char* argv[] ) | |
{ | |
log_debug( "Starting pe_export_adder" ); | |
args_parser args( argc, argv ); | |
auto file_path = args.get_arg( 0 ); | |
if ( !file_path ) | |
{ | |
print_usage(); | |
return 1; | |
} | |
expmod e; | |
if ( !e.load_file( std::string( *file_path ) ) ) | |
{ | |
log_error( "Failed to load file" ); | |
return 1; | |
} | |
bool ok = false; | |
DWORD64 img_base = e.get_image_base(); | |
if ( args.has_flag( "--random" ) || args.has_flag( "-r" ) ) | |
{ | |
// this is just for examples sake... change it or remove it | |
// | |
int count = 512; | |
DWORD64 min_va = 0x140001000; | |
DWORD64 max_va = 0x140003000; | |
auto count_opt = args.get_option( "--random" ); | |
if ( !count_opt ) count_opt = args.get_option( "-r" ); | |
if ( count_opt && !count_opt->empty() && !count_opt->starts_with( "-" ) ) | |
{ | |
try | |
{ | |
count = std::stoi( std::string( *count_opt ) ); | |
if ( count <= 0 ) count = 512; | |
} | |
catch ( ... ) | |
{ | |
} | |
} | |
else if ( auto count_arg = args.get_arg( 1 ) ) | |
{ | |
if ( !count_arg->starts_with( "-" ) ) | |
{ | |
try | |
{ | |
count = std::stoi( std::string( *count_arg ) ); | |
if ( count <= 0 ) count = 512; | |
} | |
catch ( ... ) | |
{ | |
} | |
} | |
} | |
if ( auto val = args.get_address( "--min-va" ) ) | |
{ | |
min_va = *val; | |
} | |
if ( auto val = args.get_address( "--max-va" ) ) | |
{ | |
max_va = *val; | |
} | |
if ( auto val = args.get_address( "--min-rva" ) ) | |
{ | |
min_va = *val + img_base; | |
} | |
if ( auto val = args.get_address( "--max-rva" ) ) | |
{ | |
max_va = *val + img_base; | |
} | |
log_info( "adding " << count << " random exports with VA range 0x" | |
<< std::hex << min_va << " - 0x" << max_va << std::dec ); | |
for ( int i = 0; i < count; i++ ) | |
{ | |
std::string name = gen_rand_name( 8, 16 ); | |
DWORD64 addr = gen_rand_va( min_va, max_va ); | |
if ( !e.add_export( name, addr ) ) | |
{ | |
log_error( "failed to add random export #" << ( i + 1 ) ); | |
return 1; | |
} | |
} | |
ok = true; | |
} | |
else | |
{ | |
auto name = args.get_arg( 1 ); | |
auto addr_str = args.get_arg( 2 ); | |
if ( name && addr_str ) | |
{ | |
try | |
{ | |
DWORD64 addr = args.parse_address( *addr_str ); | |
bool is_rva = args.has_flag( "--rva" ); | |
if ( is_rva ) | |
{ | |
addr += img_base; | |
log_info( "adding export: " << *name << " -> RVA 0x" << std::hex | |
<< ( addr - img_base ) << " (VA 0x" << addr << ")" << std::dec ); | |
} | |
else | |
{ | |
log_info( "adding export: " << *name << " -> VA 0x" << std::hex | |
<< addr << " (RVA 0x" << ( addr - img_base ) << ")" << std::dec ); | |
} | |
if ( !e.add_export( std::string( *name ), addr ) ) | |
{ | |
log_error( "failed to add export" ); | |
return 1; | |
} | |
ok = true; | |
} | |
catch ( const std::exception& e ) | |
{ | |
log_error( "error parsing address: " << e.what() ); | |
return 1; | |
} | |
} | |
else | |
{ | |
print_usage(); | |
return 1; | |
} | |
} | |
if ( ok ) | |
{ | |
if ( !e.update_exports() ) | |
{ | |
log_error( "failed to update export directory" ); | |
return 1; | |
} | |
e.print_exports(); | |
log_info( "exports successfully added to " << *file_path ); | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment