CreateRemoteThread를 사용한 DLL 인젝션 구현
- 리버싱/리버싱
- 2020. 3. 18. 23:49
간단한 32bit exe인 '지뢰찾기.exe'에 스레드 내에서 메시지창을 실행시키는 DLL을 제작해 인젝션하는 인젝터를 제작해보려 한다. 지뢰찾기.exe를 사용한 특별한 이유는 없고 32bit 프로그램이라면 어떤것이든 (아주아주 예외적으로 내부에 kernel32.dll을 로드하지 않는 프로그램은 제외) 상관없다.
- 빌드 : Visual Studio 2015
- 환경 : Windows10 x64, Windows7 x32 둘다 됨
인젝션시킬 dll 코드
인젝션시킬 dll 코드는 아래와 같다. dll이 인젝션되면 DllMain이 호출되고 DllMain이 왜 호출되었는지에 대한 인자값인 fdwReason이 DLL_PROCESS_ATTACH이면 (Dll이 프로세스에 로드되면) CreateThread로 ShowMessage라는 스레드를 생성하게된다. 그리고 ShowMessage 스레드는 간단히 메시지창을 띄우는 역할을 한다.
굳이 스레드를 생성하고 MessageBox를 호출한 이유는 리버스코어 페이지 참조 시, 물론 다른 함수를 사용하긴 했지만 직접호출할 때 간혹 hang이 걸리는 경우가 있다고 해서이기도 하고 게임핵을 많이 봤던 것은 아니지만 대부분의 인젝션된 dll들이 dllMain에 주요코드가 있기보다는 스레드를 생성하는 경우가 많았기 때문이다.
#include <stdio.h>
#include <windows.h>
DWORD WINAPI ShowMessage(LPVOID lParam)
{
MessageBox(NULL, TEXT("DLL Injection Success"), TEXT("MessageBox"), MB_OK);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
HANDLE hThread = NULL;
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
hThread = CreateThread(NULL, 0, ShowMessage, NULL, 0, NULL);
CloseHandle(hThread);
}
return TRUE;
}
해당 dll이 제대로 생성됐는지 간단히 테스트해보기 위해 Process Hacker를 사용해서 지뢰찾기.exe에 dll을 inject해보았다. 메시지창이 잘 뜨는 것을 보니 제대로 빌드가 된 것으로 보인다.
dll을 인젝션시키는 코드
다음은 위에서 생성한 dll을 목표 프로세스에 인젝션시키는 코드로 CreateToolhelp32Snapshot과 Process32First/Next로 얻어온 프로세스 정보를 PROCESSENTRY32 구조체에 담고 목표 프로세스 이름과 같은 프로세스일 경우 PID를 얻는다. 참고로 szExeFile이 WCHAR 타입으로 와이트캐릭터이기 때문에 strcmp가 아닌 _tcsicmp를 사용해 주었고 MSDN의 'Taking a Snapshot and Viewing Processes" 페이지가 도움이 됐다.
int main(int argc, char* argv[])
{
HANDLE hSnapshot, hProcess, hMod, hThread;
DWORD PPid = 0;
LPVOID DllBuf, hModAddr;
LPCTSTR DllPath = "C:\\hack.dll";
// 프로세스 정보를 담을 구조체
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
// 프로세스 정보를 얻음
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);
if (Process32First(hSnapshot, &pe32))
{
do
{
//_tprintf(TEXT("PROCESS NAME: %s\n"), pe32.szExeFile);
//목표 프로세스 명와 같은 프로세스일 경우 PID를 얻음
if (!_tcsicmp(pe32.szExeFile, TEXT("지뢰찾기.exe")))
{
printf("[*] Find 지뢰찾기.exe\n");
PPid = pe32.th32ProcessID;
printf("[*] Pid : %d\n\n", PPid);
break;
}
} while (Process32Next(hSnapshot, &pe32));
CloseHandle(hSnapshot);
}
//...
}
목표 프로세스의 PID를 얻었으면 얻은 PID를 이용해 OpenProcess 함수로 목표 프로세스의 핸들을 먼저 얻는다. 그리고 VirtualAllocEx로 목표 프로세스에 hack.dll 경로 길이만큼 메모리를 할당하고 WriteProcessMemory를 사용해 dll 경로를 써준다. 할당한 메모리 주소가 WriteProcessMemory의 2번째 인자로 들어가게 된다. dll 경로를 목표 프로세스의 메모리 공간에 써주었으니 이제 LoadLibraryA("~\hack.dll")을 호출할 일만 남았다.
// 얻은 PID를 이용해 목표 프로세스의 핸들을 구함
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PPid)))
printf("OpenProcess Failed\n");
printf("- Handle : %x\n", hProcess);
// 목표 프로세스 메모리에 dll 크기만큼 메모리 할당
if (!(DllBuf = VirtualAllocEx(hProcess, NULL, lstrlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE)))
printf("VirtualAllocEx Failed\n");
printf("- Virtual Memory : %x\n", DllBuf);
// 할당한 메모리에 hack.dll 경로를 씀
if (!(WriteProcessMemory(hProcess, DllBuf, (LPVOID)DllPath, lstrlen(DllPath) + 1, NULL)))
printf("WriteProcessMemory Failed\n");
GetModuleHandle과 GetProcAddress를 사용해 kernel32.dll의 LoadLibraryA 주소를 구해온다. 이때, 목표 프로세스에서 주소를 구하는 것이 아니라 현재 프로세스에서 구하는 이유는 윈도우 운영체제에서 kernel32.dll이 프로세스마다 같은 주소에 로딩되기 때문이다. 즉, 윈도우는 핵심 dll들은 고유 주소에 로딩될 수 있도록 하는 특징이 있다. 그리고 kernel32.dll 같은 경우 따로 로드를 안해도 로더가 강제로 로드시키기 때문에 무조건 로드가 되어있는 dll이다.
CreateRemoteThread 첫번째 인자에 목표 프로세스의 핸들값, 네번째 인자(스레드 함수 주소)에 LoadLibrary 주소, 다섯번째 인자(스레드 파라미터 주소)에 hack.dll 경로를 써준 메모리 주소를 넘기면 목표 프로세스에 LoadLibraryA("~hack.dll") 스레드가 실행되게 된다.
// kernel32.dll 핸들 얻어옴
if (!(hMod = GetModuleHandle(TEXT("kernel32.dll"))))
printf("GetModuleHandle Failed, error code: %d\n", GetLastError());
printf("- kernel32.dll : %x\n", hMod);
// kernel32.dll의 LoadLibraryA() 주소를 구함
if (!(hModAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryA")))
printf("GetProcAddress Failed, error code: %d\n", GetLastError());
printf("- kernel32.LoadLibraryA : %x\n", hModAddr);
// 목표 프로세스에 스레드를 실행시킴
if (!(hThread = CreateRemoteThread(hProcess, NULL, 0, hModAddr, DllBuf, 0, NULL)))
printf("CreateRemoteThread Failed, error code: %d\n", GetLastError());
WaitForSingleObject(hThread, INFINITE);
printf("- Thread Handle : %x\n\n", hThread);
하지만 한가지 문제가 생겼는데 아래와 같이 호출에 실패한 함수는 없었고 kernel32.dll 핸들값이나 LoadLibraryA의 주소 등 전부 정상이었는데 dll이 인젝션되었다면 떠야 할 메시지창이 뜨지 않았다.
혹시나 해서 dll 경로가 제대로 써져있는지 windbg로 붙어서 확인해봤는데 잘 써져있었다.
CreateRemoteThread 함수 결과로 스레드 핸들값을 잘 얻어는 오는데 인젝션이 안되니 함수 호출 자체는 성공이지만 스레드가 제대로 실행이 안되는 듯 했다. 그래서 계속 구글링을 하던 중 여기서 dll 경로 사이즈계산에 +1을 빼보라고 해서 빼봤더니 제대로 인젝션이 되었다. (이유는 아직 모르겠다)
전체 코드는 아래와 같다.
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
#include <tchar.h>
int main(int argc, char* argv[])
{
HANDLE hSnapshot, hProcess, hMod, hThread;
DWORD PPid = 0;
LPVOID DllBuf, hModAddr;
LPCTSTR DllPath = "C:\\hack.dll";
// 프로세스 정보를 담을 구조체
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32);
// 프로세스 정보를 얻음
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);
if (Process32First(hSnapshot, &pe32))
{
do
{
//_tprintf(TEXT("PROCESS NAME: %s\n"), pe32.szExeFile);
//목표 프로세스 명와 같은 프로세스일 경우 PID를 얻음
if (!_tcsicmp(pe32.szExeFile, TEXT("지뢰찾기.exe")))
{
printf("[*] Find 지뢰찾기.exe\n");
PPid = pe32.th32ProcessID;
printf("[*] Pid : %d\n\n", PPid);
break;
}
} while (Process32Next(hSnapshot, &pe32));
CloseHandle(hSnapshot);
}
// 얻은 PID를 이용해 목표 프로세스의 핸들을 구함
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PPid)))
printf("OpenProcess Failed\n");
printf("- Handle : %x\n", hProcess);
// 목표 프로세스 메모리에 dll 크기만큼 메모리 할당
if (!(DllBuf = VirtualAllocEx(hProcess, NULL, lstrlen(DllPath) + 1, MEM_COMMIT, PAGE_READWRITE)))
printf("VirtualAllocEx Failed\n");
printf("- Virtual Memory : %x\n", DllBuf);
// 할당한 메모리에 hack.dll 경로를 씀
if (!(WriteProcessMemory(hProcess, DllBuf, (LPVOID)DllPath, lstrlen(DllPath) + 1, NULL)))
printf("WriteProcessMemory Failed\n");
// kernel32.dll 핸들 얻어옴
if (!(hMod = GetModuleHandle(TEXT("kernel32.dll"))))
printf("GetModuleHandle Failed, error code: %d\n", GetLastError());
printf("- kernel32.dll : %x\n", hMod);
// kernel32.dll의 LoadLibraryA() 주소를 구함
if (!(hModAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryA")))
printf("GetProcAddress Failed, error code: %d\n", GetLastError());
printf("- kernel32.LoadLibraryA : %x\n", hModAddr);
// 목표 프로세스에 스레드를 실행시킴
if (!(hThread = CreateRemoteThread(hProcess, NULL, 0, hModAddr, DllBuf, 0, NULL)))
printf("CreateRemoteThread Failed, error code: %d\n", GetLastError());
WaitForSingleObject(hThread, INFINITE);
printf("- Thread Handle : %x\n\n", hThread);
CloseHandle(hThread);
CloseHandle(hProcess);
system("pause");
return TRUE;
}
그외 DLL 인젝션 방법
본 포스팅에서는 CreateRemoteThread()를 사용한 DLL Injection 방법을 알아보았는데 SetWindowsHookEx() API를 사용할 수도 있고, 이렇게 API를 사용하는 방법 외에도 AppInit_DLLs/LoadAppInit_DLLs 레지스터를 활용하는 방법도 있다. user32.dll을 로드하는 프로세스의 경우 로딩 시 해당 레지스터 값을 읽어서 값이 존재하면 해당 DLL을 로드하는 방법을 이용한 것인데, AppInit_DLLs에 DLL 경로를 쓰고 LoadAppInit_DLLs 값을 1로 변경한 후 재부팅하면 실행중인 모든 프로세스에 DLL이 인젝션되게 된다.
직접 PE 파일을 패치해서 IMPORT Directory Table 리스트에 dll을 추가시키는 방법도 있다. 그러기 위해선 IMAGE_IMPORT_DESCRIPTOR(IID) 구조체를 하나 더 추가시킬만큼 공간이 충분해야 하고, 충분하지 않다면 빈 영역을 찾아서 IID 구조체 배열 전체를 이동시킨 후 늘어난 크기만큼 섹션의 크기를 증가시키는 등 복잡하다.
참고로, 여기서 제작한 DLL 인젝터를 가지고 코드 후킹, API 후킹, IAT 후킹을 진행해보았는데 관련 내용은 아래 포스팅에서 확인할 수 있다.
관련포스팅
DLL 인젝션을 통한 지뢰찾기 IAT 후킹 (Hooking) 구현
DLL 인젝션을 통한 지뢰찾기 API 후킹 (Hooking) 구현
DLL 인젝션을 통한 지뢰찾기 코드 후킹 (Code Hooking) 구현
'리버싱 > 리버싱' 카테고리의 다른 글
DLL 인젝션을 통한 지뢰찾기 API 후킹 (Hooking) 구현 (4) | 2020.03.22 |
---|---|
DLL 인젝션을 통한 지뢰찾기 코드 후킹 (Code Hooking) 구현 (2) | 2020.03.21 |
닷넷 디컴파일 툴 .Net Reflector, reflexil 추가 및 사용법 (0) | 2020.03.16 |
NtQueryInformationProcess를 이용한 안티디버깅 (0) | 2020.03.14 |
[원데이 취약점] GOM Player 2.0.12.3375 - '.asx' Local Stack Overflow (0) | 2020.03.11 |