ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 28장 어셈블리 언어를 이용한 Code 인젝션
    Reverse Engineering 2021. 1. 15. 23:03

    이번 실습에서는 28장의 Code injection과 동일하지만 ThreadProc() 함수를 어셈블리 언어로 작성하여 code 인젝션을 구현한다.

     

    1. OllyDbg로 코드 작성

    OllyDbg로 예제 파일인 asmtest.exe를 열고 401000 주소에 다음과 같이 New origin here 항목을 이용하여 EIP를 401000으로 변경해보자.

     

    EIP 변경

    이제 여기다 ThreadProc() 함수를 작성해볼 것이다. (asmtest.exe는 단순히 OllyDbg를 이용하여 어셈블리 코드를 작성하기 위한 아무 의미 없는 프로그램으로 보면 된다.) 아래와 같이 작성해보자.

     

    ThreadProc() 작성

    이제 문자열을 입력하기 위해 dump 창의 00401033(함수가 끝난 다음 주소)에 아래와 같이 "ReverseCore:를 입력해주자. (이때, NULL 문자로 끝나야 함에 주의하자)

    ReverseCore 문자열 입력

    문자열이 끝나는 지점 이후인 0040103F에 다음과 같은 어셈블리 명령어를 입력하자.

     

    명령어 입력

    다시 dump창의 00401044 주소(위 명령어가 끝나는 지점 이후)에 문자열 "www.reversecore.com"을 입력하자.

    www.reversecore.com 문자열 입력

    마지막으로 해당 문자열이 끝나는 지점 이후인 00401061에 아래와 같이 명령어를 작성하자.

    ThreadProc() 마지막 부분

    이로써 ThreadProc() 함수의 작성이 완료되었다. OllyDbg가 문자열이 아닌 코드로 잘못 인식하는 경우도 있어 완벽한 코드로 나타나지는 않는다.

    ThreadProc() 작성 완료

    이제 해당 부분을 파일로 저장하고 x32dbg로 열어보자.

    해당 부분을 바이트 단위로 copy하여 Injector 소스코드에 삽입해야 한다. x32dbg에서는 덤프 창에서 C언어의 바이트 배열로의 copy를 편리하게 지원하므로 해당 기능을 이용하자.

    ThreadProc() 복사

     

    2. 인젝터(Injector) 제작

    복사한 내용을 바탕으로 아래와 같은 코드를 작성해보자.

    #include "Windows.h"
    #include "stdio.h"
    
    typedef struct _THREAD_PARAM {
    	FARPROC pFunc[2];
    } THREAD_PARAM, * PTHREAD_PARAM;
    
    BYTE g_InjectionCode[] = {
    0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00, 0x00, 0x68, 0x33, 0x32, 0x2E, 0x64,
    0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68, 0x61, 0x67,
    0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54, 0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C,
    0x00, 0x00, 0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F, 0x72, 0x65, 0x00, 0xE8,
    0x14, 0x00, 0x00, 0x00, 0x77, 0x77, 0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
    0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00, 0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5,
    0x5D, 0xC3
    };
    
    BOOL InjectCode(DWORD dwPID) {
    	HMODULE hMod = NULL;
    	THREAD_PARAM param = { 0, };
    	HANDLE hProcess = NULL;
    	HANDLE hThread = NULL;
    	LPVOID pRemoteBuf[2] = { 0, };
    
    	hMod = GetModuleHandleA("kernel32.dll");
    
    	param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    	param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
    
    	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
    
    	pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, sizeof(THREAD_PARAM), MEM_COMMIT, PAGE_READWRITE);
    	WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)&param, sizeof(THREAD_PARAM), NULL);
    
    	pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, sizeof(g_InjectionCode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    	WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)&g_InjectionCode, sizeof(g_InjectionCode), NULL);
    
    	hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL);
    	WaitForSingleObject(hThread, INFINITE);
    
    	CloseHandle(hThread);
    	CloseHandle(hProcess);
    
    	return TRUE;
    }
    
    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;
    }

    위 코드는 27장의 CodeInjection.cpp와 매우 유사하다. 다만 차이점은 인젝션하는 코드에 문자열 데이터를 같이 포함시켰다는 점이다. 이로 인해 _THREAD_PARAM 구조체에 문자열 멤버를 별도로 지정하지 않아도 된다. 또한 기존의 ThreadProc() 함수가 OllyDbg에서 작성한 코드의 바이트 배열로 이루어져 있다.

     

    3. 디버깅 실습

    위 코드를 컴파일하여 CodeInjection2.exe 파일을 만들고, 27장의 실습과 똑같은 방식으로 디버깅을 진행한다. OllyDbg로 메모장을 켜놓고 F9를 눌러 실행시킨 다음, Break on new thread 옵션을 활성화시킨다. 이후에 인젝터를 통해 코드가 인젝션 되면 OllyDbg의 특정 지점(인젝션 된 코드)에서 디버깅이 멈출 것이다.

    notepad.exe의 ThreadProc()

    위와 같이 동일한 code가 notepad.exe 디버깅 중 발견되는 것을 확인할 수 있다. 이제 코드를 하나하나 분석해보자.

     

     

    1) 스택 프레임 생성

    스택 프레임 형성

    이쯤되면 익숙한 명령어 PUSH EBP / MOV EBP, ESP를 통해 스택 프레임을 생성하고 있다.

     

     

    2) THREAD_PARAM 구조체 포인터

    THREAD_PARAM 구조체

    [EBP+8]은 함수로 넘어온 첫 번째 파라미터를 가리킨다. 위 경우에는 THREAD_PARAM 구조체 포인터로, 구조체에는 각각 "LoadLibraryA"와 "GetProcAddress()"의 포인터가 저장된다. (인젝터에서 코드를 인젝션 한 후, 스레드가 실행될 때 파라미터로 넣어줬다.) 위 명령어까지 실행시킨 후 ESI에 들어있는 값을 dump 창에서 확인해보면 다음과 같다.

     

    3) "user32.dll" 문자열

    "user32.dll" 문자열

    위 코드는 스택에 문자열을 저장하는 기법이다. 스택에 직접 접근할 수 있는 어셈블리 프로그래밍의 특징을 이용한 방법으로 PUSH 72657375까지 명령을 수행한 뒤 dump 창에 스택 주소를 따라가보면 아래와 같은 값이 저장되어 있는 것을 확인할 수 있다.

    스택에 쌓인 "user32.dll" 문자열

    이때, Little Endian 표기법과 스택이 거꾸로 자란다는 특성 때문에 스택에 문자를 쌓을 때는 거꾸로 쌓인다는 점에 주의해야 한다.

     

    4) "user32.dll" argument 넘겨주기 & LoadLibraryA("user32.dll") 호출

    argument 넘기기

    3)에서 "user32.dll"을 쌓고 난 직후, ESP에는 "user32.dll"이라는 문자열이 쌓여있다. 또한 2)에서 [ESI]에는 LoadLibrary API의 주소가 담겨있는 것을 확인하였다. 따라서 001F0016의 CALL 명령이 호출되기 위해 "user32.dll"문자열을 argument로 넘기기 위해 PUSH ESP를 해준다.

    LoadLibraryA API 호출

    위 부분을 실행시키고 나면 반환 값으로 EAX 레지스터에 user32.dll의 주소가 저장된 것을 확인할 수 있다.

    LoadLibraryA("user32.dll") 직후

    5) "MessageBoxA" 문자열

    앞서 "user32.dll"을 하나하나 스택에 쌓은 것과 똑같이, MessageBoxA 또한 같은 방식으로 쌓게 된다. PUSH 7373654D 명령이 끝난 후 스택에 저장된 값을 dump 창에서 확인해보자.

    "MessageBoxA" 문자열 쌓기

    6) GetProcAddress(hMod, "MessageBox") 호출

    GetProcAddress() API 호출

    ESP는 앞에서 스택에 쌓았던 MessageBoxA의 문자열 주소이고, EAX에는 4)에서 LoadLibraryA를 호출하여 반횐된 user32.dll의 시작 주소를 나타낸다. 해당 명령어를 수행함으로써 user32.dll에 있는 MessageBoxA API의 시작 주소가 EAX 레지스터에 저장되게 된다.

    MessageBoxA 주소값 저장

     

    7) MessageBoxA() 파라미터 입력 ("ReverseCore")

    MessageBox() API는 다음과 같은 매개변수를 갖는다.

    int WINAPI MessageBox(
    	__in_opt HWND hWnd,
        __in_opt LPCTSTR lpText,
        __in_opt LPCTSTR lpCaption,
        __in UINT uType
     );

     

    함수가 호출되기 전에 파라미터를 역으로 쌓아 넘기므로, push 0(==MB_OK) 이후에 추가적으로 push 명령이 나와야 할 것 같지만 CALL 00FC003F 명령이 나온다. 이 또한 어셈블리 프로그래밍의 트릭을 이용한 것 방법으로 해당 명령에서 Step Into(F7)을 해보자.

     

    dump창

    이때 dump 창을 주목해서 보자. CALL 명령을 수행하고 스택에 쌓인 값을 따라가 보니 "ReverseCore" 라는 문자열이 저장되어 있는 것을 확인할 수 있다. 이 방법은 CALL 명령어의 동작 원리를 이용한 것으로 CALL 명령어는 근본적으로는 PUSH, JMP 명령어를 합쳐놓은 것과 같다는 점을 응용한 것이다. 즉 CALL 다음에 나와있는 명령어 (되돌아 가야 할 주소)를 스택에 쌓는데, 이때 쌓아지는 값은 실행코드가 아닌 "ReverseCore" 문자열이고, 문자열이 끝나는 지점의 명령어로 JMP 하는 구조인 것이다.

     

    8) MessageBoxA() 파라미터 입력 "www.reversecore.com"

    7)과 마찬가지로 8)에서도 CALL 명령을 통해 스택에 쌓고 다음 실행되어야 할 코드로 넘어간다.

    dump 창

    9) MessageBoxA() 파라미터 입력 NULL & MessageBoxA() 호출

    마지막으로 첫 번째 파라미터에 NULL 값을 넘겨줘야하므로 push 0 명령을 실행해준다.

    NULL 넘기기 & API 호출

    CALL EAX를 통해 이전에 저장되었던 MessageBoxA() API를 호출하면 메모장에 메시지 박스가 나타나는 것을 확인할 수 있다.

    메시지 박스

    10) ThreadProc() 리턴 값 세팅

    마짐가으로 XOR EAX, EAX 및 스택 프레임 정리 명령어니 MOV ESP, EBP / POP EBP를 통해서 ThreadProc() 함수를 종료시킨다.

    반응형

    댓글

Designed by Tistory.