logo

INTRODUKTION TIL PE FILRESSOURCER

Hilsen, vores kære læsere!

I denne artikel vil vi fortælle dig om en vigtig og interessant del af PE-filer: PE IMAGE RESOURCES (data knyttet til programmet efter dets kompilering). Vi vil dykke ned i de interne strukturer af ressource-træet og skrive vores egen native ressource-parser uden at bruge WinAPI.

Ressourcer i PE (Portable Executable) filer er indlejrede data, der udgør en integreret del af applikationen. De omfatter en række elementer såsom billeder, ikoner, tekststrenge, dialogbokse, skrifttyper. Disse ressourcer er integreret direkte ind i den eksekverbare fil, hvilket sikrer deres tilgængelighed for applikationen under dens udførelse. Det er vigtigt at bemærke, at ressourcer såsom billeder, tekster og lyde ofte tilføjes til programmet, efter det er blevet kompileret. Denne tilgang har flere betydelige fordele, hvilket gør udviklingsprocessen mere fleksibel og effektiv.

Forestil dig, at du er en del af et udviklingsteam, der arbejder på at skabe en ny applikation. Dit team består af programmører, designere og indholdsforvaltere. Hver af jer bidrager til at skabe noget unikt og nyttigt.

I begyndelsen af projektet fokuserer programmørerne på at skrive og teste kode. De opretter rammerne for applikationen og sikrer, at alle funktioner fungerer korrekt. Samtidig arbejder designere og indholdsforvaltere på at skabe ressourcer - billeder, lyde, tekster til grænsefladen. Dette parallelle arbejde gør det muligt for teamet at skride hurtigt og effektivt frem.

Når hoveddelen af programmeringen er færdig, er det tid til at integrere ressourcerne i applikationen. Dette gøres ved hjælp af specielle værktøjer, der tillader tilføjelse af ressourcer til en allerede kompileret applikation uden behov for rekompilering. Dette er meget bekvemt, især hvis du har brug for at foretage ændringer eller opdatere ressourcer - der er ingen grund til at rekompilere hele projektet.

En af de centrale aspekter ved denne proces er lokalisering. Takket være adskillelsen af ressourcer fra hovedkoden, bliver det meget enklere at lokalisere applikationen. Interfacetekster, fejlmeddelelser kan nemt oversættes og udskiftes uden at forstyrre hovedkoden. Dette gør det muligt at tilpasse applikationen til forskellige sprogmarkeder, hvilket gør den tilgængelig og forståelig for et bredt udvalg af brugere rundt om i verden.
 

EKSEMPLER PÅ RESSOURCEANVENDELSE

  • Applikationsikoner:Ikoner, der vises i Windows Stifinder eller på proceslinjen, giver en visuel repræsentation af applikationen.
  • Dialogbokse:Definitioner af dialogbokse, som applikationen viser for brugerinteraktion, såsom indstillinger eller advarselsvinduer.
  • Menuer: Menustrukturer brugt i brugergrænsefladen giver navigation og funktionalitet.
  • Strenge: Lokaliserede strenge, der bruges til at vise tekst i applikationen, herunder fejlmeddelelser, værktøjstips og andre brugergrænseflader.
  • Lyde: Lydfiler, som kan afspilles af applikationen i visse situationer, som lydnotifikationer.
  • Cursors: Grafiske markører brugt til interaktion med brugergrænsefladen, såsom pile, pegepinde eller animerede markører.
  • Version Information (Versionsoplysninger):Indeholder oplysninger om applikationsversionen, ophavsret, produktnavn og andre versionsrelaterede data.
  • Manifest: En XML-fil, der indeholder oplysninger om applikationskonfigurationen, herunder krav til Windows-version og sikkerhedsindstillinger.
  • RC_DATA: Vilkårlige data defineret af udvikleren, som kan omfatte binære data, konfigurationsfiler eller andre applikationsspecifikke ressourcer.

HVORDAN VISER OG REDIGERER MAN RESSOURCER I EN BÆRBAR EKSEKVERBAR FIL?

Kan vi se og redigere ressourcer i en kompileret applikation? Absolut! Alt hvad du behøver er det rigtige værktøj. Der findes værktøjer, der tilbyder et bredt udvalg af funktioner til at arbejde med ressourcer i kompilerede eksekverbare filer, herunder visning, redigering, tilføjelse og sletning af ressourcer.

Her er en liste over ressourceeditorer, der kan bruges til at se eller redigere ressourcer i allerede kompilerede eksekverbare filer:

  • Resource Hacker: Dette er en ressourceeditor for 32-bit og 64-bit Windows-applikationer. Den giver dig mulighed for at se og redigere ressourcer i eksekverbare filer (.exe, .dll, .scr osv.) og kompilerede ressourcebiblioteker (.res, .mui). Resource Hacker
  • ResEdit: En gratis ressourceeditor til Win32-programmer. Egnet til arbejde med dialoger, ikoner, versionsinformationer og andre typer ressourcer. ResEdit
  • Resource Tuner: En ressourceeditor, der giver dig mulighed for at åbne eksekverbare filer med problemer og redigere skjulte data, som andre redaktører simpelthen ikke ser. Resource Tuner
  • Resource Builder: En kraftfuld, fuldt udstyret ressourceeditor til Windows. Giver dig mulighed for at oprette, redigere og kompilere ressourcefiler (.RC, .RES og andre), samt redigere ressourcer i kompilerede eksekverbare filer. Resource Builder
  • Visual Studio: Indeholder en ressourceeditor, der gør det muligt for dig at tilføje, slette og ændre ressourcer. Visual Studio
  • Resource Tuner Console: Et kraftfuldt kommandolinjeværktøj til redigering af ressourcer, ideelt til brug i batch (.bat) filer. Resource Tuner Console
  • Qt Centre: Gør det muligt for dig at redigere ressourcefiler af kompilerede eksekverbare filer ved hjælp af Qt. Qt Centre

Lad os tage et nærmere kig på det første værktøj på listen: Resource Hacker.

Dette værktøj giver dig ikke kun mulighed for at se og udtrække ressourcer fra en eksekverbar fil, men også at redigere dem!

Disse ressourcer, som kan omfatte ikoner, menuer, dialogbokse og andre typer data, er typisk placeret i en specifik sektion af PE-filen kendt som .rsrc (ressourceafsnittet). Det er dog vigtigt at bemærke, at dette ikke er en streng regel, og undtagelser kan forekomme.

En væsentlig del af navigation og adgang til disse ressourcer inden for en PE-fil erIMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES] Denne mappepost er en del af PE-filens valgfrie header, specifikt inden for datakatalogarrayet. Det fungerer som en pegere eller reference til ressourcerne i billedet. The IMAGE_DIRECTORY_ENTRY_RESOURCES giver information om placeringen (såsom den relative virtuelle adresse) og størrelsen af ressource dataene.

 

RESSOURCESTRUKTUR I BÆRBARE EKSEKVERBARE FILER

Generelt Overblik

Lad os se nærmere på de strukturer, der anvendes i ressourceafsnittet af en PE (Portable Executable) fil. Ressourceafsnittet i Windows PE-filer har en unik tre-niveau hierarkisk træstruktur. Dette træ bruges til at organisere og tilgå ressourcer som ikoner, cursorer, strenge, dialoger og andre. Her er hvordan det er struktureret:

Niveau 1: Ressourcetyper

Øverst i træet er ressourcetyperne. Hver ressourcetype kan identificeres enten ved et numerisk identifikator (ID) eller ved et strengnavn.

Niveau 2: Ressourcenavne

På andet niveau har hver ressourcetype sine egne navne eller identifikatorer. Dette giver dig mulighed for at have flere ressourcer af samme type, såsom flere ikoner eller rækker.

Niveau 3: Ressourcesprog

På det tredje niveau har hver ressource varianter for forskellige sproglige lokaliseringer. Dette gør det muligt for den samme ressource, såsom en dialog, at blive lokaliseret på forskellige sprog.

Datastrukturer

De følgende datastrukturer bruges til at repræsentere denne hierarki: 

  • IMAGE_RESOURCE_DIRECTORY: Denne struktur repræsenterer en overskrift for hvert niveau af træet og indeholder generelle oplysninger om posterne på det pågældende niveau.
  • IMAGE_RESOURCE_DIRECTORY_ENTRY: Dette er elementer, der enten kan være undermapper (der peger på en anden IMAGE_RESOURCE_DIRECTORY) eller de sidste blade på træet, der peger på de faktiske resourcedata. 
  • IMAGE_RESOURCE_DATA_ENTRY: Denne struktur peger på selve ressourcedataene og indeholder dens størrelse og forskydning.

Visualiseringen af ressource-træet kan se således ud:

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 knude i dette træ repræsenterer en IMAGE_RESOURCE_DIRECTORY, og bladene er IMAGE_RESOURCE_DATA_ENTRIES, som direkte peger på ressource dataene. Når man manuelt parser ressourcer, skal en udvikler gå igennem dette træ, startende fra roden, og sekventielt navigere alle niveauer for at finde de nødvendige data.

 

IMAGE_RESOURCE_DIRECTORY

Denne struktur fungerer som en overskrift for hvert niveau af ressource-træet og indeholder oplysninger om posterne på det pågældende niveau.

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;
  • Karakteristika: Typisk ubrugt og indstillet til 0. 
  • TidsDatoStempel: Tidsstempel for oprettelse af ressourcen. 
  • MajorVersion og MinorVersion:Versionen af ressourcebiblioteket.
  • AntalNavngivnePoster:Antallet af ressourceposter med navne.
  • NumberOfIdEntries:Antallet af ressourceposter med numeriske identifikatorer.

 

IMAGE_RESOURCE_DIRECTORY_ENTRY

Elementer, der kan være enten undermapper eller de endelige blade på træet.

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 sat til 1, indeholder dette felt en offset, der peger på en UNICODE-streng, der repræsenterer navnet på ressourcen. Hvis NameIsString er sat til 0, bruges Id-feltet til at identificere ressourcen ved et numerisk identifikator.
  • OffsetToData Hvis DataIsDirectory er sat til 1, indeholder dette felt en offset, der peger på en anden IMAGE_RESOURCE_DIRECTORY (dvs. en undermappe). Hvis DataIsDirectory er sat til 0, peger denne offset på en IMAGE_RESOURCE_DATA_ENTRY.

 

IMAGE_RESOURCE_DATA_ENTRY

Denne struktur peger på de faktiske data for ressourcen.

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  • OffsetToData: Forskydningen fra begyndelsen af ressourceafsnittet til ressourcedataene. 
  • Størrelse: Størrelsen af resursdataene i bytes. 
  • Kodeside: Kodesiden, der bruges til kodning af ressource data.
  • Reserveret: Reserveret; normalt indstillet til 0.

VIGTIGT! Forskud i IMAGE_RESOURCE_DIRECTORY og IMAGE_RESOURCE_DIRECTORY_ENTRY beregnes fra starten af ressourcerne  (IMAGE_DATA_DIRECTORY [IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress), og kun forskellene i  IMAGE_RESOURCE_DATA_ENTRY beregnes fra starten af basisbilledet!

 

LAD OS SKRIVE EN NATIV RESSOURCE-PARSER!

Det er tid til at skrive vores egen native ressource-parser uden at bruge WinAPI! Manuel parsing af ressourcer i stedet for at bruge Windows API-funktioner som EnumResourceTypesellerEnumResourceNames, har flere fordele, især inden for sikkerhedsanalyse og antivirus-scanning:

  • Sikkerhed: API-funktioner som EnumResourceTypes og EnumResourceNames kræver, at den eksekverbare fil indlæses i processens adresseområde, hvilket kan føre til eksekvering af ondsindet kode, hvis filen indeholder virusser eller trojanere. Manuel parsing af ressourcer undgår denne risiko. 
  • Platformuafhængighed: Manuel ressourceanalyse er ikke afhængig af operativsystemets version og dets WinAPI, hvilket gør det til en mere universel løsning. 
  • Heuristisk Analyse: Manuel parsing muliggør anvendelsen af komplekse heuristikker og detektionsalgoritmer, der kan være nødvendige for at identificere nye eller ukendte trusler.
  • Ydeevne: Parsing kan optimeres for bedre ydeevne sammenlignet med brugen af WinAPI, især når der scannes et stort antal filer. 
  • Kontrol Med manuel parsing har analytikeren fuld kontrol over processen og kan finjustere den til specifikke analysebehov, mens API-funktioner giver begrænset kontrol og måske ikke afslører alle aspekter af ressourcerne.
  • Beskyttelse Malware kan bruge forskellige metoder til at undgå opdagelse, herunder at manipulere ressourcer på en måde, så de ikke opdages af standard API`er. Manuel parsing muliggør opdagelse af sådanne manipulationer.
  • Fuld Adgang: API-funktioner giver muligvis ikke adgang til alle ressourcer, især hvis de er korrupte eller bevidst ændrede. Manuel parsing gør det muligt at analysere alle data uden de begrænsninger, som API`en pålægger.
  • Fejlhåndtering: Når du bruger API-funktioner, kan fejlhåndtering være begrænset, mens manuel parsing tillader mere fleksible reaktioner på ikke-standard situationer 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 finde koden til hele projektet på vores github:

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

Ressourceparseren returnerer en vektor med strukturer, der indeholder pegepinde til ressourcetyper, deres navne og sprogidentifikatoren for hver ressource. I hver struktur er der en pegepind til ressourcedataene. Hver struktur forbliver gyldig, så længe den eksekverbare fil vi læser og parserer er i hukommelsen. Dette er meget praktisk til at skrive dit eget antivirus og scanne filer. Efter filen er frigivet, bliver pegepindene ugyldige.

 

LISTE OVER BRUGTE VÆRKTØJER

  1. PE Værktøjer: https://github.com/petoolse/petools Dette er et open-source værktøj til at manipulere header PE felter. Understøtter x86 og x64 filer.
  2. Resource Hacker: https://www.angusj.com/resourcehacker. Dette er en ressourceeditor til 32-bit og 64-bit Windows-applikationer. Den giver dig mulighed for at se og redigere ressourcer i eksekverbare filer (.exe, .dll, .scr osv.) og kompilerede ressourcebiblioteker (.res, .mui).

 

KONKLUSION

Og det var det, venner! 

Vi har udforsket ressourcerne af BÆRBAR_UDFØRELIG filer af Windows-operativsystemet og skrev vores egen, enkle, men ret effektive native ressource parser!

Vi værdsætter din støtte og ser frem til din fortsatte engagement i vores fællesskab!

Eventuelle spørgsmål fra artiklens forfattere kan sendes til e-mailen: articles@stofu.io

Tak!