logo

INTRODUKTION TILL PE-FILRESURSER

Hälsningar, våra kära läsare!

I den här artikeln kommer vi att berätta om en viktig och intressant del av PE-filer: PE IMAGE RESOURCES (data länkad till programmet efter dess kompilering). Vi kommer att fördjupa oss i de interna strukturerna av resursträdet och skriva vår egen inhemska resursparsing utan att använda WinAPI.

Resurser i PE (Portable Executable)-filer är inbäddade data som utgör en integrerad del av applikationen. De inkluderar en mängd element såsom bilder, ikoner, textsträngar, dialogrutor, teckensnitt. Dessa resurser är integrerade direkt i den körbara filen, vilket säkerställer deras tillgänglighet för applikationen under dess körning. Det är viktigt att notera att resurser såsom bilder, texter och ljud ofta läggs till i programmet efter att det har kompilerats. Detta tillvägagångssätt har flera betydande fördelar, vilket gör utvecklingsprocessen mer flexibel och effektiv.

Föreställ dig att du är en del av ett utvecklingsteam som arbetar med att skapa en ny applikation. Ditt team består av programmerare, designers och innehållsansvariga. Var och en av er bidrar till att skapa något unikt och användbart.

I början av projektet fokuserar programmerare på att skriva och testa kod. De skapar ramverket för applikationen, med säkerställande att alla funktioner fungerar korrekt. Samtidigt arbetar designers och innehållsansvariga med att skapa resurser - bilder, ljud, texter för gränssnittet. Detta parallella arbete möjliggör att teamet kan gå framåt snabbt och effektivt.

När huvuddelen av programmeringen är avslutad är det dags att integrera resurserna i applikationen. Detta görs med hjälp av specialverktyg som möjliggör tillägg av resurser till en redan kompilerad applikation utan behov av omkompilering. Detta är mycket praktiskt, särskilt om du behöver göra ändringar eller uppdatera resurser - det finns inget behov av att kompilera om hela projektet.

En av de viktiga aspekterna av denna process är lokalisering. Tack vare separationen av resurser från huvudkoden, blir lokaliseringen av applikationen mycket enklare. Gränssnittstexter, felmeddelanden kan enkelt översättas och ersättas utan att störa huvudkoden. Detta möjliggör att applikationen kan anpassas för olika språkmarknader, vilket gör den tillgänglig och begriplig för en bred användargrupp över hela världen.
 

EXEMPEL PÅ RESURSANVÄNDNING

  • Applikationsikoner: Ikoner som visas i Windows Utforskare eller på aktivitetsfältet som ger en visuell representation av applikationen.
  • Dialogrutor: Definitioner av dialogrutor som applikationen visar för användarinteraktion, såsom inställnings- eller varningsfönster.
  • Menyer: Menystrukturer som används i användargränssnittet för navigering och funktionalitet.
  • Strängar: Lokaliserade strängar som används för att visa text i applikationen, inklusive felmeddelanden, verktygstips och andra användargränssnitt.
  • Ljud: Ljudfiler som kan spelas upp av applikationen i vissa situationer, som ljudnotifikationer.
  • Markörer: Grafiska markörer som används för interaktion med användargränssnittet, såsom pilar, pekare eller animerade markörer.
  • Versioninformation (Version Info): Innehåller information om applikationens version, upphovsrätt, produktnamn och annan versionsrelaterad data.
  • Manifest: En XML-fil som innehåller information om applikationens konfiguration, inklusive krav på Windowsversion och säkerhetsinställningar.
  • RC_DATA: Godtycklig data definierad av utvecklaren, vilket kan inkludera binärdata, konfigurationsfiler eller andra applikationsspecifika resurser.
     

HUR MAN VISAR OCH REDIGERAR RESURSER I EN PORTABEL EXEKVERBAR FIL?

Kan vi visa och redigera resurser i ett kompilerat program? Absolut! Allt du behöver är rätt verktyg. Det finns verktyg som erbjuder ett brett utbud av funktioner för att arbeta med resurser i kompilerade exekverbara filer, inklusive att visa, redigera, lägga till och ta bort resurser.

Här är en lista över resursredigerare som kan användas för att visa eller redigera resurser i redan kompilerade exekverbara filer:

  • Resource Hacker: Detta är en resursredigerare för 32-bitars och 64-bitars Windows-applikationer. Den möjliggör att du kan visa och redigera resurser i exekverbara filer (.exe, .dll, .scr, etc.) samt kompilerade resursbibliotek (.res, .mui). Resource Hacker
  • ResEdit: En gratis resursredigerare för Win32-program. Lämplig för arbete med dialoger, ikoner, versionsinformation och andra typer av resurser. ResEdit
  • Resource Tuner: En resursredigerare som tillåter dig att öppna exekverbara filer med problem och redigera dold data som andra redigerare helt enkelt inte ser. Resource Tuner
  • Resource Builder: En kraftfull, fullt utrustad resursredigerare för Windows. Tillåter dig att skapa, redigera och kompilera resursfiler (.RC, .RES och andra), samt redigera resurser i kompilerade exekverbara filer. Resource Builder
  • Visual Studio: Tillhandahåller en resursredigerare som låter dig lägga till, ta bort och modifiera resurser. Visual Studio
  • Resource Tuner Console: Ett kraftfullt kommandoradsverktyg för redigering av resurser, idealiskt för användning i batch-filer (.bat). Resource Tuner Console
  • Qt Centre: Gör det möjligt att redigera resursfiler av kompilerade exekverbara filer med hjälp av Qt. Qt Centre

Låt oss titta närmare på det första verktyget på listan: Resource Hacker.

Detta verktyg låter dig inte bara visa och extrahera resurser från en exekverbar fil, utan också att redigera dem!

Dessa resurser, som kan inkludera ikoner, menyer, dialogrutor och andra typer av data, är vanligtvis placerade i en specifik del av PE-filen som är känd som .rsrc (resurssektionen). Det är dock viktigt att notera att detta inte är en strikt regel och undantag kan förekomma.

En väsentlig aspekt av att navigera och komma åt dessa resurser i en PE-fil är IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES]. Denna katalogpost är en del av PE-filens valfria huvud, specifikt inom datakatalogernas array. Den fungerar som en pekare eller referens till resurserna i bilden. IMAGE_DIRECTORY_ENTRY_RESOURCES ger information om platsen (som den relativa virtuella adressen) och storleken på resursdatan.

 

RESURSSTRUKTUR I BÄRBARA EXEKVERBARA FILER

Allmän översikt

Låt oss ta en detaljerad titt på de strukturer som används i resursdelen av en PE (Portable Executable)-fil. Resursdelen i Windows PE-filer har en unik treskikts hierarkisk trädstruktur. Detta träd används för att organisera och få tillgång till resurser såsom ikoner, markörer, strängar, dialogrutor och andra. Så här är det strukturerat:

Nivå 1: Resurstyper

På det översta nivån av trädet finns resurstyperna. Varje resurstyp kan identifieras antingen genom ett numeriskt identifieringsnummer (ID) eller genom ett strängnamn.

Nivå 2: Resursnamn

På andra nivån har varje resurstyp sina egna namn eller identifierare. Detta gör att du kan ha flera resurser av samma typ, såsom flera ikoner eller rader.

Nivå 3: Resursspråk

På den tredje nivån har varje resurs varianter för olika språkanpassningar. Detta möjliggör att samma resurs, såsom en dialog, kan lokaliseras på olika språk.

Datastrukturer

Följande datastrukturer används för att representera denna hierarki:

  • IMAGE_RESOURCE_DIRECTORY: Denna struktur representerar en rubrik för varje nivå av trädet och innehåller allmän information om posterna på den nivån.
  • IMAGE_RESOURCE_DIRECTORY_ENTRY: Dessa är element som antingen kan vara underkataloger (som pekar på en annan IMAGE_RESOURCE_DIRECTORY) eller de slutliga bladen på trädet, som pekar på själva resursdatan.
  • IMAGE_RESOURCE_DATA_ENTRY: Denna struktur pekar på själva resursdatan och innehåller dess storlek och offset.

Visualiseringen av resursträdet kan se ut som följer:

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)

Varje nod i detta träd representerar en IMAGE_RESOURCE_DIRECTORY, och löven är IMAGE_RESOURCE_DATA_ENTRIES, som direkt pekar på resursdatan. När en utvecklare manuellt analyserar resurser måste denne gå igenom detta träd, börja från roten och sekventiellt navigera genom alla nivåer för att hitta nödvändig data.

 

IMAGE_RESOURCE_DIRECTORY

Denna struktur fungerar som en rubrik för varje nivå i resursträdet och innehåller information om posterna på den nivån.

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;
  • Egenskaper: Vanligtvis oanvända och sätts till 0.
  • TimeDateStamp: Tidsstämpeln för resursens skapande.
  • MajorVersion och MinorVersion: Versionen av resurskatalogen.
  • NumberOfNamedEntries: Antalet resursposter med namn.
  • NumberOfIdEntries: Antalet resursposter med numeriska identifierare.

 

IMAGE_RESOURCE_DIRECTORY_ENTRY

Element som kan vara antingen underkataloger eller de slutliga löven på trädet.

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;
  • Namn: Om NameIsString är satt till 1, innehåller detta fält en offset som pekar på en UNICODE-sträng som representerar resursens namn. Om NameIsString är satt till 0 används Id-fältet för att identifiera resursen med ett numeriskt identifierare.
  • OffsetToData: Om DataIsDirectory är satt till 1, innehåller detta fält en offset som pekar på en annan IMAGE_RESOURCE_DIRECTORY (dvs. en underkatalog). Om DataIsDirectory är satt till 0, pekar denna offset på en IMAGE_RESOURCE_DATA_ENTRY.

 

IMAGE_RESOURCE_DATA_ENTRY

Denna struktur pekar på den faktiska datan för resursen.

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  • OffsetToData: Offset från början av resurssektionen till resursdatan.
  • Size: Storleken på resursdatan i byte.
  • CodePage: Kodsidan som används för att koda resursdatan.
  • Reserved: Reserverad; vanligtvis satt till 0.

VIKTIGT! Offset i IMAGE_RESOURCE_DIRECTORY och IMAGE_RESOURCE_DIRECTORY_ENTRY beräknas från starten av resurserna (IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress), och endast offset i IMAGE_RESOURCE_DATA_ENTRY beräknas från starten av basbilden!

 

LÅT OSS SKRIVA EN INHEMSK RESURSPARSER!

Det är dags att skriva vår egen inbyggda resursparsare utan att använda WinAPI! Manuell tolkning av resurser, istället för att använda Windows API-funktioner som EnumResourceTypes eller EnumResourceNames, har flera fördelar, särskilt inom kontexten för säkerhetsanalys och virusskanning:

  • Säkerhet: API-funktioner som EnumResourceTypes och EnumResourceNames kräver att den exekverbara filen laddas in i processens adressutrymme, vilket kan leda till exekvering av skadlig kod om filen innehåller virus eller trojaner. Manuell tolkning av resurser undviker denna risk.
  • Plattformsoberoende: Manuell resursanalys är inte beroende av operativsystemets version och dess WinAPI, vilket gör det till en mer universell lösning.
  • Heuristisk analys: Manuell tolkning möjliggör tillämpning av komplexa heuristiker och detektionsalgoritmer som kan vara nödvändiga för att identifiera nya eller okända hot.
  • Prestanda: Tolkning kan optimeras för bättre prestanda jämfört med att använda WinAPI, särskilt vid skanning av ett stort antal filer.
  • Kontroll: Med manuell tolkning har analytikern full kontroll över processen och kan finjustera den för specifika analysbehov, medan API-funktioner ger begränsad kontroll och kanske inte avslöjar alla aspekter av resurser.
  • Skydd: Skadlig programvara kan använda olika metoder för att undvika upptäckt, inklusive att manipulera resurser på ett sätt så att de inte upptäcks av standard-APIer. Manuell tolkning möjliggör upptäckt av sådana manipulationer.
  • Fullständig åtkomst: API-funktioner kanske inte ger åtkomst till alla resurser, särskilt om de är skadade eller avsiktligt ändrade. Manuell tolkning tillåter analys av alla data utan de begränsningar som APIet påtvingar.
  • Felhantering: När man använder API-funktioner kan felhantering vara begränsad, medan manuell tolkning tillåter mer flexibla svar på icke-standard situationer och 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 hitta koden för hela projektet på vår github:

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

Resursparsaren returnerar en vektor med strukturer som innehåller pekare till resurstyper, deras namn och språkidentifieraren för varje resurs. I varje struktur finns det en pekare till resursdata. Varje struktur förblir giltig så länge som den exekverbara filen vi läste och tolkade finns i minnet. Detta är mycket praktiskt för att skriva ditt eget antivirus och skanna filer. Efter att filen släppts blir pekarna ogiltiga.

 

LISTA ÖVER ANVÄNDA VERKTYG

  1. PE Tools: https://github.com/petoolse/petools Det här är ett öppen källkodsverktyg för att manipulera PE-huvudfält. Stöder x86- och x64-filer.
  2. Resource Hacker: https://www.angusj.com/resourcehacker. Det här är en resursredigerare för 32-bitars och 64-bitars Windows-applikationer. Den möjliggör att du kan visa och redigera resurser i exekverbara filer (.exe, .dll, .scr, etc.) och kompilerade resursbibliotek (.res, .mui).

 

SLUTSATS

Och det var det, vänner!

Vi har utforskat resurserna i PORTABLE_EXECUTABLE-filer från Windows-operativsystemet och skrivit vår egen, enkel men ganska effektiv native-resursparser!

Vi uppskattar ditt stöd och ser fram emot ditt fortsatta engagemang i vår gemenskap!

Frågor till artikelförfattarna kan skickas till e-postadressen: articles@stofu.io

Tack!