[그림 1. 실제 악성코드에서 사용한 GetProcAddress]

 보통 WinAPI를 사용하면 IAT에 사용하는 함수가 무엇인지 그대로 박혀있거나, 최소한 GetProcAddress 등 함수를 이용해 동적으로 API를 로드해서 사용하기 때문에 IAT에 GetModuleHandle이나 GetProcAddress 등이 나와 어디에서 어떤 함수를 로드하는지, 결국 어떤 함수를 사용해서 대강 무슨 행위를 하는지 노출됩니다. 그런데 최근 분석해봤던 악성코드는 이를 회피하여 GetModuleHandle이나 GetProcAddress 함수조차도 IAT에 없는데도 자유자재로 API를 가져다 사용했습니다. 이를 직접 구현해보기 위해 이전에 작성했던 글에서 API 없이 현재 로드된 모듈의 정보를 알아내는 방법에 대해 알아봤습니다. 이번엔 이렇게 가져온 모듈에서 API 주소를 가져와 사용하는 방법에 대해 알아보겠습니다. 그렇게 복잡하고 어려운 내용은 아니므로 먼저 소스코드를 놓고, 하나하나 짚어가도록 하겠습니다.


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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <Windows.h>
#include <winternl.h>
#include <stdio.h>
#include <wchar.h>
 
PIMAGE_DOS_HEADER GetBaseAddress(const wchar_t *moduleName)
{
    
    PLDR_DATA_TABLE_ENTRY orgPtr = nullptr;
    PLDR_DATA_TABLE_ENTRY curPtr = nullptr;
    wchar_t *foundDllName = nullptr;
    __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
    loadOrderLoop :
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        mov foundDllName, edx
    }
    if(!_wcsicmp(foundDllName, moduleName))
        return (PIMAGE_DOS_HEADER)curPtr->InMemoryOrderLinks.Flink; // -0x10, as used InInitializationOrderLinks
    __asm{
    loadOrderFailed :
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
    return nullptr;
}
 
int main()
{
 
    // 1. Get Base Address of ntdll.dll
    PIMAGE_DOS_HEADER ntdllBase = GetBaseAddress(L"ntdll.dll");
    if (ntdllBase == nullptr) {
        printf("[-] Failed to get ntdll.dll base address\n");
        return -1;
    }
    printf("[+] ntdll.dll : 0x%08x\n", (DWORD)ntdllBase);
    
    // 2. Parse Module - IMAGE_NT_HEADERS
    PIMAGE_NT_HEADERS peSignature = (PIMAGE_NT_HEADERS)((PBYTE)ntdllBase + ntdllBase->e_lfanew);
    if (peSignature->Signature != 'EP') {
        printf("[-] Failed to find PE Signature\n");
        return -1;
    }
    printf("[+] PE Signature : 0x%08x\n", (DWORD)peSignature);
 
    // 3. Parse Module - Export Table
    PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)ntdllBase + peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    DWORD sizeofIED = peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    printf("[+] Export Table : 0x%08x, Size : 0x%08x\n", (DWORD)pIED, sizeofIED);
    
    // 4. Find LdrGetProcedureAddress Function
    PDWORD funcAddresses      = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfFunctions);
    PDWORD funcNames          = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfNames);
    PWORD  funcOrdinals       = (PWORD)((PBYTE)ntdllBase + pIED->AddressOfNameOrdinals);
    void(__stdcall *pLdrGetProcedureAddress)(HMODULE, PANSI_STRING, DWORD, PVOID) = nullptr;
    for (unsigned int i = 0; i < pIED->NumberOfFunctions; i++)
    {
        if (!strcmp("LdrGetProcedureAddress", (char *)((PBYTE)ntdllBase + funcNames[i]))) 
        {
            pLdrGetProcedureAddress = (void(__stdcall *)(HMODULE, PANSI_STRING, DWORD, PVOID))((PBYTE)ntdllBase + funcAddresses[funcOrdinals[i]]);
            break;
        }
    }
    if (pLdrGetProcedureAddress == nullptr)
    {
        printf("[-] Failed to find LdrGetProcedureAddress\n");
        return -1;
    }
    printf("[+] LdrGetProcedureAddress : 0x%08x\n", (DWORD)pLdrGetProcedureAddress);
    
    // 5. Find Functions With LdrGetProcedureAddress
    HMODULE hKernel32 = (HMODULE)GetBaseAddress(L"kernel32.dll");
    if (hKernel32 == nullptr)
    {
        printf("[-] Can't find kernel32.dll\n");
        return -1;
    }
    printf("[+] kernel32.dll : 0x%08x\n", (DWORD)hKernel32);
 
    ANSI_STRING targetFunction;
    RtlInitAnsiString(&targetFunction, "CreateFileA");
    HANDLE(__stdcall *pCreateFileA)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCreateFileA);
    printf("[+] CreateFileA : 0x%p\n", pCreateFileA);
 
    RtlInitAnsiString(&targetFunction, "WriteFile");
    BOOL(__stdcall *pWriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pWriteFile);
    printf("[+] WriteFile : 0x%p\n", pWriteFile);
 
    RtlInitAnsiString(&targetFunction, "CloseHandle");
    BOOL(__stdcall *pCloseHandle)(HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCloseHandle);
    printf("[+] CloseHandle : 0x%p\n", pCloseHandle);
 
    // 6. Use Functions
    DWORD Length = 0;
    HANDLE hFile = pCreateFileA("Test.txt", GENERIC_ALL, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    pWriteFile(hFile, "PEB Test String"15&Length, NULL);
    pCloseHandle(hFile);
    return 0;
}


 전체적인 소스코드는 위와 같습니다. 내용을 요약하면 ntdll.dll 모듈의 BaseAddress를 이용하여 PE 헤더를 파싱하고, Export Table을 찾아서 여기서 LdrGetProcedureAddress 함수의 주소를 얻어온 다음 이 API를 이용하여 원하는 함수 주소를 가져와 사용하는 것입니다. 

빌드를 위해서는 소스코드 내에서 RtlInitAnsiString을 사용하기 때문에 [Visual Studio 프로젝트 속성->링커->입력->추가 종속성]에 'ntdll.lib'을 넣어줘야 합니다.


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
PIMAGE_DOS_HEADER GetBaseAddress(const wchar_t *moduleName)
{
    
    PLDR_DATA_TABLE_ENTRY orgPtr = nullptr;
    PLDR_DATA_TABLE_ENTRY curPtr = nullptr;
    wchar_t *foundDllName = nullptr;
    __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
    loadOrderLoop :
        mov edx, [ebx]            // InInitializationOrderLinks->Flink
        mov curPtr, edx
        mov edx, [edx + 0x20]   // LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer
        test edx, edx
        je loadOrderFailed
        mov foundDllName, edx
    }
    if(!_wcsicmp(foundDllName, moduleName))
        return (PIMAGE_DOS_HEADER)curPtr->InMemoryOrderLinks.Flink; // -0x10, as used InInitializationOrderLinks
    __asm{
    loadOrderFailed :
        mov ebx, curPtr
        mov ebx, [ebx]
        mov edx, orgPtr
        cmp ebx, edx
        jne loadOrderLoop
    }
    return nullptr;
}


 가장 먼저 볼 부분은 GetBaseAddress라고 이름붙인 이 함수입니다. 이전 글에서 사용한 방식과 완전히 동일한 방법을 사용하여 원하는 모듈의 LDR_DATA_TABLE_ENTRY를 찾은 후 BaseAddress를 가져오는 함수입니다. InLoadOrderLinks나 InMemoryOrderLinks가 아닌 InInitializationOrderLinks를 사용한 이유는 Windows 7 환경에서 실행시켰을 때, InLoadOrderLinks나 InMemoryOrderLinks에서는 ntdll.dll이 순환 연결 리스트 내부에 존재하지 않았기 때문입니다. 여기에서의 내용이 관련이 있을지도 모르겠습니다. 그래서인지 실제 악성코드에서도 InInitializationOrderLinks를 사용했었습니다.


1
2
3
4
5
6
7
    // 1. Get Base Address of ntdll.dll
    PIMAGE_DOS_HEADER ntdllBase = GetBaseAddress(L"ntdll.dll");
    if (ntdllBase == nullptr) {
        printf("[-] Failed to get ntdll.dll base address\n");
        return -1;
    }
    printf("[+] ntdll.dll : 0x%08x\n", (DWORD)ntdllBase);


 위에서 설명한 GetBaseAddress 함수에 L"ntdll.dll"을 인자로 넘겨 ntdll.dll의 BaseAddress를 가져옵니다. BaseAddress는 모듈이 메모리에 매핑된 기준점이 되는 주소이기 때문에 해당 메모리 주소에 접근하면 모듈 바이너리를 통째로 보는것과 다름없습니다. PE 헤더 파싱을 위해 PIMAGE_DOS_HEADER형으로 받아줍니다.


1
2
3
4
5
6
7
    // 2. Parse Module - IMAGE_NT_HEADERS
    PIMAGE_NT_HEADERS peSignature = (PIMAGE_NT_HEADERS)((PBYTE)ntdllBase + ntdllBase->e_lfanew);
    if (peSignature->Signature != 'EP') {
        printf("[-] Failed to find PE Signature\n");
        return -1;
    }
    printf("[+] PE Signature : 0x%08x\n", (DWORD)peSignature);


그렇게 받아온 모듈을 파싱하여 IMAGE_DOS_HEADER에서 IMAGE_NT_HEADER를 찾아내어 마찬가지로 PIMAGE_NT_HEADER형으로 받아줍니다.


1
2
3
4
    // 3. Parse Module - Export Table
    PIMAGE_EXPORT_DIRECTORY pIED = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)ntdllBase + peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    DWORD sizeofIED = peSignature->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
    printf("[+] Export Table : 0x%08x, Size : 0x%08x\n", (DWORD)pIED, sizeofIED);


 DLL 파일이므로 Export Table에 제공하는 API의 이름 및 RVA가 작성되어 있을 것입니다. 이를 얻어오기 위해 우선 Export Table을 가져옵니다. 딱히 쓰지는 않지만 가져오는김에 Export Table의 크기도 구해봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    // 4. Find LdrGetProcedureAddress Function
    PDWORD funcAddresses      = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfFunctions);
    PDWORD funcNames          = (PDWORD)((PBYTE)ntdllBase + pIED->AddressOfNames);
    PWORD  funcOrdinals       = (PWORD)((PBYTE)ntdllBase + pIED->AddressOfNameOrdinals);
    void(__stdcall *pLdrGetProcedureAddress)(HMODULE, PANSI_STRING, DWORD, PVOID) = nullptr;
    for (unsigned int i = 0; i < pIED->NumberOfFunctions; i++)
    {
        if (!strcmp("LdrGetProcedureAddress", (char *)((PBYTE)ntdllBase + funcNames[i]))) 
        {
            pLdrGetProcedureAddress = (void(__stdcall *)(HMODULE, PANSI_STRING, DWORD, PVOID))((PBYTE)ntdllBase + funcAddresses[funcOrdinals[i]]);
            break;
        }
    }
    if (pLdrGetProcedureAddress == nullptr)
    {
        printf("[-] Failed to find LdrGetProcedureAddress\n");
        return -1;
    }
    printf("[+] LdrGetProcedureAddress : 0x%08x\n", (DWORD)pLdrGetProcedureAddress);


 그렇게 파싱한 Export Table 내부에서 LdrGetProcedureAddress 함수를 찾아 만들어둔 함수 포인터에 저장합니다. Export Table 파싱은 여기를 참고하였습니다. 

 함수 포인터에 __stdcall을 붙인 이유는 VS 기본 설정상 함수 호출 규약(Calling Convention)이 __cdecl로 설정되어 있지만 WinAPI의 경우 __stdcall을 사용하기 때문입니다. 이를 지정하지 않을 경우 스택 정리가 엉망이 되어 가져온 함수를 호출하고 난 이후가 정상 작동하지 않습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    // 5. Find Functions With LdrGetProcedureAddress
    HMODULE hKernel32 = (HMODULE)GetBaseAddress(L"kernel32.dll");
    if (hKernel32 == nullptr)
    {
        printf("[-] Can't find kernel32.dll\n");
        return -1;
    }
    printf("[+] kernel32.dll : 0x%08x\n", (DWORD)hKernel32);
 
    ANSI_STRING targetFunction;
    RtlInitAnsiString(&targetFunction, "CreateFileA");
    HANDLE(__stdcall *pCreateFileA)(LPCSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCreateFileA);
    printf("[+] CreateFileA : 0x%p\n", pCreateFileA);
 
    RtlInitAnsiString(&targetFunction, "WriteFile");
    BOOL(__stdcall *pWriteFile)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pWriteFile);
    printf("[+] WriteFile : 0x%p\n", pWriteFile);
 
    RtlInitAnsiString(&targetFunction, "CloseHandle");
    BOOL(__stdcall *pCloseHandle)(HANDLE) = nullptr;
    pLdrGetProcedureAddress(hKernel32, &targetFunction, 0&pCloseHandle);
    printf("[+] CloseHandle : 0x%p\n", pCloseHandle);


 이제 위에서 구한 LdrGetProcedureAddress 함수 주소를 이용하여 원하는 함수의 주소를 찾아올 수 있습니다.

 LdrGetProcedureAddress는 첫번째 인자로 모듈 핸들, 두 번째 인자로 ANSI_STRING으로 만든 함수명, 세 번째는 Ordinal이라는 파라미터(이지만 0으로 넘김), 마지막에 결과를 전달받을 함수 포인터의 주소값을 넣으면 됩니다. 여기서 첫 번째 인자인 모듈 핸들의 경우, 사실 놀랍게도 HMODULE 타입은 모듈의 Base Address와 완전히 같은 값이기 때문에(https://stackoverflow.com/questions/9545732/what-is-hmodule) GetBaseAddress 함수의 결과를 HMODULE로 캐스팅하기만 하면 핸들로 사용할 수 있습니다. 이렇게 원하는 함수 주소를 구할 수 있습니다.


1
2
3
4
5
    // 6. Use Functions
    DWORD Length = 0;
    HANDLE hFile = pCreateFileA("Test.txt", GENERIC_ALL, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    pWriteFile(hFile, "PEB Test String"15&Length, NULL);
    pCloseHandle(hFile);


 마지막으로 그렇게 받아온 함수를 사용하기만 하면 됩니다. 저는 CreateFileA / WriteFile / CloseHandle을 가져와서 테스트용 텍스트 파일을 만들고, 쓰고, 정상적으로 닫는 작업까지 진행해 봤습니다.


[그림 2. 프로그램 실행 결과]

 결과는 위와 같이, 정상적으로 텍스트 파일이 만들어졌습니다.


[그림 3. IAT]

 위는 빌드한 파일의 IAT입니다. kernel32.dll 내부에서 임포트하는 함수 중 어딜 봐도 GetProcAddress나 CreateFileA, WriteFile, CloseHandle은 없는 것을 볼 수 있습니다. 이제 함수명과 위 루틴들을 조금만 난독화하면 분석가들이 정적으로 분석하기 무척 까다로워지는 환경을 만들 수 있겠습니다. :)

'Analysis > Technique' 카테고리의 다른 글

TIB, PEB를 이용해 로드된 DLL 정보 가져오기  (0) 2018.10.22
CFG(Control Flow Guard)  (0) 2018.08.29
GS + DEP + SafeSEH + SEHOP 우회 Exploit  (0) 2018.07.25
GS + SafeSEH 우회 SEH Overwrite Exploit  (0) 2018.07.11
SEH Overwrite  (0) 2018.05.29
블로그 이미지

__미니__

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

,