security/리버싱핵심원리

[미완][API 후킹] 디버그(Debug) 기법을 이용한 메모장 WriteFile() 후킹

민사민서 2023. 3. 25. 21:01

Target API 확인

- Process Monitor로 Notepad.exe에서 파일을 저장하는 순간을 확인해보자

- 열려있는 notepad.exe의 PID, 저장할 파일의 이름을 필터에 세팅 후 저장 진행해보면

- 어떤 API들의 호출이 발생했는지 확인 가능 => kernel32.dll!WriteFile() API 의심 가능!

BOOL WriteFile(
  [in]                HANDLE       hFile,
  [in]                LPCVOID      lpBuffer, // 파일에 쓸 데이터를 포함하는 버퍼 포인터
  [in]                DWORD        nNumberOfBytesToWrite, // 파일에 쓸 바이트 수
  [out, optional]     LPDWORD      lpNumberOfBytesWritten,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

- 검증: Ollydbg에서 notepad 프로세스를 열고 WriteFile() API에 bp를 걸고 달려보자

- CPU 탭 - 우클릭 - Select module - notepad 선택 후 Search for intermodular calls - find WriteFile - bp setting

* notepad.exe에서 호출하는 API에 관심이 있기 때문에 notepad 모듈을 선택해야 한다

* 만약 kernel32.dll 모듈을 선택하고 해당 모듈의 WriteFile export 함수에 bp를 걸면 다른 모든 모듈에서 WriteFile을 임포트  할 때마다 bp에 걸려 분석이 힘들다

=> notepad에서 저장 시 WriteFile API가 호출되고, 두번째 인자 buffer에 데이터가 들어있다

 

코드 작성

// hookdbg.exe
#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"

LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;

// API 시작 부분에 hook 설치 (0xCC)
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
	// WriteFile() API 주소 구하기 (디버거 프로세스의 API 주소 = 디버기도 동일)
	g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

	// API hook setting, 첫바이트 백업 후 0xCC로 변경
	memcpy(&g_cpdi, &(pde->u.CreateProcessInfo), sizeof(CREATE_PROCESS_DEBUG_INFO));
	ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
	WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);

	return TRUE;
}

// hook 제거, 원래 API 호출, User 원하는 동작, 다시 훅 설치
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
	CONTEXT ctx;
	PBYTE lpBuffer = NULL;
	DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
	PEXCEPTION_RECORD per = &(pde->u.Exception.ExceptionRecord);

	// BP exception이고 exception 발생한 위치가 WriteFile API 주소인 경우
	if(per->ExceptionCode == EXCEPTION_BREAKPOINT && per->ExceptionAddress == g_pfWriteFile) {
		// 1. Unhook
		WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
		// 2. Thread context 구하기
		ctx.ContextFlags = CONTEXT_CONTROL;
		GetThreadContext(g_cpdi.hThread, &ctx);
		// 3. WriteFile의 2nd(lpBuffer), 3rd(NumOfBytesToWrite) parameter 구하기
		// 현재 ESP=RET, ESP+4=1st param, ESP+8=2nd param, ESP+C=3rd param
		ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
		ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0xC), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);
		// 4. 임시 버퍼 할당
		lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
		memset(lpBuffer, 0,dwNumOfBytesToWrite+1);
		// 5. WriteFile의 버퍼를 임시 버퍼에 복사
		ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
		printf("\n### original string: %s ###\n", lpBuffer);
		// 6. 소문자 → 대문자 변환
		for(i=0; i<dwNumOfBytesToWrite; i++) {
			if(lpBuffer[i]>=97 && lpBuffer[i]<=122)
				lpBuffer[i] -= 32;
		}
		printf("\n### converted string: %s ###\n", lpBuffer);
		// 7. 임시 버퍼를 WriteFile의 버퍼에 복사
		WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
		// 8. 임시 버퍼 해제
		free(lpBuffer);
		lpBuffer = NULL;
		// 9. Thread context의 EIP 1 감소 (WriteFile 시작 부분으로)
		ctx.Eip = (DWORD)g_pfWriteFile;
		SetThreadContext(g_cpdi.hThread, &ctx);
		// 10. Debuggee 프로세스 진행시킴
		ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
		Sleep(0);
		// 11. Hook
		WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);

		return TRUE;
	}
    // Handle the 0x000006BA exception
	else if(per->ExceptionCode == 0x000006BA) {
		// 디버거에서 프로세스로 exception 다시 넘겨줘서 SEH가 처리하도록
		ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
		return TRUE;
	}
	return FALSE;
}

void DebugLoop() {
	DEBUG_EVENT de;
	DWORD dwContinueStatus;

	// Debuggee로부터 event 기다리다가 발생 시 de 변수에 정보 setting 후 리턴
	while( WaitForDebugEvent(&de, INFINITE) ) {
		dwContinueStatus = DBG_CONTINUE;
		// Debuggee 프로세스 생성 혹은 attach 이벤트
		if(de.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
			OnCreateProcessDebugEvent(&de);
		}
		// 예외 이벤트
		else if(de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
			if(OnExceptionDebugEvent(&de))
				continue;
		}
		// Debuggee 프로세스 종료 이벤트
		else if(de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
			// Debuggee 종료 - Debugger 종료
			break;
		}
		// Debuggee의 실행 재개
		ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
	}
}

BOOL IsNotepadProcess(DWORD dwPID) {
	BOOL found = FALSE;
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if(hSnapshot != INVALID_HANDLE_VALUE) {
		PROCESSENTRY32 pe;
		pe.dwSize = sizeof(PROCESSENTRY32);
		if(Process32First(hSnapshot, &pe)) {
			do {
				// pe.szExeFile(프로세스 이름) 비교는 무조건 unicode 비교로!
				// _stricmp로 멀티바이트 비교했더니 notepad.exe 프로세스도 FALSE라 뜸
				if(pe.th32ProcessID==dwPID && !_wcsicmp(pe.szExeFile, L"notepad.exe")) {
					found = TRUE;
					break;
				}
			} while(Process32Next(hSnapshot, &pe));
		}
		CloseHandle(hSnapshot);
	}
	return found;
}

int main(int argc, char* argv[]) {
	DWORD dwPID;
	if(argc != 2) {
		printf("USAGE: %s <PID> \n", argv[0]);
		return 1;
	}

	dwPID = atoi(argv[1]);
	// check if dwPID's process is notepad.exe
	if( !IsNotepadProcess(dwPID) ) {
		printf("Given PID(%d) is not the one of notepad.exe!!\n", dwPID);
		return 1;
	}
	printf("Yes, Given PID(%d) is one of notepad.exe!!\n", dwPID);

	// 실행중인 프로세스에 debugger를 attach
	if( !DebugActiveProcess(dwPID) ) {
		printf("DebugActiveProcess(%d) failed!!!\nError Code = %d\n", dwPID, GetLastError());
		return 1;
	}

	// debugger loop
	DebugLoop();

	return 0;
}

- Dll Ejection 챕터를 참고해 notepad의 PID인지 확인하는 IsNotepadProcess() 함수를 추가했다

- 파일탐색기 사용 시 exception 발생해 먹통이 되어서 ollydbg로 exception code 파악 후 OnExceptionDebugEvent에 핸들링 코드를 추가했다

   * CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32 등등...

   * pe.szExeFile을 통해 프로세스 이름을 비교할 때 Unicode 비교를 해야 한다!! (안 그럼 notepad 프로세스도 실패 ㅋㅋ)

- BOOL WINAPI DebugActiveProcess(DWORD dwPID)를 이용해 실행 중인 프로세스에 attach하여 디버깅 시작

   * 디버거는 디버기의 레지스터/메모리에 대한 모든 접근 권한 가짐

   * ReadProcessMemory, WriteProcessMemory 등등 모두 가능함!!!

 

주요 API들 / 구조체들

<DebugLoop()>

DEBUG_EVENT 구조체

typedef struct _DEBUG_EVENT {
  DWORD dwDebugEventCode; // 디버그 이벤트 식별 코드
  DWORD dwProcessId; // 디커깅 이벤트 발생한 프로세스의 PID
  DWORD dwThreadId; // 디버깅 이벤트 발생한 스레드 식별자
  union {
    EXCEPTION_DEBUG_INFO      Exception;
    CREATE_THREAD_DEBUG_INFO  CreateThread;
    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
    EXIT_THREAD_DEBUG_INFO    ExitThread;
    EXIT_PROCESS_DEBUG_INFO   ExitProcess;
    LOAD_DLL_DEBUG_INFO       LoadDll;
    UNLOAD_DLL_DEBUG_INFO     UnloadDll;
    OUTPUT_DEBUG_STRING_INFO  DebugString;
    RIP_INFO                  RipInfo;
  } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

 

- dwDebugEventCode에 따라 적절한 u의 멤버가 세팅된다
=> EXCEPTION_DEBUG_EVENT일 때 u.Exception에 EXCEPTION_DEBUG_INFO 구조체 세팅

=> CREATE_PROCESS_DEBUG_EVENT일 때 u.CreateProcessInfo에 CREATE_PROCESS_DEBUG_INFO 구조체 세팅

EXCEPTION_DEBUG_INFO 구조체 & EXCEPTION_RECORD 구조체

typedef struct _EXCEPTION_DEBUG_INFO {
  EXCEPTION_RECORD ExceptionRecord;
  DWORD            dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

- 첫번째 인자 ExceptionRecord에 예외와 관련된 정보가 세팅된다

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;
  DWORD                    ExceptionFlags;
  struct _EXCEPTION_RECORD *ExceptionRecord;
  PVOID                    ExceptionAddress;
  DWORD                    NumberParameters;
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

- ExceptionCode: 예외 발생한 이유 (ex. EXCEPTION_BREAKPOINT, EXCEPTION_ACCESS_VIOLATION)

- ExceptionAddress: 예외가 발생한 주소

WaitForDebugEvent() & ContinueDebugEvent()

BOOL WaitForDebugEvent(
  [out] LPDEBUG_EVENT lpDebugEvent,
  [in]  DWORD         dwMilliseconds
);

- INFINITE인 경우 디버깅 이벤트가 발생할 때까지 기다렸다가 de에 이벤트 정보를 저장하고 리턴

BOOL ContinueDebugEvent(
  [in] DWORD dwProcessId,
  [in] DWORD dwThreadId,
  [in] DWORD dwContinueStatus
);

- 디버거가 디버깅 이벤트가 발생한 스레드를 계속할 수 있도록(실행 재개) 하는 함수

- 프로세스&스레드 식별자를 건네주고 DBG_CONTINUE로 status 세팅

 

<OnCreateProcessDebugEvent()>

CREATE_PROCESS_DEBUG_INFO 구조체

typedef struct _CREATE_PROCESS_DEBUG_INFO {
  HANDLE                 hFile;
  HANDLE                 hProcess;
  HANDLE                 hThread;
  LPVOID                 lpBaseOfImage;
  DWORD                  dwDebugInfoFileOffset;
  DWORD                  nDebugInfoSize;
  LPVOID                 lpThreadLocalBase;
  LPTHREAD_START_ROUTINE lpStartAddress;
  LPVOID                 lpImageName;
  WORD                   fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;

- hProcess: 프로세스에 대한 핸들, 이 핸들을 이용해 디버거는 프로세스의 메모리 읽고 쓸 수 있다

- hThread: hProcess가 가리키는 프로세스의 초기(첫번째) 스레드

 

<OnExceptionDebugEvent()>

CONTEXT 구조체

typedef struct _CONTEXT {
  ULONG              ContextFlags;
  ULONG              Dr0;
  ULONG              Dr1;
  ULONG              Dr2;
  ULONG              Dr3;
  ULONG              Dr6;
  ULONG              Dr7;
  FLOATING_SAVE_AREA FloatSave;
  ULONG              SegGs;
  ULONG              SegFs;
  ULONG              SegEs;
  ULONG              SegDs;
  ULONG              Edi;
  ULONG              Esi;
  ULONG              Ebx;
  ULONG              Edx;
  ULONG              Ecx;
  ULONG              Eax;
  ULONG              Ebp;
  ULONG              Eip;
  ULONG              SegCs;
  ULONG              EFlags;
  ULONG              Esp;
  ULONG              SegSs;
  UCHAR              ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

- ContextFlags: bitmask that determines which registers are saved in the CONTEXT structure. By specifying different values, you can choose to save only specific parts of the processor state

=> CONTEXT_CONTROL is minimal, it only records the stack and instruction pointer registers 

=> RIP, RSP, RBP, EFLAGS, SegCs, SegSs, SegDs, SegEs, SegFs, and SegGs 등

GetThreadContext() & SetThreadContext()

BOOL GetThreadContext(
  [in]      HANDLE    hThread,
  [in, out] LPCONTEXT lpContext
);
BOOL SetThreadContext(
  [in] HANDLE        hThread,
  [in] const CONTEXT *lpContext
);

- 지정된 스레드(여기서는 디버그 이벤트가 발생한 프로세스의 스레드)의 context를 가져오거나 세팅한다

 

kernel32.dll vs KERNELBASE.dll ??

초반에 kernel32.dll 모듈을 선택해놓고 Search for - Names으로 WriteFile API를 찾아보았더니

1) Import type으로는 kernelbase.dll의 API가 나오고

2) Export type으로는 kernel32.dll의 API 나옴

 => kernel32.dll!WriteFile 내부에서 KERNELBASE.dll!WriteFile 호출하는 식

 

kernelbase.dll은 Windows 운영 체제의 핵심 구성 요소로서, 기본 시스템 기능과 낮은 수준의 작업을 처리하는 데 사용됩니다. 반면에 kernel32.dll은 Windows 응용 프로그램과 상호 작용하는 데 사용되는 고수준 API를 제공합니다.

대부분의 경우 kernel32.dll의 API는 내부적으로 kernelbase.dll의 API를 호출합니다. 이렇게 함으로써 응용 프로그램 개발자들은 kernel32.dll을 사용하여 시스템 리소스와 상호 작용할 수 있으며, kernelbase.dll이 그 아래의 낮은 수준 작업을 처리하게 됩니다.

 

API hooking 된 상태에서 notepad 파일 저장할때 "내컴퓨터" 폴더 선택하면 먹통!!

"내 컴퓨터"는 일반 폴더랑 달리 네임스페이스 계층 구조의 루트를 나타내는 가상 폴더로 사용 가능한 드라이브와 일부 시스템 폴더를 표시합니다.

일반적으로, "내 컴퓨터" 폴더는 파일을 직접 저장하기 위한 목적으로 사용되지 않으며, 실제 드라이브 또는 폴더에 파일을 저장해야 합니다. 따라서 hookdbg.exe를 실행하지 않을 때에는 정상적으로 "내 컴퓨터" 폴더에 접근할 수 있지만, 실행 중일 때는 예기치 않은 동작이 발생할 수 있습니다.

=> "C:", "D:", "문서" 또는 기타 일반 디렉토리와 같은 특정 드라이브 또는 폴더에 파일 저장 시에는 문제 없더라~

=> exception handling 하면 된다!!

 

디버깅 혹은 API 후킹 시 파일탐색기에서 문제 생긴다

- <Ollydbg 상에서> 또는 <hookdbg.exe의 디버그 기법을 이용한 API 후킹을 한 상태>에서 파일 저장을 하려고 하면

- 파일 탐색기 상에서 상단에 특정 경로를 입력하하거나 왼쪽 탭의 '컴퓨터' 디렉토리를 선택하려고 하면

- 프로세스가 먹통이 되면서 아래와 같이 exception이 발생한다. 

KERNELBASE.RaiseException에서 멈춤

- Exception code 0x000006BA - exception is non-continuable (RPC_S_SERVER_UNAVAILABLE) 발생!

- It indicates that the remote procedure call (RPC) server is unavailable. In the context of your debugging scenario, it's possible that the breakpoint you set in OllyDbg is interfering with the normal operation of notepad.exe or the system libraries it depends on, which might involve RPC calls. As a result, an exception is raised, and the program fails to execute correctly.

- Shift+F7/F8/F9로 원래 프로세스에게 제대로 exception 넘겨주니 잘 동작하더라

=> 원래 코드에서도 OnExceptionDebugEvent에서 per->ExceptionCode에 따라 적절한 error handling을 하면 되겠다

else if(per->ExceptionCode == 0x000006BA) {
	// 디버거에서 프로세스로 exception 다시 넘겨줘서 SEH가 처리하도록
	ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
	return TRUE;
}

- 해당 exception을 처리하지 않은 채 무시하고 진행하는 게 아니라, 프로세스의 SEH에 의해 처리되게 하고 싶으므로 DBG_CONTINUE가 아니라 DBG_EXCEPTION_NOT_HANDLED 사용!

인자로 TCHAR* argv[] 받아와 사용할 땐 _tmain으로 세팅하자

이거땜에 1시간 삽품 ㅋㅋㅋㅋ

DWORD FindProcessID(LPCTSTR procName) {
	DWORD dwPID = 0xFFFFFFFF;
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if(hSnapshot != INVALID_HANDLE_VALUE) {
		PROCESSENTRY32 pe; // PROCESSENTRY32W
		pe.dwSize = sizeof(PROCESSENTRY32);
		if(Process32First(hSnapshot, &pe)) {
			do {
				if(!_tcsicmp(procName, pe.szExeFile)) {
					dwPID = pe.th32ProcessID;
					break;
				}
			} while(Process32Next(hSnapshot, &pe));
		}
		CloseHandle(hSnapshot);
	}
	return dwPID;
}

int _tmain(int argc, TCHAR* argv[]) {
	if(argc != 2) {
		printf("USAGE: %s <Process Name to Hook API> \n", argv[0]);
		return 1;
	}

	DWORD dwPID = FindProcessID(argv[1]);

	if(dwPID==0xFFFFFFFF) {
		printf("Cannot find any process with given name!!!\n");
		return 1;
	}
	printf("Process found!!! PID: <%d>!!!\n", dwPID);

 

1) TCHAR* argv[]로 받아와 FindProcessID 인자로 넘길 때 main이면 작동 안하고, _tmain일때만 잘 작동함

1-1) 인자로 L"notepad.exe" (constant) 넘기면 잘 작동함

=> TCHAR을 main 함수 인자로 받아서 사용하고 싶으면 _tmain을 사용하자

2) main(int argc, char* argv[])로 받아서 FindProcessID 안에서 LPCTSTR 캐스트하는 것도 안된다

3) main(int argv, char* argv[])로 받아서 pe.szExeFile을 LPCSTR로 캐스트한 후에 _stricmp 하는 것도 안된다

=> wide character과 multibyte 간에 type casting할 생각 말고 애초에 올바른 타입으로 인자를 건네주자

 

Notepad++를 후킹하니까 내부 메시지들까지 후킹됨. 어떻게 user input만 바꿀까?

- Notepad++ 내부의 internal message와 data까지 가로채서 가져오는 것 같다

- Notepad++은 XML 기반 파일 형식을 사용해 세팅, 세션정보, 데이터 등을 저장하는데, 이 과정에서 WriteFile() API를 사용하나보네 (이렇게 여러 메시지가 intercept 되는거 보니까)

- WriteFile() API 는 general purpose API임으로 모든 호출을 후킹하는 건 바람직하지 못하다

 

시도1. WriteFile의 인자 hFile을 가져와서 파일 타입, 파일 확장자 확인

- GetFileType()으로 disk file인지 확인, GetFinalPathNameByHandle()로 file extension 확인

#define MAX_FILE_PATH_LEN 260

BOOL isEffectiveExtension(LPCTSTR filePath) {
	LPCTSTR fileExtension[] = {L".hwp", L"txt", L".docs"};
	size_t n = sizeof(fileExtension) / sizeof(fileExtension[0]);
	LPCTSTR ext = wcsrchr(filePath, '.');
	if(ext==NULL) 
		return FALSE;
	for(size_t i=0; i<n; i++) {
		if(!_tcsicmp(ext,fileExtension[i]))
			return TRUE;
	}
	return FALSE;
}

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
	TCHAR filePath[MAX_FILE_PATH_LEN];
    HANDLE hFile;
    
    // unhook, context 구한 이후
    ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0x4), &hFile, sizeof(HANDLE), NULL);
	// 4. 파일 확인 (유효한 확장자인지 확인)
	if(GetFileType(hFile)!=FILE_TYPE_DISK)
		goto funcEpilogue;
	GetFinalPathNameByHandle(hFile, filePath, MAX_FILE_PATH_LEN, FILE_NAME_NORMALIZED);
	if(!isEffectiveExtension(filePath)) {
		printf("Not effective file extension!!!\n");
		wprintf(L"%s\n", filePath);
		goto funcEpilogue;
	}
	// 후킹 작업들~
    
  funcEpilogue:
	// 10. Thread EIP 정상화, 디버기 프로세스 재개, Hook
}

 

- hFile로 filePath를 구하면 관련없는 dll 이름들이 보이거나 아예 이름 안보이거나 (중간에 temporary file 있는건가?)

- WriteFile의 hFile 핸들로는 제대로 된 이름을 확보할 수 없음

 

시도2. CreateFile API 후킹

notepad.exe에서 호출하는 모든 CreateFileW API call들에 bp
notepad.exe에서 파일 저장했을 때
notepad++.exe에서 파일 저장했을 때

- notepad, notepad++ 모두 파일 저장 시 CreateFileW 호출 - WriteFile 호출 - 데이터 저장 순으로 이루어진다

- CreateFileW, WriteFile 두 개의 API를 후킹해서 FileName이 유효하면 Buffer의 값을 변경하는 식으로 구현

- 물론 아래의 코드는 실패한 코드이다 (방향성만 참고)

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

#define MAX_FILE_NAME_LEN 260

LPVOID g_pfWriteFile = NULL, g_pfCreateFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chWFile = 0, g_chCFile = 0;
TCHAR fileName[MAX_FILE_NAME_LEN] = {0,};

// API 시작 부분에 hook 설치 (0xCC)
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
	// WriteFile(), CreateFileW()
	HMODULE hMod = GetModuleHandleA("kernel32.dll");
	g_pfWriteFile = GetProcAddress(hMod, "WriteFile");
	g_pfCreateFile = GetProcAddress(hMod, "CreateFileW");

	// CREATE_PROCESS_DEBUG_INFO 정보 저장
	memcpy(&g_cpdi, &(pde->u.CreateProcessInfo), sizeof(CREATE_PROCESS_DEBUG_INFO));
	// WriteFile API hook
	ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chWFile, sizeof(BYTE), NULL);
	WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
	// CreateFileW API hook
	ReadProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chCFile, sizeof(BYTE), NULL);
	WriteProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chINT3, sizeof(BYTE), NULL);

	return TRUE;
}

BOOL isValidFileName() {
	LPCTSTR fileExtension[] = {L".hwp", L".docs", L".txt"}; // 여기다 허용할 확장자 추가
	size_t num = sizeof(fileExtension) / sizeof(fileExtension[0]);
	LPCTSTR extensionIdx = wcsrchr((LPCTSTR)fileName, L'.');
	if(extensionIdx == NULL)
		return FALSE;
	for(size_t i=0; i<num; i++) {
		if(_tcsicmp(extensionIdx, fileExtension[i])==0)
			return TRUE;
	}
	return FALSE;
}

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
	CONTEXT ctx;
	PBYTE lpBuffer = NULL;
	TCHAR currChar = 0;
	DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, dwAddrOfFileName, i;
	PEXCEPTION_RECORD per = &(pde->u.Exception.ExceptionRecord);

	// Breakpoint Exception(INT 3)인 경우
	if(per->ExceptionCode == EXCEPTION_BREAKPOINT) {
		// exception 발생 위치가 CreateFile() API 주소인 경우
		if(per->ExceptionAddress == g_pfCreateFile) {
			// Unhook
			WriteProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chCFile, sizeof(BYTE), NULL);
			// Thread context
			ctx.ContextFlags = CONTEXT_CONTROL;
			GetThreadContext(g_cpdi.hThread, &ctx);
			// ReadFile의 1st param 가져옴
			ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0x4), &dwAddrOfFileName, sizeof(DWORD), NULL);
			// fileName에 파일 이름 읽어온다
			i = 0;
			do {
				ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(dwAddrOfFileName+i*sizeof(TCHAR)), &currChar, sizeof(TCHAR), NULL);
				fileName[i] = currChar;
				i++;
			} while(currChar != L'\0');
			wprintf(L"File Name: %s\n", fileName);

			// Thread EIP 원래대로
			ctx.Eip = (DWORD)g_pfCreateFile;
			SetThreadContext(g_cpdi.hThread, &ctx);
			// continue Debuggee process
			ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
			Sleep(0);
			// hook again
			WriteProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chINT3, sizeof(BYTE), NULL);
			return TRUE;
		}
		// exception 발생 위치가 WriteFile() API 주소인 경우
		else if(per->ExceptionAddress == g_pfWriteFile) {
			// Unhook
			WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chWFile, sizeof(BYTE), NULL);
			// Thread context
			ctx.ContextFlags = CONTEXT_CONTROL;
			GetThreadContext(g_cpdi.hThread, &ctx);
			if(isValidFileName()) {
				// 현재 ESP=RET, WriteFile의 2nd&3rd param 가져옴
				ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
				ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0xC), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);
				// 임시 버퍼 할당 후 버퍼 읽어옴
				lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
				memset(lpBuffer, 0,dwNumOfBytesToWrite+1);
				ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
				// 소문자 → 대문자 변환
				printf("\n### original string: %s ###\n", lpBuffer);
				for(i=0; i<dwNumOfBytesToWrite; i++) {
					if(lpBuffer[i]>=97 && lpBuffer[i]<=122)
						lpBuffer[i] -= 32;
				}
				printf("\n### converted string: %s ###\n", lpBuffer);
				WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL);
				// 임시 버퍼 해제
				free(lpBuffer);
				lpBuffer = NULL;
			}
			// Thread EIP 원래대로
			ctx.Eip = (DWORD)g_pfWriteFile;
			SetThreadContext(g_cpdi.hThread, &ctx);
			// continue Debuggee process
			ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
			Sleep(0);
			// Hook Again
			WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
			return TRUE;
		}
	}
	// Handle the 0x000006BA exception (per->ExceptionCode)
	else {
		// 디버거에서 프로세스로 exception 다시 넘겨줘서 SEH가 처리하도록
		ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
		return TRUE;
	}
	return FALSE;
}
// 아래는 동일

뻘짓 1. GetProcAddress(hMod, "CreateFile")

- bp가 제대로 안 걸린 것 같아 확인해보니 g_pfCreateFile 값이 0이었음

- dependency walker로 확인해보니 kernel32.dll이 export하는 함수에는 WriteFile, CreateFileA, CreateFileW 등이 있음

- Ollydbg로 분석 결과 CreateFileW를 내부적으로 사용하므로 해당 API의 주소를 가져왔음

=> GetProcAddress 할 때에는 export하는 함수 이름을 정확히 파악하자!

뻘짓 2. dwAddressOfFileName 주소로 바로 파일 이름 버퍼에 접근하려했음

- ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp+0x4), &dwAddrOfFileName, sizeof(DWORD), NULL); 로 CreateFileW의 첫 번째 인자[주소]를 가져온다 해도 이것은 프로세스 메모리 상의 주소

- hookdbg.exe에서 파일 이름 버퍼에 접근할 수 없다 (invalid address)

- ReadProcessMemory를 통해 프로세스의 버퍼 주소에 가서 TCHAR 한 글자씩 받아와 fileName[MAX_LEN] 버퍼에 담아서 비교해야 함

뻘짓 3. 디버거 attach 하면 ExceptionCode = EXCEPTION_BREAKPOINT 인 이벤트 바로 발생

- ExceptionAddress로 확인해보니 디버거 attach 후 디버기 모듈 실행되기 전에 ntdll 모듈에서 INT3 exception 발생

- 디버그 API 후킹 기법 사용 시 이 exception을 처리해줘야함: OnExceptionDebugEvent() 내에서 처리하고 TRUE를 리턴하던가, FALSE를 리턴하도록  한 후 DebugLoop()에서 처리하거나

- 처리는 ContinueDebugEvent( dwPID, dwTID , DBG_CONTINUE) 이렇게!

- 처리 안 해주면 hookdbg.exe notepad.exe 하자마자 프로세스 먹통된다

 

리버싱 핵심원리에 따르면 이는 ntdll.dll 모듈에 존재하는 'system breakpoint'로, 프로그램을 디버기 모드로 실행시킬 때 무조건 발생하는 예외라고 한다

 

해결 안 됨 4. CreateFileW 후킹이 제대로 안 됨

- 저장하려고 Saving Dialog를 띄우려고 하면 프로세스가 먹통이 된다

- Ollydbg로 kernel32!CreateFileW API에 bp걸고 달렸더니 Saving Dialog를 띄우기 위해 수많은 CreateFileW 호출이 이루어진다

- 그 과정에서 무언가 꼬인 것 같음

- 디버그 API 후킹은 제약이 많은 것 같다...

 

* 일단 API hooking 선택적으로 이뤄지게 하는 건 힘드네

* 특히 이름 가지고 구분하는건...