스레드 열거하기

@codemaru · July 05, 2012 · 23 min read

ToolHelp를 사용해서 스레드 열거하기

윈도우 환경에서 시스템에서 수행중인 스레드를 열거하는 가장 손쉬운 방법은 ToolHelp 라이브러리를 사용하는 방법이다. ToolHelp 라이브러리는 kernel32.dll에 포함된 프로세스/스레드/모듈 열거 관련 라이브러리를 말한다. kernel32.dll에 포함돼 있기 때문에 별도로 외부 파일을 별도로 설치할 필요 없이 바로 사용할 수 있다.

ToolHelp 라이브러리에서 스레드 열거에 사용되는 함수 원형이 <리스트 1>에 나와 있다. 가장 중요한 함수는 CreateToolhelp32Snapshot 함수다. 이 함수는 호출 시점에 시스템 정보에 대한 스냅샷을 만들어주는 역할을 한다. dwFlags에 TH32CS_SNAPTHREAD를 전달하면 시스템에서 실행되고 있는 스레드 스냅샷을 생성할 수 있다. th32ProcessID는 무시된다. 함수가 성공하면 스냅샷 핸들이 반환된다. 해당 스냅샷 핸들은 사용하고 나서 CloseHandle 함수를 사용해서 닫아주면 된다.

스냅샷 핸들을 만든 다음에는 해당 스냅샷에 포함된 엔트리 정보를 구할 차례다. 해당 엔트리 정보를 구하는데 Thread32First와 Thread32Next 함수가 사용된다. 각 함수는 첫 번째 엔트리와 다음 엔트리를 반환하는 역할을 한다. 엔트리가 없는 경우에는 함수가 실패한다. 각 함수의 첫 번째 인자에는 스냅샷 핸들을 두 번째 인자에는 정보를 반환 받을 구조체 포인터를 전달하면 된다. 이 때 주의해야 할 점은 구조체의 dwSize 필드를 함수 호출 전에 구조체 크기로 초기화 시켜주어야 한다는 점이다. 스레드 정보를 담고 있는 THREADENTRY32 구조체의 구조와 자주 사용하는 필드의 의미는 <리스트 2>에 나와 있다.

리스트 1 스레드 열거 관련 함수 원형

HANDLE WINAPI CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID);
BOOL WINAPI Thread32First(HANDLE hSnapshot, LPTHREADENTRY32 lpte);
BOOL WINAPI Thread32Next(HANDLE hSnapshot, LPTHREADENTRY32 lpte);

리스트 2 THREADENTRY32 구조체

typedef struct tagTHREADENTRY32 {
  DWORD dwSize; // 구조체 크기
  DWORD cntUsage;
  DWORD th32ThreadID; // 스레드 아이디
  DWORD th32OwnerProcessID; // 스레드가 포함된 프로세스 아이디
  LONG  tpBasePri; // 스레드 우선순위
  LONG  tpDeltaPri;
  DWORD dwFlags;
} THREADENTRY32, *PTHREADENTRY32;

ToolHelp 라이브러리를 사용해서 시스템에 수행중인 스레드를 열거하는 프로그램이 <리스트 3>에 나와 있다. 각 스레드의 프로세스 아이디와 스레드 아이디를 출력하는 역할을 한다. thread1 프로그램의 실행 결과는 <화면 4>에 나와 있다. 화면을 살펴보면 알 수 있겠지만 이 프로그램은 시스템에 수행중인 모든 스레드를 열거하는 역할을 한다. 특정 프로세스에 포함된 스레드 정보를 구하고 싶다면 각 엔트리의 th32OwnerProcessID 필드 값으로 필터링 하면 된다.

리스트 3 thread1 프로그램

#include <windows.h>
#include <tlhelp32.h>

template <class T>
class Visitor
{
public:
    virtual BOOL Visit(T &data) = 0;
};

typedef Visitor<THREADENTRY32> ThreadVisitor;

BOOL
EnumThreads(ThreadVisitor &visitor)
{
    HANDLE snapshot;
    THREADENTRY32 te32;
    
    snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if(snapshot == INVALID_HANDLE_VALUE)
        return FALSE;

    te32.dwSize = sizeof(te32);
    if(!Thread32First(snapshot, &te32))
    {
        CloseHandle(snapshot);
        return FALSE;
    }

    do
    {
        try
        {
            if(!visitor.Visit(te32))
                break;
        }
        catch(...)
        {
            CloseHandle(snapshot);
            return FALSE;
        }

    } while(Thread32Next(snapshot, &te32));

    CloseHandle(snapshot);
    return TRUE;
}

class ThreadPrinter : public ThreadVisitor
{
public:
    virtual BOOL Visit(THREADENTRY32 &data)
    {
        printf("PID => %5d, TID => %5d\n"
                 , data.th32OwnerProcessID
                 , data.th32ThreadID);
        return TRUE;
    }
};

int main()
{
    ThreadPrinter printer;
    EnumThreads(printer);
    return 0;
}

         md 0
화면 4 ToolHelp 라이브러리를 사용해서 스레드를 열거한 결과

NtQuerySystemInformation을 사용해서 스레드 열거하기

NtQuerySystemInformation 함수는 원래 문서화되지 않은 NT 커널 내부 함수였으나 워낙 강력한 기능을 가지고 있고 다양한 곳에서 광범위하게 사용되면서 현재는 MSDN을 통해서 일부 기능이 공개된 상태다. <리스트 4>에 함수 원형이 나와 있다. 함수의 구조는 단순해 보이지만 처음 이 함수를 사용하면 생각보다 사용 방법이 만만하진 않다. 우선 파라미터부터 차근차근 살펴보도록 하자.

첫 번째 인자는 어떤 정보를 구하고 싶은지를 나타낸다. SYSTEM_INFORMATION_CLASS는 함수 원형 다음에 나와 있는 것과 같이 선언된 열거형 자료 구조다. 중간 중간 값이 비어있는 부분이 있는데 해당 부분은 외부로 공개되지 않은 기능이라고 생각하면 된다. 실제로는 0번부터 차례로 모든 번호에 해당하는 자료가 구해지도록 되어 있다. 이번 시간에 우리가 사용할 스레드 정보를 구하기 위해서는 5번으로 선언된 SystemProcessInformation을 사용하면 된다. 해당 값을 전달하면 현재 수행 중인 프로세스 목록과 각 프로세스 내에 포함된 스레드 목록에 대한 정보가 반환된다.

두 번째 인자는 해당 정보를 전달받을 버퍼를 전달한다. 이 버퍼는 우리 쪽에서 할당해서 전달해야 하며 첫 번째 인자인 SystemInformationClass에 따라서 미리 약속된 형태로 자료가 반환된다. 세 번째 인자인 SystemInformationLength는 두 번째 인자로 전달한 버퍼의 크기를 나타낸다. 마지막 인자인 ReturnLength는 해당 정보를 기록하기 위해서 필요한 버퍼의 크기를 전달 받는 곳이다. 만약 전달한 버퍼가 전체 정보를 저장하기에 부족하다면 NtQuerySystemInformation 함수는 에러를 리턴하고 필요한 크기를 ReturnLength에 전달해 준다.

리스트 4 NtQuerySystemInformation 함수 원형

NTSTATUS WINAPI NtQuerySystemInformation(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
);

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation = 0,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemProcessInformation = 5,
    SystemProcessorPerformanceInformation = 8,
    SystemInterruptInformation = 23,
    SystemExceptionInformation = 33,
    SystemRegistryQuotaInformation = 37,
    SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

이 함수의 사용법을 어렵게 만드는 요인은 바로 리턴되는 버퍼 구조가 명확하지 않다는 점이다. 함수를 범용적으로 사용할 수 있도록 디자인해서 리턴되는 버퍼 구조가 함수 원형을 통해서 명시화되지 않기 때문이다. 따라서 이 경우에는 개별 항목별로 리턴되는 자료의 형태를 개발자가 일일이 숙지하고 있어야 한다.

SystemProcessInformation를 전달해서 NtQuerySystemInformation 함수를 호출하면 <리스트 5>에 나타나 있는 RSYSTEM_PROCESS_INFORMATION 정보와 <리스트 6>에 있는 RSYSTEM_THREAD_INFORMATION 구조체 정보가 리턴된다. 각 구조체에서 자주 사용되는 필드는 설명과 함께 진하게 표시되어 있기 때문에 해당 내용을 참고하도록 하자. <그림 1>에는 실제로 버퍼에 어떤 구조로 정보가 리턴 되는지가 나와 있다. 그림에서 SPI는 RSYSTEM_PROCESS_INFORMATION 구조체를 STI는 RSYSTEM_THREAD_INFORMATION 구조체를 나타낸다. SPI 구조체는 현재 실행된 프로세스 개수만큼 존재하며, STI 구조체는 각 프로세스에서 실행된 스레드 개수만큼 SPI 구조체 뒤에 따라 붙어 나온다. 이 구조를 열거하기 위해서는 SPI 구조체에 포함된 두 개의 필드가 중요하다. 첫 번째 필드인 NextEntryOffset은 현재 SPI 구조체 포인터에서 다음 SPI 구조체가 있는 곳까지의 오프셋이 저장돼 있다. 두 번째 필드인 NumberOfThreads에는 해당 프로세스에 몇 개의 스레드가 포함돼 있는지가 나와 있다. 따라서 이 필드에 저장된 개수만큼 STI 구조체가 이어진다고 생각하면 된다.

         md 1
그림 1 SystemProcessInformation 버퍼 리턴 구조

리스트 5 SYSTEM_PROCESS_INFORMATION 구조체

typedef struct _RSYSTEM_PROCESS_INFORMATION 
{
    ULONG NextEntryOffset; // 다음 프로세스 정보 오프셋
    ULONG NumberOfThreads; // 이 프로세스 포함된 스레드 개수
    LARGE_INTEGER WorkingSetPrivateSize;
    ULONG HardFaultCount;
    ULONG NumberOfThreadsHighWatermark;
    ULONGLONG CycleTime; // 프로세스 수행에 소모된 사이클 시간
    LARGE_INTEGER CreateTime; // 생성 시간
    LARGE_INTEGER UserTime; // 유저 모드에서 수행된 시간
    LARGE_INTEGER KernelTime; // 커널 모드에서 수행된 시간
    UNICODE_STRING ImageName; // 프로세스 이미지 이름
    ULONG BasePriority;
    HANDLE UniqueProcessId; // 프로세스 아이디
    HANDLE InheritedFromUniqueProcessId; // 부모 프로세스 아이디
    ULONG HandleCount;
    ULONG SessionId;
    ULONG_PTR UniqueProcessKey;
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    ULONG PageFaultCount;
    SIZE_T PeakWorkingSetSize;
    SIZE_T WorkingSetSize;
    SIZE_T QuotaPeakPagedPoolUsage;
    SIZE_T QuotaPagedPoolUsage;
    SIZE_T QuotaPeakNonPagedPoolUsage;
    SIZE_T QuotaNonPagedPoolUsage;
    SIZE_T PagefileUsage;
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
} RSYSTEM_PROCESS_INFORMATION, *PRSYSTEM_PROCESS_INFORMATION;

리스트 6 SYSTEM_THREAD_INFORMATION 구조체

typedef struct _CLIENT_ID
{
    HANDLE UniqueProcess; // 프로세스 아이디
    HANDLE UniqueThread; // 스레드 아이디
} CLIENT_ID, *PCLIENT_ID;

typedef struct _RSYSTEM_THREAD_INFORMATION 
{
    LARGE_INTEGER KernelTime; // 커널 모드에서 수행된 시간
    LARGE_INTEGER UserTime; // 유저 모드에서 수행된 시간
    LARGE_INTEGER CreateTime; // 생성 시간
    ULONG WaitTime;
    PVOID StartAddress; // 시작 주소
    CLIENT_ID ClientId; // 프로세스/스레드 아이디
    ULONG Priority; 
    LONG BasePriority;
    ULONG ContextSwitches; 
    ULONG ThreadState; // 현재 스레드 수행 상태
    ULONG WaitReason; // 대기 사유
} RSYSTEM_THREAD_INFORMATION, *PRSYSTEM_THREAD_INFORMATION;

<리스트 7>에는 앞서 설명한 내용을 토대로 NtQuerySystemInformation 함수를 사용해서 시스템에서 실행중인 스레드를 열거하는 예제가 나와 있다. 여기서 주의 깊게 살펴볼 점은 NtQuerySystemInformation 함수를 반복적으로 호출하는 부분이다. 통상적으로 NtQuerySystemInformation과 같은 함수는 버퍼 크기를 0으로 해서 필요한 버퍼 크기를 구한 다음 해당 크기만큼 버퍼를 할당하고 다시 호출해서 값을 반환 받는 형태로 사용한다. 그런데 이 때 주의해야 할 점이 이 두 함수를 아주 짧은 시간 간격으로 호출하겠지만 시스템에서는 그 사이에도 스레드가 생성된다는 점이다. 따라서 바로 이어서 호출하는 두 번째 호출이 실패할 수도 있다. <리스트 7>의 프로그램은 이런 문제를 해결하기 위해서 반복적으로 성공할 때까지 호출하도록 되어 있다. 또한 NtQuerySystemInformation 함수에서 제공받은 버퍼 보다 NTQSI_BUFFER_MARGIN만큼 더 크게 할당해서 재 호출을 하는데, 이렇게 하는 이유는 NtQuerySystemInformation 함수의 호출 회수를 줄이기 위함이다.

리스트 7 thread2 프로그램

#include <windows.h>
#include <winternl.h>

#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
#define XGetPtr(base, offset) ((PVOID)((ULONG_PTR) (base) + (ULONG_PTR) (offset)))

typedef 
NTSTATUS 
(WINAPI *NtQuerySystemInformationT)
(SYSTEM_INFORMATION_CLASS, PVOID, ULONG, PULONG);

template <class T>
class Visitor
{
public:
    virtual BOOL Visit(T &data) = 0;
};

typedef Visitor<RSYSTEM_THREAD_INFORMATION> ThreadVisitor;

BOOL NtqsiEnumThreads(ThreadVisitor &visitor)
{
    HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
    NtQuerySystemInformationT pNTQSI;
    pNTQSI = (NtQuerySystemInformationT) 
             GetProcAddress(ntdll, "NtQuerySystemInformation");

    const int NTQSI_MAX_TRY = 20;
    const ULONG NTQSI_BUFFER_MARGIN = 4096;
    const ULONG NTQSI_BUFFER_INIT_SIZE = 200000;

    ULONG buffer_size = NTQSI_BUFFER_INIT_SIZE;
    PUCHAR buffer = new UCHAR[buffer_size];
    ULONG req_size;
    NTSTATUS s;

    for(int i=0; i<NTQSI_MAX_TRY; ++i)
    {
        s = pNTQSI(SystemProcessInformation, buffer, buffer_size, &req_size);
        if(NT_SUCCESS(s))
            break;

        if(buffer)
            delete [] buffer;

        if(s == STATUS_INFO_LENGTH_MISMATCH)
        {
            buffer_size = req_size + NTQSI_BUFFER_MARGIN;
            buffer = new UCHAR[buffer_size];
        }
        else
        {
            return FALSE;
        }
    }

    PRSYSTEM_PROCESS_INFORMATION p = (PRSYSTEM_PROCESS_INFORMATION) buffer;
    
    while(p->NextEntryOffset != 0)
    {
        PRSYSTEM_THREAD_INFORMATION t;
        t = (PRSYSTEM_THREAD_INFORMATION) XGetPtr(p, sizeof(*p));
        for(ULONG i=0; i<p->NumberOfThreads; ++i)
        {
            try
            {
                if(!visitor.Visit(t[i]))
                {
                    if(buffer)
                        delete [] buffer;
                    
                    return TRUE;
                }
            }
            catch(...)
            {
                if(buffer)
                    delete [] buffer;

                return FALSE;
            }
        }

        p = (PRSYSTEM_PROCESS_INFORMATION) XGetPtr(p, p->NextEntryOffset);
    } 

    if(buffer)
        delete [] buffer;

    return TRUE;
}

class ThreadPrinter : public ThreadVisitor
{
public:
    virtual BOOL Visit(RSYSTEM_THREAD_INFORMATION &data)
    {
        printf("PID => %5d TID => %5d\n"
                , data.ClientId.UniqueProcess
                , data.ClientId.UniqueThread);

        return TRUE;
    }
};

int main()
{
    ThreadPrinter printer;
    NtqsiEnumThreads(printer);
    return 0;
}

스레드 시작 주소 구하기

시스템 프로그래밍, 특히나 보안 프로그래밍을 하면 특정 스레드의 시작 주소를 구하는 일이 자주 있다. 스레드의 시작 주소를 통해서 해당 스레드의 보다 많은 특성에 대해서 알 수 있기 때문이다. 이럼에도 불구하고 안타깝게도 윈도우는 아직까지도 스레드 시작 주소를 구하는 편리한 방법을 제공하지 않는다.

아마 똑똑한 독자라면 앞서 살펴본 RSYSTEM_THREAD_INFORMATION 구조체의 StartAddress 필드를 기억하고는 NtqsiEnumThreads 함수를 사용해서 <리스트 8>에 나온 것과 같은 코드를 만들면 된다고 생각할지도 모르겠다. 하지만 실제로 <리스트 8>의 예제 코드를 수행해 보면 <화면 5>에 나온 것과 같이 우리가 원하는 스레드 시작 주소가 출력되지 않는 것을 알 수 있다.

리스트 8 NtqsiGetThreadStartAddress 함수

class StartAddressFinder : public ThreadVisitor
{
public:
    HANDLE target_;
    PVOID address_;

    StartAddressFinder(ULONG tid)
    {
        target_ = (HANDLE) tid;
        address_ = NULL;
    }

    virtual BOOL Visit(RSYSTEM_THREAD_INFORMATION &data)
    {
        if(data.ClientId.UniqueThread == target_)
        {
            address_ = data.StartAddress;
            return FALSE;
        }

        return TRUE;
    }
};

BOOL NtqsiGetThreadStartAddress(ULONG tid, PVOID *address)
{
    StartAddressFinder finder(tid);
    EnumThreads(finder);
    
    if(finder.address_)
    {
        *address = finder.address_;
        return TRUE;
    }

    return FALSE;
}

ULONG
CALLBACK
DummyThread(PVOID)
{
    Sleep(3000);
    return 0;
}

int main()
{
    ULONG tid;
    HANDLE thread;

    thread = CreateThread(NULL, 0, DummyThread, NULL, 0, &tid);
    if(thread)
    {
        PVOID address = NULL;
        if(GetThreadStartAddress(tid, &address))
            printf("DummyThread = %p, StartAddress = %p\n", DummyThread, address);

        if(WaitForSingleObject(thread, INFINITE) != WAIT_OBJECT_0)
            TerminateThread(thread, 0);

        CloseHandle(thread);
    }

    return 0;
}

         md 2
화면 5 NtqsiGetThreadStartAddress 함수 수행 결과

왜 이렇게 엉뚱한 값이 출력된 것일까? 그 이유를 알기 위해서는 윈도우 내부 구조에 대해서 알아야 한다. 윈도우 2000 이후로 모든 윈도우 운영체제는 NT 커널로 되어 있다. 지금은 대부분의 개발자가 NT 커널이 윈도우 커널이라고 생각하지만 사실 NT 커널은 윈도우뿐만 아니라 OS/2, POSIX같은 다른 환경도 수행할 수 있는 별도의 커널이다. 즉, NT 커널에서 윈도우는 하나의 표현 계층에 지나지 않는다. 이번 시간에 살펴본 ToolHelp 라이브러리와 같은 윈도우 API를 Win32 API라고 부르고, 뒤에 살펴본 NtQuerySystemInformation과 같은 함수들은 윈도우 네이티브 API 또는 NT API라고 부른다. 실제로 Win32 API는 이러한 네이티브 API를 조합해서 만든 것이다.

NT 커널은 내부적으로 스레드를 관리하기 위해서 ETHREAD라는 구조체 리스트를 사용한다. windbg를 통해서 해당 구조체를 덤프해보면 <리스트 9>에 나와 있는 것과 같이 구성된 것을 알 수 있다. 구조체를 살펴보면 스레드 시작 주소와 관련해서 두 개의 필드가 존재함을 알 수 있다. 하나는 0x218 오프셋에 위치한 StartAddress이고, 다른 하나는 0x260 오프셋에 위치한 Win32StartAddress다.

리스트 9 ETHREAD 구조체

3: kd> dt nt!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x200 CreateTime       : _LARGE_INTEGER
   +0x208 ExitTime         : _LARGE_INTEGER
   +0x208 KeyedWaitChain   : _LIST_ENTRY
   +0x210 ExitStatus       : Int4B
   +0x214 PostBlockList    : _LIST_ENTRY
   +0x214 ForwardLinkShadow : Ptr32 Void
   +0x218 StartAddress     : Ptr32 Void
   +0x21c TerminationPort  : Ptr32 _TERMINATION_PORT
   +0x21c ReaperLink       : Ptr32 _ETHREAD
   +0x21c KeyedWaitValue   : Ptr32 Void
   +0x220 ActiveTimerListLock : Uint4B
   +0x224 ActiveTimerListHead : _LIST_ENTRY
   +0x22c Cid              : _CLIENT_ID
   +0x234 KeyedWaitSemaphore : _KSEMAPHORE
   +0x234 AlpcWaitSemaphore : _KSEMAPHORE
   +0x248 ClientSecurity   : _PS_CLIENT_SECURITY_CONTEXT
   +0x24c IrpList          : _LIST_ENTRY
   +0x254 TopLevelIrp      : Uint4B
   +0x258 DeviceToVerify   : Ptr32 _DEVICE_OBJECT
   +0x25c CpuQuotaApc      : Ptr32 _PSP_CPU_QUOTA_APC
   +0x260 Win32StartAddress : Ptr32 Void
   +0x264 LegacyPowerObject : Ptr32 Void
   +0x268 ThreadListEntry  : _LIST_ENTRY

<리스트 8>의 코드는 이 두 가지 필드 중에서 0x218에 위치한 StartAddress를 출력한 것이다. 그런데 실제로 일반적으로 우리가 필요로 하는 주소는 0x260에 위차한 Win32StartAddress다.

그렇다면 윈도우는 왜 스레드 시작 주소에 대해서 두 가지 주소를 저장하고 있는 것일까? 이는 앞서 설명한 것과 같이 NT와 Win32의 계층 구조 때문에다. 우리는 스레드를 생성하기 위해서 CreateThread라는 Win32 API를 사용한다. CreateThread는 내부적으로 실제로 스레드를 생성하기 위해서 NtCreateThread라는 네이티브 API를 호출한다. 여기서 우리가 CreateThread로 전달한 시작 주소가 Win32StartAddress가 되고, CreateThread가 내부적으로 NtCreateThread를 호출하면서 전달한 시작 주소가 StartAddress가 된다.

이런 설명이 윈도우 XP까지는 유효하지만 그 이후의 윈도우부터는 구조가 좀 변경됐다. Vista부터는 CreateThread 함수는 단순히 NtCreateThreadEx 함수의 래퍼 역할만 하고 있으며, 실제로 모든 처리를 NtCreateThreadEx 함수에서 모두 수행한다. NtCreateThreadEx 함수는 이전에 CreateThread가 수행하던 역할을 동일하게 수행한다. 별도의 래퍼함수 주소를 만들어서 NT 커널에 전달하는 것이다. 따라서 이 경우에도 계층이 명확하지는 않지만 Win32StartAddress는 우리가 전달한 함수 주소가 되고 StartAddress는 NtCreateThreadEx 함수가 임의로 생성한 주소가 된다.

여기까지 설명을 듣고 나면 “왜 윈도우는 내가 전달한 시작함수가 아닌 다른 함수에서 스레드가 시작되도록 만드는 것일까?”라는 의문이 또 생기게 마련이다. 그 이유는 5월호에 잠깐 설명했던 것과 같이 우리가 만든 스레드 함수가 리턴되면 자동으로 ExitThread가 호출되도록 만들기 위해서다.

복잡하게 설명했는데 결론은 간단하다. 윈도우는 내부적으로 두 개의 스레드 시작 주소를 관리하고 있으며, 우리가 앞서 살펴보았던 NtQuerySystemInformation으로는 우리가 원하는 스레드 시작 주소를 출력할 수 없다는 점이다. 앞서 설명한 것과 같이 이런 목적에 부합하는 Win32 API는 없기 때문에 스레드 시작 주소를 구하기 위해서는 NtQueryInformationThread라는 네이티브 API를 사용해야 한다. <리스트 10>에 NtQueryInformationThread 함수를 사용해서 스레드 시작 주소를 구하는 GetThreadStartAddress 함수 코드가 나와 있다. NtQueryInformationThread 함수 사용 방법은 크게 복잡하지 않기 때문에 직접 코드를 실행해 보면서 사용 방법을 알아 보도록 하자.

리스트 10 GetThreadStartAddress

#include <windows.h>
#include <tlhelp32.h>
#include <winternl.h>

#define ThreadQuerySetWin32StartAddress 9

typedef 
NTSTATUS 
(WINAPI *NtQueryInformationThreadT)
(HANDLE, ULONG, PVOID, ULONG, PULONG);

BOOL
GetThreadStartAddress(ULONG tid, PVOID *address)
{
    HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
    NtQueryInformationThreadT pNTQIT;
    pNTQIT = (NtQueryInformationThreadT) 
             GetProcAddress(ntdll, "NtQueryInformationThread");
    if(!pNTQIT)
        return FALSE;

    HANDLE h = OpenThread(THREAD_QUERY_INFORMATION, FALSE, tid);
    if(!h)
        return FALSE;

    PVOID addr;
    NTSTATUS s;
    
    s = pNTQIT(h
                , ThreadQuerySetWin32StartAddress
                , &addr
                , sizeof(addr)
                , NULL);

    CloseHandle(h);

    if(!NT_SUCCESS(s))
        return FALSE;

    *address = addr;
    return TRUE;
}
@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중