최근 악성코드를 분석해 보던 중, IAT에 Import하는 함수가 하나도 없는데 WinAPI를 자유자재로 사용하는 것을 보고 방법이 궁금해져서 공부한 내용을 작성하기로 했습니다. 여기서는 TIB, PEB를 이용해 API를 사용하지 않고 현재 로드된 모듈의 핸들을 가져오는 방법에 대해서 알아보겠습니다. 이후 추가로 글을 작성하여 자신이 원하는 WinAPI의 주소를 가져오는 방법까지 알아보겠습니다.


 API 없이 로드된 정보를 가져오기 위해서는 우선 TIB에 대하여 알아 두어야 합니다.

 TIB(Thread Information Block)는 현재 실행 중인 Thread에 대한 정보를 저장하고 있는 자료 구조이며, TEB(Thread Environment Block)이라고도 합니다. TIB의 경우 SEH Overwrite를 공부하면서 몇번 본 적이 있습니다. (SEH Overwrite : http://5kyc1ad.tistory.com/308)

 이 TIB의 주소는 x86 환경에서 FS 레지스터에 저장되며, 구조는 다음과 같습니다.


[그림 1. Thread Information Block (출처)]


 특이하게도 FS 자체에 TIB의 주소가 담겨 있는데,  FS:[0x18]에 TIB(또는 Thread Environment Block)의 주소가 또 존재하는 것을 볼 수 있습니다. 여기서도 그렇게 할 것이지만, 보통 TIB의 멤버에 접근할 때는 FS:[0x18]을 한번 거쳐 TIB의 주소를 얻고 거기에서 offset을 이용하여 접근합니다. 예를 들어 TIB + 0x30에 위치한 PEB에 접근하고 싶은 경우, 단순히 FS:[0x30]을 하는 것이 아닌 *(FS:[0x18] + 0x30) 이렇게 접근하는 것입니다. 이렇게 하는 이유는 차후 FS가 가리키는 구조체가 TIB가 아닌 다른 구조체로 변경된다고 하더라도 해당 구조체의 0x18 Offset에 TIB의 주소를 넣어주기만 하면 기존의 코드도 정상 동작할 수 있기 때문입니다(간단히 말하면 호환성을 높이기 위해서입니다). 모듈 정보를 얻기 위해서는 PEB가 필요하므로 이렇게 얻은 PEB를 이용하여 계속 진행하겠습니다.

 - PEB 까지 *(FS:[0x18] + 0x30) 로 접근했습니다.


[그림 2. Process Environment Block ()]


 PEB(Process Environment Block)의 구조는 위와 같습니다. 여기서 사용할 멤버는 0x0C offset에 존재하는 Ldr 이라는 포인터 변수입니다. Ldr은 _PEB_LDR_DATA라는 struct를 가리키는 포인터로, 현재 프로세스에서 로드한 모듈들에 대한 정보를 담고 있습니다.


 - Ldr 까지 (*(FS:[0x18] + 0x30) + 0x0C) 로 접근했습니다.


[그림 3. _PEB_LDR_DATA (출처)]


 _PEB_LDR_DATA는 위와 같은 구조이며, LIST_ENTRY 멤버 셋을 갖고 있는 것을 볼 수 있습니다. 이 LIST_ENTRY는 멤버로 Flink와 Blink를 가지는 순환 참조 리스트로, LDR_DATA_TABLE_ENTRY에서는 LIST_ENTRY를 struct의 멤버 변수로 사용하여 현재 사용중인 모듈에 대한 정보를 담은 LDR_DATA_TABLE_ENTRY들을 순환 참조 리스트로 연결하는 역할을 합니다. 

LDR_DATA_TABLE_ENTRY의 구조는 다음과 같습니다. 


[그림 4. LDR_DATA_TABLE_ENTRY (출처)]


 _PEB_LDR_DATA와 마찬가지로 LIST_ENTRY 구조체를 다른 이름으로 세 개 갖고 있는것을 볼 수 있는데, 이는 모듈의 로드된 순서, 메모리에 매핑된 순서, 초기화된 순서대로 각각 다른 순환 참조 리스트에 LDR_DATA_TABLE_ENTRY들을 저장하고 있기 때문입니다. 여기서, InInitializationOrderLinks의 경우 다른 LIST_ENTRY와 달리 첫 번째 LDR_DATA_TABLE_ENTRY가 아닌 두 번째 LDR_DATA_TABLE_ENTRY를 가리킨다고 합니다. (http://limjunyoung.tistory.com/186) 그래서 출력해보면 InInitializationOrderLinks만 출력되는 결과가 다릅니다.


[그림 5. 전체적인 연결 구조]


 그리고 각각의 LIST_ENTRY들은 같은 종류끼리 연결되어 있기 때문에, InLoadOrderLinks의 Flink나 Blink는 다음, 혹은 이전 노드의 InLoadOrderLinks를 가리키며, InMemoryOrderLinks나 InInitializationOrderLinks도 마찬가지가 됩니다. 이는 접근할 LDR_DATA_TABLE_ENTRY의 멤버 변수가 같더라도 어떤 LIST_ENTRY를 사용하느냐에 따라 Offset이 달라진다는 이야기입니다. 이해를 돕기 위해 아래 그림을 첨부하였습니다.


[그림 6. LDR_DATA_TABLE_ENTRY Offset]


 위 그림에서와 같이, InLoadOrderLinks를 사용하여 LDR_DATA_TABLE_ENTRY에 접근했을 때는 평범하게 LDR_DAT_TABLE_ENTRY에서의 멤버 변수 Offset을 사용하면 되지만, InMemoryOrderLinks나 InInitializationOrderLinks를 사용할 경우 각각 0x08, 0x10만큼의 Offset이 추가되기 때문에 그만큼을 뺀 Offset으로 접근해야 같은 멤버 변수로의 접근이 가능합니다. 위 이미지는 BaseDllName.Buffer 에 접근할 때, 어떤 LIST_ENTRY를 기준으로 하느냐에 따라서 Offset이 어떻게 달라지는지를 표현한 것입니다.


 마지막으로, 지금까지 정리한 내용을 이용하여 현재 로드된 모듈의 BaseName을 출력하는 코드를 작성해 보았습니다.


main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdio.h>
 
int main()
{
    void *orgPtr = nullptr;
    void *curPtr = nullptr;
    // InLoadOrderLinks
    puts("[*] InLoadOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x0C]    // Ldr->InLoadOrderLinks
        mov orgPtr, ebx            
    loadOrderLoop:
        mov edx, [ebx]            // InLoadOrderLinks->Flink
        mov curPtr, edx            
        mov edx, [edx + 0x30]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        push edx
        call _putws                // Print module name
    loadOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
 
    // InMemoryOrderLinks
    puts("[*] InMemoryOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x14]    // Ldr->InMemoryOrderLinks
        mov orgPtr, ebx
    memoryOrderLoop :
        mov edx, [ebx]            // InMemoryOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x28]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je memoryOrderFailed
        push edx
        call _putws                // Print module name
    memoryOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne memoryOrderLoop
    }
 
    // InInitializationOrderLinks
    puts("[*] InInitializationOrderLinks");
    __asm
    {
        mov eax, fs:[0x18]        // TIB
        mov eax, [eax + 0x30]    // TIB->PEB
        mov eax, [eax + 0x0C]     // PEB->Ldr
        lea ebx, [eax + 0x1C]    // Ldr->InInitializationOrderLinks
        mov orgPtr, ebx
    InitOrderLoop:
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je InitOrderFailed
        push edx
        call _putws                // Print module name
    InitOrderFailed:
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne InitOrderLoop
    }
    return 0;
}


 어떤 LIST_ENTRY를 사용하느냐에 따라 사용하는 Offset이 달라지는 모습을 볼 수 있습니다. 

프로그램의 실행 결과는 다음과 같습니다.



 이렇게 WinAPI를 전혀 사용하지 않고도 로드된 모듈에 대한 정보를 알아오는 데에 성공했습니다. 추후엔 이렇게 가져온 모듈 정보를 이용해 다른 API의 주소를 가져와 사용하는 방법에 대해 알아보겠습니다.

블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

Tag ,

댓글을 달아 주세요