본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 28장 - 어셈블리 언어를 이용한 Code 인젝션

어셈블리 언어는 C언어보다 더 자유로운 (정형화되지 않은) 코드를 생성할 수 있다. OllyDbg의 Assemble 기능을 이용해 어셈블리 언어로 인젝션할 코드를 만들어 본다.

 

28.1 OllyDbg의 Assemble 명령

코드 인젝션할 실행파일을 OllyDbg로 실행시킨다.

Assemble 기능은 명령어 수정(혹은 추가)를 원하는 부분에서 스페이스 바를 누르면 된다.

(이때, Fill with NOP's 항목은 기존의 코드보다 짧은 길이의 코드를 입력했을 때 남은 부분은 NOP로 채워 Code Alignment를 맞추는 것이다.)

 

(+ EIP를 원하는 주소로 바꾸는 기능 : New origin here ==> 단순히 EIP만 변경시키는 것으로, 레지스터와 스택의 내용은 전혀 바뀌지 않아서 직접 디버깅을 해 그 주소로 가는 것과 다르다.)

 

1. ThreadProc() 작성

27장에서 본 ThreadProc() 함수와의 다른 점은 Code 사이에 필요한 Data(문자열)을 포함한다는 것이다.

어셈블리 명령어를 위와 같이 입력한다. 그 다음으로 문자열을 입력한다.(Edit : [Ctrl+E]를 이용하면 된다.)

문자열은 반드시 NULL로 끝나야 하므로, 'HEX' 항목에서 00 값을 추가해야 한다.

Code위치에 문자열을 입력했기 때문에 OllyDbg의 Disassembler가 문자열을 IA-32 Instruction으로 잘못 해석해 위와 같은 코드가 나온다. 이상하겠지만, 위 영역이 "ReverseCore" 문자열 영역이다.

 

문자열 영역을 선택한 상태에서 'Analysis' 명령을 내려보면, 아래 사진과 같이 바뀐 것을 볼 수 있다.

Analysis 명령 [Ctrl + A]은 코드를 다시 해석하라는 명령어로 주로 Unpack된 코드를 재해석할 때 많이 사용한다. 

원래는 위 사진에서 선택된 영역에 "ReverseCore"라는 문자열 하나로 나와야 하는데 내 컴퓨터에선 계속 이상하게 나온다. 또한, 문자열 외에 잘 표시되었던 코드들도 잘못 해석된 것을 볼 수 있다. 코드와 데이터를 정확하게 구별하지 못하기 때문이다. Analysis 명령 이전이 코드를 보기 더 쉽기 때문에 명령을 취소한다.(Analysis - Remove analysis from module)

 

문자열 코드 다음 주소(0040103F)부터 명령어를 다시 입력한다.

그리고 다시 Edit 명령을 이용해 문자열을 입력한다. (문자열 마지막은 NULL인걸 잊지말자 / 또한, 코드 부분에 문자열을 입력하는 것이기 때문에 잘못된 해석이 보일 수 있다.)

이후 코드를 계속 입력하면 최종적으로 ThreadProc() 코드가 완성된다.

2. Save File

위에서 생성한 ThreadProc() 코드를 저장한다. 'Copy to executable - All modifications'를 선택한다.

확인 메시지 박스가 나오면 'Copy all'을 선택한다. 그러면 아래 사진과 같은 창이 나온다.

아래 사진과 같이 'Save file'을 눌러주고 저장한다.

 

28.2 인젝터 제작

1. ThreadProc() 함수의 바이너리 코드 얻기

앞 단계에서 생성한 파일을 OllyDbg로 열고 우리가 메모리 창에서 입력한 ThreadProc() 코드 영역의 시작 주소 401000로 이동한다. 아래 사진이 입력한 코드의 영역이다.

사진처럼 영역을 선택하고 'Copy - To file' 항목을 이용해 저장한다.

저장한 파일을 텍스트 에디터로 열어 값을 수정한다. 텍스트 파일의 내용은 Hex 값으로 표현된 ThreadProc()함수인데, IA-32 명령어로 표현되어 있다. (상대방 프로세스에 인젝션할 코드가 된다.)

위 상태에서 불필요한 부분 (주소와 아스키 값)을 제거하고, 모든 바이트마다 0x 표시를 붙이고 , 으로 연결해준다.

완성된 상태는 C언어의 바이트 배열처럼 보인다. (아래 코드의 BYTE g_InjectionCode[] 를 참고하면 된다.)

(저처럼 하나씩 고치지 말고 x32dbg를 이용해서 간편하게 복사하세요.......)

 

2. CodeInjection2.cpp

인젝터 프로그램 소스코드 중 일부를 가져왔다. 

// CodeInjection2.cpp
// reversecore@gmail.com
// http://www.reversecore.com

#include "windows.h"
#include "stdio.h"

typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
} 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");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

    // Open Process
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,               // dwDesiredAccess
                                  FALSE,                            // bInheritHandle
                                  dwPID)) )                         // dwProcessId
    {
        printf("OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for THREAD_PARAM
    if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(THREAD_PARAM),      // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_READWRITE)) )         // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[0],                          // lpBaseAddress
                            (LPVOID)&param,                         // lpBuffer
                            sizeof(THREAD_PARAM),                   // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for ThreadProc()
    if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(g_InjectionCode),   // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_EXECUTE_READWRITE)) ) // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[1],                          // lpBaseAddress
                            (LPVOID)&g_InjectionCode,               // lpBuffer
                            sizeof(g_InjectionCode),                // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !(hThread = CreateRemoteThread(hProcess,                    // hProcess
                                       NULL,                        // lpThreadAttributes
                                       0,                           // dwStackSize
                                       (LPTHREAD_START_ROUTINE)pRemoteBuf[1],
                                       pRemoteBuf[0],               // lpParameter
                                       0,                           // dwCreationFlags
                                       NULL)) )                     // lpThreadId
    {
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);	

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

27장에 나온 CodeInjection.cpp의 코드와 유사하지만, 인젝션하는 코드에 필요한 문자열 데이터를 같이 포함시켰다는 점에서 다르다. 그래서 _THEAD_PARAM 구조체에서 문자열 멤버가 사라진 것을 볼 수 있다.

 

28.3 상세 분석

앞에서 완성한 CodeInjection2.exe를 실행시켜 notepad.exe에 코드 인젝션을 한다. 코드 인젝션이 성공하면 위 사진과 같이 인젝션된 스레드 코드 시작 위치에서 디버깅이 멈춘다.

이제 해당 코드들을 하나씩 살펴보자

1. THREAD_PARAM 구조체 포인터

스택 프레임이 생성된 후 [EBP+8]이 의미하는 것은 함수로 넘어온 첫 번째 파라미터이다.

이 경우엔, THREAD_PARAM 구조체 포인터가 된다. 해당 구조체의 멤버는 LoadLibraryA()와 GetProcAddress()의 포인터이다. 해당 명령어를 실행한 후 레지스터와 메모리창을 확인해 보면 아래와 같다.

메모리
레지스터

ESI의 값인 150000의 주소를 메모리에서 찾아가면 4바이트씩 2개가 저장된 것을 볼 수 있다. 각각의 값들은 앞서 설명했듯이, 함수의 포인터 주소이다. 메모리창을 보기 쉽게 변경(Long)하면, 아래 사진처럼 2개의 함수 시작 주소를 볼 수 있다.

2. "user32.dll" 문자열

위 코드는 스택에 문자열을 저장하는 기법이다. 스택에 직접 접근할 수 있는 어셈블리 프로그래밍 언어에서만 가능한 독특한 기법이다. (코드는 순서대로 [Null Null l l] , [d.23], [resu] 의 값을 PUSH 하고 있다.)

x86 CPU의 리틀 엔디언 표기법과 스택의 거꾸로 자라는 특성 때문에 문자열을 거꾸로 뒤집어서 입력하는 것을 볼 수 있다. (==> user32.dll\0\0 이라는 문자열이다.)

00160015까지 디버깅을 하면 아래 그림과 같다.

이처럼 PUSH 명령어를 이용하면 원하는 문자열을 스택에 입력할 수 있고 Code 인젝션을 할 때 문자열 데이터를 따로 인젝션 하지 않고 코드에 포함시켜 코드만 인젝션 할 수 있다.

3. "user32.dll" 문자열 파라미터 입력

LoadLibraryA() API는 파라미터로 로딩시킬 DLL 파일 이름 문자열 주소를 받는다.

아래 그림을 보면 ESP의 값은 0043FF64이고 해당 주소를 메모리에서 보면 "user32.dll" 문자열 시작 주소를 나타내는 것을 알 수 있다. 따라서 해당 명령어는 "user32.dll" 문자열을 스택에 입려가는 명령이다.

4. LoadLibraryA("user32.dll") 호출

파일 문자열 주소를 스택에 저장했기 때문에 LoadLibraryA()를 호출할 수 있다. 현재 ESI의 값은 00150000으로 해당 주소에 저장된 값은 LoadLibraryA() API의 시작 주소이다. (파라미터 설명을 참고하면 좋다)

==> CALL[ESI] = CALL[00150000] = CALL 75C91240 = CALL Kernel32.LoadLibraryA

해당 명령어를 실행하면 LoadLibraryA() API가 호출되면서 파라미터로 입력된 'user32.dll'이 로딩된다. notepad.exe 프로세스는 실행될 때 이미 user32.dll을 로딩했기 때문에 그 로딩 주소만 리턴한다. (EAX = 750F0000)

함수의 리턴 값은 EAX에 저장된다. 'View - Excutable modules [Alt + E]' 항목을 선택해 아래와 같은 창을 볼 수 있다. 해당 사진을 잘 보면, EAX에 저장된 주소에 user32.dll 파일이 로딩된 것을 확인할 수 있다. (해당 주소가 user32.dll의 로딩주소이다.)

5. "MessageBoxA" 문자열

user32.dll 문자열 입력 방법과 동일한 코드이다. 해당 코드에선 "MessageBoxA"라는 문자열을 입력하고 있다.

6. GetProcessAddress(hMod, "MessageBoxA") 호출

해당 코드는 "MessageBoxA" 문자열 주소(PUSH ESP)와 user32.dll의 시작주소(PUSH EAX)를 스택에 입력한다. 

이 2개의 값들은 각각 GetProcessAddress 함수의 파라미터로 사용된다.(스택의 성질로 파라미터 값이 거꾸로 입력된다.)

아래 사진은 3개의 코드를 모두 실행시킨 후의 스택의 모습니다.

위 코드를 보면 CALL [ESI + 4] 형태이다. 기존 ESI 레지스터의 값은 00150000로, 4바이트를 더하면 THREAD_PARAM의 두 번째 멤버인 GetProcessAddress() API의 시작주소를 나타내는 것이다.

==> CALL [ESI+4] = CALL [ 0015004] = CALL [75C8FB60] = CALL Kernel32.GetProcAddress 

해당 함수 실행 후 EAX에 MessageBoxA() API의 시작 주소가 저장된 것을 볼 수 있다.

7. MessageBoxA() 파라미터 입력 - MB_OK

MessageBoxA() API는 4개의 파라미터를 받는다. 해당 값은 가장 마지막  파라미터인 uType의 값으로 0이다.

해당 값이 0이면 MB_OK를 의미하며, 단순히 OK(확인) 버튼 한 개만 보여준다.

8. MessageBoxA() 파라미터 입력 - "ReverseCore"

해당 코드는 앞에서 나온 PUSH를 이용한 문자열 입력 방식이 아닌, CALL을 이용한 명령이다. 코드 사이에 포함된 문자열 데이터 주소를 스택에 입력하는 기법이다.(어셈블리 프로그래밍 언어에서만 가능하다.)

 

00160033~0016003E 주소 영역은 프로그램 코드 영역에 존재하지만, 그 내용은 "ReverseCore" 문자열 데이터이다. 즉, "ReverseCore" 문자열의 시작 주소는 00160033이고 이 값이 MessageBoxA()의 세 번째 파라미터(IpCation)으로 사용된다. 

 

제일 위 CALL 명령어를 실행하면 스택에 "RevserseCore" 문자열 시작 주소인 00160033이 입력된다. 이는 CALL 명령을 호출하면 다음 주소를 리턴 주소로 스택에 저장하는 특성을 이용한 방법이다.  CALL 명령을 수행하면 리턴 주소를 스택에 입력(PUSH)한 후 해당 함수 주소로 이동(JMP)하기 때문에, CALL 명령어는 PUSH, JMP 명령어를 합쳐 놓은 것이 된다.

 

그리고 JMP로 이동한 0016003F는 함수가 아니기 때문에 RETN 명령어로 인해 리턴 주소로 이동하지 않는다. 

( 0016003F의 명령어 = CALL 00160058) 여기서의 CALL 명령어는 바로 뒤에 이어지는 "ReverseCore" 문자열 주소를 스택에 입력하고 그 다음 코드 명령어로 가기 위해서 사용하는 것이다.

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

(0016003F : CALL 00160058이 짤렸다...)

해당 코드도 세 번째 파라미터 입력 방식과 동일하다. 이번에 입력하는 값은 "www.reversecore.com"으로 MessageBoxA()의 두 번째 파라미터(IpText)이다.  문자열에 해당되는 부분은 00160044 ~ 00160057까지이다.

10. MessageBoxA() 파라미터 입력 - NULL

00160058	6A 00		PUSH 0

MessageBoxA()의 첫 번째 파라미터인 hWnd 값을 입력한다. 일반적으로는 메시지 박스가 소속된 창의 핸들을 입력하지만, 여기선 NULL을 입력했다...

11. MesaageBoxA() 호출

현재 EAX에는 위에서 호출한 GetProcAddress()에 의해 리턴된 MessageBoxA() API의 시작 주소가 저장되어 있다. 해당 명령어를 실행하면 다음과 같은 창(메시지 박스)이 나온다.

12. ThreadProc() 리턴 값 세팅 & 스택 프레임 해제 및 함수 리턴

먼저 notepad.exe 프로세스에 인젝션된 코드(ThreadProc() 스레드 함수)가 종료될 준비를 한다. 함수의 리턴 값은 EAX를 이용하기 때문에 해당 값은 0으로 만들어 준다.

 

이후 ThreadProc() 함수를 시작할 때 생성한 스택 프레임을 해제한다. 그 후 RETN 명령어로 함수가 종료된다. 앞에서 PUSH를 이용해 입력했던 문자열을 POP 명령으로 하나씩 지울 필요 없이 스택 프레임을 해제해 한 번에 초기화한다.