logo

PE 파일 리소스 소개

안녕하세요, 친애하는 독자 여러분!

이 기사에서는 PE 파일의 중요하고 흥미로운 부분 하나인 PE IMAGE RESOURCES (컴파일 후 프로그램에 연결된 데이터)에 대해 알려 드리겠습니다. 우리는 리소스 트리의 내부 구조를 살펴보고 WinAPI를 사용하지 않고 자체 네이티브 리소스 파서를 작성할 것입니다.

PE (Portable Executable) 파일의 리소스는 응용 프로그램의 중요한 부분을 구성하는 내장 데이터입니다. 이미지, 아이콘, 텍스트 문자열, 대화 상자, 폰트와 같은 다양한 요소를 포함합니다. 이러한 리소스는 실행 파일에 직접 통합되어 응용 프로그램이 실행될 때 이용할 수 있습니다. 이미지, 텍스트, 사운드와 같은 리소스가 종종 컴파일된 후 프로그램에 추가된다는 점을 유의해야 합니다. 이 접근 방식은 여러 중요한 장점을 가지고 있어 개발 과정을 더욱 유연하고 효율적으로 만듭니다.

새로운 애플리케이션을 개발하는 개발 팀의 일원이라고 상상해보세요. 여러분의 팀은 프로그래머, 디자이너, 그리고 콘텐츠 매니저로 구성되어 있습니다. 각자가 독특하고 유용한 것을 만드는 데 기여합니다.

프로젝트 시작 시, 프로그래머들은 코드 작성과 테스트에 집중합니다. 그들은 모든 기능이 제대로 작동하도록 애플리케이션의 프레임워크를 만듭니다. 동시에 디자이너들과 콘텐츠 매니저들은 인터페이스를 위한 자원 - 이미지, 소리, 텍스트를 생성하는 작업을 합니다. 이러한 병행 작업은 팀이 빠르고 효율적으로 전진할 수 있도록 돕습니다.

프로그래밍의 주요 부분이 완료되면, 리소스를 애플리케이션에 통합할 시간입니다. 이 작업은 재컴파일 없이 이미 컴파일된 애플리케이션에 리소스를 추가할 수 있는 특별한 도구를 사용하여 수행됩니다. 변경사항을 적용하거나 리소스를 업데이트해야 할 경우 특히 편리합니다 - 전체 프로젝트를 재컴파일할 필요가 없습니다.

이 과정의 핵심 측면 중 하나는 로컬라이제이션입니다. 리소스를 메인 코드로부터 분리함으로써, 애플리케이션의 로컬라이제이션은 훨씬 간단해집니다. 인터페이스 텍스트, 오류 메시지들은 쉽게 번역되고 교체될 수 있으며, 이는 메인 코드에 방해가 되지 않습니다. 이를 통해 애플리케이션은 다양한 언어 시장에 맞춰 적응할 수 있으며, 전 세계 다양한 사용자들에게 접근성과 이해도를 높일 수 있습니다.
 

자원 사용 예시

  • 응용 프로그램 아이콘: Windows 탐색기나 작업 표시줄에 표시되는 아이콘은 응용 프로그램의 시각적 대표입니다.
  • 대화 상자: 설정이나 경고 창과 같이 사용자 상호작용을 위해 응용 프로그램이 표시하는 대화 상자의 정의입니다.
  • 메뉴: 사용자 인터페이스에서 사용되는 메뉴 구조는 탐색과 기능성을 제공합니다.
  • 문자열: 오류 메시지, 툴팁 및 기타 사용자 인터페이스 등 응용 프로그램에서 텍스트를 표시하는 데 사용되는 지역화된 문자열입니다.
  • 소리: 소리 알림과 같은 특정 상황에서 응용 프로그램이 재생할 수 있는 오디오 파일입니다.
  • 커서: 화살표, 포인터 또는 애니메이션 커서와 같이 사용자 인터페이스와 상호작용하는 데 사용되는 그래픽 커서입니다.
  • 버전 정보 (Version Info): 응용 프로그램 버전, 저작권, 제품 이름 및 기타 버전 관련 데이터를 포함하고 있습니다.
  • 매니페스트: Windows 버전 요구 사항과 보안 설정을 포함한 응용 프로그램 구성에 대한 정보가 포함된 XML 파일입니다.
  • RC_DATA 개발자가 정의한 임의의 데이터로서, 바이너리 데이터, 구성 파일 또는 기타 응용 프로그램 특정 리소스를 포함할 수 있습니다.

휴대용 실행 파일에서 리소스를 보고 편집하는 방법은?

컴파일된 애플리케이션에서 리소스를 보고 편집할 수 있나요? 물론입니다! 필요한 것은 적합한 도구뿐입니다. 실행 파일에서 리소스를 다루는 것을 포함하여 보기, 편집, 추가, 삭제 등 다양한 기능을 제공하는 도구들이 있습니다.

여기 이미 컴파일된 실행 파일에서 리소스를 보거나 편집할 수 있는 리소스 에디터 목록이 있습니다:

  • Resource Hacker: 32비트 및 64비트 Windows 애플리케이션용 리소스 편집기입니다. 실행 파일(.exe, .dll, .scr 등) 및 컴파일된 리소스 라이브러리(.res, .mui)에서 리소스를 보고 편집할 수 있습니다. Resource Hacker
  • ResEdit: Win32 프로그램을 위한 무료 리소스 편집기입니다. 대화상자, 아이콘, 버전 정보 및 기타 리소스 작업에 적합합니다. ResEdit
  • Resource Tuner: 실행파일을 열고 다른 편집기에서는 보지 못하는 숨겨진 데이터를 편집할 수 있는 리소스 편집기입니다. Resource Tuner
  • Resource Builder: Windows용 강력하고 전체 기능을 갖춘 리소스 편집기입니다. 리소스 파일(.RC, .RES 등)을 생성, 편집, 컴파일하고 컴파일된 실행 파일에서 리소스를 편집할 수 있습니다. Resource Builder
  • Visual Studio: 리소스를 추가, 삭제 및 수정할 수 있는 리소스 편집기를 제공합니다. Visual Studio
  • Resource Tuner Console: 배치(.bat) 파일에서 사용하기에 이상적인 강력한 커맨드 라인 도구로 리소스를 편집합니다. Resource Tuner Console
  • Qt Centre: Qt를 사용하여 컴파일된 실행 파일의 리소스 파일을 편집할 수 있습니다. Qt Centre

리스트의 첫 번째 도구를 자세히 살펴보겠습니다: Resource Hacker.

이 도구는 실행 파일에서 리소스를 보고 추출하는 것뿐만 아니라 편집할 수도 있습니다!

이러한 리소스에는 아이콘, 메뉴, 대화 상자 및 기타 유형의 데이터가 포함될 수 있으며, 일반적으로 PE 파일의 특정 섹션인 .rsrc (리소스 섹션)에 위치하고 있습니다. 그러나 이것이 엄격한 규칙은 아니며 예외가 발생할 수 있다는 점을 주목하는 것이 중요합니다.

PE 파일 내의 이러한 리소스를 탐색하고 접근하는 핵심적인 측면은 IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES]입니다. 이 디렉터리 항목은 PE 파일의 선택적 헤더, 특히 데이터 디렉터리 배열 내에 부분적으로 포함됩니다. 이는 이미지 내의 리소스를 가리키거나 참조하는 역할을 합니다. IMAGE_DIRECTORY_ENTRY_RESOURCES는 리소스 데이터의 위치(예: 상대 가상 주소)와 크기에 대한 정보를 제공합니다.

 

포터블 실행 파일의 리소스 구조

일반 개요

PE (Portable Executable) 파일의 리소스 섹션에 사용된 구조에 대해 자세히 살펴보겠습니다. Windows PE 파일의 리소스 섹션은 고유한 세 단계 계층적 트리 구조를 가지고 있습니다. 이 트리는 아이콘, 커서, 문자열, 대화 상자 등과 같은 리소스를 구성하고 접근하는 데 사용됩니다. 구조는 다음과 같습니다:

레벨 1: 자원 유형

트리의 최상위 레벨에는 리소스 유형이 있습니다. 각 리소스 유형은 숫자 식별자(ID) 또는 문자열 이름으로 식별할 수 있습니다.

레벨 2: 리소스 이름

두 번째 수준에서 각 리소스 유형은 고유한 이름이나 식별자를 가집니다. 이를 통해 동일한 유형의 여러 리소스를 가질 수 있습니다, 예를 들어 여러 아이콘이나 행들처럼.

레벨 3: 자원 언어

세 번째 레벨에서, 각 리소스는 다양한 언어 지역화를 위한 변형을 가지고 있습니다. 이를 통해 대화 상자와 같은 동일한 리소스가 다른 언어로 지역화될 수 있습니다.

데이터 구조

다음 데이터 구조들이 이 계층구조를 표현하는 데 사용됩니다:

  • IMAGE_RESOURCE_DIRECTORY: 이 구조체는 트리의 각 레벨에 대한 헤더를 나타내며 해당 레벨의 항목에 대한 일반적인 정보를 포함합니다. 
  • IMAGE_RESOURCE_DIRECTORY_ENTRY: 이 요소들은 다른 IMAGE_RESOURCE_DIRECTORY를 가리키는 하위 디렉토리일 수도 있고, 실제 리소스 데이터를 가리키는 트리의 마지막 잎사귀일 수도 있습니다. 
  • IMAGE_RESOURCE_DATA_ENTRY: 이 구조체는 리소스 데이터 자체를 가리키며 크기와 오프셋을 포함합니다. 

리소스 트리의 시각화는 다음과 같을 수 있습니다:

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)

이 트리의 각 노드는 IMAGE_RESOURCE_DIRECTORY를 나타내며, 리프는 리소스 데이터를 직접 가리키는 IMAGE_RESOURCE_DATA_ENTRIES입니다. 수동으로 리소스를 파싱할 때, 개발자는 루트에서 시작하여 모든 레벨을 순차적으로 탐색하며 필요한 데이터를 찾아야 합니다.

 

IMAGE_RESOURCE_DIRECTORY

이 구조는 리소스 트리의 각 레벨에 대한 헤더로 사용되며 해당 레벨의 항목에 대한 정보를 포함합니다.

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: 일반적으로 사용되지 않으며 0으로 설정됩니다.
  • TimeDateStamp: 리소스 생성의 타임스탬프입니다.
  • MajorVersion and MinorVersion: 리소스 디렉토리의 버전입니다.
  • NumberOfNamedEntries: 이름이 있는 리소스 항목의 수입니다.
  • NumberOfIdEntries: 숫자 식별자가 있는 리소스 항목의 수입니다.

 

IMAGE_RESOURCE_DIRECTORY_ENTRY

하위 디렉토리 또는 트리의 최종 리프가 될 수 있는 요소들.

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;
  • 이름: NameIsString이 1로 설정되어 있으면 이 필드에는 리소스의 이름을 나타내는 UNICODE 문자열을 가리키는 오프셋이 포함됩니다. NameIsString이 0으로 설정된 경우, Id 필드가 숫자 식별자를 사용하여 리소스를 식별하는 데 사용됩니다.
  • OffsetToData: DataIsDirectory가 1로 설정되어 있으면 이 필드에는 다른 IMAGE_RESOURCE_DIRECTORY(즉, 하위 디렉토리)를 가리키는 오프셋이 포함됩니다. DataIsDirectory가 0으로 설정된 경우, 이 오프셋은 IMAGE_RESOURCE_DATA_ENTRY를 가리킵니다.

 

IMAGE_RESOURCE_DATA_ENTRY

이 구조는 리소스의 실제 데이터를 가리킵니다.

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    DWORD   OffsetToData;
    DWORD   Size;
    DWORD   CodePage;
    DWORD   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  • OffsetToData: 리소스 섹션의 시작부터 리소스 데이터까지의 오프셋입니다.
  • Size: 리소스 데이터의 크기(바이트 단위).
  • CodePage: 리소스 데이터 인코딩에 사용된 코드 페이지입니다.
  • Reserved: 예약됨; 일반적으로 0으로 설정됩니다.

중요! IMAGE_RESOURCE_DIRECTORYIMAGE_RESOURCE_DIRECTORY_ENTRY의 오프셋은 리소스의 시작(IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCES].VirtualAddress)으로부터 계산되며, IMAGE_RESOURCE_DATA_ENTRY의 오프셋만 기본 이미지의 시작으로부터 계산됩니다!

 

네이티브 리소스 파서를 작성해 봅시다!

WinAPI를 사용하지 않고 우리만의 네이티브 리소스 파서를 작성할 시간입니다! EnumResourceTypes EnumResourceNames 같은 Windows API 함수를 사용하는 대신 리소스를 수동으로 파싱하는 것은 보안 분석 및 안티바이러스 스캔의 맥락에서 여러 가지 이점이 있습니다:

  • 보안: EnumResourceTypes 및 EnumResourceNames와 같은 API 기능은 실행 파일을 프로세스의 주소 공간으로 로드해야 하며, 파일에 바이러스나 트로이 목마가 포함되어 있는 경우 악의적인 코드 실행으로 이어질 수 있습니다. 자원을 수동으로 파싱하는 것은 이러한 위험을 피할 수 있습니다.
  • 플랫폼 독립성: 수동 자원 파싱은 운영 체제 버전 및 해당 WinAPI에 의존하지 않아 더욱 범용적인 해결책입니다.
  • 휴리스틱 분석: 수동 파싱을 통해 새롭거나 알려지지 않은 위협을 식별하는 데 필요할 수 있는 복잡한 휴리스틱 및 탐지 알고리즘을 적용할 수 있습니다.
  • 성능: 파일 수가 많을 때 특히, WinAPI를 사용하는 것보다 파싱을 최적화하여 성능을 향상시킬 수 있습니다.
  • 제어: 수동 파싱을 통해 분석가는 프로세스를 완전히 제어할 수 있으며 특정 분석 요구에 맞게 조정할 수 있습니다. 반면 API 기능은 제한된 제어를 제공하며 자원의 모든 측면을 드러내지 않을 수 있습니다.
  • 보호: 악성코드는 표준 API에서 감지되지 않는 방식으로 자원을 조작하는 등 다양한 방법으로 탐지를 회피할 수 있습니다. 수동 파싱을 통해 이러한 조작을 감지할 수 있습니다.
  • 전체 접근: API 기능은 특히 손상되었거나 고의로 변경된 경우 모든 자원에 대한 접근을 제공하지 않을 수 있습니다. 수동 파싱을 통해 API가 부과한 제한 없이 모든 데이터를 분석할 수 있습니다.
  • 오류 처리: API 기능을 사용할 때 오류 처리가 제한될 수 있습니다. 그러나 수동 파싱을 통해 파일 구조의 비표준 상황 및 이상에 더 유연하게 대응할 수 있습니다.
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;
    };
}

전체 프로젝트의 코드는 우리의 github에서 찾을 수 있습니다:

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

리소스 파서는 리소스 유형, 이름 및 각 리소스의 언어 식별자를 가리키는 포인터를 포함하는 구조체가 들어 있는 벡터를 반환합니다. 각 구조체에는 리소스 데이터를 가리키는 포인터가 있습니다. 읽고 구문 분석한 실행 파일이 메모리에 있는 동안 각 구조체는 유효한 상태를 유지합니다. 이는 자체 바이러스 백신을 작성하고 파일을 스캔하는 데 매우 편리합니다. 파일이 해제되면 포인터는 유효하지 않게 됩니다.

 

사용된 도구 목록

  1. PE Tools: https://github.com/petoolse/petools 이는 헤더 PE 필드를 조작하는 오픈 소스 도구입니다. x86 및 x64 파일을 지원합니다.
  2. Resource Hacker: https://www.angusj.com/resourcehacker. 이는 32비트 및 64비트 Windows 응용 프로그램용 리소스 편집기입니다. 실행 파일(.exe, .dll, .scr 등) 및 컴파일된 리소스 라이브러리(.res, .mui)에서 리소스를 보고 편집할 수 있습니다.

 

결론

여러분, 이게 다입니다!

우리는 Windows 운영 시스템의 PORTABLE_EXECUTABLE 파일 리소스를 탐색하고 자체적으로 간단하지만 상당히 효과적인 네이티브 리소스 파서를 작성했습니다!

여러분의 지원에 감사드리며, 앞으로도 우리 커뮤니티에 지속적으로 참여해 주실 것을 기대합니다!

이 기사의 저자에게 질문이 있으시면 다음 이메일로 보내주십시오: articles@stofu.io

감사합니다!