본문 바로가기

Security

Code Injection

Code Injection

Code Injection은 크게 2가지의 의미로 해석될 수 있다. 첫 번째는 소프트웨어의 버그(취약점)의 일종으로 애플리케이션의 신뢰할 수 없는 데이터를 입력받아 악의적인 행위를 수행하거나 코드를 실행하는 것을 의미한다. 두 번째로는 역공학(Reverse engineering, Reversing)에서의 Code Injection이다. 해당 글에서는 바로 후자의 관한 코드 인젝션에 대해 살펴보도록 하겠다.

Code Injection은 목적 프로세스에 독립적인 실행 코드를 삽입한 후, 이를 실행하는 기법을 의미한다. DLL Injection과 마찬가지로 보통 CreateRemoteProcess( ) API를 이용하여 실행한다.

Code Injection vs DLL Injection

Code 인젝션과 DLL 인젝션은 원하는 행위를 Target 프로세스에 수행할 수 있다는 측면에서 동일하지만 그 방법에서 차이가 있다. DLL 인젝션은 실행될 코드를 DLL 파일로 저장하여 이를 인젝션 하는 방식으로 동작한다. 이 경우에는 코드에 사용되는 모든 데이터는 DLL의 데이터 영역에 위치하게 된다. 따라서 코드와 데이터 모두 메모리에 존재하기 때문에 정상적으로 실행될 수 있다. Code 인젝션은 코드와 데이터가 존재하는 DLL 파일이 아닌, 코드만 삽입한다. 그렇기 때문에 코드에서 사용될 데이터도 별도로 인젝션 해줘야한다.

Code Injection의 장점

  1. 적은 메모리 차지
  2. 흔적을 찾기가 힘듬
  3. 별도의 DLL 파일 필요 X
  4. ETC

코드 구현

//Code_Inject.h
#include <stdio.h>
#include <Windows.h>

typedef struct _THREAD_PARAM {
	FARPROC pFunc[2];
	char szBuf[4][128];
} THREAD_PARAM, *PTHREAD_PARAM;

typedef HMODULE(WINAPI *PFLOADLIBRARYA) (
	LPCSTR lpLibFileName
	);

typedef FARPROC(WINAPI *PFGETPROCADDRESS) (
	HMODULE hModule,
	LPCSTR lpProcName
	);

typedef int (WINAPI *PFMESSAGEBOXA) (
	HWND hWnd,
	LPCSTR lpText,
	LPCSTR lpCaption,
	UINT uType
	);

DWORD WINAPI ThreadProc(LPVOID lParam);
BOOL InjectCode(DWORD dwPID);

THREAD_PARAM 구조체는 호출할 API와 삽입(Inject)할 데이터를 저장하는 용도로 사용된다. 그 밖의 typedef 문은 각각 LoadLibraryA, GetProcAddress, MessageBoxA 함수의 포인터를 나타낸다.

잠깐 함수포인터에 대해 설명하자면 일반적인 포인터와 같은 개념이지만 함수를 가리킨다는 차이점이 있다. 선언은 다음과 같이 한다.

반환값자료형 (*함수포인터 이름)(매개변수)

int add(int a, int b){
  return a+b;
}

int main(){
  int (*fp)(int, int);
  fp = add;	//fp에 add 함수의 주소 저장
  printf("%d\n", fp(1,2));
  return 0;
}

코드를 보다가 의문이 든 점은 기존의 선언 방식이 반환값자료형 (*함수포인터 이름)(매개변수) 이지만 Code_inject.h 헤더 파일에서의 함수포인터를 보면 다음과 같다.

typedef HMODULE(WINAPI *PFLOADLIBRARYA) (
	LPCSTR lpLibFileName
	);

일반적인 선언과 달리 이름 앞에 WINAPI 매크로가 추가된 것을 확인할 수 있다. 그 이유는 URL에서 확인할 수 있었다. 설명하자면, 우리가 작성하고 사용하는 함수들은 WINAPI 호출 규약을 사용하기 때문에 이를 명시해준 것이다. (WINAPI calling convention은 보통 __stdcall로 정의된다.)

//Code_Inject.cpp
#include "Code_Inject.h"

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

	dwPID = (DWORD)atol(argv[1]);
	InjectCode(dwPID);

	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lParam) {
	PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
	HMODULE hMod = NULL;
	FARPROC pFunc = NULL;

	hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
	pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
	((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);


	return 0;
}

BOOL InjectCode(DWORD dwPID) {
	HMODULE hMod = NULL;
	THREAD_PARAM param = { 0, };
	HANDLE hProcess = NULL;
	HANDLE hThread = NULL;
	LPVOID pRemoteBuf[2] = { 0, };
	DWORD dwSize = 0;

	hMod = GetModuleHandleA("kernel32.dll");

	param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
	param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
	strcpy_s(param.szBuf[0], "user32.dll");
	strcpy_s(param.szBuf[1], "MessageBoxA");
	strcpy_s(param.szBuf[2], "CODE INJECTION EXAMPLE");
	strcpy_s(param.szBuf[3], "LEEHAHOON");

	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);

	dwSize = sizeof(THREAD_PARAM);
	pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);

	WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL);

	dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
	pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL);

	hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL);

	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

코드출처: 리버스코어(ReverseCore)

 

Code_Inject.cpp은 함수단위로 코드를 살펴보도록 하겠다.

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

	dwPID = (DWORD)atol(argv[1]);
	InjectCode(dwPID);

	return 0;
}

메인 함수다. 사용자로부터 pid를 입력받아 InjectCode( )함수를 실행한다.

DWORD WINAPI ThreadProc(LPVOID lParam) {
	PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
	HMODULE hMod = NULL;
	FARPROC pFunc = NULL;

	hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
	pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
	((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);


	return 0;
}

ThreadProc 함수는 상대방 프로세스에 삽입할 코드(스레드 함수)이다. 함수포인터를 이용하여 복잡해보이지만 이는 다음과 같은 의미를 가진다.

hMod = LoadLibraryA("user32.dll");
pFunc = GetProcAddress(hMod, "MessageBoxA");
pFunc(NULL, "CODE INJECTION EXAMPLE", "LEEHAHOON", MB_OK);

pParam->pFunc에는 각각 LoadLibraryA, GetProcAddress의 주소가 들어있다. (이는 InjectCode 함수에서 GetProcAddress를 이용하여 구함) 그리고 이를 Code_Inject.h에서 정의한 함수포인터를 이용하여 이를 호출하는 것으로 생각하면 될 것 같다. (사실 앞에 함수포인터를 안붙여도 될꺼 같은데 친구한테 물어보니 GetProcAddress를 이용해 주소를 구한것이므로 FARPROC 형일 것이고 이는 호출이 불가능하기때문에 형변환을 위해 선언한 것 같다고 함.)

BOOL InjectCode(DWORD dwPID) {
	HMODULE hMod = NULL;
	THREAD_PARAM param = { 0, };
	HANDLE hProcess = NULL;
	HANDLE hThread = NULL;
	LPVOID pRemoteBuf[2] = { 0, };
	DWORD dwSize = 0;

	hMod = GetModuleHandleA("kernel32.dll");

	param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
	param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
	strcpy_s(param.szBuf[0], "user32.dll");
	strcpy_s(param.szBuf[1], "MessageBoxA");
	strcpy_s(param.szBuf[2], "CODE INJECTION EXAMPLE");
	strcpy_s(param.szBuf[3], "LEEHAHOON");

	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);

	dwSize = sizeof(THREAD_PARAM);
	pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);

	WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL);

	dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
	pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

	WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL);

	hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL);

	WaitForSingleObject(hThread, INFINITE);

	CloseHandle(hThread);
	CloseHandle(hProcess);

	return TRUE;
}

ThreadProc 함수 부분을 프로세스에 삽입하는 역할을 하는 함수이다. 앞의 부분에서는 GetProcAddress, strcpy_s를 이용하여 코드와 함께 인젝션할 데이터들을 구조체에 세팅한다. 그리고 DLL Injection과 유사한 코드가 실행된다. OpenProcess를 이용해 Target Process의 핸들을 구하고 삽입할 구조체와 코드의 크기만큼 메모리공간을 확보하여 이를 메모리에 쓴다. 마지막으로 CreateRemoteThread를 이용하여 원격 스레드를 실행한다. 이를 간단히 정리하면 다음과 같다.

hProcess = OpenProcess(Target);
pRemoteBuf[0] = VirtualAlloc(hProcess에 구조체 크기만큼 메모리 할당);
WriteProcessMemory(hProcess, pRemoteBuf[0], 구조체_크기);

pRemoteBuf[1] = VirtualAlloc(hProcess에 코드 크기만큼 메모리 할당);
WriteProcessMemory(hProcess, (LPVOID)ThreadProc, 코드_크기);

CreateRemoteThread(hProcess에 pRemoteBuf[0]을 파라미터로 받아 pRemoteBuf[1]를 실행);

 

Debug vs Release

위의 코드를 Visual Studio로 컴파일하고 실행해보면 Crash가 발생하여 비정상적으로 종료된다. 그 이유는 Debug 모드에서는 런타임 체크 코드(RTC)가 자동으로 들어가기 때문이다. 코드인젝션 후에 런타임 체크 코드를 실행하려고 할때, 주소에 코드가 존재하지 않으므로 정상적으로 실행되지 않는다. 이를 해결하기 위해서는 Visual Studio 설정을 Debug 모드에서 Release 모드로 변경해주면 된다. 해당 방법은 참고문헌에 URL을 적도록 하겠다.

[+] 원래 위에 방법대로 런타임 체크 코드가 문제가 되는 줄 알았지만, 직접 디버깅해본 결과, Debug 모드에서 ThreadProc 코드의 크기를 구하는 부분에서 에러가 발생하여 실패하는 것으로 확인했습니다.

dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

제 컴퓨터 환경에서는 InjectCode =02F1040h, ThreadProc =02F12E0h의 값이였기 때문에 이를 빼는 과정에서 dwSize가 굉장히 큰 값이 나오게되며 VirtualAllocEx 함수가 실패하며 NULL을 리턴하게 됩니다. 그러면서 pRemoteBuf[1]에는 NULL(0x00000000)이 들어가고 CreateRemoteThread 함수가 실행될 때, EIP는 0x00000000가 되며 메모리 위반 에러가 발생했습니다. 그래서 dwSize에 임의의 상수값을 넣어 실행하니 Debug 모드에서도 정상 작동 하는 것을 확인할 수 있었습니다.

Release 모드

 

참고문헌

  • 리버싱 핵심 원리 (URL)
  • Why need to use WINAPI? (URL)
  • Debug vs Release (URL)
  • Visual Studio Debug --> Release (URL)

'Security' 카테고리의 다른 글

HITCON Training lab4 ~ 5  (0) 2020.10.30
Oneshot 가젯  (0) 2020.10.16
HITCON Training lab1 ~ 3  (0) 2020.10.12
CODEGATE 2019 컨퍼런스 후기  (0) 2019.03.30
DLL Injection  (0) 2019.01.20