ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 13장 PE File Format (3)
    Reverse Engineering 2020. 12. 28. 23:40
     

    [리버싱 핵심원리 study] 13장 PE File Format (2)

    [리버싱 핵심원리 study] 13장 PE File Format (1) PE(Portable Executable) 파일은 Windows 운영체제에서 사용되는 실행 파일 형식이다. 기존 UNIX의 COEF(Common Object File Format)을 기반으로 Microsoft에서..

    maple19out.tistory.com

     

    - IAT

    IAT(Import Address Table)에는 Windows 운영체제의 핵심 개념인 process, memory, DLL 구조 등에 대한 내용이 함축되어 있다. IAT란 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지를 기술하는 테이블이다.

     

    - DLL

    DLL(Dynamic Linked Library)는 프로그램에 라이브러리를 포함시키지 않고 별도의 파일(DLL)로 구성하여 필요할 때마다 불러 쓰는 방식으로, 한 번 로딩된 DLL은 다른 process들이 공유해서 사용함으로써 memory를 효율적으로 사용할 수 있는 방법이다. DLL에는 두 가지 방식이 있는데 프로그램 내부에서 사용되는 순간에 로딩하고 끝나면 해제되는 Explicit Linking(혹은 dynamic loading)방법, 프로그램이 시작될 때 로딩되고 종료될 때 해제되는 Implicit Linking(혹은 dynamic linking)방법이 있다. IAT에서는 후자인 Implicit Linking에 대한 메커니즘을 제공하는 역할을 한다. OllyDbg에서 notepad.exe를 통해 IAT가 사용되는 부분을 알아보겠다.

     

    CreateFileW() 호출

    위와 같이 01001104에 들어있는 값을 호출하는 방식으로 CreateFileW를 호출한다. (모든 API호출은 이와 같은 방식을 취한다.) 01001104의 주소 값은 75833F50이며 해당 주소는 notepad.exe 프로세스 메모리에 로딩된 kernel32.dll의 CreateFileW 함수다. 그냥 CALL 75833F50이라 하면 될 듯한데, 왜 이렇게 indirect 하게 호출하는 것일까? 두 가지 이유로 이와 같은 방식을 취하는데 다음과 같다.

     

    (1) 프로그램을 컴파일 하는 순간에는 notepad.exe 프로그램이 어떤 환경에서 실행될지 알 수 없다. (xp, vista, windows 10 등등...) 즉 위의 환경에 따라 kernel32.dll의 버전이 달라지고 CreateFileW 함수의 위치가 달라지기 때문에, 해당 함수의 호출을 보장하기 위해 함수가 저장될 위치만을 준비하고 CALL DWORD PTR DS:[1001104] 형식의 명령어를 적어두기만 한다.

     

    (2) 기존에 DLL이 이미 로딩된 상태에서 다른 DLL이 로딩될 경우, DLL Relocation이 발생하므로 실제 함수의 주소를 하드 코딩할 수 없다.

     

    - IMAGE_IMPORT_DESCRIPTOR

    PE 파일은 자신이 어떤 라이브러리를 import하는지 IMAGE_IMPORT_DESCRIPTOR 구조체에 명시한다.

    typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    	union {
        	DWORD Characteristics;
            DWORD OriginalFirstThunk;	//INT(Import Name Table) address (RVA)
    	};
        DWORD TimeDateStamp;
        DWORD ForwarderChain;
        DWORD Name;	//library name string address (RVA)
        DWORD FirstThunk;	//IAT(Import Address Table) address (RVA)
        
    } IMAGE_IMPORT_DESCRIPTOR;
    
    typedef struct _IMAGE_IMPORT_BY_NAME {
    	WORD Hint;	//ordinal
        BYTE Name[1];	//function name string
    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

    일반적인 프로그램에서는 여러 개의 라이브러리를 import하기 때문에, 라이브러리의 개수만큼 위 구조체의 배열 형식으로 존재하며, 구조체 배열의 마지막은 NULL 구조체로 끝나게 된다. IMAGE_IMPORT_DESCRIPTOR 구조체에서 중요한 멤버는 다음과 같다. 

     

    #1 OriginalFirstThunk : INT(Import Name Table)의 주소 (RVA)

    #2 Name : Library 이름 문자열의 주소(RVA)
    #3 FirstThunk : IAT(Import Address Table)의 주소 (RVA)

     

    PE로더가 임포트 함수 주소를 IAT에 입력하는 순서는 다음과 같다.

    (1) IID(IMAGE_IMPORT_DESCRIPTOR)의 Name 멤버를 읽어 "kernel32.dll" 문자열을 얻는다.

    (2) 해당 라이브러리를 LoadLibrary("kernel32.dll")을 통하여 로딩한다.

    (3) IID의 OriginalFirstThunk 멤버를 읽어서 INT 주소를 얻는다.

    (4) INT에서 배열의 값을 하나씩 읽어 해당 IMAGE_IMPORT_BY_NAME(RVA)를 얻는다.

    (5) IMAGE_IMPORT_BY_NAME의 Hint(ordinal) 또는 Name 항목을 이용하여 해당 함수의 시작 주소를 얻는다. (GetProcAddresss 이용)

    (6) IID의 FirstThunk멤버를 읽어서 IAT 주소를 얻는다.

    (7) 해당 IAT 배열 값에 위에서 구한 함수의 주소를 입력한다.

    (8) INT가 끝날 때까지(NULL) (4) ~ (7)을 반복한다.

     

    - notepad.exe를 이용한 실습

    우선 IMAGE_IMPORT_DESCRIPTOR가 위치한 곳을 찾아야 하는데, 이전 포스팅에서 언급산 IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress 값이 IID 구조체 배열의 시작 주소이다.

    DataDirectory[1]

    위 정보에서 앞 4byte는 RVA를, 뒤 4byte는 size를 나타낸다. RVA가 7604이니까 File Offset은 6A04가 된다. (6A04 = 7604 - 1000 + 400) 해당 부분을 HxD로 찾아가 보면 다음과 같다.

    IMAGE_IMPORT_DESCRIPTOR 배열

    size가 C8이였으므로 위와 같이 드래그 한 부분이 IID 배열이 된다. (마지막은 NULL 구조체로 되어 있다.) 앞에서 부터 20byte(첫 IID)에 대해 각 구조체 멤버를 나타내면 다음과 같다.

    Member RVA RAW
    OriginalFirstThunk(INT) 00007990 00006D90
    TimeDateStamp FFFFFFFF -
    ForwarderChain FFFFFFFF -
    Name 00007AAC 00006EAC
    FirstThunk(IAT) 000012C4 000006C4

    1. 라이브러리 이름(Name)

    Name 항목은 import 함수가 소속된 라이브러리 파일 이름 문자열 포인터이다. Name의 RAW인 6EAC를 찾아가 보자.

    comdlg32.dll

    위와 같이 "comdlg32.dll"이라는 라이브러리에서 import 하는 것임을 확인할 수 있다.

     

    2. OriginalFirstThunk - INT(Import Name Table)

    INT는 import하는 함수의 정보가 담긴 구조체 포인터 배열로, 해당 정보를 얻어야 프로세스 메모리에 로딩된 라이브러리에서 해당 함수의 시작 주소를 정확히 구할 수 있다. OriginalFirstThunk의 멤버를 따라가면 다음과 같다.

    INT

    3. IMAGE_IMPORT_BY_NAME

    위의 7A7A(RVA) -> 6E7A(RAW)를 따라가 보면 IMAGE_IMPORT_BY_NAME 구조체가 나온다.

    IMAGE_IMPORT_BY_NAME

    최초 2 byte 값(000F)은 Ordinal로 라이브러리에서 함수의 고유번호이며 그 뒤로 PageSetupDlgW라는 함수가 나오는 것을 확인할 수 있다.

     

    4. FirstThunk - IAT(Import Address Table)

    IAT의 RAW인 000006C4를 따라가 보자.

    IAT

    위에서 드래그한 영역이 comdlg32.dll 라이브러리에 해당되는 IAT 배열 영역으로 마지막은 NULL로 끝나는 것을 확인할 수 있다. 

     

     

    - EAT

    EAT(Export Address Table)은 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 하는 메커니즘이다. EAT를 통해서만 해당 라이브러리에서 export 하는 함수의 시작 주소를 정확히 구할 수 있다. IMAGE_EXPORT_DIRECTORY 구조체는 IAT와는 다르게 PE 파일에 하나만 존재한다. (export는 하나만 할 수 있기 때문) PE 파일에서 IMAGE_EXPORT_DIRECTORY는 IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 값의 시작 주소가 된다. notepad.exe에서 사용하는 라이브러리인 kernel32.dll의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0]을 확인해보겠다.

     

    kenel32.dll의 IMAGE_OPTIONAL_HEADER32.DataDirectory[0]

    RVA = 262C이므로 File offset은 1A2C이다.

     

    - IMAGE_EXPORT_DIRECTORY

    IMAGE_EXPORT_DIRECTORY의 구조체는 다음과 같다.

    typedef struct _IMAGE_EXPORT_DIRECTORY {
    	DWORD Characteristics;
        DWORD TimeDateStamp;	//creation time date stamp
        WORD MajorVersion;
        WORD MinorVersion;
        DWORD Name;	//address of library file name
        DWORD Base;	//ordinal base
        DWORD NumberOfFunctions;	//number of functions
        DWORD NumberOfNames;	//number of names
        DWORD AddressOfFunctions;	//address of function start address array
        DWORD AddressOfNames;	//address of function name string array
        DWORD AddressOfNameOrdinals;	//address of ordinal array
    } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

    중요 멤버들은 다음과 같다.

    #1 NumberOfFunctions : 실제 Export 함수 개수

    #2 NumberOfNames : Export 함수 중에서 이름을 가지는 함수 개수

    #3 AddressOfFunctions : Export 함수 주소 배열

    #4 AddressOfNames : 함수 이름 주소 배열

    #5 AddressOfNameOrdinals Ordinal 배열

     

    라이브러리에서 함수 주소를 얻기 위해 GetProcAddress()라는 API를 이용하는데, 그 과정은 다음과 같다.

    (1) AddressOfNames 멤버를 이용해 함수 이름 배열로 간다.

    (2) 함수 이름 배열에는 문자열 주소가 저장되어 있다. strcmp를 통해 원하는 함수 이름을 찾는다. (해당되는 index를 name_index라고 가정

    (3) AddressOfNameOrdinals 멤버를 이용해 Ordinal 배열 접근

    (4) ordinal 배열에서 name_index로 ordinal 값 발견

    (5) AddressOfFunctions 멤버를 이용해 EAT 접근

    (6) EAT에 아까 구한 ordinal 값을 배열 인덱스로 하여 원하는 함수의 시작 주소를 얻는다.

     

    - kernel32.dll을 이용한 실습

    실제로 kernel32.dll 파일의 EAT에서 AddConsoleAliasW 함수 주소를 찾는 실습을 진행해보자.

    앞에서 구한 IMAGE_EXPORT_DIRECTORY의 RAW인 1A2C로 이동해보자.

    kernel32.dll의 IMAGE_EXPORT_DIRECTORY 구조체

    위에서 드래그된 부분이 구조체의 각 멤버를 나타낸다. 멤버별로 해당 값을 살펴보면 다음과 같다.

    Member Value RAW
    Characteristics 00000000  
    TimeDateStamp 48025BE1  
    MajorVersion 0000  
    MinorVersion 0000  
    Name 00004B8E 3F8E
    Base 00000001  
    NumberOfFunctions 000003B9  
    NumberOfNames 000003B9  
    AddressOfFunctions 00002654 1A54
    AddressOfNames 00003538 2938
    AddressOfNameOrdinals 0000441C 3824

    앞에서 설명한 GetProcAddress() 동작 원리 순서대로 진행해보자.

     

    1. 함수 이름 배열

    AddressOfNames의 RAW = 2938이다. 해당 부분을 HxD에서 찾아가 보자.

    AddressOfNames

    2. 원하는 함수 이름 찾기

    RVA(4BCD) -> RAW(3FCD)를 따라가면 다음과 같은 문자열을 만날 수 있다.

    이때 'AddConsoleAliasW'의 배열 index는 3이다.

     

    3. Ordinal 배열

    AddressOfNameOrdinals의 멤버 값은 RAW 3824(위 표 참고)이다.

    AddressOfNameOrdinals

     

    4. Ordinal

    AddressOfNameOrdinals[index] = ordinal로부터 index = 3, ordinal = 7 값을 얻었다.

     

    5. 함수 주소 배열 - EAT

    마지막으로 AddConsoleAliasW의 실제 함수 주소를 찾아갈 수 있다. AddressOfFunctions의 RAW는 1A54이다.

    AddressOfFunctions

     

    6. AddConsoleAliasW 함수 주소

    4에서 계산한 ordinal을 index로 하여 AddressOfFunctions에 적용하면, RVA = 0002BEF9라는 값을 얻을 수 있다. kernel32.dll의 ImageBase는 7C7D0000으로 'AddConsoleAliasW'의 함수의 VA는 7C7D0000 + 0002BEF9 = 7C7FBEF9가 된다.

     

     

    반응형

    댓글

Designed by Tistory.