타켓 API 확인
- 파일로 저장할 버퍼 관련: kernel32!WriteFile API
- 파일로 불러올 버퍼 관련: ???
=> ReadFile, CreateFile, CreateFileMapping 등이 의심스럽다
1) ReadFile인지 검증
- notepad 모듈에서 해당 API 호출하는 코드에 bp 걸고 달렸는데 멈추지 않음. 아니다!!
2) CreateFile & CreateFileMapping (+ MapViewOfFile ?)
-알아보니 지정된 파일에 대한 명명되거나 명명되지 않은 파일 매핑 개체를 만들거나 여는 함수는 MapViewOfFile라고 함!
- 파일 open시 CreateFile -> CreateFileMapping -> MapViewOfFile 순으로 API 호출이 이루어진다
- 3개의 API 호출 시 파라미터/리턴값 모두 확인해봤는데, MapViewOfFile() 리턴값에서 버퍼 주소를 확인 가능했다
- 왜 Process Monitor에선 안떴을까?
=> MapViewOfFile의 첫번째 인자는 hMapping으로, CreateFileMapping 호출되고 나온 리턴값이 들어간다 (연쇄적)
=> 따라서 CreateFileMapping만 잡히고 MapViewOfFile 호출은 안 잡힌듯 ?
메모리 맵 파일 사용 순서
- 메모리 맵 파일 기능은 가상 메모리처럼 프로세스 주소 공간을 예약하고, 예약한 영역에 물리 저장소를 커밋함
- 파일의 데이터를 프로세스의 주소 공간에 매핑하여, 메모리와 마찬가지로 파일에 액세스할 수 있게 하는 기술
- CreateFileMappingA() 및 MapViewOfFile() API 함수가 사용됨
- 메모리 맵 파일로 사용할 디스크 상의 파일을 나타내는 파일 커널 오브젝트를 생성하거나 연다 (CreateFile)
- 파일의 크기와 접근 방식을 고려하여, 파일 매핑 커널 오브젝트를 생성한다 (CreateFileMapping)
- 프로세스의 주소 공간 상에 파일 매핑 오브젝트의 전체나 일부를 매핑시킨다 (MapViewOfFile)
- 프로세스의 주소 공간으로부터 파일 매핑 오브젝트의 매핑을 해제한다 (UnmapViewOfFile)
- 파일 커널 매핑 오브젝트를 닫는다 (CloseHandle)
- 파일 커널 오브젝트를 닫는다 (CloseHandle)
http://egloos.zum.com/sweeper/v/2990023
구현 전 고려사항
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 값을 가져오면 됨
2. 파일 불러오기 시 API hooking 잘 되나 Ollydbg로 먼저 확인?
- kernel32!MapViewOfFile API 호출 직후 코드에 bp 걸고 진행
- bp 걸렸을 때 EAX가 가리키는 hex dump로 가서 버퍼 내용 바꾸고 F9해보았더니 에러 발생
=> 메모리 매핑을 위해 예약된 프로세스 주소 공간에 쓰기 권한이 없어서(read-only) 그렇다
=> VirtualProtectEx()를 이용해 지정된 프로세스의 가상주소 공간에서 해당 영역에 대한 권한을 PAGE_READWRITE으로 변경 후 WriteProcessMemory를 하면 되겠다
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 과정에서 문제가 생기기도 하고, 리턴값을 다루기 어려움)
'security > 리버싱핵심원리' 카테고리의 다른 글
[API 후킹] IAT 조작을 이용한 메모장 암호화/복호화 (완) (0) | 2023.04.01 |
---|---|
IAT 조작하여 API 후킹 - 한글이 출력되는 계산기 (0) | 2023.03.30 |
[미완][API 후킹] 디버그(Debug) 기법을 이용한 메모장 WriteFile() 후킹 (0) | 2023.03.25 |
API 후킹 Tech Map (0) | 2023.03.25 |
Code Injection - using assembly (0) | 2023.03.24 |