1.  Introdution

특정 악성코드 파일을 찾고자 할 때 우리가 사용할 수 있는 방법은 각 파일의 HASH 값을 비교한다거나 특정 바이너리가 그 파일에 포함되어 있는지 확인하는 등의 방법이 있다. 그렇다면 어떤 악성코드가 다른 정상적인 프로세스에 인젝션하여 동작하는 경우에는 어떻게 진단해야 할까? 파일로 존재하는 악성코드는 파일의 바이너리를 비교하여 쉽게 찾을 수가 있을 것이다. 하지만 파일의 형태가 아닌 프로세스로 메모리에 존재하는 경우는 위와 같은 방법으로 접근할 수 없을 것이다.

위 경우가 실제로 존재할까라고 생각할 수 있지만, 인젝션 방식을 사용하는 악성코드가 꽤 많이 존재한다. 물론 explorer.exe notepa.exe 와 같이 특정 프로세스에만 인젝션 한다면 상대적으로 처리가 쉬워질 것이다. 하지만 특정 프로세스가 아닌 임의의 프로세스에 인젝션 한다면 매우 번거로워질 것이다. 일반적인 사용자의 PC 에서 평소에 돌고 있는 프로세스의 수는 결코 적지 않을뿐더러 각 프로세스가 하나 이상의 스레드를 가진다는 점을 생각하면 문제는 더 복잡해진다. 따라서 이번 문서에서는 이러한 문제를 해결하기 위한 코드를 제작해보려 한다.


2. Body

이번 문서에서의 주 목적은 특정 코드를 가진 스레드를 찾는 것이다. 그러므로 파일을 탐색할 때 FindFirstFile FindNextFile API 를 사용하듯이 스레드를 탐색하는 API 를 사용할 것이다. 윈도우 환경에서 시스템의 스레드를 열거하는 가장 편리한 방법은 바로 ToolHelp 라이브러리를 사용하는 것이다. 해당 라이브러리는 프로세스, 스레드, 모듈 열거와 관련된 라이브러리로 가장 중요한 함수는 바로 CreateToolhelp32Snapshot API 이다. 이 함수는 호출 시점에 시스템 정보에 대한 스냅샷을 만들어주는 역할을 한다. 여기서 dwFlags TH32CS_SNAPTHREAD 를 전달하면 시스템에서 실행되고 있는 스레드 스냅샷을 생성할 수 있다

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

스냅샷 핸들을 통해 스레드 탐색을 시작할 수 있다. Thread32First Thread32Next API 의 인자를 보면 스냅샷 핸들과 THREADENTRY32 형태의 인자를 받는 것을 확인할 수 있다. 해당 구조체는 아래와 같은 구조를 가진다. 구조체에는 스레드 ID 와 스레드가 속한 프로세스의 ID 값까지 존재하고 있는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
typedef struct tagTHREADENTRY32 {
    DWORD dwSize;
    DWORD cntUsage;
    DWORD th32ThreadID;
    DWORD th32OwnerProcessID;
    LONG  tpBasePri;
    LONG  tpDeltaPri;
    DWORD dwFlags;
} THREADENTRY32, *PTHREADENTRY32;
cs

지금까지의 과정을 코드로 나타내면 다음과 같다. 여기서 유의해야 할 점은 Thread32First API 를 호출하기 전에 THREADENTRY dwSize 를 초기화 해주어야 한다. 이를 초기화 해주지 않으면 Thread32First 함수는 실패하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
THREADENTRY32 t32;
HANDLE hSnap;
 
/* Create a Snapshot Handle */
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE)
    return 0;
/* if you do not initialize THREADENTRY32.dwSize, Thread32First API fails */
t32.dwSize = sizeof(THREADENTRY32);
if (Thread32First(hSnap, &t32))
{
    do
    { /* insert code what you want */
        …
    } while (Thread32Next(hSnap, &t32));
}
CloseHandle(hSnap);
 
cs

스레드의 목록과 스레드가 속한 프로세스의 ID 정보를 출력하는 프로그램을 구현할 수 있게 되었다. 하지만 우리는 특정 코드가 포함된 스레드를 찾는 것이 목표이므로 계속해서 알아보자. 다음으로 알아볼 것은 탐색한 스레드의 Start Address 이다. 일반적인 경우 Code Injection 을 수행할 때 VirtualAllocEx 를 통해 할당한 공간에 코드를 기록한 뒤, 이 할당된 공간의 주소를 스레드가 실행하도록 CreateRemoteThread 의 인자로 넘겨준다. 따라서 우리는 스레드의 시작 위치를 알아낸 후 그곳의 코드 및 바이너리를 비교하면 된다.

스레드의 시작 위치를 알아내기 위한 핵심 API Ntdll.dll NtQueryInformationThread 이다. API 는 특정 스레드에 대한 정보를 얻기 위한 API 로 두 번째 인자인 ThreadInformationClass ThreadQuerySetWin32StartAddress(0x9) 를 넘겨주면 세 번째 인자 ThreadInformation 에 스레드의 시작 주소를 반환해준다.

1
2
3
4
5
6
7
8
NTSTATUS WINAPI NtQueryInformationThread(
    _In_      HANDLE            ThreadHandle,
    _In_      THREADINFOCLASS  ThreadInformationClass,
    _Inout_   PVOID              ThreadInformation,
    _In_      ULONG             ThreadInformationLength,
    _Out_opt_ PULONG           ReturnLength
);
 
cs

Ntdll 의 함수를 사용하기 위해 LoadLibrary 를 통해 해당 모듈을 로드하고 GetProcAddress 를 통해 우리가 사용하고자 하는 NtQueryInformationThread 의 주소를 가지고 올 것이다. 다음과 같은 코드로 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef NTSTATUS(WINAPI *NtQueryInfoThread)(HANDLE, ULONG, PVOID, ULONG, PULONG);
 
PVOID ThreadInfo;
ULONG ThreadInfoLength;
PULONG ReturnLength;
NtQueryInfoThread NtQueryInformationThread;
 
HMODULE hNtdll = LoadLibrary("ntdll.dll");
NtQueryInformationThread = (NtQueryInfoThread) GetProcAddress(hNtdll, "NtQueryInformationThread");
 
if (!NtQueryInformationThread)
    return FALSE;
 
/* if THREADINFOCALSS is a ThreadQurtySetWin32StartAddress, return start address of thread */
HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, 0, tid);
NTSTATUS NtStat = NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, &ThreadInfo, sizeof(ThreadInfo), NULL);
 
return TRUE;
 
cs

이제 탐색한 스레드의 시작 주소를 알 수 있으니, 그곳에 있는 데이터를 읽어와 우리가 비교하고자 하는 데이터와 비교해볼 것이다. 이 부분은 오히려 쉽다. 아래 코드와 같이 OpenProcess 를 통해 스레드가 속한 프로세스의 핸들을 얻고 ReadProcessMemory 의 인자로 위에서 얻은 스레드 시작 주소를 넘겨주면 된다. 그리고 한 바이너리씩 비교하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CHAR Buffer[0x100];
CHAR CmpCode[0x100= { '\x6A''\x00''\x68''\x00''\x20', ….};
DWORD NumberofByteRead;
HANDLE hProc = OpenProcess(PROCESS_VM_READ, 0, t32.th32OwnerProcessID);
ReadProcessMemory(hProc, EntryPoint, Buffer, 0x100&NumberofByteRead);
 
for (int i = 0; i < sizeof(CmpCode); i++)
{
    if ((BYTE) CmpCode[i] != (BYTE)Buffer[i])
    {
        return FALSE;
    }
}
return TRUE;
cs

이를 통해 프로그램을 제작한 뒤 테스트를 하기 위해 Reverse_L01.exe 란 프로그램을 선택하였다. 해당 프로그램은 아주 간단한 프로그램으로 본 코드가 0x67 정도밖에 되지 않는다.

이 코드를 기준 바이너리로 하여 각 스레드의 시작 주소에서 0x67 의 크기를 비교할 것이다. 테스트를 위해 해당 프로세스를 8개 실행하였다.

실행 결과 아래 그림과 같이 8개 모두 선별해낸 것을 확인할 수 있다.


3. Conclusion

다소 간단한 코드를 제작해보았다. 하지만 파일로 진단할 수 없는 악성코드의 경우 이 코드를 기반으로 진단 프로그램을 만들 수 있을 것이다. 그렇다면 진단만으로 무엇을 할 수 있을까? 사실 진단은 그에 따른 조치를 취하기 위한 이전 단계라 할 수 있다.

분석한 악성코드의 내용에 따라 해당 프로세스를 종료시키거나 특정 스레드만 걸러내어 ResumeThread 와 같은 API 를 사용하여 동작하지 않도록 할 수 있다. 이러한 추가적인 동작은 추후에 다루어 보자.

악성코드를 다루며 분석에만 집중하는 것도 중요하지만, 실제 기업이나 고객은 조치를 원할 것이다. 따라서 충분한 분석이 이루어졌다면 다른 샘플을 보는 것이 아니라, 진단이나 치료 코드를 제작해보는 것이 매우 큰 도움이 될 것이라 생각한다.


Reference

[+] Microsoft, MSDN API : https://msdn.microsoft.com/

[+] 괴짜 프로그래머의 일상사, “스레드 열거하기” : http://www.jiniya.net/wp/archives/7676

'Reversing > Theory' 카테고리의 다른 글

DoubleAgent 공격  (1) 2017.03.28
암호학 기초 개념  (2) 2016.11.23
Assembly로 보는 코드, strcmp 문자열 비교  (0) 2016.08.08
WFP 무력화  (0) 2016.06.21
DLL이란?  (4) 2016.05.29