security/리버싱핵심원리

[API 후킹] IAT 조작을 이용한 메모장 암호화/복호화 (완)

민사민서 2023. 4. 1. 01:46

InjectEjectDll.exe 코드

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

// Debug API를 사용하기 위해 엑세스 토큰의 privilege 변경
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;
}

BOOL InjectDll(DWORD dwPID, LPCWSTR szDllName) {
	HANDLE hProcess = NULL, hThread = NULL;
	LPVOID pRemoteBuf = NULL;
	LPTHREAD_START_ROUTINE pThreadProc;
	TCHAR szExePath[MAX_PATH] = {0,};
	TCHAR* ptr;

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

	// dll 이름만 건네준 경우 (동일 디렉토리 내에 있다는 가정 아래 full path로 만들어줌)
	if(!wcsrchr(szDllName, L'\\')) {
		GetModuleFileName(NULL, szExePath, MAX_PATH);
		ptr = wcsrchr(szExePath, L'\\');
		wcscpy(ptr+1, szDllName);
		szDllName = szExePath;
	}

	DWORD dwBufSize = (DWORD)(wcslen(szDllName)+1) * sizeof(TCHAR);

	pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
	WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllName, dwBufSize, NULL);

	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryW");
	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

BOOL EjectDll(DWORD dwPID, LPCWSTR szDllName) {
	HANDLE hSnapshot, hProcess, hThread;
	MODULEENTRY32 me;
	LPTHREAD_START_ROUTINE pThreadProc;
	BOOL bFound=FALSE;

	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
	me.dwSize = sizeof(MODULEENTRY32);

	if(Module32First(hSnapshot, &me)) {
		do {
			// dll 모듈 경로 + 모듈 이름 둘 다 비교
			if(_wcsicmp((LPCWSTR)me.szExePath, szDllName)==0 || _wcsicmp((LPCWSTR)me.szModule,szDllName)==0) {
				bFound = TRUE;
				break;
			}
		} while(Module32Next(hSnapshot, &me));
	}

	if(!bFound) {
		CloseHandle(hSnapshot);
		return FALSE;
	}

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

	pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32.dll"), "FreeLibrary");

	hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);
	WaitForSingleObject(hThread, INFINITE);

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

	return TRUE;
}

DWORD FindPIDFromName(LPCTSTR procName) {
	DWORD dwPID = 0xFFFFFFFF;
	HANDLE hSnapshot = INVALID_HANDLE_VALUE;
	PROCESSENTRY32 pe;

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

	return dwPID;
}

int _tmain(int argc, TCHAR* argv[]) {
	if(argc!=4) {
		wprintf(L"USAGE : %s [i,e] [Process Name] [Dll Name] \n", argv[0]);
		return 1;
	}
	if(!SetPrivilege(SE_DEBUG_NAME, TRUE)) {
		wprintf(L"Failed to set privilege! \n");
		return 1;
	}
	DWORD dwPID = FindPIDFromName(argv[2]);
	if(dwPID==0xFFFFFFFF) {
		wprintf(L"Can't find any process from given name : <%s> !!\n", argv[2]);
		return 1;
	}
	wprintf(L"Process found!! PID is <%d>!!\n", dwPID);
	if(_wcsicmp(argv[1], L"i") == 0) {
		if(InjectDll(dwPID, argv[3])) {
			wprintf(L"InjectDll(\"%s\") success!!\n", argv[3]);
		} else {
			wprintf(L"InjectDll(\"%s\") failed!!\n", argv[3]);
		}
	} 
	else if(_wcsicmp(argv[1], L"e") == 0) {
		if(EjectDll(dwPID, argv[3])) {
			wprintf(L"EjectDll(\"%s\") success!!\n", argv[3]);
		} else {
			wprintf(L"EjectDll(\"%s\") failed!!\n", argv[3]);
		}
	} 
	else {
		wprintf(L"Invalid option!! \n");
		return 1;
	}

	return 0;
}

뭐 이런 식으로 사용하면 된다..

 

hook_iat.dll 코드

// hook_iat.dll
#include "stdio.h"
#include "windows.h"
#include "tchar.h"

typedef BOOL(WINAPI* PFWRITEFILE)(HANDLE,LPCVOID,DWORD,LPDWORD,LPOVERLAPPED);
typedef LPVOID(WINAPI* PFMAPVIEWOFFILE)(HANDLE,DWORD,DWORD,DWORD,SIZE_T);
typedef HANDLE(WINAPI* PFCREATEFILEMAPPINGW)(HANDLE,LPSECURITY_ATTRIBUTES,DWORD,DWORD,DWORD,LPCWSTR);
typedef HANDLE(WINAPI* PFCREATEFILEW)(LPCWSTR,DWORD,DWORD,LPSECURITY_ATTRIBUTES,DWORD,DWORD,HANDLE);

// global variable
FARPROC g_pWriteOrg, g_pMapViewOrg, g_pCreateMappingOrg, g_pCreateFileOrg;

BOOL WINAPI MyWriteFile(HANDLE hFile, LPVOID lpBuffer, DWORD nBytesToWrite, LPDWORD nBytesWritten, LPOVERLAPPED lpOverlapped) {
	// lpBuffer 조작
	LPSTR buffer = (LPSTR)lpBuffer;
	SIZE_T n = strlen(buffer);

	for(SIZE_T i=0; i<n; i++) {
		buffer[i] = buffer[i]^0x0f;
	}

	return ((PFWRITEFILE)g_pWriteOrg)(hFile,(LPCVOID)lpBuffer,nBytesToWrite,nBytesWritten,lpOverlapped);
}

LPVOID WINAPI MyMapViewOfFile(HANDLE hFileMappingObj, DWORD dwDesiredAccess, DWORD dwFileOffsetH, DWORD dwFileOffsetL, SIZE_T nBytesToMap) {
	LPSTR buffer = (LPSTR)((PFMAPVIEWOFFILE)g_pMapViewOrg)(hFileMappingObj,dwDesiredAccess|FILE_MAP_WRITE,dwFileOffsetH,dwFileOffsetL,nBytesToMap);
	// 리턴값 조작하기
	DWORD oldProtect;
	SIZE_T n;
	for(n=0; buffer[n]!='\x0'; n++);

	VirtualProtect(buffer, n, PAGE_READWRITE, &oldProtect);
	for(SIZE_T i=0; i<n; i++) {
		buffer[i] = buffer[i]^0x0f;
	}
	VirtualProtect(buffer, n, oldProtect, &oldProtect);

	return (LPVOID)buffer;
}

HANDLE WINAPI MyCreateFileMappingW(HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttr, DWORD flProtect, DWORD dwMaxSizeH, DWORD dwMaxSizeL, LPCWSTR lpName) {
	// change 'page protection' of file mapping object to PAGE_READWRITE
	return ((PFCREATEFILEMAPPINGW)g_pCreateMappingOrg)(hFile, lpAttr, PAGE_READWRITE, dwMaxSizeH, dwMaxSizeL, lpName);
}

HANDLE WINAPI MyCreateFileW(LPCWSTR lpFileName, DWORD dwAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpAttr, DWORD dwCreationDisposition, DWORD dwFlagsAndAttr, HANDLE hTempFile) {
	// change 'file access privilege' to GENERIC_READ | GENERIC_WRITE
	return ((PFCREATEFILEW)g_pCreateFileOrg)(lpFileName, dwAccess|GENERIC_WRITE, dwShareMode, lpAttr, dwCreationDisposition, dwFlagsAndAttr, hTempFile);
}

BOOL hook_iat(LPCSTR dllName, FARPROC pOrgFunc, FARPROC pNewFunc) {
	HANDLE hMod;
	DWORD dwRVA, dwOldProtect;
	PBYTE pFile;
	LPCSTR szLibName;
	PIMAGE_IMPORT_DESCRIPTOR pIID; // DataDirectory[1]이 가리키는 배열(IDT)의 원소
	PIMAGE_THUNK_DATA pITD; // INT, IAT가 가리키는 배열의 원소

	// pFile = VA of mapped file
	hMod = GetModuleHandle(NULL);
	pFile = (PBYTE)hMod;

	// pFile = VA to IMAGE_NT_HEADER
	dwRVA = *((DWORD*)(pFile+0x3C));
	pFile += dwRVA;

	// VA to IDT(Array of IMAGE_IMPORT_DESCRIPTOR)
	dwRVA = *((DWORD*)(pFile+0x80));
	pIID = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod + dwRVA);

	for( ; pIID->Name ; pIID++) {
		// Library 이름 비교
		szLibName = (LPCSTR)((DWORD)hMod + pIID->Name);
		if(!_stricmp(dllName, szLibName)) {
			// IAT 확보
			pITD = (PIMAGE_THUNK_DATA)((DWORD)hMod + pIID->FirstThunk); 
			for( ; pITD->u1.Function ; pITD++) {
				// IAT에서 pOrgFunc 발견 시 pNewFunc으로 교체
				if(pITD->u1.Function == (DWORD)pOrgFunc) {
					VirtualProtect(&(pITD->u1.Function), 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
					pITD->u1.Function = (DWORD)pNewFunc;
					VirtualProtect(&(pITD->u1.Function), 4, dwOldProtect, &dwOldProtect);

					return TRUE;
				}
			}
		}
	}

	return FALSE;
}

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdReason, LPVOID lpReserved) {
	HMODULE hMod;
	switch(fdReason) {
	case DLL_PROCESS_ATTACH:
		hMod = GetModuleHandleA("kernel32.dll");
		g_pWriteOrg = GetProcAddress(hMod, "WriteFile");
		g_pMapViewOrg = GetProcAddress(hMod, "MapViewOfFile");
		g_pCreateMappingOrg = GetProcAddress(hMod, "CreateFileMappingW");
		g_pCreateFileOrg = GetProcAddress(hMod, "CreateFileW");

		// hook
		hook_iat("kernel32.dll", g_pWriteOrg, (FARPROC)MyWriteFile);
		hook_iat("kernel32.dll", g_pMapViewOrg, (FARPROC)MyMapViewOfFile);
		hook_iat("kernel32.dll", g_pCreateMappingOrg, (FARPROC)MyCreateFileMappingW);
		hook_iat("kernel32.dll", g_pCreateFileOrg, (FARPROC)MyCreateFileW);
		break;
	case DLL_PROCESS_DETACH:
		// unhook
		hook_iat("kernel32.dll", (FARPROC)MyWriteFile, g_pWriteOrg);
		hook_iat("kernel32.dll", (FARPROC)MyMapViewOfFile, g_pMapViewOrg);
		hook_iat("kernel32.dll", (FARPROC)MyCreateFileMappingW, g_pCreateMappingOrg);
		hook_iat("kernel32.dll", (FARPROC)MyCreateFileW, g_pCreateFileOrg);
		break;
	}
	return TRUE;
}

 

실행 결과

암호화하기 전
암호화 후

- hook_iat.dll이 inject된 notepad.exe에서 저장 시 암호화되어 저장된다

- hook_iat.dll이 inject된 notepad.exe에서 암호화된 파일을 불러오면 복호화되어 보여진다 (파일 자체가 복호화된다, 복호화하여 불러오기/저장하기 동시에 되는 느낌)

 

IMAGE_IMPORT_DESCRIPTOR, IMAGE_THUNK_DATA 메모

// defined in winnt.h
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
// defined in winnt.h
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

typedef struct _IMAGE_THUNK_DATA64 {
    union {
        ULONGLONG ForwarderString;  // PBYTE 
        ULONGLONG Function;         // PULONGLONG
        ULONGLONG Ordinal;
        ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA64;
typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64;

#ifdef _WIN64
typedef IMAGE_THUNK_DATA64          IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA64         PIMAGE_THUNK_DATA;
#else
typedef IMAGE_THUNK_DATA32          IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA32         PIMAGE_THUNK_DATA;
#endif

- windows.h는 winnt.h를 포함한다

- IMAGE_THUNK_DATA의 function은 pThunk->u1.Function 으로 접근해야한다 

 

수많은 시행착오들 (hook_iat.dll 만들며...)

1. __stdcall vs. __cdecl (WINAPI는 __stdcall)

증상: 파일을 저장하거나(WriteFile API), 열려고 하면(MapViewOfFile API) 프로그램 crash

분석: Ollydbg로 분석해보니 MyMapViewOfFile / MyWriteFile 호출 종료 후 리턴하는 주소가 이상했음

왜 그런지 살펴보니 MyWriteFile / MyMapViewOfFile 후킹 함수들과 원 API들 간의 호출 규약 차이 때문이었음.

MyWriteFile 내부에서 WriteFile을 호출하는 모습, caller에서 스택을 정리한다 (ADD ESP, 14)
WriteFile 함수 에필로그. WINAPI(__stdcall) 함수이므로 callee에서 스택을 정리한다 (ADD ESP, 14)
결국 WriteFile을 호출한 것에 대해 스택 정리(ADD ESP,14)가 불필요하게 두 번 이루어지고 RET 주소가 이상해짐 (0x0)

 

BOOL MyWriteFile(HANDLE hFile, LPVOID lpBuffer, DWORD nBytesToWrite, LPDWORD nBytesWritten, LPOVERLAPPED lpOverlapped)
LPVOID MyMapViewOfFile(HANDLE hFileMappingObj, DWORD dwDesiredAccess, DWORD dwFileOffsetH, DWORD dwFileOffsetL, SIZE_T nBytesToMap)

- Microsoft Visual C++ / Visual Studio로 c++ 코드를 빌드할 때 사용자 정의 함수들의 호출 규약을 따로 명시하지 않으면, default calling convention은 '__cdecl'로 정해진다. 

- 따라서 후킹 함수들에 'WINAPI' 키워드를 통해 __stdcall 호출 규약을 사용하도록 명시해야 한다 (후킹 함수에서 호출하는 원 window API는 __stdcall 호출규약을 이미 사용하고 있으므로)

- __stdcall: callee에서 stack cleanup / __cdecl: caller에서 stack cleanup

2. MapViewOfFile이 리턴하는 Mapped View는 READ ONLY

- Ollydbg로 MapViewOfFile이 리턴한 주소의 데이터를 조작하려 했더니 에러 메시지 뜸

- 아니나 다를까 해당 mapped 영역은 Read-only 였다

2-1. VirtualProtect를 이용해 Mapped View 영역을 READWRITE로 ?

- MyMapViewOfFile 함수 내에서 VirtualProtect(buffer, n, PAGE_READWRITE, &oldProtect);를 이용해 page protection을 바꾸려고 했지만 여전히 buffer에 값을 쓸 때 access violation error 떴다

Mapped View(005A0000)에 여전히 write 불가

- 바로 VirtualProtect(buffer, n, PAGE_READWRITE, &oldProtect); 가 실패해서였다

- Error code: 0x57 (ERROR_INVALID_PARAMETER) // PAGE_READWRITE를 넣을 수 없다??

2-2. VirtualProtect(buffer,n,PAGE_READWRITE,&oldProtect) 왜 실패?

- MSDN의 VirtualProtect 관련 문서에 따르면 3번째 인자 flNewProtect(메모리 보호 옵션)는 MapViewOfFile에 의해 매핑된 뷰의 경우 뷰가 매핑될 때 지정된 액세스 보호와 호환되어야 한다

- VirtualProtect function can be used only to change the protection of the memory from the initial protection set when the view was mapped

MapViewOfFile 호출 시 Access는 4(FILE_MAP_READ)로 고정해서 넘겨주네

- If you create a mapped view with a specific protection level, you can only tighten the protection level, not loosen it.

- MapViewOfFile에 의해 READ_ONLY mapped view가 생성되었기 때문에 VirtualProtect를 이용해 protection을 readwrite으로 바꿀 수 (loosen) 없다

=> MyMapViewOfFile에서 원 API를 호출할 때 FILE_MAP_WRITE access까지 추가해서 전달하자

(PFMAPVIEWOFFILE)g_pMapViewOrg)(hFileMappingObj,dwDesiredAccess|FILE_MAP_WRITE,dwFileOffsetH,dwFileOffsetL,nBytesToMap)

2-3. CreateFileMappingW도 후킹 필요?

- 위와 같이 FILE_MAP_WRITE access까지 추가해서 전달하니 MapViewOfFile 함수 호출에 실패하였다

- 왜 그런지 살펴보았더니 FILE_MAP_WRITE access를 사용하려면 아래와 같은 제약이 있다

file mapping object가 PAGE_(EXECUTE_)READWRITE 보호로 생성되어야 한다

- CreateFileMapping의 세 번째 인자(flProtect)는 파일 매핑 개체의 보호를 지정하는데, 개체의 모든 매핑된 뷰는 이 보호화 와 호환되어야 한다

- 하지만 CreateFileMapping API의 세번째 인자로는 PAGE_READONLY(2)가 고정된 값으로 들어간다

=> MyCreateFileMapping 후킹 함수를 만들어 CreateFileMapping API를 후킹한다. 세번째 인자로 PAGE_READWRITE(0x4)를 넘겨주어 file mapping object를 생성한다

HANDLE WINAPI MyCreateFileMappingW(HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttr, DWORD flProtect, DWORD dwMaxSizeH, DWORD dwMaxSizeL, LPCWSTR lpName) {
	// change 'page protection' of file mapping object to PAGE_READWRITE
	return ((PFCREATEFILEMAPPINGW)g_pCreateMappingOrg)(hFile, lpAttr, PAGE_READWRITE, dwMaxSizeH, dwMaxSizeL, lpName);
}

2-4. CreateFileW도 후킹 필요?

- 위 사진처럼 CreateMappingW 함수 호출에 실패해 리턴값이 0이다

- Error code=0x5로 ERROR_ACCESS_DENIED이다. 즉, 충분한 권한이 존재하지 않는다는 것이다

- 왜 그런지 살펴보니 파일 매핑 개체의 페이지 보호를 PAGE_READWRITE로 하려면 제약 사항이 존재한다

- GENERIC_READ | GENERIC_WRITE 엑세스 권한으로 생성된(CreateFile) 파일 핸들(hFile)을 매개변수로 받아야 한다

- 하지만 CreateFile의 DesiredAccess에는 고정된 값 80000000(GENERIC_READ)만 들어간다

=> MyCreateFileW 후킹 함수를 만들어 CreateFileW 후킹한다. 엑세스 권한으로 GENERIC_WRITE도 넘겨준다

HANDLE WINAPI MyCreateFileW(LPCWSTR lpFileName, DWORD dwAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpAttr, DWORD dwCreationDisposition, DWORD dwFlagsAndAttr, HANDLE hTempFile) {
	// change 'file access privilege' to GENERIC_READ | GENERIC_WRITE
	return ((PFCREATEFILEW)g_pCreateFileOrg)(lpFileName, dwAccess|GENERIC_WRITE, dwShareMode, lpAttr, dwCreationDisposition, dwFlagsAndAttr, hTempFile);
}

2-5. 정리하자면

- MapViewOfFile에 의해 리턴된 mapped view는 read-only이므로 버퍼 조작이 불가능하다

- CreateFileW, CreateFileMappingW, MapViewOfFile을 후킹해 엑세스/보호 권한에 READ/WRITE를 추가하면 

- VirtuaProtect 호출 필요 없이 해당 주소가 writable 하므로 버퍼 수정하면 된다!