security/리버싱핵심원리

EjectDll.exe 구현

민사민서 2023. 3. 20. 15:42
// EjectDll.exe

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

#define DEF_PROC_NAME L"notepad.exe"
#define DEF_DLL_NAME L"myhack.dll"

// process 이름 가지고 PID 찾기
DWORD FindProcessID(LPCTSTR szProcessName) {
	DWORD dwPID = 0xFFFFFFFF;
	HANDLE hSnapshot = INVALID_HANDLE_VALUE; // 시스템 스냅샷의 핸들
	PROCESSENTRY32 pe; // 프로세스 정보 저장할 구조체

	pe.dwSize = sizeof(PROCESSENTRY32); // 구조체 크기 초기화
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL); // 시스템 스냅샷 생성

	Process32First(hSnapshot, &pe); // 스냅샷에 있는 첫번째 프로세스 정보를 pe에 저장
	do {
		if(!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile)) { // szProcessName과 비교
			dwPID = pe.th32ProcessID;
			break;
		}
	} while(Process32Next(hSnapshot, &pe)); // 다음 프로세스 정보를 pe에 저장

	CloseHandle(hSnapshot);

	return dwPID;
}

// Debug API를 사용하기 위해 엑세스 토큰의 privilege 변경
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;
	}

	if(!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) {
		_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;

	if(!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL)) {
		_tprintf(L"AdjuestTokenPrivileges 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;
}

// CreateRemoteThread를 이용해 대상 프로세스에서 dll eject
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName) {
	HANDLE hSnapshot, hProcess, hThread;
	HMODULE hModule = NULL;
	MODULEENTRY32 me; // 모듈 정보를 저장할 구조체
	LPTHREAD_START_ROUTINE pThreadProc;
	BOOL bMore=FALSE, bFound=FALSE;

	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID); // 프로세스의 모듈에 대한 스냅샷 생성
	me.dwSize = sizeof(MODULEENTRY32); // 구조체 크기 초기화

	bMore = Module32First(hSnapshot, &me);
	for( ; bMore ; bMore = Module32Next(hSnapshot, &me)) {
		// 모듈의 파일 이름과 실행 경로 둘 중 하나라도 szDllName과 일치하는지 확인
		if(!_tcsicmp((LPCTSTR)me.szModule, szDllName) || !_tcsicmp((LPCTSTR)me.szExePath, szDllName)) {
			bFound = TRUE;
			break;
		}
	}

	if(!bFound) {
		CloseHandle(hSnapshot);
		return FALSE;
	}

	if(!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) {
		_tprintf(L"OpenProcess(%d) failed!! [%d]\n", dwPID, GetLastError());
		return FALSE;
	}

	hModule = GetModuleHandle(L"kernel32.dll");
	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);

	WaitForSingleObject(hThread, INFINITE); // 원격 스레드 종료시까지 대기, 종료 후 제어 반환

	CloseHandle(hThread);
	CloseHandle(hProcess);
	CloseHandle(hSnapshot);

	return TRUE;
}

int _tmain(int argc, TCHAR* argv[]) {
	DWORD dwPID = 0xFFFFFFFF;

	dwPID = FindProcessID(DEF_PROC_NAME);
	if(dwPID == 0xFFFFFFFF) {
		_tprintf(L"There is no %s process!\n", DEF_PROC_NAME);
		return 1;
	}
	_tprintf(L"PID of \"%s\" is %d!\n", DEF_PROC_NAME, dwPID);

	if(!SetPrivilege(SE_DEBUG_NAME, TRUE))
		return 1;

	if(EjectDll(dwPID, DEF_DLL_NAME))
		_tprintf(L"EjectDll(%d, \"%s\") success!!\n", dwPID, DEF_DLL_NAME);
	else
		_tprintf(L"EjectDll(%d, \"%s\") failed!!\n", dwPID, DEF_DLL_NAME);

	return 0;
}

 

1. tlhep32.h

- HANDLE CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID)

* 지정된 프로세스에서 사용되는 힙, 모듈 및 스레드의 스냅샷을 만듭니다

* dwFlags: 

TH32CS_SNAPALL 시스템의 모든 프로세스와 스레드와 th32ProcessID에 지정된 프로세스의 힙 및 모듈을 포함합니다. OR 연산(' |')을 사용하여 결합된 TH32CS_SNAPHEAPLIST, TH32CS_SNAPMODULETH32CS_SNAPPROCESS 및 TH32CS_SNAPTHREAD 값을 지정하는 것과 같습니다.
TH32CS_SNAPHEAPLIST (0x00000001) 스냅샷의 th32ProcessID 에 지정된 프로세스의 모든 힙을 포함합니다. 힙을 열거하려면 Heap32ListFirst, Heap32ListNext를 사용합니다.
TH32CS_SNAPMODULE (0x00000008) 스냅샷의 th32ProcessID 에 지정된 프로세스의 모든 모듈을 포함합니다. 모듈을 열거하려면 Module32First, Module32Next를 사용합니다. 
TH32CS_SNAPPROCESS (0x00000002) 스냅샷에 시스템의 모든 프로세스를 포함합니다. 프로세스를 열거하려면 Process32First, Process32Next를 사용합니다.
TH32CS_SNAPTHREAD0x00000004 스냅샷에 시스템의 모든 스레드를 포함합니다. 스레드를 열거하려면 Thread32First, Thread32Next를 사용합니다.

* th32ProcessID: 스냅샷에 포함할 프로세스의 프로세스 식별자, NULL 혹은 0을 주면 모든 프로세스가 스냅샷에 포함

 

- BOOL Process32First([in] HANDLE hSnapshot, [in,out] LPPROCESSENTRY32 lppe)

* 시스템 스냅샷에서 발생한 첫 번째 프로세스에 대한 정보를 검색

* 프로세스 목록의 첫 번째 항목이 버퍼에 복사되었으면 TRUE 를 반환하고 그렇지 않으면 FALSE 를 반환

* 프로세스가 없거나 스냅샷에 프로세스 정보가 없는 경우 GetLastError에서 ERROR_NO_MORE_FILES 오류 값 반환

 

- BOOL Process32Next([in] HANDLE hSnapshot, [in,out] LPPROCESSENTRY32 lppe)

* 시스템 스냅샷에 기록된 다음 프로세스에 대한 정보를 검색

* 프로세스 목록의 다음 항목이 버퍼에 복사되었으면 TRUE 를 반환하고 그렇지 않으면 FALSE 를 반환

* 프로세스가 없거나 스냅샷에 프로세스 정보가 없는 경우 GetLastError에서 ERROR_NO_MORE_FILES 오류 값 반환

 

- Module32First, Module32Next도 동일 (두번째 인자가 MODULEENTRY 구조체 포인터)

 

- PROCESSENTRY32 구조체

DWORD dwSize 구조체의 크기(바이트), dwSize를 초기화하지 않으면 Process32First가 실패
DWORD th32ProcessID 프로세스 식별자
DWORD cntThreads 프로세스에서 시작한 실행 스레드 수
DWORD th32ParentProcessID 이 프로세스를 만든 프로세스의 식별자(부모 프로세스)
CHAR szExeFile[MAX_PATH] 프로세스에 대한 실행 파일의 이름 // CHAR 배열이라 (LPCTSTR) cast 필요

 

- MODULEENTRY32 구조체

DWORD dwSize 구조체의 크기(바이트), 
dwSize를 초기화하지 않으면 Module32First가 실패
DWORD th32ProcessID 모듈을 검사할 프로세스의 식별자
BYTE *modBaseAddr 소유 프로세스의 컨텍스트에서 모듈의 기본 주소
DWORD modBaseSize 모듈의 크기(바이트)
HANDLE hModule 소유 프로세스의 컨텍스트에서 모듈에 대한 핸들
char szModule[MAX_MODULE_NAME32+1] 모듈 이름
char szExePath[MAX_PATH] 모듈 경로

 

2. 코드 변경?

- szDllName comparison

// in EjectDll()
if(!_tcsicmp((LPCTSTR)me.szModule, szDllName) || !_tcsicmp((LPCTSTR)me.szExePath, szDllName)) {
	bFound = TRUE;
	break;
}

- me.szExePath (전체경로)와의 비교를 생략해도 Ejection 잘 동작하지만

- me.szModule (모듈명)과의 비교를 생략하면 Ejection 실패함

=> szDllName의 다양한 형태(전체경로 or 파일명)에 대응하기 위한 구현이라고 볼 수 있다

 

- DEF_PROC_NAME 과 DEF_DLL_NAME을 user input으로 받도록 구현

int _tmain(int argc, TCHAR* argv[]) {
	if(argc != 3) {
		_tprintf(L"USAGE: %s <Process Name> <Dll Name>\n", argv[0]);
		_tprintf(L"You can give us either name or full path!\n");
		return 1;
	}

	DWORD dwPID = 0xFFFFFFFF;
	dwPID = FindProcessID(argv[1]);
	if(dwPID == 0xFFFFFFFF) {
		_tprintf(L"There is no %s process!\n", argv[1]);
		return 1;
	}
	_tprintf(L"PID of \"%s\" is %d!\n", argv[1], dwPID);

	if(!SetPrivilege(SE_DEBUG_NAME, TRUE))
		return 1;

	if(EjectDll(dwPID, argv[2]))
		_tprintf(L"EjectDll(%d, \"%s\") success!!\n", dwPID, argv[2]);
	else
		_tprintf(L"EjectDll(%d, \"%s\") failed!!\n", dwPID, argv[2]);

	return 0;
}

- argv[1]에는 Dll Ejection을 하고싶은 프로세스의 이름을 건네주어야 되지만

- argv[2]에는 Dll Name / Dll Path 둘 중 어느 것을 건네주어도 상관 없다 (Comparison 둘 다 고려)

 

3. 강제로 인젝션한 Dll에 대해서만 FreeLibrary()를 이용한 언로딩 가능하다던데?

 

EjectDll.exe를 사용하여 DLL을 제거할 때 FreeLibrary 함수를 호출합니다. 이 함수는 DLL의 참조 카운터를 감소시키고, 참조 카운터가 0이 되면 DLL을 메모리에서 언로드합니다. 그러나 이미 로드된 DLL이 PE 파일(실행 파일)에 의해 사용되고 있으면, 참조 카운터가 0보다 크기 때문에 FreeLibrary 호출로 DLL을 언로드할 수 없습니다.

1. 이미 로드된 DLL은 실행 파일에 의해 사용되고 있어 참조 카운터가 0이 아닙니다. (1보다 클 것)
2. FreeLibrary 함수는 참조 카운터가 0이 될 때까지 DLL을 언로드하지 않습니다.
3. 따라서 이미 로드된 DLL에 대해서는 FreeLibrary를 사용하여 언로드할 수 없습니다.