security/리버싱핵심원리

CreateRemoteThread()를 이용한 DLL Injection

민사민서 2023. 3. 18. 15:03
// InjectDll.exe

#include "windows.h"
#include "tchar.h"

// MSDN에서 제공
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) {
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

	// 현재 프로세스의 핸들을 가져와 관련된 액세스토큰을 가져옴
    if( !OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
			              &hToken) ) {
        _tprintf(L"OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

	// 로컬 시스템에 대한 LUID를 가져옴.
    if( !LookupPrivilegeValue(NULL,           // lookup privilege on local system
                              lpszPrivilege,  // privilege to lookup 
                              &luid) )  {      // receives LUID of privilege
        _tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError() ); 
        return FALSE; 
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if( bEnablePrivilege )
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // Enable the privilege or disable all privileges.
    if( !AdjustTokenPrivileges(hToken,	// 엑세스토큰 핸들
                               FALSE,	// TRUE:모든 권한 비활성화, FALSE: 모든 권한 활성화
                               &tp,		// TOKEN_PRIVILEGES 구조체 포인터 
                               sizeof(TOKEN_PRIVILEGES), 
                               (PTOKEN_PRIVILEGES) NULL, 
                               (PDWORD) NULL) ) { 
        _tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED ) {
        _tprintf(L"The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) {
	HANDLE hProcess = NULL, hThread = NULL;
	HMODULE hMod = NULL;
	LPVOID pRemoteBuf = NULL;
	DWORD dwBufSize = (DWORD)(_tcslen(szDllPath)+1) * sizeof(TCHAR);
	LPTHREAD_START_ROUTINE pThreadProc;

	// dwPID를 이용해 inject하고자 하는 프로세스의 핸들 구한다
	if( !(hProcess=OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) {
		_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
		return FALSE;
	}
	// 대상 프로세스의 메모리에 szDllPath를 저장할 메모리 할당
	pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
	// 할당받은 메모리에 dll의 경로를 write한다
	WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
	// kernel32.dll의 LoadLibraryW API 주소를 구한다
	hMod = GetModuleHandle(L"kernel32.dll");
	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

	// 대상 프로세스에 원격 스레드 실행 (LoadLibraryW("myhack.dll"); 실행)
	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
	// 해당 스레드 종료시까지 대기
	WaitForSingleObject(hThread, INFINITE);

	// 원격 스레드 종료, 프로세스 핸들 종료
	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}
int _tmain(int argc, TCHAR *argv[]) {
	if(argc!=3) {
		_tprintf(L"USAGE: %s <pid> <DLL path>\n", argv[0]);
		return 1;
	}

	// set "SeDebugPrivilege" privilege
	if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
		return 1;

	if(InjectDll((DWORD)_tstol(argv[1]), argv[2]))
		_tprintf(L"InjectDll(\"%s\") success!!\n", argv[2]);
	else
		_tprintf(L"InjectDll(\"%s\") failed!!\n", argv[2]);

	return 0;
}
// myhack.dll

#include "windows.h"
#include "tchar.h"

#pragma comment(lib, "urlmon.lib")
// 달고나 BOF pdf 다운로드
#define DEF_URL (L"https://t1.daumcdn.net/cfile/tistory/994558495AB2DB330D")
#define DEF_FILE_NAME (L"dalgona_BOF.pdf")
// dll handle
HMODULE g_hMod = NULL;

DWORD WINAPI ThreadProc(LPVOID lParam) {
	TCHAR szPath[_MAX_PATH] = {0,};

	// dll 파일의 경로를 얻는다
	if(!GetModuleFileName(g_hMod, szPath, MAX_PATH))
		return FALSE;

	TCHAR *p = _tcsrchr(szPath, '\\');
	if(!p)
		return FALSE;
	// 파일 저장 경로(절대경로) 생성 (dll 파일과 같은 폴더)
	_tcscpy_s(p+1, _MAX_PATH, DEF_FILE_NAME);

	URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

	return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved) {
	HANDLE hThread = NULL;
	g_hMod = (HMODULE)hinstDll;

	switch(fdwReason) {
	case DLL_PROCESS_ATTACH:
		OutputDebugString(L"myhack.dll Injection!!!");
		hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
		CloseHandle(hThread);
		break;
	}

	return TRUE;
}

 

1. BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) 사용 이유?

- 엑세스토큰의 privilege를 enable.함으로써 프로세스는 이전에는 수행하지 못했던 system-level actions들 수행 가능

- 내 코드가 필요로 하는 privilege에 따라 아래와 같은 privilege constant 들을 enable/disable 할 수 있음!

example of powerful privileges

- MSDN에서 엑세스 토큰의 privilege를 enable/disable하게 해주는 SetPrivilege() 함수를 제공하고 있음

=> LookupPrivilegeValue() 이용해 로컬 시스템이 privilege를 식별하기 위해 사용하는 LUID(locally unique identifier)를 받아오고, bEnablePrivilege 인자에 따라 AdjustTokenPrivileges()를 이용해 privilege를 enable/disable 한다

 

- Windows 운영체제는 Debugging API를 제공하여 다른 프로세스 공간에 접근(/읽기/쓰기)할 수 있게 함

- 대표적인 Debug API에는 VirtualAllocEx(), WriteProcessMemory(), VirtualFreeEx(), ReadProcessMemory() 가 있다.

- InjectDll()에서는 Debugging API를 사용해야 하므로 권한의 문제가 있다

SetPrivilege(SE_DEBUG_NAME, TRUE)

=> 다른 계정이 소유한 프로세스의 메모리를 디버그하고 조정하는 데 필요한 권한SE_DEBUG_NAME을 Enable!!

 

2. CreateThread 함수

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);
// in myhack.cpp
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);

- 호출 프로세스의 가상 주소 공간 내에서 실행할 스레드를 만듭니다

- 인자 설명

1) 반환된 핸들을 자식 프로세스에서 상속할 수 있는지 여부 결정하는 구조체의 포인터, NULL이면 상속 불가

2) 스택의 초기 크기(바이트), 0이면 새 스레드는 실행 파일의 기본 크기를 사용

3) 스레드에서 실행할 애플리케이션 정의 함수에 대한 포인터, 스레드의 시작 주소

4) 스레드에 전달할 매개변수에 대한 포인터

5) 스레드 생성을 제어하는 플래그, 0: 스레드는 만든 직후에 실행됨

6) 스레드 식별자를 받는 변수에 대한 포인터입니다. NULL이면 스레드 식별자가 반환되지 않음

 

- DllMain에서 직접 URLDownloadToFile() 호출해도 되지만 간혹 hang이 걸리는 경우가 있다고 한다

// Hang: 시스템, 네트워크, 어플리케이션이 동작하지 않고 서비스가 응답하지 않는 상태 즉, 시스템 입출력에 대한 반응이 없는 상태로 시스템 운영이 불가능한 상태

// 원인으로는 Thread 혹은 Memory 부족, 리소스 경합이나 DeadLock이 발생하는 경우 등

- 따라서 별도의 스레드를 생성하여 호출하도록 코드 구성

2-1. CreateRemoteThread

HANDLE CreateRemoteThread(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);

다른 프로세스의 가상 주소 공간에서 실행되는 스레드를 만듭니다

- 인자 설명

1) 스레드를 생성하고자 하는 프로세스의 핸들

// 해당 핸들은 PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION,  PROCESS_VM_WRITE, PROCESS_VM_READ 접근 권한을 가져야 함

// InjectDll()에서는 PROCESS_ALL_ACCESS를 통해 프로세스 개체에 대해 가능한 모든 액세스 권한을 가져옴!!

2~7) CreateThread와 동일

2-2. CreateRemoteThread를 이용해 어떻게 원격으로 LoadLibrary("myhack.dll") 호출?

DWORD WINAPI ThreadProc(LPVOID lpParameter);
HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);

- CreateRemoteThread 네 번째 인자로 들어가는 스레드 함수 ThreadProc()과 LoadLibrary() 함수 형태가 동일하다 : 4byte parameter 1개, 4byte 값 리턴

=> 1번째 인자에 대상 프로세스에 대해 모든 액세스 권한 가진 핸들 넘겨주고, 4번째 인자 lpStartAddress에 LoadLibrary() 주소 입력하고, 5번째 인자 lpParameter에 inject하고자 하는 DLL의 경로 문자열 주소를 입력하면

=> 다른 프로세스 공간에서 스레드 함수가 실행된다 = LoadLibrary(DLL경로)가 호출된다 !!!

*  4번째, 5번째 parameter로 inject 대상 프로세스의 메모리 상의 유효한 주소를 입력해야 됨

// InjectDll.exe
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);

2-3. CreateRemoteThread() 이후 WaitForSingleObject(hThread, INFINITE); 사용 이유?

- 스레드 생성 시 CPU를 다른 프로세스와 경쟁하면서 사용, 독립적으로 실행 됨

- 때로는 한 스레드가 다른 스레드의 종료 여부(작업 완료 여부)를 확인해야 할 때가 생김

- 두번째 인자로 밀리초 단위의 값을 넘겨주면, 이 시간 동안 기다리고 그동안 스레드 종료 시 WAIT_OBJECT_O 리턴, 미 종료 시 WAIT_TIMEOUT 리턴, INFINITE 값을 넘겨주면 스레드 종료 시까지 무한 대기

=> InjectDll.exe()에서 특정 프로세스에 원격 스레드를 실행시킴, 실행 종료를 보장하기 위해 사용?

 

 

3. OS 핵심 DLL: 자신만의 고유한 주소에 로딩 보장 (x86)

- 일반적인 DLL 파일의 ImageBase는 기본적으로 0x10000000으로 셋팅됨

- Microsoft에서는 OS 핵심 DLL 파일들의 ImageBase값을 따로 정해두어서 자신들끼리는 절대로 로딩 영역이 겹치지 않고 따라서 DLL Relocation 발생하지 않음

- Windows 운영체제에서 핵심 DLL 파일들은 프로세스마다 같은 주소에 로딩

// InjectDll.exe
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");

- 즉 InjectDll.exe과 inject 대상 프로세스 모두 동일한 주소에 kernel32.dll 로딩될 것 => LoadLibraryW() API 주소 일정

 

4. AdjustTokenPrivileges error: ERROR_NOT_ALL_ASSIGNED ??

You can't grant yourself privileges that you don't already have.

Some other process (with higher privileges) has to grant them to you. 

=> cmd 창을 관리자권한으로 실행 후 하면 된다

 

5. #pragma comment(lib, "xxxx.lib")

명시적인 라이브러리의 링크

여러사람이 같이 수행하는 프로젝트의 경우 이와 같은 방법을 사용하여 lib를 링크하는 것이 라이브러리가 링크되어있다는 사실을 알기에도 좋고 굳이 주석다라 설명할 필요도 없어 좋지 않나 싶다

 

6. WINAPI, CALLBACK

Windows header file에 정의되어있는 매크로, 함수의 호출 규약(calling convention)을 specify하기 위해 쓴다

#ifndef WINAPI
#define WINAPI __stdcall
#endif

#ifndef CALLBACK
#define CALLBACK __stdcall
#endif

 

7. GetModuleFileNameA() 함수

// myhack.dll
GetModuleFileName(g_hMod, szPath, MAX_PATH);
// KeyHook.dll
GetModuleFileName(NULL, szPath, MAX_PATH);

- 분명 메시지 훅 실습 때는 DLL이 inject 된(훅 프로시져가 실행된) 프로세스의 이름을 가져오는 용도로 사용됐는데, 여기선 inject하고자 하는 DLL의 경로를 가져오는 것 같아서..

- 첫번째 인자 HMODULE hModule: 경로를 알고 싶은 프로세스 or 모듈의 핸들

- NULL이면 the path of the executable file of the current process

 

8. L"", TEXT(""), _T("")

- Visual C++ 에서의 문자 처리 방식: 멀티바이트 & 유니코드1) 멀티바이트: 영어는 1바이트, 그 외 문자는 2바이트 // "a" 이렇게 문자 입력2) 유니코드: 모든 문자를 2바이트 처리 // L"a" 이렇게 문자 입력

 

하지만 이렇게 개발해놓으면 유니코드 ↔ 멀티바이트 변경이 쉽지 않다 (외부 라이브러리 호환성 때문에 변경 요할때)

 

1) WinNT.h, windows.h 에서는 TEXT("") 매크로를 #define 해두었다2) tchar.h 에서는 _T("") 매크로를 #define 해두었다 // "tchar.h" include 시 둘 다 사용 가능

 

1) 혹은 2) 매크로를 사용한다면project setting에 따라 narrow character string(ANSI) 혹은 wide character string(UNICODE) 선택 가능

문자 집합 setting 변경하면 됨

=> 윈도우 프로그래밍 할 때 TEXT() 혹은 _T() 매크로를 사용하는 습관을 들이자

 

9. 디버깅...

myhack.dll 사용 위한 다른 모듈들 로딩이 끝나면 EP 등장
__DllMainCRTStartup 내리다보면 DllMain 발견 가능
DllMain의 CreateThread 인자를 통해 ThreadProc 분석 가능