Code Injection 기법 (2)

study 2010/06/23 00:15

Code Injection 실습 예제인 CodeInjection.exe 의 소스 코드(CodeInjection.cpp)를 살펴보도록 하겠습니다. 


이전 강좌의 내용은 아래 링크를 참조하시기 바랍니다.

Code Injection 기법 (1)



CodeInjection.cpp

* 참고!
CodeInjection.cpp 는 Visual C++ 2008 Express Edition 으로 개발되었으며 Windows 7 32bit 환경에서 테스트 되었습니다. 또한 Visual C++ 의 코드 최적화 기능을 사용하지 않고 빌드 하였습니다. (/Od) 

아래 소개되는 코드들은 설명의 편의를 위하여 에러 처리 부분을 생략하였습니다. 완전한 코드는 첨부된 CodeInjection.cpp 파일을 참고하시기 바랍니다.

main()

먼저 main() 함수를 살펴 보겠습니다.

int main(int argc, char *argv[])
{
    DWORD dwPID     = 0;

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

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

    return 0;
}

<코드 1 – main() 함수>

main() 함수의 역할은 InjectCode() 함수를 호출하는 것입니다. 이때 함수 파라미터로 상대방 프로세스의 PID 값을 넘겨줍니다.

ThreadProc()

이제 상대방 프로세스에 인젝션 시킬 코드(스레드 함수)를 살펴보겠습니다.

// Thread Parameter
typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
    char    szBuf[4][128];          // "user32.dll", "MessageBoxA", 
                                    // "www.reversecore.com", "ReverseCore"
} 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”)
    //   pParam->pFunc[0] = kernel32!LoadLibraryA()
    //   pParam->szBuf[0] = “user32.dll”
    hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);

    // GetProcAddress(“MessageBoxA”)
    //   pParam->pFunc[1] = kernel32!GetProcAddress()
    //   pParam->szBuf[1] = “MessageBoxA”
    pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);

    // MessageBoxA(NULL, “www.reversecore.com”, “ReverseCore”, MB_OK)
    //   pParam->pFunc[1] = kernel32!GetProcAddress()
    //   pParam->szBuf[1] = “MessageBoxA”
    ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

    return 0;
}

<코드 2 – ThreadProc() 함수>

위 코드에서 실제로 인젝션 되는 부분은 ThreadProc() 함수입니다. 그 위의 typedef 문은 C 언어 문법을 위한 것이므로 인젝션 시킬 필요가 없습니다.

ThreadProc() 의 코드는 함수 포인터를 많이 사용해서 얼핏 복잡하게 보이지만 사실 내용은 아래와 같이 간단합니다.

hMod = LoadLibraryA(“user32.dll”);
pFunc = GetProcAddress(hMod, “MessageBoxA”);
pFunc(“www.reversecore.com”, “ReverseCore”);

<코드 2> 의 주석을 참조하시면 ThreadProc() 의 코드는 쉽게 이해가 가실 것입니다.

중요한 것은 ThreadProc() 코드의 개념입니다. 

Code Injection 기법의 핵심은 독립 실행 코드를 인젝션 시키는 것입니다. 

그러기 위해서 코드와 (코드에서 참조하는) 데이터를 같이 인젝션 시키는 것입니다. 그리고 인젝션 시키는 코드에서 역시 인젝션 시킨 데이터를 정확히 참조할 수 있도록 해야 합니다.

위 ThreadProc() 함수를 보시면 직접 API 를 호출 하지 않습니다. 또한 문자열도 직접 정의해서 사용하지 않습니다. 전부 스레드 파라미터로 넘어온 THREAD_PARAM 구조체에서 가져다 사용하고 있습니다.

만약 일반적인 프로그램이라면 ThreadProc() 의 코드는 아래와 같이 간단히 작성할 수 있습니다.

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK);

    return 0;
}

<코드 3 – 일반적인 프로그램에서의 ThreadProc()>

위 <코드 3>을 빌드하여 생성된 파일을 디버거로 보면 아래 그림과 같습니다.

 
<Fig. 1>

위 그림의 코드(10001000 ~ 10001018 영역)를 다른 프로세스에 그대로 인젝션 시킨다면 정상적으로 실행되지 않습니다. 그 이유는 코드에서 사용되는 10009290, 1000929C, 100080F0 주소의 내용이 상대방 프로세스에는 없기 때문입니다.

따라서 저 주소에 해당하는 문자열과 API 주소를 같이 인젝션 시켜야 합니다. 또한 <Fig. 1>의 코드 역시 그 인젝션된 데이터의 주소를 정확히 참조하도록 프로그래밍 되어야 합니다.

이와 같은 조건을 만족시키기 위해서 <코드 2> 의 ThreadProc() 함수는 THREAD_PARAM 구조체를 이용해서 2 개의 API 주소와 4 개의 문자열 데이터를 받아들입니다. 2 개의 API 는 바로 “LoadLibraryA()”“GetProcAddress()” 입니다. 이 2 개의 API 만 있으면 모든 라이브러리의 함수를 호출 할 수 있습니다. 

* 참고 사항

1. 위 실습 예제의 경우에 LoadLibraryA() 와 GetProcAddress() 의 주소 말고 MessageBoxA() 의 주소를 직접 전달하여 사용해도 됩니다. 하지만 정석은 LoadLibraryA() 와 GetProcAddress() 만을 전달한 후 이를 이용해서 필요한 DLL 을 로딩시켜 원하는 함수 주소를 직접 구하는 것입니다. 이 방식의 장점은 해당 라이브러리를 프로세스에 정확히 로딩시킨다는 것입니다. 가령 notepad.exe 프로세스에 윈도우 소켓 API 인 ws2_32!connect() 의 주소를 넘겨주면 에러가 발생할 것입니다. (notepad.exe 에 기본적으로 ws2_32.dll 이 로딩되지 않았으니까요.)

2. 대부분의 유저 모드 프로세스는 kernel32.dll 을 로딩하므로 LoadLibraryA(), GetProcAddress() 의 주소를 직접 넘기는 것은 크게 무리가 없습니다. 단, kernel32.dll 을 로딩하지 않는 시스템 프로세스(예: smss.exe)도 있으니 사전에 꼭 확인하시기 바랍니다.

3. Kernel32.dll 같은 시스템 라이브러리는 OS 가 부팅되어 있는 상태에서는 모든 프로세스에서 동일한 주소에 로딩되어 있습니다. OS 버전이 틀리거나 재부팅(Vista, 7 의 경우)을 하거나 하면 같은 모듈이라도 로딩 주소는 틀려집니다.

위 <코드 2> 의 내용을 디버거로 살펴보면 아래 그림과 같습니다.

 
<Fig. 2>

위의 <Fig. 2> 의 코드를 보시면 모든 중요한 데이터는 스레드 파라미터인 pParam 으로 받아서 사용하는 것을 알 수 있습니다. 즉, <Fig. 2> 의 ThreadProc() 함수는 독립 실행 코드라고 말 할 수 있습니다. 위의 <Fig. 2> 와 앞에서 소개한 <Fig. 1> 를 비교해 보시면 그 차이점을 확인하실 수 있습니다.

* 참고!
Visual C++ 2008 Express Edition 에서 프로젝트의 [Release/Debug] 모드와 [최적화] 옵션에 따라서 CodeInjection.cpp 파일은 <Fig. 2> 와는 다른 형태로 빌드 될 수 있습니다. 위 실습 예제는 Release 모드에서 최적화 옵션 사용 안함(/Od)으로 빌드 하였습니다. 

InjectCode()

아래는 Code Injection 기법의 핵심인 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");

    // set THREAD_PARAM
    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");

    // Open Process
    hProcess = OpenProcess(PROCESS_ALL_ACCESS,  // dwDesiredAccess
                           FALSE,                   // bInheritHandle
                           dwPID);                  // dwProcessId

    // Allocation for THREAD_PARAM
    dwSize = sizeof(THREAD_PARAM);
    pRemoteBuf[0] = VirtualAllocEx(hProcess,        // hProcess
                                   NULL,            // lpAddress
                                   dwSize,          // dwSize
                                   MEM_COMMIT,      // flAllocationType
                                   PAGE_READWRITE); // flProtect

    WriteProcessMemory(hProcess,                    // hProcess
                       pRemoteBuf[0],               // lpBaseAddress
                       (LPVOID)&param,              // lpBuffer
                       dwSize,                      // nSize
                       NULL);                       // [out] lpNumberOfBytesWritten

    // Allocation for ThreadProc()
    dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    pRemoteBuf[1] = VirtualAllocEx(hProcess,        // hProcess
                                   NULL,            // lpAddress
                                   dwSize,          // dwSize
                                   MEM_COMMIT,      // flAllocationType
                                   PAGE_EXECUTE_READWRITE); // flProtect

    WriteProcessMemory(hProcess,                    // hProcess
                       pRemoteBuf[1],               // lpBaseAddress
                       (LPVOID)ThreadProc,          // lpBuffer
                       dwSize,                      // nSize
                       NULL);                       // [out] lpNumberOfBytesWritten

    hThread = CreateRemoteThread(hProcess,          // hProcess
                                 NULL,              // lpThreadAttributes
                                 0,                 // dwStackSize
                                 (LPTHREAD_START_ROUTINE)pRemoteBuf[1],
                                 pRemoteBuf[0],     // lpParameter
                                 0,                 // dwCreationFlags
                                 NULL);             // lpThreadId

    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

<코드 4 - InjectCode() 함수>

위 코드는 DLL Injection 코드와 매우 유사합니다. 

InjectCode() 함수의 앞 부분은 THREAD_PARAM 구조체 변수를 세팅하고 있습니다. 이 값들은 상대방 프로세스에 인젝션 되어 ThreadProc() 스레드 함수에 파라미터로 전달될 것입니다.

* 참고!
Windows 7 에서 모든 프로세스에 로딩된 kernel32.dll 의 주소가 동일하므로 CodeInjection.exe 프로세스에서 구한 API(“LoadLibraryA”, “GetProcAddress”) 주소와 notepad.exe 프로세스에서 구한 API(“LoadLibraryA”, “GetProcAddress”) 주소가 서로 동일하다는 것을 기억하시기 바랍니다.

그리고 API 함수 호출이 이어지는데요, 핵심 API 함수들의 호출 흐름만 살펴보면 아래와 같습니다.

OpenProcess()

// data : THREAD_PARAM
VirtualAllocEx()
WriteProcessMemory()

// code : ThreadProc()
VirtualAllocEx()
WriteProcessMemory()

CreateRemoteThread()

위 코드의 핵심은 상대방 프로세스에 data 와 code 를 각각 메모리 할당하고 인젝션 시켜 준다는 것입니다. 

마지막으로 CreateRemoteThread() API 를 이용해서 원격 스레드를 실행시킵니다.

+---+

이로써 Code Injection 기법을 이용한 실습 예제 소스코드에 대한 설명을 마치도록 하겠습니다.

설명의 편의성을 위하여 매우 기초적인 실습 예제를 소개하였습니다. Code Injection 의 개념을 이해하시는데 어려움이 없을 거라 생각됩니다. 위 개념을 이해하셨다면 다양한 아이디어로 자신만의 Code Injection 기법을 연습해 보시기 바랍니다

* 참고!
제가 Code Injection 기법을 구현할 때 인젝션 시킬 코드 부분은 어셈블리 언어로 프로그래밍 합니다. 복잡한 것은 MASM 을 사용하고, 간단한 것은 OllyDbg 의 “Assemble” 명령을 사용합니다. (단축키 [Space]) 이렇게 만들어진 Hex 코드 버퍼를 위 InjectCode() 함수 내에서 상대방 프로세스에 인젝션 시켜줍니다. 이러한 방법은 좀 더 직관적인 인젝션 코드를 만드는데 도움이 됩니다. Code Injection 마지막 강좌에서 간단히 실습해보도록 하겠습니다.

다음 강좌에서는 Code Injection 기법을 디버깅 하는 방법에 대해서 알아보도록 하겠습니다.


ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~

Trackback Address :: http://www.reversecore.com/trackback/82 관련글 쓰기

  1. Ezbeat 2010/06/23 06:58 댓글주소 | 수정 | 삭제 | 댓글

    와아~ 축구도 16강 진출해서 기쁜 마음에 들어와봤는데.. 글이 올라와서 있어서 더욱 기쁘네요!
    역시나 좋은 강좌 잘 봤습니다.

    제가 코드 인젝션을 응용해서 프로그램을 하나 만들어 볼건데.. 인젝션 시킬 Thread에서 API함수가 아닌 일반 함수(직접 만든 함수)를 사용해보려고 합니다.
    막상 구현해보려니.. 상당히 난해하군요..ㅠㅠ
    1. Thread에서 사용할 함수의 내부에서 사용된 모든 API함수 또한 넘겨진 구조체에 있는 값을 사용해야한다.
    2. 해당 함수도 인젝션을 시키고 들어간 주소를 구조체에 담아야한다..;;
    대충 이렇게 구현해야할 꺼같은데.. Code Injection을 사용해 API후킹을 한다면..-_-;;
    구현하는데 쫌 시간이 걸리겠군요..ㅠㅠ

    아 맞다.. 7월 5일날 입대(합격!)를 해야하는데 -_-;; 강좌를 다보고갈 수 있겠죠?? 하핫;;

    • reversecore 2010/06/23 21:18 댓글주소 | 수정 | 삭제

      원리는 동일하기 때문에 알고 계신 내용을 곰곰히 생각해 보시면 충분히 해결하실 수 있으실 겁니다.

      Code Injection 기법 설명이 끝나면 바로 이어서 Code Injection 을 이용한 API Hooking 에 대한 강좌를 진행할 예정입니다.

      원하시는 바를 꼭 이루시기 바랍니다.

      감사합니다.

  2. 궁금합니다.. 2011/01/01 21:01 댓글주소 | 수정 | 삭제 | 댓글

    궁금한게 있는데요!!

    컴파일을 하고나서 하면 CreateRemoteThread() fail : err_code = 5

    이렇게 뜨는데요.

    Error Lookup에서 찾아보니. 엑새스가 거부 되었습니다. 라고 나오는데

    어떻게 해야되나요 ? 관리자 권한으로도 실행해 보앗는데.. 안되네요!

    • reversecore 2011/01/03 01:03 댓글주소 | 수정 | 삭제

      안녕하세요.

      이전 글에 소개된 CodeInjection.exe 를 사용해도 제대로 동작하지 않는지요? (http://www.reversecore.com/attachment/cfile21.uf@20491A014C1F87C766AA91.exe)

      에러 메시지만 보면 권한이 없는것 같은데요. 위에 첨부된 CodeInjection.zip 코드 그대로 하신것 맞으시죠? 블로그 본문에 있는 코드는 실제 코드를 간략하게 편집한 것입니다. 따라서 권한 상승 코드는 보여주지 않았지요. 첨부된 소스 코드에 해당 코드가 포함되어 있습니다.

      잘 안되시면 사용하시는 실행환경을 좀 알려주시기 바랍니다.

      저도 좀 더 다양한 PC 환경에서 테스트 해보도록 하겠습니다.

      감사합니다.

  3. gg 2011/02/16 19:14 댓글주소 | 수정 | 삭제 | 댓글

    인젝션 함수에서 스레드 메모리 할당할때 스레드의 사이즈를 어떻게 구한건가요?
    (dword)인젝션함수 - (dword)스레드 라고 되어잇는데 이러면 단지 주소값을뺀건데
    자세한 설명부탁드림니다.

    • reversecore 2011/02/18 22:27 댓글주소 | 수정 | 삭제

      안녕하세요.

      네, 본문에는 문의하신 내용에 대한 설명이 부족하네요.

      인젝션 시킬 함수의 코드 크기를 구하면 되는건데요.

      일반적으로 C 언어 소스를 만들어 컴파일 하면 소스 순서대로 바이너리에 생성됩니다. 예를 들어 A(), B() C() 순으로 프로그래밍 되어있다면, 빌드된 바이너리에도 같은 순서대로 배치 된다는 뜻입니다.

      위 전체 코드를 받아보시면 ThreadProc() 함수 다음에 바로 InjectCode() 함수가 나타납니다.

      따라서 InjectCode - ThreadCode 는 ThreadCode() 함수의 크기로 볼 수 있는 것입니다.

      감사합니다.

  4. 2011/06/01 22:05 댓글주소 | 수정 | 삭제 | 댓글

    비밀댓글입니다

    • reversecore 2011/06/03 21:23 댓글주소 | 수정 | 삭제

      안녕하세요.

      개념은 잡으신것 같습니다. 실제로 그 의미를 정확히 이해하셔야 합니다. 왜? 정교하게 계산된 어셈블리 코드를 인젝션 해야 하는지? 왜? 함부로 API 호출 코드를 인젝션 하기 어려운지? 에대한 이유를 아셔야 합니다.

      감사합니다.

  5. 흠; 2011/07/18 22:53 댓글주소 | 수정 | 삭제 | 댓글

    .. 어렵군요 ! ㅠㅠ API 공부더하고와야하는가..

    • reversecore 2011/07/19 21:57 댓글주소 | 수정 | 삭제

      안녕하세요.

      위에 나온 내용은 사실 어렵습니다.

      기초 API Hooking 부터 차근차근 읽으시면 도움이 되실 것 같습니다.

      감사합니다.

  6. hyun 2011/09/22 23:23 댓글주소 | 수정 | 삭제 | 댓글

    정말 유익하게 보구 있습니다 ㅎㅎ

    악성코드를 치료하는 용도로 code injection 방법을 사용하려고 하는데요.
    악성 DLL이 특정 프로세스에 인젝션 되어 있는상태인데,
    제가 FreeLibrary를 호출하는 코드를 인젝트함으로써, 악성 DLL를 강제로 언로드 시킬수 있을까요?ㅎ

    • reversecore 2011/09/28 20:57 댓글주소 | 수정 | 삭제

      안녕하세요.

      악성코드의 자체 보호 기능이 없고, 다른 객체(파일, 네트워크, etc)를 열고 있지 않다면...
      강제로 인젝션 된 DLL 은 FreeLibrary() 로 다 내릴 수 있습니다.

      감사합니다.

  7. 저기요 2011/11/24 20:02 댓글주소 | 수정 | 삭제 | 댓글

    user32.dll 을 올렸는데 FreLibrary로 안내려줘도 괜찮은가요?
    또 VirtualAlloc으로 할당된 메모리도 해제해야 될거 같고
    메모장 종료되면 자동적으로 해제되는건가요?

    • reversecore 2011/11/30 01:47 댓글주소 | 수정 | 삭제

      안녕하세요.

      보통은 말씀하신대로 안쓰는 자원은 그때 그때 반환하는 것이 좋습니다.

      인젝션의 경우는 프로세스 종료 시 자동 해제되기 때문에 크게 신경쓰지 않는 것입니다. (필요하다면 이젝션 시켜서 자원을 반환하는 경우도 있습니다.)

      감사합니다.


◀ PREV : [1] : ... [10] : [11] : [12] : [13] : [14] : [15] : [16] : [17] : [18] : ... [91] : NEXT ▶