security/리버싱핵심원리

[미완][API 후킹] 디버그(Debug) 기법을 이용한 후킹 - 메모장 암호화/복호화

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

타켓 API 확인

- 파일로 저장할 버퍼 관련: kernel32!WriteFile API

- 파일로 불러올 버퍼 관련: ???

Process Monitor로 aa.txt 파일을 notepad.exe에 불러온 순간 호출된 API들

=> ReadFile, CreateFile, CreateFileMapping 등이 의심스럽다

1) ReadFile인지 검증

- notepad 모듈에서 해당 API 호출하는 코드에 bp 걸고 달렸는데 멈추지 않음. 아니다!!

2) CreateFile & CreateFileMapping (+ MapViewOfFile ?)

-알아보니 지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 여는 함수는 MapViewOfFile라고 함!

- 파일 open시 CreateFile -> CreateFileMapping -> MapViewOfFile 순으로 API 호출이 이루어진다

혹시 몰라 kernel32!CreateFile, CreateFileMapping, MapViewOfFile API 호출부에 모두 bp 걸었다
버퍼 내용이 MapViewOfFile 리턴값(EAX)에서 확인되었다

- 3개의 API 호출 시 파라미터/리턴값 모두 확인해봤는데, MapViewOfFile() 리턴값에서 버퍼 주소를 확인 가능했다

- 왜 Process Monitor에선 안떴을까?

  => MapViewOfFile의 첫번째 인자는 hMapping으로, CreateFileMapping 호출되고 나온 리턴값이 들어간다 (연쇄적)

  => 따라서 CreateFileMapping만 잡히고 MapViewOfFile 호출은 안 잡힌듯 ?

메모리 맵 파일 사용 순서

- 메모리 맵 파일 기능은 가상 메모리처럼 프로세스 주소 공간을 예약하고, 예약한 영역에 물리 저장소를 커밋함

- 파일의 데이터를 프로세스의 주소 공간에 매핑하여, 메모리와 마찬가지로 파일에 액세스할 수 있게 하는 기술

- CreateFileMappingA() 및 MapViewOfFile() API 함수가 사용됨

메모리 맵 파일을 사용하려면 다음의 세 가지 단계를 수행해야 한다.
  1. 메모리 맵 파일로 사용할 디스크 상의 파일을 나타내는 파일 커널 오브젝트를 생성하거나 연다 (CreateFile)
  2. 파일의 크기와 접근 방식을 고려하여, 파일 매핑 커널 오브젝트를 생성한다 (CreateFileMapping)
  3. 프로세스의 주소 공간 상에 파일 매핑 오브젝트의 전체나 일부를 매핑시킨다 (MapViewOfFile)
메모리 맵 파일을 더 이상 사용할 필요가 없다면, 다음이 세 가지 단계를 수행해야 한다.
  1. 프로세스의 주소 공간으로부터 파일 매핑 오브젝트의 매핑을 해제한다 (UnmapViewOfFile)
  2. 파일 커널 매핑 오브젝트를 닫는다 (CloseHandle)
  3. 파일 커널 오브젝트를 닫는다 (CloseHandle)

http://egloos.zum.com/sweeper/v/2990023

 

Memory Mapped File

1. 메모리 맵 파일 메모리 맵 파일 기능은 가상 메모리처럼 프로세스 주소 공간을 예약하고, 예약한 영역에 물리 저장소를 커밋하는 기능을 제공한다. 가상 메모리와 유일한 차이점이라면, 시스

egloos.zum.com

 

구현 전 고려사항

1. BP를 어디다 설치해야 하는가?

- kernel32!WriteFile() : parameter로 전달된 버퍼 주소를 가로채야 하므로 API의 첫바이트에 bp

- kernel32!MapViewOfFile() : 호출 후의 리턴값을 가로채야 함.

    => notepad 모듈에서 MapViewOfFile API 호출하고 난 직후에 bp 세팅?

    => ASLR 때문에 notepad 모듈의 매핑 주소가 계속 바뀌므로 불가능

    => 항상 고정된 위치에 매핑되는 system dll의 API 내부에 bp를 세팅해야 함

    => MapViewOfFile() 호출 - 스택 push/pop 후 JMP - 스택 push/pop JMP - KERNELBASE!MapViewOfFile 이런 흐름!

    => MapViewOfFile() 호출 후 실행 흐름의 제일 마지막 부분인 KERNELBASE!MapViewOfFile 의 에필로그 부분에 bp

    => bp 걸렸을 때의 context의 EAX 값을 가져오면 됨

hookdbg2.exe에서 구한 주소와 notepad.exe에 ollydbg를 attach시켜 구한 주소랑 동일하다

2. 파일 불러오기 시 API hooking 잘 되나 Ollydbg로 먼저 확인?

- kernel32!MapViewOfFile API 호출 직후 코드에 bp 걸고 진행

- bp 걸렸을 때 EAX가 가리키는 hex dump로 가서 버퍼 내용 바꾸고 F9해보았더니 에러 발생

EAX가 가리키고 있는 버퍼의 1바이트 수정 후 F9
이런 경고창 여러 개 뜸

=> 메모리 매핑을 위해 예약된 프로세스 주소 공간에 쓰기 권한이 없어서(read-only) 그렇다

Ollydbg-View-Memory Map으로 확인해보니 Read Only 라고만 뜬다

=> VirtualProtectEx()를 이용해 지정된 프로세스의 가상주소 공간에서 해당 영역에 대한 권한을 PAGE_READWRITE으로 변경 후 WriteProcessMemory를 하면 되겠다

VirtualProtectEx 없이 WriteProcess

3. 파일 내용 멀티바이트&유니코드 동시에 어떻게 처리?

한글이랑 기호 번갈아가며 입력

 - WriteFile이나 MapViewOfFile API로부터 얻은 버퍼를 확인해보니 유니코드(한글)은 2byte, 멀티바이트(영어,기호)는 1byte로 기록되고, NULL 바이트로 끝난다

- Byte 단위로 (널바이트 만날때까지) 버퍼 조작하면 될 듯 (xor 연산? xor 한 번 하면 암호화, 두 번 하면 복호화)

- 바이트 값을 비교해서 대소문자 바꾸는 식의 구현은 조심해야되지 않을까 (유니코드 2byte 중 한 바이트가 해당 비교 범위에 들어있을 수 있으므로)

 

구현 코드

// hookdbg2.exe
// hook WriteFile, MapViewOfFile

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

#define MAX_LEN 1000

CREATE_PROCESS_DEBUG_INFO cpdi;
LPVOID pfWriteFile = NULL, pfMapViewOfFile = NULL; 
BYTE bINT3 = 0xCC, bOrgMap = 0x0, bOrgWrite = 0x0;

void OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
	// cpdi를 통해 프로세스에 대한 핸들, 초기스레드(=메인스레드)에 대한 핸들 가져옴
	// 전역변수로 셋팅해 프로세스 메모리를 읽고 쓰거나, 메인 스레드의 context 조작
	memcpy(&cpdi, &(pde->u.CreateProcessInfo), sizeof(CREATE_PROCESS_DEBUG_INFO));
	
	// kernel32!WriteFile hook : 0x777F53EE(함수 시작)
	pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
	ReadProcessMemory(cpdi.hProcess, pfWriteFile, &bOrgWrite, sizeof(BYTE), NULL);
	WriteProcessMemory(cpdi.hProcess, pfWriteFile, &bINT3, sizeof(BYTE), NULL);

	// kernelbase!MapViewOfFile hook : 0x75B5AE92(함수 시작)이 아니라 0x75B5AF0D(에필로그 LEAVE instruction)
	pfMapViewOfFile = (LPVOID)((DWORD)(GetProcAddress(GetModuleHandleA("kernelbase.dll"), "MapViewOfFile"))+0x7B);
	ReadProcessMemory(cpdi.hProcess, pfMapViewOfFile, &bOrgMap, sizeof(BYTE), NULL);
	WriteProcessMemory(cpdi.hProcess, pfMapViewOfFile, &bINT3, sizeof(BYTE), NULL);
}

void manipulateData(char* dataBuffer, DWORD bufSize) {
	DWORD i;

	for(i=0; i<bufSize; i++) {
		dataBuffer[i] = dataBuffer[i] ^ 0x0f;
	}
}

void printData(LPCSTR dataBuffer, int n) {
	if(n==0) {
		printf("### ORIGINAL DATA ###\n");
		printf("%s\n", dataBuffer);
		printf("#####################\n");
	} else {
		printf("### CONVERTED DATA ###\n");
		printf("%s\n", dataBuffer);
		printf("#####################\n");
	}
}

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
	CONTEXT ctx;
	EXCEPTION_RECORD er = pde->u.Exception.ExceptionRecord;
	DWORD dwAddrOfBuffer, dwLengthOfBuffer, i, oldProtect;
	CHAR ch = 0;
	char dataBuffer[MAX_LEN];

	if(er.ExceptionCode == EXCEPTION_BREAKPOINT) {
		if(er.ExceptionAddress == pfWriteFile) { // WriteFile API
			// unhook - get context - manipulation - set context - continue API - hook again
			WriteProcessMemory(cpdi.hProcess, pfWriteFile, &bOrgWrite, sizeof(BYTE), NULL);

			ctx.ContextFlags = CONTEXT_CONTROL; // EIP, ESP 필요
			GetThreadContext(cpdi.hThread, &ctx);

			ReadProcessMemory(cpdi.hProcess, (LPVOID)(ctx.Esp+0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL);
			ReadProcessMemory(cpdi.hProcess, (LPVOID)(ctx.Esp+0xC), &dwLengthOfBuffer, sizeof(DWORD), NULL);

			memset(dataBuffer, 0, MAX_LEN);
			ReadProcessMemory(cpdi.hProcess, (LPVOID)dwAddrOfBuffer, dataBuffer, dwLengthOfBuffer, NULL);
			
			printData(dataBuffer, 0);
			manipulateData(dataBuffer, dwLengthOfBuffer);
			printData(dataBuffer, 1);

			WriteProcessMemory(cpdi.hProcess, (LPVOID)dwAddrOfBuffer, dataBuffer, dwLengthOfBuffer, NULL);

			ctx.Eip = (DWORD)pfWriteFile;
			SetThreadContext(cpdi.hThread, &ctx);

			ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
			Sleep(0);

			WriteProcessMemory(cpdi.hProcess, pfWriteFile, &bINT3, sizeof(BYTE), NULL);
			return TRUE;
		} else if(er.ExceptionAddress == pfMapViewOfFile) { // MapViewOfFile API
			// unhook - get context - manipulation - set context - continue API - hook again
			WriteProcessMemory(cpdi.hProcess, pfMapViewOfFile, &bOrgMap, sizeof(BYTE), NULL);

			ctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; // EIP, EAX 필요
			GetThreadContext(cpdi.hThread, &ctx);

			i = 0;
			memset(dataBuffer, 0, MAX_LEN);
			while(TRUE) {
				ReadProcessMemory(cpdi.hProcess, (LPVOID)(ctx.Eax+i*sizeof(CHAR)), &ch, sizeof(CHAR), NULL);
				if(ch=='\0') break;
				dataBuffer[i++] = ch;
			}

			printData(dataBuffer, 0);
			manipulateData(dataBuffer, i);
			printData(dataBuffer, 1);
	
			if(VirtualProtectEx(cpdi.hProcess, (LPVOID)(ctx.Eax), i*sizeof(CHAR), PAGE_READWRITE, &oldProtect)) {
				WriteProcessMemory(cpdi.hProcess, (LPVOID)(ctx.Eax), dataBuffer, i*sizeof(CHAR), NULL);
				VirtualProtectEx(cpdi.hProcess, (LPVOID)(ctx.Eax), i*sizeof(CHAR), oldProtect, NULL);
			} else {
				printf("Failed to change memory protection in %x ~ %x\n", ctx.Eax, ctx.Eax+i-1);
			}

			ctx.Eip = (DWORD)pfMapViewOfFile;
			SetThreadContext(cpdi.hThread, &ctx);

			ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
			Sleep(0);

			WriteProcessMemory(cpdi.hProcess, pfMapViewOfFile, &bINT3, sizeof(BYTE), NULL);
			return TRUE;
		} else { 
			// 다른 곳에서 INT3(0xCC) 마주치면 무시
			return FALSE;
		}
	} else {
		// Exception Event 중 INT3(0xCC) 때문이 아닌 것들은 프로세스의 SEH가 처리하도록 함
		ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_EXCEPTION_NOT_HANDLED);
		return TRUE;
	}
	return FALSE;
}

void DebugLoop() {
	DEBUG_EVENT de;

	while(WaitForDebugEvent(&de, INFINITE)) {
		// DebugEventCode에 따라 분기 달라짐
		// 각 분기 내에서 ProcessID, ThreadID 이용해 실행 재개
		if(de.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT) {
			OnCreateProcessDebugEvent(&de);
		} else if(de.dwDebugEventCode==EXCEPTION_DEBUG_EVENT) {
			if(OnExceptionDebugEvent(&de))
				continue;
		} else if(de.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT) {
			break;
		}
		// 디버기 프로세스 재개
		ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
	}
}

DWORD getPIDFromName(LPCTSTR procName) {
	DWORD dwPID = 0xFFFFFFFF;
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if(hSnapshot != INVALID_HANDLE_VALUE) {
		PROCESSENTRY32 pe;
		pe.dwSize = sizeof(PROCESSENTRY32);

		if(Process32First(hSnapshot, &pe)) {
			do {
				if(_wcsicmp(pe.szExeFile, procName)==0) {
					dwPID = pe.th32ProcessID;
					break;
				}
			} while(Process32Next(hSnapshot, &pe));
		}
		CloseHandle(hSnapshot);
	}

	return dwPID;
}

int _tmain(int argc, TCHAR* argv[]) {
	if(argc!=2) {
		wprintf(L"USAGE: %s <process name>\n", argv[0]);
		return 1;
	}

	DWORD dwPID = getPIDFromName(argv[1]);
	if(dwPID == 0xFFFFFFFF) {
		wprintf(L"Cannot find <%s> process\n", argv[1]);
		return 1;
	}
	wprintf(L"Found %s process!! PID = %d \n", argv[1], dwPID);

	if(!DebugActiveProcess(dwPID)) {
		wprintf(L"DebugActiveProcess(%d) failed!\n", dwPID);
		return 1;
	}

	DebugLoop();
}

 

실패한 이유 & 해결법

1. DebugLoop()이 아닌 각각의 서브루틴에서 ContinueDebugEvent() 호출하도록 구현했더니

- CREATE_PROCESS_DEBUG_EVENT까지 발생하고 그 뒤로 메모장 먹통된다

 

1-1. why?

- CREATE_PROCESS_DEBUG_EVENT가 발생하여 OnCreateProcessDebugEvent() 호출되고, 그 안에서 ContinueDebugEvent()에 의해 실행 재개된다

- OnCreateProcessDebugEvent() 서브루틴이 끝나기 전에 디버그 이벤트를 받아(아마 MapViewOfFile 관련?) 해당 이벤트를 핸들링하지 못해 먹통된다 (hookdbg2.exe를 디버거로서 attach하는 순간 모든 디버그 이벤트를 처리해야 함)

- 특별한 경우가 아닌 이상 이벤트 핸들링 코드가 구현된 DebugLoop()에서 프로세스 재개하자

=> 우리가 후킹한 API들의 경우 hook 제거/버퍼 조작 후 hook을 재설치하기 전에 프로세스를 재개해야하므로 서브루틴 안에서 ContinueDebugEvent() 필요 (hook 재설치하고 Continue 시 bp에 다시 걸리면서 무한루프 발생)

=> OnExceptionDebugEvent()의 리턴값이 TRUE이면 서브루틴 내에서 실행 재개했다는 뜻 / FALSE면 DebugLoop()에서 실행 재개하라는 뜻~

 

// 해결

 

2. 리턴값을 받아오기 위해 어쩔 수 없이 KERNELBASE!MapViewOfFile에 bp를 걸었더니

- 메모장에 한글자 한글자 입력할때마다 MapViewOfFile API hooking이 이루어진다

- saving dialog / open dialog 창이 뜨는 과정에서 process crash

 

2-1. why?

- KERNELBASE!MapViewOfFile 함수가 너무 low-level API다보니 파일 불러오기 외에도 다양한 상황에서 호출된다

(ex. 한글자 입력할때마다, dialog 창을 띄우려 할 때)

- Ollydbg로 kernelbase 모듈의 MapViewOfFile export 함수에 bp 걸고 확인해봄 직접..

- MapViewOfFile 함수 실패 시 리턴값(EAX)이 0인데, 유효하지 않은 주소(ctx.Eax = 0)에 대해 VirtualProtectEx()에서 권한을 수정하려다보니 crash 발생

 

// 해결 실패... 다른 API 기법들 공부 후 다시 시도

 

=> 자주 호출되는 API들을 후킹해야하는 경우 디버그 방식을 이용하기보다 DLL Injection 방식을 사용하는 식으로 가야 될 것 같다 (unhook - continue - hook 과정에서 문제가 생기기도 하고, 리턴값을 다루기 어려움)