ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 27장 Code 인젝션
    Reverse Engineering 2021. 1. 14. 23:51

    1. Code Injection

    Code 인젝션이란 상대방 프로세스에 독립 실행 코드를 삽입한 후 실행하는 기법이다. CreateRemoteThread() API를 이용하며 Thread 인젝션이라고 불리기도 한다. 대상 프로세스에 코드와 데이터를 삽입 하주는데  앞서 다뤘던 DLL 인젝션과 비교했을 때 다음과 같은 특징이 있다.

     

    (1) 메모리를 조금만 차지한다.

    (2) 흔적을 찾기 어렵다.

    (3) 별도의 DLL 파일이 필요 없다. (Injection하는 프로그램만 있으면 됨)

     

    Code 인젝션은 규모가 작고 간단한 일을 수행할 때 용이하다.

     

    2. 실습 예제

    실습에서는 notepad.exe 프로세스를 실행시킨 후, Process Explorer를 통해 notepad.exe의 프로세스 ID를 알아낸다. 알아낸 PID를 인자로 CodeInjection.exe 프로그램을 실행시키면 notepad.exe 프로세스에 메시지 박스가 출력된다.

     

    notepad.exe의 PID
    CodeInjection 프로그램 실행
    메시지 박스 출력

     

    3. 소스코드 분석

    아래에 첨부된 코드는 실습에 이용되는 프로그램의 코드다. 똑같이 타이핑도 쳐보고, 첨부된 코드 자체를 복사해서 직접 프로그램을 만들 때는 오류가 생겼다. (Visual Studio 2019 기준) 빌드 환경을 Visual C++ 2010 Express Edition으로 맞추고 컴파일을 해야 정상 작동할 것 같다.

    더보기
    #include <Windows.h>
    #include <stdio.h>
    
    typedef struct _THREAD_PARAM {
    	FARPROC pFunc[2];
    	char szBuf[4][128];
    } THREAD_PARAM, * PTHREAD_PARAM;
    
    //LoadLibraryA()
    typedef HMODULE(WINAPI* PFLOADLIBRARYA) (LPCSTR lpLibFileName);
    //GetProcAddress()
    typedef FARPROC(WINAPI* PFGETPROCADDRESS) (HMODULE hModule, LPCSTR lpProcName);
    //MessageBoxA()
    typedef int (WINAPI* PFMESSAGEBOXA) (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
    
    //Thread Procedure
    DWORD WINAPI ThreadProc(LPVOID lParam) {
    	PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
    	HMODULE hMod = NULL;
    	FARPROC pFunc = NULL;
    
    	//LoadLibrary("user32.dll")
    	hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
    	//GetProcAddress("MessaegBoxA")
    	pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
    	//MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK)
    	((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], "www.reversecore.com");
    	strcpy_s(param.szBuf[3], "ReverseCore");
    
    	if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) {
    		printf("OpenProcess() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	dwSize = sizeof(THREAD_PARAM);
    	if (!(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE))) {
    		printf("VirtualAllocEx() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL)) {
    		printf("WriteProcessMemory() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    	if (!(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE))) {
    		printf("VirtualAllocEx() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL)) {
    		printf("WriteProcessMemory() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL))) {
    		printf("CreateRemoteThread() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	WaitForSingleObject(hThread, INFINITE);
    	CloseHandle(hThread);
    	CloseHandle(hProcess);
    
    	return TRUE;
    }
    
    BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) {
    	TOKEN_PRIVILEGES tp;
    	HANDLE hToken;
    	LUID luid;
    
    	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
    		printf("OpenProcessToken error: %u\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) {
    		printf("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)) {
    		printf("AdjustTokenPrivileges error: %u\n", GetLastError());
    		return FALSE;
    	}
    
    	if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
    		printf("The token does not have the specified privilege.\n");
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    int main(int argc, char* argv[]) {
    	DWORD dwPID = 0;
    
    	if (argc != 2) {
    		printf("\n USAGE : %s <pid>\n", argv[0]);
    		return 1;
    	}
    
    	if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
    		return 1;
    
    	dwPID = (DWORD)atol(argv[1]);
    	InjectCode(dwPID);
    
    	return 0;
    }
    
    

     

    1) main 함수

    int main(int argc, char *argv[])
    {
        DWORD dwPID     = 0;
    
    	if( argc != 2 )
    	{
    	    printf("\n USAGE  : %s <pid>\n", argv[0]);
    		return 1;
    	}
    
    	// change privilege
    	if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
            return 1;
    
        // code injection
        dwPID = (DWORD)atol(argv[1]);
        InjectCode(dwPID);
    
    	return 0;
    }

    main 함수에서는 위와 같이 인자로 받은 PID를 통해 InjectCode 함수를 호출한다.

     

    2) ThreadProc()

    typedef struct _THREAD_PARAM {
    	FARPROC pFunc[2];
    	char szBuf[4][128];
    } THREAD_PARAM, * PTHREAD_PARAM;
    
    //LoadLibraryA()
    typedef HMODULE(WINAPI* PFLOADLIBRARYA) (LPCSTR lpLibFileName);
    //GetProcAddress()
    typedef FARPROC(WINAPI* PFGETPROCADDRESS) (HMODULE hModule, LPCSTR lpProcName);
    //MessageBoxA()
    typedef int (WINAPI* PFMESSAGEBOXA) (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
    
    //Thread Procedure
    DWORD WINAPI ThreadProc(LPVOID lParam) {
    	PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
    	HMODULE hMod = NULL;
    	FARPROC pFunc = NULL;
    
    	//LoadLibrary("user32.dll")
    	hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
    	//GetProcAddress("MessaegBoxA")
    	pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
    	//MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK)
    	((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
    
    	return 0;
    }

    ThreadProc은 상대방 프로세스에 인젝션할 코드로, 결국에는 LoadLibrary("user32.dll")->GetProcAddresss(hMod, "MessageBox")->MessageBox 호출의 순서로 이어진다. 단순히 MessageBox API를 호출하는 것과는 다른 점이 있는데 바로 argument를 넘겨주는 방식이다. 일반적으로 MessageBox를 호출하는 부분을 디버거로 살펴보면 아래와 같다.

     

    MessageBox 호출

    위와 같이 스택에 push 1000781C 등의 명령을 통해 argument를 넘기는데 다른 프로세스에 해당 코드가 그대로 인젝션 되는 경우 정상적으로 실행되지 않는다. 따라서 저 주소에 해당하는 문자열과 API 주소를 같이 인젝션 해야 한다. 이와 같은 조건을 만족시키기 위해 실습 코드에서는 THREAD_PARAM 구조체를 이용해서 argument를 넘겨준다. 작성된 코드를 디버거로 살펴보면 다음과 같다.

     

    ThreadProc()

    위와 같이 ThreadProc()함수는 하드 코딩된 주소의 데이터를 직접 참조하지 않고 lParam(EBP+8)로 받아서 사용하는 것을 확인할 수 있다.

     

    3) InjectCode()

    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], "www.reversecore.com");
    	strcpy_s(param.szBuf[3], "ReverseCore");
    
    	if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) {
    		printf("OpenProcess() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	dwSize = sizeof(THREAD_PARAM);
    	if (!(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE))) {
    		printf("VirtualAllocEx() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, dwSize, NULL)) {
    		printf("WriteProcessMemory() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    	if (!(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE))) {
    		printf("VirtualAllocEx() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL)) {
    		printf("WriteProcessMemory() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	if (!(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL))) {
    		printf("CreateRemoteThread() fail : err_code=%d\n", GetLastError());
    		return FALSE;
    	}
    
    	WaitForSingleObject(hThread, INFINITE);
    	CloseHandle(hThread);
    	CloseHandle(hProcess);
    
    	return TRUE;
    }

    InjectCode의 앞부분에서는 THREAD_PARAM 구조체 변수를 세팅하고 해당 값들이 상대방 프로세스에 인젝션 되어 ThreadProc() 함수에 argument로 전달된다. 상대 프로세스에 순차적으로 data를 위한 VirtualAllocEx(), WriteProcessMemory() / code를 위한 VirtualAllocEx(), WriteProcessMemory()를 해준 후, CreateRemoteThread() API를 호출하여 스레드를 실행시키면 된다.

     

    4. notepad.exe 디버깅

    OllyDbg로 notepad를 디버깅해보자. 우선 F9를 눌러 notepad를 실행상태로 만든다. 이후에 아래와 같이 새로운 스레드가 삽입되었을 때 멈추도록 옵션에 체크표시를 해준다.

     

    Break on new thread 체크

    Process Explorer를 통해 notepad.exe의 PID를 구한다.

    PID 구하기

    이후에 CodeInjection 프로그램을 PID를 인자로 주어 실행시키면, 아래와 같이 OllyDbg가 멈추는 것을 확인할 수 있다.

    OllyDbg 중단지점

    위 부분은 CodeInjection.exe를 디버거로 봤을 때와 동일한 코드임을 확인할 수 있다.

    반응형

    댓글

Designed by Tistory.