logo

INNLEDNING TIL PE-FILRESSURSER

Hilsen, våre kjære lesere!

I denne artikkelen vil vi fortelle deg om en viktig og interessant del av PE-filer: PE IMAGE RESOURCES (data knyttet til programmet etter dets kompilering). Vi vil dykke ned i de interne strukturene av ressursstreet og skrive vår egen native ressursparser uten å bruke WinAPI.

Ressurser i PE (Portable Executable)-filer er innebygde data som utgjør en integrert del av applikasjonen. De inkluderer en rekke elementer som bilder, ikoner, tekststrenger, dialogbokser, fonter. Disse ressursene er integrert direkte inn i den kjørbare filen, noe som sikrer deres tilgjengelighet for applikasjonen under dens utførelse. Det er viktig å merke seg at ressurser som bilder, tekster og lyder ofte legges til programmet etter at det har blitt kompilert. Denne tilnærmingen har flere betydelige fordeler, noe som gjør utviklingsprosessen mer fleksibel og effektiv.

Forestill deg at du er en del av et utviklingsteam som jobber med å skape en ny applikasjon. Teamet ditt består av programmerere, designere og innholdsforvaltere. Hver av dere bidrar til å skape noe unikt og nyttig.

I begynnelsen av prosjektet fokuserer programmerere på å skrive og teste kode. De skaper rammeverket for applikasjonen, og sørger for at alle funksjoner fungerer korrekt. Samtidig jobber designere og innholdsforvaltere med å skape ressurser - bilder, lyder, tekster for grensesnittet. Dette parallelle arbeidet lar teamet gå fremover raskt og effektivt.

Når hoveddelen av programmeringen er fullført, er det på tide å integrere ressursene i applikasjonen. Dette gjøres ved bruk av spesialverktøy som tillater å legge til ressurser i en allerede kompilert applikasjon uten behov for rekompilering. Dette er svært praktisk, spesielt hvis du trenger å gjøre endringer eller oppdatere ressurser - det er ikke nødvendig å rekompilere hele prosjektet.

En av de viktigste aspektene ved denne prosessen er lokalisering. Takket være separasjonen av ressurser fra hovedkoden, blir lokalisering av applikasjonen mye enklere. Grensesnitttekster, feilmeldinger kan enkelt oversettes og erstattes uten å forstyrre hovedkoden. Dette gjør det mulig for applikasjonen å tilpasses forskjellige språkmarkeder, noe som gjør den tilgjengelig og forståelig for et bredt spekter av brukere over hele verden.
 

EKSEMPLER PÅ RESSURSBRUK

  • Applikasjonsikoner: Ikoner vist i Windows Utforsker eller på oppgavelinjen gir en visuell representasjon av applikasjonen.
  • Dialogbokser: Definisjoner av dialogbokser som applikasjonen viser for brukerinteraksjon, som innstillinger eller advarselsvinduer.
  • Menyer: Menystrukturer som brukes i brukergrensesnittet gir navigasjon og funksjonalitet.
  • Strenger: Lokaliserte strenger brukt til å vise tekst i applikasjonen, inkludert feilmeldinger, verktøytips og andre brukergrensesnitt.
  • Lyder: Lydfiler som kan spilles av applikasjonen i visse situasjoner, som lydvarsler.
  • Markører: Grafiske markører brukt for å samhandle med brukergrensesnittet, som piler, pekere eller animerte markører.
  • Versjonsinformasjon (Version Info): Inneholder informasjon om applikasjonens versjon, opphavsrett, produktnavn og andre versjonsrelaterte data.
  • Manifest: En XML-fil som inneholder informasjon om applikasjonens konfigurasjon, inkludert krav til Windows-versjon og sikkerhetsinnstillinger.
  • RC_DATA: Vilkårlige data definert av utvikleren, som kan inkludere binærdata, konfigurasjonsfiler eller andre applikasjonsspesifikke ressurser.

HVORDAN VISE OG REDIGERE RESSURSER I EN PORTABELT UTFØRBAR FIL?

Kan vi se og redigere ressurser i et kompilert program? Absolutt! Alt du trenger er det riktige verktøyet. Det finnes verktøy som tilbyr et bredt spekter av muligheter for å arbeide med ressurser i kompilerte eksekverbare filer, inkludert visning, redigering, tillegging, og sletting av ressurser.

Her er en liste over ressursredigerere som kan brukes til å vise eller redigere ressurser i allerede kompilerte eksekverbare filer:

  • Resource Hacker: Dette er en ressursredigerer for 32-bit og 64-bit Windows-applikasjoner. Den lar deg se og redigere ressurser i kjørbare filer (.exe, .dll, .scr, osv.) og kompilerte ressursbiblioteker (.res, .mui). Resource Hacker
  • ResEdit: En gratis ressursredigerer for Win32-programmer. Egnet for arbeid med dialoger, ikoner, versjonsinformasjon og andre typer ressurser. ResEdit
  • Resource Tuner: En ressursredigerer som lar deg åpne kjørbare filer med problemer og redigere skjulte data som andre redigerere rett og slett ikke ser. Resource Tuner
  • Resource Builder: En kraftig, fullfunksjons ressursredigerer for Windows. Lar deg opprette, redigere og kompilere ressursfiler (.RC, .RES og andre), samt redigere ressurser i kompilerte kjørbare filer. Resource Builder
  • Visual Studio: Gir en ressursredigerer som lar deg legge til, slette og modifisere ressurser. Visual Studio
  • Resource Tuner Console: Et kraftig kommandolinjeverktøy for redigering av ressurser, ideelt for bruk i batch (.bat) filer. Resource Tuner Console
  • Qt Centre: Lar deg redigere ressursfiler av kompilerte kjørbare filer ved bruk av Qt. Qt Centre

La oss se nærmere på det første verktøyet på listen: Resource Hacker.

Dette verktøyet lar deg ikke bare se og trekke ut ressurser fra en kjørbar fil, men også redigere dem!

Disse ressursene, som kan inkludere ikoner, menyer, dialogbokser og andre typer data, er vanligvis plassert i en spesifikk del av PE-filen kjent som .rsrc (ressursseksjonen). Det er imidlertid viktig å merke seg at dette ikke er en streng regel og unntak kan forekomme.

En vesentlig del av navigeringen og tilgangen til disse ressursene i en PE-fil er IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES]. Denne katalogoppføringen er en del av PE-filens valgfrie topptekst, spesifikt inne i rekken av datakataloger. Den fungerer som en peker eller referanse til ressursene i bildet. IMAGE_DIRECTORY_ENTRY_RESOURCES gir informasjon om plasseringen (som den relative virtuelle adressen) og størrelsen på ressursdataene.

 

RESSURSSTRUKTUR I PORTABLE EXECUTABLE-FILER

Generelt Oversikt

La oss ta en detaljert titt på strukturene som brukes i ressursdelen av en PE (Portable Executable)-fil. Ressursdelen i Windows PE-filer har en unik tre-nivå hierarkisk trestruktur. Dette treet brukes til å organisere og få tilgang til ressurser som ikoner, markører, strenger, dialoger og andre. Her er hvordan det er strukturert:

Nivå 1: Ressurstyper

På toppnivået av treet er ressurstypene. Hver ressurstype kan identifiseres enten ved et numerisk identifikator (ID) eller ved et strengnavn.

Nivå 2: Ressursnavn

På det andre nivået har hver ressurstype sine egne navn eller identifikatorer. Dette lar deg ha flere ressurser av samme type, som flere ikoner eller rader.

Nivå 3: Ressursspråk

På det tredje nivået har hver ressurs varianter for ulike språklokaliseringer. Dette gjør det mulig for samme ressurs, slik som en dialog, å bli lokalisert på forskjellige språk.

Datastrukturer

Følgende datastrukturer brukes for å representere denne hierarkien:

  • IMAGE_RESOURCE_DIRECTORY: Denne strukturen representerer en header for hvert nivå av treet og inneholder generell informasjon om oppføringene på det nivået.
  • IMAGE_RESOURCE_DIRECTORY_ENTRY: Dette er elementer som enten kan være underkataloger (som peker til et annet IMAGE_RESOURCE_DIRECTORY) eller de endelige bladene på treet, som peker til selve ressursdataene.
  • IMAGE_RESOURCE_DATA_ENTRY: Denne strukturen peker på selve ressursdataene og inneholder dens størrelse og forskyvning.

Visualiseringen av ressurstreet kan se slik ut:

Root (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress)
|
+-- Type (RT_ICON, RT_STRING, ...)
    |
    +-- Name (ID or String)
        |
        +-- Language (Locale ID)
            |
            +-- Data (Actual resource data)
IMAGE_RESOURCE_DIRECTORY
|
|-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Resource Types)
|   |-- IMAGE_RESOURCE_DIRECTORY (Resource names)
|   |   |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Names)
|   |   |   |-- IMAGE_RESOURCE_DIRECTORY (Languages)
|   |   |   |   |-- IMAGE_RESOURCE_DIRECTORY_ENTRY (Languages)
|   |   |   |   |   |-- IMAGE_RESOURCE_DATA_ENTRY (Resource data)

Hver node i dette treet representerer en IMAGE_RESOURCE_DIRECTORY, og bladene er IMAGE_RESOURCE_DATA_ENTRIES, som peker direkte til ressursdataene. Når man manuelt analyserer ressurser, må en utvikler gå gjennom dette treet, starte fra roten, og sekvensielt navigere alle nivåer for å finne de nødvendige dataene.

 

IMAGE_RESOURCE_DIRECTORY

Denne strukturen fungerer som en overskrift for hvert nivå av ressurs-treet og inneholder informasjon om oppføringene på det nivået.

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    WORD    NumberOfNamedEntries;
    WORD    NumberOfIdEntries;
    //  IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
  • Characteristics: Vanligvis ubrukt og satt til 0. 
  • TimeDateStamp: Tidsstempel for opprettelsen av ressursen. 
  • MajorVersion and MinorVersion: Versjonen av ressurskatalogen.
  • NumberOfNamedEntries: Antall ressursoppføringer med navn.
  • NumberOfIdEntries: Antall ressursoppføringer med numeriske identifikatorer.

 

IMAGE_RESOURCE_DIRECTORY_ENTRY

Elementer som kan være enten underkataloger eller de siste bladene på treet.

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    union {
        struct {
            DWORD NameOffset:31;
            DWORD NameIsString:1;
        };
        DWORD   Name;
        WORD    Id;
    };
    union {
        DWORD   OffsetToData;
        struct {
            DWORD   OffsetToDirectory:31;
            DWORD   DataIsDirectory:1;
        };
    };
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
  • Navn: Hvis NameIsString er satt til 1, inneholder dette feltet en forskyvning som peker til en UNICODE-streng som representerer navnet på ressursen. Hvis NameIsString er satt til 0, brukes Id-feltet til å identifisere ressursen med et numerisk identifikator.
  • OffsetToData: Hvis DataIsDirectory er satt til 1, inneholder dette feltet en forskyvning som peker til en annen IMAGE_RESOURCE_DIRECTORY (dvs. en underkatalog). Hvis DataIsDirectory er satt til 0, peker denne forskyvningen til en IMAGE_RESOURCE_DATA_ENTRY.

 

IMAGE_RESOURCE_DATA_ENTRY

Denne strukturen peker på de faktiske dataene til ressursen.

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  • OffsetToData: Avstanden fra begynnelsen av ressursseksjonen til ressursdataene.
  • Size: Størrelsen på ressursdataene i byte.
  • CodePage: Tegnsettet som brukes for koding av ressursdataene.
  • Reserved: Reservert; vanligvis satt til 0.

VIKTIG! Offsets i IMAGE_RESOURCE_DIRECTORY og IMAGE_RESOURCE_DIRECTORY_ENTRY er beregnet fra starten av ressursene (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress), og kun offsetene i IMAGE_RESOURCE_DATA_ENTRY er beregnet fra starten av basisbildet!

 

LA OSS SKRIVE EN INNFØDT RESSURSPARSER!

Det er på tide å skrive vår egen native ressursparser uten å bruke WinAPI! Manuell parsing av ressurser, i stedet for å bruke Windows API-funksjoner som EnumResourceTypes eller EnumResourceNames, har flere fordeler, spesielt i sammenheng med sikkerhetsanalyse og antivirus-skanning:

  • Sikkerhet: API-funksjoner som EnumResourceTypes og EnumResourceNames krever at den kjørbare filen lastes inn i prosessens adresseområde, noe som kan føre til kjøring av skadelig kode hvis filen inneholder virus eller trojanere. Manuell parsing av ressurser unngår denne risikoen.
  • Plattformuavhengighet: Manuell parsing av ressurser avhenger ikke av operativsystemversjonen og dens WinAPI, noe som gjør det til en mer universell løsning.
  • Heuristisk analyse: Manuell parsing muliggjør anvendelse av komplekse heuristikker og deteksjonsalgoritmer som kan være nødvendig for å identifisere nye eller ukjente trusler.
  • Ytelse: Parsing kan optimeres for bedre ytelse sammenlignet med å bruke WinAPI, spesielt når mange filer skal skannes.
  • Kontroll: Med manuell parsing har analytikeren full kontroll over prosessen og kan finjustere den for spesifikke analysebehov, mens API-funksjoner gir begrenset kontroll og kan ikke avsløre alle aspekter av ressurser.
  • Beskyttelse: Skadelig programvare kan bruke ulike metoder for å unngå deteksjon, inkludert manipulering av ressurser på en måte som ikke blir oppdaget av standard-APIer. Manuell parsing tillater deteksjon av slike manipulasjoner.
  • Full tilgang: API-funksjoner gir kanskje ikke tilgang til alle ressurser, spesielt hvis de er korrupte eller med vilje endret. Manuell parsing tillater analyse av alle data uten de begrensningene som pålegges av APIet.
  • Feilhåndtering: Ved bruk av API-funksjoner kan feilhåndtering være begrenset, mens manuell parsing tillater mer fleksible svar på ikke-standard situasjoner og anomalier i filstrukturen.
struct ResourceInfo
{
    DWORD Size; 	// Size of the resource data
    PBYTE data; 	// Offset of the resource data from the beginning of the file

    union {
        WORD TypeID;                        // Resource type ID or
        PIMAGE_RESOURCE_DIR_STRING_U Type;  // resource type
    };

    union {
        WORD NameID;                        // Resource name ID or
        PIMAGE_RESOURCE_DIR_STRING_U Name;  // resource name
    };

    WORD  Language; // Language of the resource
};

std::optional> getAllResources(BYTE* pBase, uint64_t fileSize)
{
    IMAGE_RESOURCE_DIRECTORY* pTypesDirectory = nullptr;
    std::vector resources;

    try
    {
        //********************************************************
        //  parse PE header
        //********************************************************
        IMAGE_DOS_HEADER* pDosHeader = reinterpret_cast(pBase);
        IMAGE_NT_HEADERS* pNtHeaders = reinterpret_cast(pBase + pDosHeader->e_lfanew);

        // Verify that the PE signature is valid, indicating a valid PE file.
        if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
            return std::nullopt;

        // Depending on the machine type (32-bit or 64-bit), obtain the resource directory data.
        IMAGE_DATA_DIRECTORY resourceDirectory;
        switch (pNtHeaders->FileHeader.Machine)
        {
        case IMAGE_FILE_MACHINE_I386:
            resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
            break;
        case IMAGE_FILE_MACHINE_AMD64:
            resourceDirectory = reinterpret_cast(pNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
            break;
        default:
            return std::nullopt;
        };

        // If the resource directory is empty, exit as there are no resources.
        if (resourceDirectory.Size == 0)
            return std::nullopt;

        // Convert the RVA of the resources to a RAW offset
        uint64_t resourceBase = ntpe::RvaToOffset(pBase, resourceDirectory.VirtualAddress);
        IMAGE_RESOURCE_DIRECTORY* pResourceDir = reinterpret_cast(pBase + resourceBase);

        //********************************************************
        // Start parsing the resource directory
        //********************************************************
        // Iterate through type entries in the resource directory.
        // parse types
        pTypesDirectory = pResourceDir;
        IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pTypesDirectory + 1);

        for (uint64_t ti = 0; ti < pTypesDirectory->NumberOfNamedEntries + pTypesDirectory->NumberOfIdEntries; ti++)
        {
            // parse names
            IMAGE_RESOURCE_DIRECTORY_ENTRY* pTypeEntry = &pTypeEntries[ti];
            IMAGE_RESOURCE_DIRECTORY* pNamesDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pTypeEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
            for (uint64_t ni = 0; ni < pNamesDirectory->NumberOfNamedEntries + pNamesDirectory->NumberOfIdEntries; ni++)
            {
                //  parse langs
                IMAGE_RESOURCE_DIRECTORY_ENTRY* pNamesEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pNamesDirectory + 1);
                IMAGE_RESOURCE_DIRECTORY_ENTRY* pNameEntry = &pNamesEntries[ni];
                IMAGE_RESOURCE_DIRECTORY* pLangsDirectory = (IMAGE_RESOURCE_DIRECTORY*)(pBase + (pNameEntry->OffsetToDirectory & 0x7FFFFFFF) + resourceBase);
                for (uint64_t li = 0; li < pLangsDirectory->NumberOfNamedEntries + pLangsDirectory->NumberOfIdEntries; li++)
                {
                    //  parse data
                    IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangsEntries = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)(pLangsDirectory + 1);
                    IMAGE_RESOURCE_DIRECTORY_ENTRY* pLangEntry = &pLangsEntries[li];
                    IMAGE_RESOURCE_DATA_ENTRY* pDataEntry = (IMAGE_RESOURCE_DATA_ENTRY*)(pBase + resourceBase + pLangEntry->OffsetToData);

                    // Save the resource information in a structured format.
                    ResourceInfo entry = {};
                    entry.Language = pLangsEntries->Id;
                    entry.Size = pDataEntry->Size;
                    entry.Type = (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pTypeEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pTypeEntry->Id);
                    entry.Name = (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->NameIsString) ? (PIMAGE_RESOURCE_DIR_STRING_U)(pBase + pNameEntry->NameOffset + resourceBase) : (PIMAGE_RESOURCE_DIR_STRING_U)(pNameEntry->Id);
                    entry.data = ntpe::RvaToRaw(pBase, pDataEntry->OffsetToData);
                    resources.push_back(entry);
                }
            }
        }

        return resources;
    }
    catch (std::exception&)
    {
        return std::nullopt;
    };
}

Du kan finne koden til hele prosjektet på vår github:

https://github.com/SToFU-Systems/DSAVE

Ressursparseren returnerer en vektor med strukturer som inneholder pekere til ressurstyper, deres navn, og språkidentifikatoren for hver ressurs. I hver struktur er det en peker til ressursdataene. Hver struktur forblir gyldig så lenge den kjørbare filen vi leser og analyserer er i minnet. Dette er svært praktisk for å skrive ditt eget antivirus og skanne filer. Etter at filen er frigitt, blir pekerne ugyldige.

 

LISTE OVER BRUKTE VERKTØY

  1. PE Tools: https://github.com/petoolse/petools Dette er et åpen kildekode-verktøy for å manipulere header PE-felt. Støtter x86 og x64-filer.
  2. Resource Hacker: https://www.angusj.com/resourcehacker. Dette er en ressursredigerer for 32-bit og 64-bit Windows-applikasjoner. Den lar deg vise og redigere ressurser i eksekverbare filer (.exe, .dll, .scr osv.) og kompilerte ressursbiblioteker (.res, .mui).

 

KONKLUSJON

Og det var det, venner!

Vi har utforsket ressursene til PORTABLE_EXECUTABLE-filer i Windows-operativsystemet og skrevet vår egen, enkel, men ganske effektiv native ressursparser!

Vi setter pris på din støtte og ser frem til din fortsatte engasjement i vårt samfunn!

Eventuelle spørsmål til forfatterne av artikkelen kan sendes til e-posten: articles@stofu.io

Takk!