날먹을 위한 몸부림/리버싱

ntdll.dll에서 SSDT Index 가져오기

프로그래밍하는 지팡이 2022. 5. 26. 15:58

드라이버에서 SSDT를 후킹하기 전에 후킹하고자 하는 함수의 인덱스를 알아야합니다

windows api는 호출이되면 함수 내부에서 일부 검증과 변환 같은 처리를 한 뒤에 ntdll에 있는 Nt로 시작하는 함수들을 호출합니다

Nt함수들은 다음 사진과 같이 SSDT 인덱스를 eax레지스터에 넣고 syscall을 호출합니다

 

Zw(Nt)CreateFile의 함수 내용, eax에 SSDT 인덱스를 넣는 모습을 볼 수 있습니다

 

syscall을 하기전에 mov eax, 00000055 와 같이 SSDT 인덱스를 넣는 모습을 볼 수 있습니다

이번 글에서는 Nt함수의 시작부분에서 가까운 위치의 mov명령어를 찾고 그 뒤에 있는 SSDT 인덱스를 가져오는 방법에 대해서 알아봅니다

 

 

실습은 exe프로그램에서 진행하고 추후에 드라이버로 이식합니다

64비트에서 실습을 진행합니다(제가 64비트 쓸거라서ㅋㅋㅋ)

ntdll.dll은 프로젝트 폴더로 복사한 뒤 실습을 진행합니다

파일을 읽기만 하는거라 괜찮은데 혹시 모를 실수를 방지합니다(주로 파일 오픈 모드, 쓰기모드 실수)


ntdll.dll로드

 

디스크에 있는 ntdll.dll을 로드합니다

커널드라이버에서도 유사한 방법을 사용하기 위해서 메모리에 이미 로드된  ntdll.dll을 사용하지 않습니다

 

LPCWSTR file_name = L"\\ntdll.dll";
WCHAR file_path[MAX_PATH] = { 0, };
GetCurrentDirectoryW(MAX_PATH, file_path);
//GetSystemDirectoryW(szFilePath, MAX_PATH);
StringCchCatW(file_path, MAX_PATH, file_name);

DWORD file_size = 0;
HANDLE file_handle = CreateFile(file_path, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
unique_ptr<BYTE[], std::function<void(BYTE[])>> file_buffer;


if (file_handle == INVALID_HANDLE_VALUE)
{
    cout << "Open Fail..." << endl;
    return 0;
}

file_size = GetFileSize(file_handle, NULL);
if (file_size == INVALID_FILE_SIZE)
{
    cout << "Get File Size Fail.." << endl;
    CloseHandle(file_handle);
    return 0;
}

file_buffer = unique_ptr<BYTE[], std::function<void(BYTE[])>>(new BYTE[file_size], [](BYTE* p) {delete[] p; });
if (!ReadFile(file_handle, file_buffer.get(), file_size, NULL, NULL))
{
    cout << "Read Fail..." << endl;
    CloseHandle(file_handle);
    return 0;
}

 

ntdll.dll을 읽고 버퍼를 할당한 뒤 모두 읽어들입니다

 

 

함수의 오프셋 구하기

pe헤더를 분석하면서 찾으려고 하는 함수가 위치한 오프셋을 구합니다

헤더에 있는 주소들은 파일 오프셋 말고도 RVA를 사용하기 때문에 RVA를 파일 오프셋으로 변환하는 유틸함수를 작성합니다

 

다음은 RVA를 파일 오프셋으로 변환하는 함수입니다

#define PE_ERROR ((UINT_PTR)-1)
using namespace std;

UINT_PTR RVAToOffset(PIMAGE_NT_HEADERS64 pinh, UINT_PTR RVA, DWORD file_size)
{
    PIMAGE_SECTION_HEADER ish = IMAGE_FIRST_SECTION(pinh);
    INT number_of_section = pinh->FileHeader.NumberOfSections;
    for (INT idx = 0; idx < number_of_section; idx++)
    {
        if (ish->VirtualAddress <= RVA && RVA < ish->VirtualAddress + ish->Misc.VirtualSize)
        {
            RVA -= ish->VirtualAddress;
            RVA += ish->PointerToRawData;

            return RVA < file_size ? RVA : PE_ERROR;
        }

        ish++;
    }

    return PE_ERROR;
}

 

모든 섹션을 반복하면서 해당 RVA가 어떤 섹션에 있는지 체크한 뒤 계산합니다

파일 오프셋은 다음과 같은 방법으로 구할 수 있습니다

 

파일 오프셋 = RVA - VirtualAddress + PointerToRawData

 

 

이제 pe를 분석하면서 특정 함수의 주소를 구합니다

UINT_PTR GetExportOffset(UINT_PTR file_data, DWORD file_size, LPCSTR func_name)
{
    PIMAGE_DOS_HEADER idh = (PIMAGE_DOS_HEADER)file_data;
    if (idh->e_magic != IMAGE_DOS_SIGNATURE)
    {
        cout << "IMAGE_DOS_SIGNATURE error" << endl;
        return PE_ERROR;
    }

    PIMAGE_NT_HEADERS64 inh = (PIMAGE_NT_HEADERS64)(file_data + idh->e_lfanew);
    if (inh->Signature != IMAGE_NT_SIGNATURE)
    {
        cout << "IMAGE_NT_SIGNATURE error" << endl;
        return PE_ERROR;
    }

    PIMAGE_FILE_HEADER ifh = (PIMAGE_FILE_HEADER)&inh->FileHeader;
    PIMAGE_OPTIONAL_HEADER64 ioh = (PIMAGE_OPTIONAL_HEADER64)&inh->OptionalHeader;
    PIMAGE_DATA_DIRECTORY idd = (PIMAGE_DATA_DIRECTORY)&ioh->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    UINT_PTR ied_offset = RVAToOffset(inh, idd->VirtualAddress, file_size);
    PIMAGE_EXPORT_DIRECTORY ied = (PIMAGE_EXPORT_DIRECTORY)(file_data + ied_offset);

    UINT_PTR export_name_table_offset = RVAToOffset(inh, ied->AddressOfNames, file_size);
    UINT_PTR export_ordinal_table_offset = RVAToOffset(inh, ied->AddressOfNameOrdinals, file_size);
    UINT_PTR export_function_table_offset = RVAToOffset(inh, ied->AddressOfFunctions, file_size);
    if (export_name_table_offset == PE_ERROR
        || export_ordinal_table_offset == PE_ERROR
        || export_function_table_offset == PE_ERROR)
    {
        cout << "export director content error" << endl;
        return PE_ERROR;
    }

    PDWORD export_name_table = (PDWORD)(file_data + export_name_table_offset);
    PWORD export_ordinal_table = (PWORD)(file_data + export_ordinal_table_offset);
    PDWORD export_function_table = (PDWORD)(file_data + export_function_table_offset);

    UINT_PTR export_function_offset = PE_ERROR;
    for (INT idx = 0; idx < ied->NumberOfNames; idx++)
    {
        UINT_PTR name_offset = RVAToOffset(inh, export_name_table[idx], file_size);
        char* name = (char*)(file_data + name_offset);
        if (lstrcmpA(func_name, name) == 0)
        {
            export_function_offset = RVAToOffset(inh, export_function_table[export_ordinal_table[idx]], file_size);
            break;
        }
    }

    return export_function_offset;
}

 

RVA로 된 값을 파일 오프셋으로 변환한 뒤 파일버퍼의 시작주소에 더해주는 방식으로 진행합니다

위와 같은 방법으로 해당 함수가 위치한 파일 오프셋을 구할 수 있습니다

 

 

 

SSDT Index구하기

앞에서 구한 함수의 시작 위치부터 mov를 검색합니다

mov바로 뒤에 있는 값은 SSDT 인덱스로 볼 수 있습니다

 

반복 도중에 RET명령어를 만나면 루프를 탈출합니다

INT GetExportSSDTIndex(UINT_PTR file_data, DWORD file_size, LPCSTR func_name)
{
    UINT_PTR export_function_offset = GetExportOffset(file_data, file_size, func_name);
    if (export_function_offset == PE_ERROR)
    {
        cout << "export function offset error" << endl;
        return -1;
    }

    INT SSDT_index = -1;
    PBYTE function_data = (PBYTE)(file_data + export_function_offset);
    for (INT idx = 0; idx < 32 && export_function_offset + 1 < file_size; idx++)
    {
        if (function_data[idx] == 0xC2 || function_data[idx] == 0xC3) // ret
            break;

        if (function_data[idx] == 0xB8) //mov
        {
            SSDT_index = *(INT*)(function_data + idx + 1);
            break;
        }
    }

    if (SSDT_index == PE_ERROR)
        cout << "SSDT index not found" << endl;

    return SSDT_index;
}

 

이제 함수 이름으로 SSDT 인덱스를 구할 수 있습니다

 

 

 

테스트

실제로 올바른 SSDT 인덱스를 가져오는지 테스트를 합니다

 

vector<string> function_names = { "NtOpenProcess", "NtQuerySystemInformation", "NtCreateFile" };
for (string& name : function_names)
{
    INT ssdt_index = GetExportSSDTIndex((UINT_PTR)file_buffer.get(), file_size, name.c_str());
    cout << name << " SSDT Index : " << ssdt_index << endl;
}

 

출력은 다음과 같습니다

SSDT 인덱스를 출력한 모습

 

 

디버거 또는 메모리뷰어로 확인합니다

이번 글에서는 치트엔진을 사용해서 확인했습니다

 

NtOpenProcess의 SSDT 인덱스, 38

 

NtQuerySystemInformation의 SSDT 인덱스, 54
NtCreateFile의 SSDT 인덱스, 85

 

 

올바르게 구해온 모습을 확인했습니다

 

 

사용된 코드

 

#include <iostream>
#include <memory>
#include <functional>
#include <Windows.h>
#include <strsafe.h>
#include <vector>


#define PE_ERROR ((UINT_PTR)-1)

using namespace std;

UINT_PTR RVAToOffset(PIMAGE_NT_HEADERS64 pinh, UINT_PTR RVA, DWORD file_size)
{
    PIMAGE_SECTION_HEADER ish = IMAGE_FIRST_SECTION(pinh);
    INT number_of_section = pinh->FileHeader.NumberOfSections;
    for (INT idx = 0; idx < number_of_section; idx++)
    {
        if (ish->VirtualAddress <= RVA && RVA < ish->VirtualAddress + ish->Misc.VirtualSize)
        {
            RVA -= ish->VirtualAddress;
            RVA += ish->PointerToRawData;

            return RVA < file_size ? RVA : PE_ERROR;
        }

        ish++;
    }

    return PE_ERROR;
}

UINT_PTR GetExportOffset(UINT_PTR file_data, DWORD file_size, LPCSTR func_name)
{
    PIMAGE_DOS_HEADER idh = (PIMAGE_DOS_HEADER)file_data;
    if (idh->e_magic != IMAGE_DOS_SIGNATURE)
    {
        cout << "IMAGE_DOS_SIGNATURE error" << endl;
        return 0;
    }

    PIMAGE_NT_HEADERS64 inh = (PIMAGE_NT_HEADERS64)(file_data + idh->e_lfanew);
    if (inh->Signature != IMAGE_NT_SIGNATURE)
    {
        cout << "IMAGE_NT_SIGNATURE error" << endl;
        return 0;
    }

    PIMAGE_FILE_HEADER ifh = (PIMAGE_FILE_HEADER)&inh->FileHeader;
    PIMAGE_OPTIONAL_HEADER64 ioh = (PIMAGE_OPTIONAL_HEADER64)&inh->OptionalHeader;
    PIMAGE_DATA_DIRECTORY idd = (PIMAGE_DATA_DIRECTORY)&ioh->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    UINT_PTR ied_offset = RVAToOffset(inh, idd->VirtualAddress, file_size);
    PIMAGE_EXPORT_DIRECTORY ied = (PIMAGE_EXPORT_DIRECTORY)(file_data + ied_offset);

    UINT_PTR export_name_table_offset = RVAToOffset(inh, ied->AddressOfNames, file_size);
    UINT_PTR export_ordinal_table_offset = RVAToOffset(inh, ied->AddressOfNameOrdinals, file_size);
    UINT_PTR export_function_table_offset = RVAToOffset(inh, ied->AddressOfFunctions, file_size);
    if (export_name_table_offset == PE_ERROR
        || export_ordinal_table_offset == PE_ERROR
        || export_function_table_offset == PE_ERROR)
    {
        cout << "export director content error" << endl;
        return 0;
    }

    PDWORD export_name_table = (PDWORD)(file_data + export_name_table_offset);
    PWORD export_ordinal_table = (PWORD)(file_data + export_ordinal_table_offset);
    PDWORD export_function_table = (PDWORD)(file_data + export_function_table_offset);

    UINT_PTR export_function_offset = PE_ERROR;
    for (INT idx = 0; idx < ied->NumberOfNames; idx++)
    {
        UINT_PTR name_offset = RVAToOffset(inh, export_name_table[idx], file_size);
        char* name = (char*)(file_data + name_offset);
        if (lstrcmpA(func_name, name) == 0)
        {
            export_function_offset = RVAToOffset(inh, export_function_table[export_ordinal_table[idx]], file_size);
            break;
        }
    }

    return export_function_offset;
}

INT GetExportSSDTIndex(UINT_PTR file_data, DWORD file_size, LPCSTR func_name)
{
    UINT_PTR export_function_offset = GetExportOffset(file_data, file_size, func_name);
    if (export_function_offset == PE_ERROR)
    {
        cout << "export function offset error" << endl;
        return -1;
    }

    INT SSDT_index = -1;
    PBYTE function_data = (PBYTE)(file_data + export_function_offset);
    for (INT idx = 0; idx < 32 && export_function_offset + 1 < file_size; idx++)
    {
        if (function_data[idx] == 0xC2 || function_data[idx] == 0xC3) // RET
            break;

        if (function_data[idx] == 0xB8)
        {
            SSDT_index = *(INT*)(function_data + idx + 1);
            break;
        }
    }

    if (SSDT_index == PE_ERROR)
        cout << "SSDT index not found" << endl;

    return SSDT_index;
}

int main()
{
    LPCWSTR file_name = L"\\ntdll.dll";
    WCHAR file_path[MAX_PATH] = { 0, };
    GetCurrentDirectoryW(MAX_PATH, file_path);
    //GetSystemDirectoryW(szFilePath, MAX_PATH);
    StringCchCatW(file_path, MAX_PATH, file_name);

    DWORD file_size = 0;
    HANDLE file_handle = CreateFile(file_path, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    unique_ptr<BYTE[], std::function<void(BYTE[])>> file_buffer;


    if (file_handle == INVALID_HANDLE_VALUE)
    {
        cout << "Open Fail..." << endl;
        return 0;
    }

    file_size = GetFileSize(file_handle, NULL);
    if (file_size == INVALID_FILE_SIZE)
    {
        cout << "Get File Size Fail.." << endl;
        CloseHandle(file_handle);
        return 0;
    }

    file_buffer = unique_ptr<BYTE[], std::function<void(BYTE[])>>(new BYTE[file_size], [](BYTE* p) {delete[] p; });
    if (!ReadFile(file_handle, file_buffer.get(), file_size, NULL, NULL))
    {
        cout << "Read Fail..." << endl;
        CloseHandle(file_handle);
        return 0;
    }

    vector<string> function_names = { "NtOpenProcess", "NtQuerySystemInformation", "NtCreateFile" };
    for (string& name : function_names)
    {
        INT ssdt_index = GetExportSSDTIndex((UINT_PTR)file_buffer.get(), file_size, name.c_str());
        cout << name << " SSDT Index : " << ssdt_index << endl;
    }

    return 0;
}

 

'날먹을 위한 몸부림 > 리버싱' 카테고리의 다른 글

win32u.dll는 뭐임?  (1) 2022.05.30
ssdt index 뷰어  (0) 2022.05.27
HamsterHide 사용법 - 상세 설정  (9) 2022.04.20
HamsterHide 사용법 - 간편 사용  (0) 2022.04.20
HamsterHide v0.2.2  (4) 2022.04.19