Det bærbare eksekverbare (PE) format
Det første, man skal starte med, er PE-formatet. Viden og forståelse af dette format er en forudsætning for at udvikle antivirusmotorer til Windows-platformen (historisk set er størstedelen af verdens virusser rettet mod Windows).
Det bærbare eksekverbare (PE) format er et filformat, der anvendes af Windows-operativsystemet til at lagre eksekverbare filer som f.eks. .EXE og .DLL filer. Det blev introduceret med frigivelsen af Windows NT i 1993 og er siden blevet standardformatet for eksekverbare filer på Windows-systemer.
Før introduktionen af PE-formatet anvendte Windows en række forskellige formater til eksekverbare filer, herunder det Nye Eksekverbare (NE) format til 16-bit programmer og det Kompakte Eksekverbare (CE) format til 32-bit programmer. Disse formater havde deres eget unikke sæt regler og konventioner, hvilket gjorde det vanskeligt for operativsystemet at pålideligt indlæse og køre programmer.
For at standardisere layoutet og strukturen af eksekverbare filer introducerede Microsoft PE-formatet med frigivelsen af Windows NT. PE-formatet var designet til at være et fælles format for både 32-bit og 64-bit programmer.
En af de væsentlige egenskaber ved PE-formatet er brugen af en standardiseret header, som er placeret i begyndelsen af filen og indeholder en række felter, der giver operativsystemet vigtig information om den eksekverbare fil. Denne header omfatter IMAGE_DOS_HEADER og IMAGE_NT_HEADER-strukturerne, som er opdelt i to hovedsektioner: IMAGE_FILE_HEADER og IMAGE_OPTIONAL_HEADER.
De fleste af headere i PE-formatet er erklæret i headerfilen WinNT.h
IMAGE_DOS_HEADER
IMAGE_DOS_HEADER-strukturen er en arv fra headeren, der bruges til at understøtte bagudkompatibilitet med MS-DOS. Den bruges til at lagre information om filen, som er nødvendig for MS-DOS, såsom placeringen af programmets kode og data i filen og programmets indgangspunkt. Dette tillod programmer, der var skrevet til MS-DOS, at blive kørt på Windows NT, forudsat at de var kompileret som PE-filer.
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; // offset of IMAGE_NT_HEADER
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Der er følgende interessante felter for os:
-
e_magic feltet bruges til at identificere filen som en gyldig PE-fil. Som du kan se, er e_magic feltet et 16-bit unsigned heltal, der angiver filens "magiske nummer". Det magiske nummer er en særlig værdi, der identificerer filen som en gyldig PE-fil. Det er sat til værdien 0x5A4D (hexadecimal), som er ASCII-repræsentationen af karaktererne "MZ" (IMAGE_DOS_SIGNATURE).
-
e_lfanew feltet bruges til at specificere placeringen af IMAGE_NT_HEADERS strukturen, som indeholder information om layout og karakteristika for PE-filen. Som du kan se, er e_lfanew feltet et 32-bit signed heltal, der specificerer placeringen af IMAGE_NT_HEADERS strukturen i filen. Det er typisk sat til forskydningen af strukturen i forhold til begyndelsen af filen.
Historie
I begyndelsen af 1980'erne arbejdede Microsoft på et nyt operativsystem kaldet MS-DOS, som var designet til at være et simpelt, letvægts operativsystem til personlige computere. En af de nøglefunktioner i MS-DOS var dens evne til at køre eksekverbare filer, som er programmer, der kan køres på en computer.
For at gøre det let at identificere eksekverbare filer besluttede MS-DOS-udviklerne at bruge et særligt "magisk nummer" i begyndelsen af hver eksekverbar fil. Dette magiske nummer ville blive brugt til at skelne eksekverbare filer fra andre typer af filer, såsom datafiler eller konfigurationsfiler.
Mark Zbikowski, der var udvikler på MS-DOS-teamet, kom på ideen om at bruge bogstaverne "MZ" som det magiske nummer. I ASCII-kode er bogstavet "M" repræsenteret ved den hexadecimale værdi 0x4D, og bogstavet "Z" er repræsenteret ved den hexadecimale værdi 0x5A. Når disse værdier kombineres, danner de det magiske nummer 0x5A4D, som er ASCII-repræsentationen af karaktererne "MZ".
I dag bruges "MZ"-signaturen stadig til at identificere PE-filer, som er det primære eksekverbare filformat brugt på Windows-operativsystemet. Det er gemt i e_magic-feltet af IMAGE_DOS_HEADER-strukturen, som er den første struktur i en PE-fil.
IMAGE_NT_HEADER
IMAGE_NT_HEADER er en datastruktur, der blev introduceret med Windows NT-operativsystemet, som blev udgivet i 1993. Den blev designet til at give operativsystemet en standardmåde at læse og fortolke indholdet af eksekverbare filer (PE-filer).
Med frigivelsen af Windows NT introducerede Microsoft IMAGE_NT_HEADER som en måde at standardisere layoutet og strukturen af eksekverbare filer. Dette gjorde det lettere for operativsystemet at indlæse og køre programmer, da det kun behøvede at understøtte et enkelt format.
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64
typedef struct _IMAGE_NT_HEADERS32
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_NT_HEADERS64
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
IMAGE_NT_HEADER er en struktur, der optræder i begyndelsen af hver bærbar eksekverbar (PE) fil i Windows-operativsystemet. Den indeholder en række felter, der giver operativsystemet vigtig information om den eksekverbare fil, såsom dens størrelse, layout og tilsigtede formål.
IMAGE_NT_HEADER-strukturen er opdelt i to hovedafsnit: IMAGE_FILE_HEADER og IMAGE_OPTIONAL_HEADER.
IMAGE_FILE_HEADER
IMAGE_FILE_HEADER indeholder information om den eksekverbare fil som helhed, herunder dens maskintype (f.eks. x86, x64), antallet af sektioner i filen og datoen og tidspunktet for filens oprettelse.
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Strukturen har følgende felter:
-
Machine: Dette felt angiver målarkitekturen, som filen blev bygget til. Værdien af dette felt bestemmes af kompilatoren, når filen bygges. Nogle almindelige værdier er:
-
IMAGE_FILE_MACHINE_I386: Filen er beregnet til at køre på x86-arkitektur, også kendt som 32-bit.
-
IMAGE_FILE_MACHINE_AMD64: Filen er beregnet til at køre på x64-arkitektur, også kendt som 64-bit.
-
IMAGE_FILE_MACHINE_ARM: Filen er beregnet til at køre på ARM-arkitektur.
-
-
NumberOfSections: Dette felt angiver antallet af sektioner i PE-filen. En PE-fil er opdelt i flere sektioner, hver indeholdende forskellige typer information såsom kode, data og ressourcer. Dette felt bruges af operativsystemet til at bestemme, hvor mange sektioner der er i filen.
-
TimeDateStamp: Dette felt indeholder tidsstemplet for, hvornår filen blev bygget. Tidsstemplet er gemt som en 4-byte værdi, der repræsenterer antallet af sekunder siden 1. januar 1970, kl. 00:00:00 UTC. Dette felt kan bruges til at bestemme, hvornår filen sidst blev bygget, hvilket kan være nyttigt til fejlfinding eller versionsstyring.
-
PointerToSymbolTable: Dette felt angiver filens offset for COFF (Common Object File Format) symboltabellen, hvis til stede. COFF-symboltabellen indeholder information om symboler, der anvendes i filen, såsom funktionsnavne, variabelnavne og linjenumre. Dette felt anvendes kun til fejlsøgningsformål og er typisk ikke til stede i udgivelsesbygninger.
-
NumberOfSymbols: Dette felt angiver antallet af symboler i COFF-symboltabellen, hvis til stede. Dette felt anvendes i forbindelse med PointerToSymbolTable for at lokalisere COFF-symboltabellen i filen.
-
SizeOfOptionalHeader: Dette felt angiver størrelsen på den valgfri header, som indeholder yderligere information om filen. Den valgfri header indeholder typisk information om filens indgangspunkt, de importerende biblioteker og størrelsen på stacken og heapen.
-
Characteristics: Dette felt angiver forskellige attributter for filen. Nogle almindelige værdier er:
-
IMAGE_FILE_EXECUTABLE_IMAGE: Filen er en eksekverbar fil.
-
IMAGE_FILE_DLL: Filen er et dynamisk link bibliotek (DLL).
-
IMAGE_FILE_32BIT_MACHINE: Filen er en 32-bit fil.
-
IMAGE_FILE_DEBUG_STRIPPED: Filen er blevet fjernet for debug information.
-
Disse felter giver vigtig information om filen, som bruges af operativsystemet, når filen indlæses i hukommelsen og eksekveres. Ved at forstå felterne i IMAGE_FILE_HEADER-strukturen kan du få en dybere forståelse af, hvordan PE-filer er struktureret, og hvordan operativsystemet anvender dem.
De fleste af de mulige værdier for hvert felt er erklæret i headerfilen WinNT.h
IMAGE_OPTIONAL_HEADER
IMAGE_FILE_HEADER-strukturen følges af den valgfrie header, som beskrives af IMAGE_OPTIONAL_HEADER-strukturen. Den valgfrie header indeholder yderligere information om billedet, såsom adressen på indgangspunktet, billedets størrelse og adressen på importbiblioteket.
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64
typedef struct _IMAGE_OPTIONAL_HEADER32
{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_OPTIONAL_HEADER64
{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
Her er en detaljeret beskrivelse af hvert felt i IMAGE_OPTIONAL_HEADER-strukturen:
-
Magic: Dette felt angiver typen af valgfri header, der er til stede i PE-filen. Den mest almindelige værdi er IMAGE_NT_OPTIONAL_HDR32_MAGIC for en 32-bit fil eller IMAGE_NT_OPTIONAL_HDR64_MAGIC for en 64-bit fil.
-
MajorLinkerVersion og MinorLinkerVersion: Disse felter angiver versionen af linkeren, der blev brugt til at bygge filen. Linkeren er et værktøj, der bruges til at kombinere objektfiler og biblioteker til en enkelt eksekverbar fil.
-
SizeOfCode: Dette felt angiver størrelsen på kodeafsnittet i filen. Kodeafsnittet indeholder maskinkoden til den eksekverbare fil.
-
SizeOfInitializedData: Dette felt angiver størrelsen på det initialiserede dataafsnit i filen. Det initialiserede dataafsnit indeholder data, der initialiseres ved kørselstidspunkt, såsom globale variabler.
-
SizeOfUninitializedData: Dette felt angiver størrelsen på det uinitialiserede dataafsnit i filen. Det uinitialiserede dataafsnit indeholder data, der ikke initialiseres ved kørselstidspunkt, såsom bss-sektionen.
-
AddressOfEntryPoint: Dette felt angiver den virtuelle adresse for indgangspunktet til filen. Indgangspunktet er startadressen for programmet og er den første instruktion, der udføres, når filen indlæses i hukommelsen.
-
BaseOfCode: Dette felt angiver den virtuelle adresse for begyndelsen af kodeafsnittet.
-
ImageBase: Dette felt angiver den foretrukne virtuelle adresse, hvor filen bør indlæses i hukommelsen. Denne adresse bruges som en basisadresse for alle virtuelle adresser inden for filen.
-
SectionAlignment: Dette felt angiver justeringen af sektioner inden i filen. Sektionerne i filen er typisk justeret på multipler af denne værdi for at forbedre ydeevnen.
-
FileAlignment: Dette felt angiver justeringen af sektioner inden i filen på disken. Sektionerne i filen er typisk justeret på multipler af denne værdi for at forbedre disk-ydeevnen.
-
MajorOperatingSystemVersion og MinorOperatingSystemVersion: Disse felter angiver den mindste påkrævede version af operativsystemet, der er nødvendigt for at køre filen.
-
MajorImageVersion og MinorImageVersion: Disse felter angiver versionen af billedet. Billedversionen bruges til at identificere filens version til versionsstyring.
-
MajorSubsystemVersion og MinorSubsystemVersion: Disse felter angiver versionen af det subsystem, der er nødvendigt for at køre filen. Subsystemet er det miljø, hvor filen kører, såsom Windows Console eller Windows GUI.
-
Win32VersionValue: Dette felt er reserveret og typisk sat til 0.
-
SizeOfImage: Dette felt angiver størrelsen på billedet, i bytes, når det indlæses i hukommelsen.
-
SizeOfHeaders: Dette felt angiver størrelsen på headerne, i bytes. Headerne inkluderer IMAGE_FILE_HEADER og IMAGE_OPTIONAL_HEADER.
-
CheckSum: Dette felt bruges til at tjekke integriteten af filen. Kontrolsummen beregnes ved at summere indholdet af filen og gemme resultatet i dette felt. Kontrolsummen bruges til at opdage ændringer i filen, der kan opstå på grund af manipulation eller korruption.
-
Subsystem: Dette felt angiver det subsystem, der er nødvendigt for at køre filen. Mulige værdier inkluderer IMAGE_SUBSYSTEM_NATIVE, IMAGE_SUBSYSTEM_WINDOWS_GUI, IMAGE_SUBSYSTEM_WINDOWS_CUI, IMAGE_SUBSYSTEM_OS2_CUI, osv.
-
DllCharacteristics: Dette felt angiver karakteristika for filen, såsom om den er et dynamisk link bibliotek (DLL) eller om den kan flyttes ved indlæsningstidspunktet. Mulige værdier inkluderer IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE, IMAGE_DLLCHARACTERISTICS_NX_COMPAT, osv.
-
SizeOfStackReserve: Dette felt angiver størrelsen på stakken, i bytes, der er reserveret til programmet. Stakken bruges til at opbevare midlertidige data, såsom funktionskaldsinformation.
-
SizeOfStackCommit: Dette felt angiver størrelsen på stakken, i bytes, der er tildelt til programmet. Den tildelte stak er den del af stakken, der faktisk er reserveret i hukommelsen.
-
SizeOfHeapReserve: Dette felt angiver størrelsen på heapen, i bytes, der er reserveret til programmet. Heapen bruges til dynamisk hukommelsestildeling ved kørselstidspunkt.
-
SizeOfHeapCommit: Dette felt angiver størrelsen på heapen, i bytes, der er tildelt til programmet. Den tildelte heap er den del af heapen, der faktisk er reserveret i hukommelsen.
-
LoaderFlags: Dette felt er reserveret og typisk sat til 0.
-
NumberOfRvaAndSizes: Dette felt angiver antallet af datafortegnelsesposter i IMAGE_OPTIONAL_HEADER. Datafortegnelserne indeholder information om import, eksport, ressourcer osv. i filen.
-
DataDirectory: Dette felt er et array af IMAGE_DATA_DIRECTORY-strukturer, der angiver placeringen og størrelsen på datafortegnelserne i filen.
IMAGE_SECTION_HEADER
En sektion, i konteksten af en PE (Portable Executable) fil, er et sammenhængende blok af hukommelse i filen, der indeholder en specifik type data eller kode. I en PE-fil bruges sektioner til at organisere og opbevare forskellige dele af filen, såsom kode, data, ressourcer osv.
Hver sektion i en PE-fil har et unikt navn og beskrives af en IMAGE_SECTION_HEADER-struktur, der indeholder information om sektionen såsom dens størrelse, placering, karakteristika og så videre. Følgende er felterne i IMAGE_SECTION_HEADER:
En IMAGE_SECTION_HEADER er en datastruktur, der bruges i Portable Executable (PE) filformatet, som bruges på Windows-operativsystemet til at definere layoutet af en fil i hukommelsen. PE-filformatet bruges til eksekverbare filer, DLL'er og andre typer af filer, der indlæses i hukommelsen af Windows-operativsystemet. Hver sektionsheader beskriver en sammenhængende blok af data inden i filen og inkluderer information såsom navnet på sektionen, den virtuelle hukommelsesadresse, hvor sektionen skal indlæses, og størrelsen på sektionen. Sektionsheaderne kan bruges til at lokalisere og tilgå specifikke dele af filen, såsom kode- eller datasektionerne.
IMAGE_SECTION_HEADER-strukturen er defineret i Windows Platform SDK og kan findes i winnt.h headerfilen. Her er et eksempel på, hvordan strukturen er defineret i C++:
#pragma pack(push, 1)
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#pragma pack(pop)
Som du kan se, er strukturen defineret som en C++ struktur, og den indeholder felter for sektionens navn, virtuel størrelse, virtuel adresse, rådatastørrelse og pointer til rådata, relokationer, linjenumre samt antallet af relokationer og linjenumre. Derudover indeholder Characteristics-feltet flag, der beskriver sektionens karakteristika, såsom om den er eksekverbar, læsbar eller skrivbar.
-
Navn: Denne 8-byte array bruges til at specificere navnet på sektionen. Navnet kan være enhver null-termineret streng, men det bruges typisk til at give meningsfulde navne til forskellige dele af filen, såsom ".text" for eksekverbar kode, ".data" for initialiseret data, ".rdata" for skrivebeskyttet data og ".bss" for uinitialiseret data. Sektionens navn bruges af operativsystemet til at lokalisere sektionen inden i filen og bruges også af debuggere og andre værktøjer til at identificere sektionen og dens indhold.
-
VirtualSize: Dette felt angiver sektionens størrelse i hukommelsen, i bytes. Denne værdi repræsenterer den mængde hukommelse, som sektionen vil optage i hukommelsen, når filen er indlæst i hukommelsen.
-
VirtualAddress: Dette felt angiver startadressen for sektionen i hukommelsen, i bytes. Denne værdi er startadressen, hvor sektionen vil blive indlæst i hukommelsen, og bruges af operativsystemet til at bestemme placeringen i hukommelsen, hvor sektionen vil blive indlæst.
-
SizeOfRawData: Dette felt angiver sektionens størrelse i filen, i bytes. Denne værdi repræsenterer den mængde plads i filen, som sektionen vil optage, og bruges af operativsystemet til at bestemme sektionens størrelse i filen.
-
PointerToRawData: Dette felt angiver sektionens offset i filen, i bytes. Denne værdi repræsenterer sektionens placering inden i filen og bruges til at bestemme, hvor dataene for sektionen kan findes.
-
PointerToRelocations: Dette felt angiver offsettet for relokationsinformationen for sektionen, i bytes. Relokationsinformationen bruges til at rette adresser inden i sektionen, så de kan løses korrekt, når filen indlæses i hukommelsen.
-
PointerToLinenumbers: Dette felt angiver offsettet for linjenummerinformationen for sektionen, i bytes. Linjenummerinformationen bruges til debuggingsformål og giver information om kildekoden, der genererede sektionen.
-
NumberOfRelocations: Dette felt angiver antallet af relokationsposter for sektionen. En relokationspost er en optegnelse, der beskriver, hvordan man retter en adresse inden i sektionen, så den kan løses korrekt, når filen indlæses i hukommelsen.
-
NumberOfLinenumbers: Dette felt angiver antallet af linjenummerposter for sektionen. En linjenummerpost er en optegnelse, der giver information om kildekoden, der genererede sektionen, og bruges til debuggingsformål.
-
Characteristics: Dette felt er et sæt flag, der angiver attributterne for sektionen. Nogle af de almindelige flag, der bruges til sektioner, er: IMAGE_SCN_CNT_CODE for at indikere, at sektionen indeholder eksekverbar kode, IMAGE_SCN_CNT_INITIALIZED_DATA for at indikere, at sektionen indeholder initialiseret data, IMAGE_SCN_CNT_UNINITIALIZED_DATA for at indikere, at sektionen indeholder uinitialiseret data, IMAGE_SCN_MEM_EXECUTE for at indikere, at sektionen kan eksekveres, IMAGE_SCN_MEM_READ for at indikere, at sektionen kan læses, og IMAGE_SCN_MEM_WRITE for at indikere, at der kan skrives til sektionen.
Disse felter bruges af operativsystemet og andre programmer til at håndtere hukommelseslayoutet af filen og til at lokalisere og tilgå specifikke dele af filen, såsom kode- eller datasektionerne.
VIGTIGT: I sammenhængen med IMAGE_NT_HEADER-strukturen, som bruges i det Portable Executable (PE) filformat, henviser VirtualAddress- og PhysicalAddress-felterne til forskellige ting.
-
VirtualAddress-feltet bruges til at specificere den virtuelle adresse, hvor sektionen, der indeholder IMAGE_NT_HEADER-strukturen, indlæses i hukommelsen ved kørselstidspunktet. Denne adresse er relativ til basisadressen for processen og bruges af programmet til at få adgang til sektionens data.
-
PhysicalAddress-feltet bruges til at specificere filoffsettet for sektionen, der indeholder IMAGE_NT_HEADER-strukturen i PE-filen. Det bruges af operativsystemet til at lokalisere sektionens data i filen, når den indlæses i hukommelsen.
Alle headerfelter og offsets for IMAGE_NT_HEADER er defineret for hukommelsen og opererer på virtuelle adresser. Hvis du skal ændre noget felt på disken, skal du konvertere den virtuelle adresse til en fysisk adresse ved hjælp af rva2offset-funktionen i koden nedenfor.
Sammenfattende bruges VirtualAddress af programmet til at få adgang til sektionen i hukommelsen, og PhysicalAddress bruges af operativsystemet til at lokalisere sektionen i filen.
IMPORT
Når et program kompileres, genererer kompileren objektfiler, der indeholder maskinkoden for programmets funktioner. Dog kan objektfilerne mangle nogle af de oplysninger, der er nødvendige for, at programmet kan køre. For eksempel kan objektfilerne indeholde kald til funktioner, der ikke er defineret i programmet, men som i stedet leveres af eksterne biblioteker.
Det er her, importtabellen kommer ind i billedet. Importtabellen opregner de eksterne afhængigheder af programmet og de funktioner, som programmet skal importere fra disse afhængigheder. Den dynamiske linker bruger denne information ved kørselstidspunktet til at løse adresserne på de importerede funktioner og koble dem ind i programmet.
For eksempel, betragt et program, der bruger funktioner fra Windows-operativsystemet. Programmet kan indeholde kald til MessageBox-funktionen fra user32.dll-biblioteket, som viser en meddelelsesboks på skærmen. For at løse adressen på MessageBox-funktionen skal programmet inkludere en import for user32.dll i sin importtabel.
Ligeledes, hvis et program skal bruge funktioner fra et tredjepartsbibliotek, skal det inkludere en import for dette bibliotek i sin importtabel. For eksempel ville et program, der bruger funktionerne fra OpenSSL-biblioteket, inkludere en import for libssl.dll-biblioteket i sin importtabel.
IMAGE_IMPORT_DIRECTORY
MAGE_IMPORT_DIRECTORY er en datastruktur, der anvendes af Windows-operativsystemet til at importere funktioner og data fra dynamisk-linkede biblioteker (DLL'er) til en bærbar eksekverbar (PE) fil. Den er en del af IMAGE_DATA_DIRECTORY, som er en tabel over datastrukturer, der er gemt i IMAGE_OPTIONAL_HEADER i en PE-fil.
IMAGE_IMPORT_DIRECTORY bruges af Windows-loaderen til at løse de importerede funktioner og data, som bruges af PE-filen. Dette gøres ved at kortlægge adresserne på de importerede funktioner og data til adresserne på de tilsvarende funktioner og data i DLL'erne. Dette gør det muligt for PE-filen at bruge funktionerne og dataene fra DLL'erne, som om de var en del af selve PE-filen.
IMAGE_IMPORT_DIRECTORY består af en række IMAGE_IMPORT_DESCRIPTOR-strukturer, hvor hver af dem beskriver en enkelt DLL, der importeres af PE-filen. Hver IMAGE_IMPORT_DESCRIPTOR-struktur indeholder følgende felter:
-
OriginalFirstThunk: en pointer til en tabel over importerede funktioner.
-
TimeDateStamp: dato og tid for, hvornår DLL'en sidst blev opdateret.
-
ForwarderChain: en kæde af videresendte importerede funktioner.
-
Name: navnet på DLL'en som en null-termineret streng.
-
FirstThunk: en pointer til en tabel over importerede funktioner, der er bundet til DLL'en.
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date ime stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk-tabel (eller FirstThunk, hvis OriginalFirstThunk er 0)
Strenge peges på af tabel over forskydninger (OriginalFirstThunk-tabel eller FirstThunk, hvis OriginalFirstThunk er 0)
HVORDAN VIRKER DET?
Importmekanismen implementeret af Microsoft er kompakt og smuk!
Adresserne på alle funktioner fra tredjepartsbiblioteker (inklusiv Windows-systembiblioteker) som applikationen bruger, gemmes i en speciel tabel - importtabellen. Denne tabel bliver fyldt, når modulet indlæses (vi vil tale om andre mekanismer til at fylde import senere).
Desuden, hver gang en funktion kaldes fra et tredjepartsbibliotek, genererer kompilatoren normalt følgende kode:
call dword ptr [__cell_with_address_of_function] // for x86 architecture
call qword ptr [__cell_with_address_of_function] // for x64 architecture
Således, for at kunne kalde en funktion fra et bibliotek, behøver systemets loader kun at skrive adressen på denne funktion én gang på ét sted i billedet.
С++ PARSER
Og nu vil vi skrive den simpleste parser (kompatibel med x86 og x64) af importtabellen for den eksekverbare fil!
#include "stdafx.h"
/*
*
* Copyright (C) 2022, SToFU Systems S.L.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
namespace ntpe
{
static constexpr uint64_t g_kRvaError = -1;
// These types is defined in NTPEParser.h
// typedef std::map< std::string, std::set< std::string >> IMPORT_LIST;
// typedef std::vector< IMAGE_SECTION_HEADER > SECTIONS_LIST;
//**********************************************************************************
// FUNCTION: alignUp(DWORD value, DWORD align)
//
// ARGS:
// DWORD value - value to align.
// DWORD align - alignment.
//
// DESCRIPTION:
// Aligns argument value with the given alignment.
//
// Documentation links:
// Alignment: https://learn.microsoft.com/en-us/cpp/cpp/alignment-cpp-declarations?view=msvc-170
//
// RETURN VALUE:
// DWORD aligned value.
//
//**********************************************************************************
DWORD alignUp(DWORD value, DWORD align)
{
DWORD mod = value % align;
return value + (mod ? (align - mod) : 0);
};
//**********************************************************************************
// FUNCTION: rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
// DWORD rva - relative virtual address.
//
// DESCRIPTION:
// Parse RVA (relative virtual address) to offset.
//
// RETURN VALUE:
// int64_t offset.
// g_kRvaError (-1) in case of error.
//
//**********************************************************************************
int64_t rva2offset(IMAGE_NTPE_DATA& ntpe, DWORD rva)
{
/* retrieve first section */
try
{
/* if rva is inside MZ header */
PIMAGE_SECTION_HEADER sec = ntpe.sectionDirectories;
if (!ntpe.fileHeader->NumberOfSections || rva < sec->VirtualAddress)
return rva;
/* walk on sections */
for (uint32_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++, sec++)
{
/* count section end and allign it after each iteration */
DWORD secEnd = ntpe::alignUp(sec->Misc.VirtualSize, ntpe.SecAlign) + sec->VirtualAddress;
if (sec->VirtualAddress <= rva && secEnd > rva)
return rva - sec->VirtualAddress + sec->PointerToRawData;
};
}
catch (std::exception&)
{
}
return g_kRvaError;
};
//**********************************************************************************
// FUNCTION: getNTPEData(char* fileMapBase)
//
// ARGS:
// char* fileMapBase - the starting address of the mapped file.
//
// DESCRIPTION:
// Parses following data from mapped PE file.
//
// Documentation links:
// PE format structure: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
//
// RETURN VALUE:
// std::optional< IMAGE_NTPE_DATA >.
// std::nullopt in case of error.
//
//**********************************************************************************
#define initNTPE(HeaderType, cellSize) \
{ \
char* ntstdHeader = (char*)fileHeader + sizeof(IMAGE_FILE_HEADER); \
HeaderType* optHeader = (HeaderType*)ntstdHeader; \
data.sectionDirectories = (PIMAGE_SECTION_HEADER)(ntstdHeader + sizeof(HeaderType)); \
data.SecAlign = optHeader->SectionAlignment; \
data.dataDirectories = optHeader->DataDirectory; \
data.CellSize = cellSize; \
}
std::optional< IMAGE_NTPE_DATA > getNTPEData(char* fileMapBase, uint64_t fileSize)
{
try
{
/* PIMAGE_DOS_HEADER from starting address of the mapped view*/
PIMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER*)fileMapBase;
/* return std::nullopt in case of no IMAGE_DOS_SIGNATUR signature */
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE)
return std::nullopt;
/* PE signature adress from base address + offset of the PE header relative to the beginning of the file */
PDWORD peSignature = (PDWORD)(fileMapBase + dosHeader->e_lfanew);
if ((char*)peSignature <= fileMapBase || (char*)peSignature - fileMapBase >= fileSize)
return std::nullopt;
/* return std::nullopt in case of no PE signature */
if (*peSignature != IMAGE_NT_SIGNATURE)
return std::nullopt;
/* file header address from PE signature address */
PIMAGE_FILE_HEADER fileHeader = (PIMAGE_FILE_HEADER)(peSignature + 1);
if (fileHeader->Machine != IMAGE_FILE_MACHINE_I386 &&
fileHeader->Machine != IMAGE_FILE_MACHINE_AMD64)
return std::nullopt;
/* result IMAGE_NTPE_DATA structure with info from PE file */
IMAGE_NTPE_DATA data = {};
/* base address and File header address assignment */
data.fileBase = fileMapBase;
data.fileHeader = fileHeader;
/* addresses of PIMAGE_SECTION_HEADER, PIMAGE_DATA_DIRECTORIES, SectionAlignment, CellSize depending on processor architecture */
switch (fileHeader->Machine)
{
case IMAGE_FILE_MACHINE_I386:
initNTPE(IMAGE_OPTIONAL_HEADER32, 4);
return data;
case IMAGE_FILE_MACHINE_AMD64:
initNTPE(IMAGE_OPTIONAL_HEADER64, 8);
return data;
}
}
catch (std::exception&)
{
}
return std::nullopt;
}
//**********************************************************************************
// FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
//
// DESCRIPTION:
// Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions.
// Map key: loaded dll's names.
// Map value: set of imported functions names.
//
// Documentation links:
// Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
//
// RETURN VALUE:
// std::optional< IMPORT_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< IMPORT_LIST > getImportList(IMAGE_NTPE_DATA& ntpe)
{
try
{
/* if no imaage import directory in file returns std::nullopt */
if (ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
return std::nullopt;
IMPORT_LIST result;
/* import table offset */
DWORD impOffset = rva2offset(ntpe, ntpe.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
/* imoprt table descriptor from import table offset + file base adress */
PIMAGE_IMPORT_DESCRIPTOR impTable = (PIMAGE_IMPORT_DESCRIPTOR)(impOffset + ntpe.fileBase);
/* while names in import table */
while (impTable->Name != 0)
{
/* pointer to DLL name from offset of current section name + file base adress */
std::string modname = rva2offset(ntpe, impTable->Name) + ntpe.fileBase;
std::transform(modname.begin(), modname.end(), modname.begin(), ::toupper);
/* start adress of names in look up table from import table name RVA */
char* cell = ntpe.fileBase + ((impTable->OriginalFirstThunk) ? rva2offset(ntpe, impTable->OriginalFirstThunk) : rva2offset(ntpe, impTable->FirstThunk));
/* while names in look up table */
for (;; cell += ntpe.CellSize)
{
int64_t rva = 0;
/* break if rva = 0 */
memcpy(&rva, cell, ntpe.CellSize);
if (!rva)
break;
/* if rva > 0 function was imported by name. if rva < 0 function was imported by ordinall */
if (rva > 0)
result[modname].emplace(ntpe.fileBase + rva2offset(ntpe, rva) + 2);
else
result[modname].emplace(std::string("#ord: ") + std::to_string(rva & 0xFFFF));
};
impTable++;
};
return result;
}
catch (std::exception&)
{
return std::nullopt;
}
};
//**********************************************************************************
// FUNCTION: getImportList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// std::wstring_view filePath - path to file.
//
// DESCRIPTION:
// Retrieves IMPORT_LIST(std::map< std::string, std::set< std::string >>) with all loaded into PE libraries names and imported functions bu path.
// Map key: loaded dll's names.
// Map value: set of imported functions names.
//
// Documentation links:
// Import Directory Table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-directory-table
//
// RETURN VALUE:
// std::optional< IMPORT_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< IMPORT_LIST > getImportList(std::wstring_view filePath)
{
std::vector< char > buffer;
/* obtain base address of mapped file from tools::readFile function */
bool result = tools::readFile(filePath, buffer);
/* return nullopt if readFile failes or obtained buffer is empty */
if (!result || buffer.empty())
return std::nullopt;
/* get IMAGE_NTPE_DATA from base address of mapped file */
std::optional< IMAGE_NTPE_DATA > ntpe = getNTPEData(buffer.data(), buffer.size());
if (!ntpe)
return std::nullopt;
/* return result of overloaded getImportList function with IMAGE_NTPE_DATA as argument */
return getImportList(*ntpe);
}
//**********************************************************************************
// FUNCTION: getSectionsList(IMAGE_NTPE_DATA& ntpe)
//
// ARGS:
// IMAGE_NTPE_DATA& ntpe - data from PE file.
//
// DESCRIPTION:
// Retrieves SECTIONS_LIST from IMAGE_NTPE_DATA.
// SECTIONS_LIST - vector of sections headers from portable executable file.
// Sections names exmaple: .data, .code, .src
//
// Documentation links:
// IMAGE_SECTION_HEADER: https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header
// Section Table (Section Headers): https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers
//
// RETURN VALUE:
// std::optional< SECTIONS_LIST >.
// std::nullopt in case of error.
//
//**********************************************************************************
std::optional< SECTIONS_LIST > getSectionsList(IMAGE_NTPE_DATA& ntpe)
{
try
{
/* result vector of section directories */
SECTIONS_LIST result;
/* iterations through all image section headers poiners in IMAGE_NTPE_DATA structure */
for (uint64_t sectionIndex = 0; sectionIndex < ntpe.fileHeader->NumberOfSections; sectionIndex++)
{
/* pushing IMAGE_SECTION_HEADER from iamge section headers */
result.push_back(ntpe.sectionDirectories[sectionIndex]);
}
return result;
}
catch (std::exception&)
{
}
/* returns nullopt in case of error */
return std::nullopt;
}
}
Du kan finde koden til hele projektet på vores GitHub:
https://github.com/SToFU-Systems/DSAVE
Liste over anvendte værktøjer
- PE Tools: https://github.com/petoolse/petools Dette er et open-source værktøj til manipulation af header PE-felter. Understøtter x86 og x64 filer.
- WinDbg: https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools Microsofts system debugger. Uundværlig i arbejdet for en systemprogrammør for Windows OS.
- x64Dbg: https://x64dbg.com En simpel, letvægts open-source x64/x86 debugger for Windows.
- WinHex: http://www.winhex.com/winhex/hex-editor.html WinHex er en universel hex-editor, særligt nyttig inden for computerforensik, data recovery og redigering af data på lavt niveau.
HVAD ER NÆSTE?
Vi værdsætter jeres støtte og ser frem til jeres fortsatte engagement i vores fællesskab.
I den næste artikel vil vi sammen med jer skrive fuzzy hashing-modulet og berøre spørgsmålet om sorte og hvide lister og den simpleste importtabel-analysator.
Eventuelle spørgsmål fra artiklens forfattere kan sendes til e-mail: articles@stofu.io
Tak for jeres opmærksomhed og hav en god dag!