본문 바로가기

리버싱 핵심원리

[리버싱 핵심원리] 23장 - DLL 인젝션

23.1 DLL 인젝션

DLL 인젝션이란 다른 프로세스에게 LoadLibrary() API를 스스로 호출하도록 명령하여 사용자가 원하는 DLL을 로딩하는 것이다. 정리하자면, 실행 중인 다른 프로세스에 특정 DLL 파일을 강제로 삽입하는 것을 말한다.

DLL 인젝션과 일반적인 DLL 로딩의 차이점은 로딩 대상이다. DLL 인젝셕은 다른 프로세스가 로딩 대상이고 일반적인 DLL 로딩의 대상은 자신이기 때문이다. (DLL 인젝션은 원래는 자기를 로딩하지 않는 프로세스에 강제로 들어간다는 거!)


일반적으로 프로세스에 로딩된 dll 파일은 해당 프로세스 메모리에 대한 정당한 접근 권한을 가진다. 따라서 인젝션 된 dll 파일 또한 해당 프로세스에 대한 메모리 접근 권한을 가지게 된다. 

 

+) 프로세스에 DLL이 로딩되면 자동으로 DllMain() 함수가 실행된다. 따라서 DllMain()에 사용자가 원하는 코드를 추가하면 DLL이 로딩될 때 자연스럽게 해당 코드가 실행된다. 이런 특성을 이용해 기존의 버그를 수정하거나 새로운 기능을 추가할 수 있다.

23.2 DLL 인젝션 활용 예

1. 기능 개선 및 버그 패치

   - 프로그램의 소스코드가 없거나 수정이 여의치 않을 때 새로운 기능을 추가하거나 문제가 있는 코드와 데이터 수정 가능

2. 메시지 후킹

   - 등록된 후킹 DLL을 OS에서 직접 인젝션 시켜준다는 특징이 있다.

3. API 후킹

4. 기타 응용 프로그램

  - 주로 PC 사용자들을 감시하고 관리하기 위한 애플리케이션들에서 사용된다.

5. 악성 코드

 

23.3 DLL 인젝션 구현 방법

DLL 인젝션 방법
· 원격 스레드 생성 (CreateRemoteThread() API)
· 레지스트리 이용 (AppInit_DLLs 값)
· 메시지 후킹 (SetWindowsHookEx())

 

23.4 CreateRemoteThread()

1. 실습 예제 myhack.dll

notepad.exe 프로세스에 myhack.dll을 인젝션하는 실습이다.

 

(1)실습 파일(InjectDll.exe와 myhack.dll)을 같은 폴더에 복사한다.

(2) notepad.exe 프로그램을 실행시켜 PID 값을 얻는다.

(3) Debug View 프로그램을 실행시켜 디버그 문자열을 확인할 준비를 한다.

(4) InjectDll.exe 파일을 실행시킨다.

(5) myhack.dll 파일이 notepad.exe 프로세스에 성공적으로 인젝션 되었는지 확인한다.

Debug View
Process Explorer - Notepad.exe에 로딩된 dll 파일들

(6) 인젝션의 결과를 확인한다.

네이버 사이트의 index.html 파일이 정상적으로 다운로드가 된 것을 확인할 수 있다.

2. 예제 소스코드 분석

(1) 인젝션할 myhack.dll의 소스코드

#include "windows.h"
#include "tchar.h"

#pragma comment(lib, "urlmon.lib")

#define DEF_URL     	(L"http://www.naver.com/index.html")
#define DEF_FILE_NAME   (L"index.html")

HMODULE g_hMod = NULL;

DWORD WINAPI ThreadProc(LPVOID lParam)
{
    TCHAR szPath[_MAX_PATH] = {0,};

    if( !GetModuleFileName( g_hMod, szPath, MAX_PATH ) )
        return FALSE;
	
    TCHAR *p = _tcsrchr( szPath, '\\' );
    if( !p )
        return FALSE;

    _tcscpy_s(p+1, _MAX_PATH, DEF_FILE_NAME);

    URLDownloadToFile(NULL, DEF_URL, szPath, 0, NULL);

    return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    HANDLE hThread = NULL;

    g_hMod = (HMODULE)hinstDLL;

    switch( fdwReason )
    {
    case DLL_PROCESS_ATTACH : 
        OutputDebugString(L"<myhack.dll> Injection!!!");
        hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
        CloseHandle(hThread);
        break;
    }

    return TRUE;
}

위 코드를 보면 DllMain()에서 DLL이 로딩(DLL_PROCESS_ATTACH)될 때 디버그 문자열(Debug View로 보았던 문자열)을 출력하고 스레드(ThreadProc)를 실행한다. ThreadProc 함수는 URLDownloadToFile을 통해서 네이버 사이트의 index.html 파일을 다운받는다. DllMain() 함수는 프로세스에 DLL 인젝션이 발생하면 호출되기 때문에, 코드의 내용을 정리하면 DLL 인젝션이 발생했을 때 (myhack.dll이 인젝션 되었을 때) URLDownloadToFile API가 호출되는 것이다.

 

(2) myhack.dll 파일을 프로세스에 인젝션 해주는 InjectDLL.exe의 소스코드

#include "windows.h"
#include "tchar.h"

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) 
{
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

    if( !OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
			              &hToken) )
    {
        _tprintf(L"OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

    if( !LookupPrivilegeValue(NULL,           // lookup privilege on local system
                              lpszPrivilege,  // privilege to lookup 
                              &luid) )        // receives LUID of privilege
    {
        _tprintf(L"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;

    // Enable the privilege or disable all privileges.
    if( !AdjustTokenPrivileges(hToken, 
                               FALSE, 
                               &tp, 
                               sizeof(TOKEN_PRIVILEGES), 
                               (PTOKEN_PRIVILEGES) NULL, 
                               (PDWORD) NULL) )
    { 
        _tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
    {
        _tprintf(L"The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}

BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
    HANDLE hProcess = NULL, hThread = NULL;
    HMODULE hMod = NULL;
    LPVOID pRemoteBuf = NULL;
    DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
    LPTHREAD_START_ROUTINE pThreadProc;

    // #1. dwPID 를 이용하여 대상 프로세스(notepad.exe)의 HANDLE을 구한다.
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
    {
        _tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
        return FALSE;
    }

    // #2. 대상 프로세스(notepad.exe) 메모리에 szDllName 크기만큼 메모리를 할당한다.
    pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);

    // #3. 할당 받은 메모리에 myhack.dll 경로("c:\\myhack.dll")를 쓴다.
    WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);

    // #4. LoadLibraryA() API 주소를 구한다.
    hMod = GetModuleHandle(L"kernel32.dll");
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
	
    // #5. notepad.exe 프로세스에 스레드를 실행
    hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);	

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

int _tmain(int argc, TCHAR *argv[])
{
    if( argc != 3)
    {
        _tprintf(L"USAGE : %s <pid> <dll_path>\n", argv[0]);
        return 1;
    }

    // change privilege
    if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
        return 1;

    // inject dll
    if( InjectDll((DWORD)_tstol(argv[1]), argv[2]) )
        _tprintf(L"InjectDll(\"%s\") success!!!\n", argv[2]);
    else
        _tprintf(L"InjectDll(\"%s\") failed!!!\n", argv[2]);

    return 0;
}

InjectDll 소스코드의 main 함수에선 프로그램의 파라미터를 체크한 후 InjectDll() 함수를 호출한다. 그리고 이 InjectDll() 함수가 DLL 인젝션을 해주는 핵심 함수이다. InjectDll() 함수의 흐름을 크게 보면 대상 프로세스의 Handle을 구하고 메모리 공간을 할당 받아 인젝션할 DLL 경로를 넣은 후 원격 스레드를 통해 대상 프로세스 스레드를 실행시키는 것이다.

 

1. 대상 프로세스 핸들 구하기

   : OpenProcess() API를 이용해 PROCESS_ALL_ACCESS 권한의 notepad.exe 프로세스 핸들을 구한다. 해당 권한이 있       으면 프로세스(notepad.exe)를 제어할 수 있다.

 

2. 대상 프로세스 메모리에 인젝션할 DLL 경로 써주기

  : 상대방 프로세스에게 로딩할 DLL 파일의 경로를 알려준다.

   (경로 저장을 위해 공간을 할당하고 그곳에 저장한다 / 할당하는 공간은 상대방 프로세스의 메모리 공간이다

     => hProcess 핸들이 가리키는 곳  / 버퍼의 크기는 파일 경로 문자열 길이)

 

3. LoadLibraryW() API 주소를 구하기

  : LoadLibrary() API를 호출하기 위해선 notepad.exe 프로세스 메모링에서의 해당 API 주소가 필요하다. 해당 API는 kernel32.dll에 속해있다. Windows 운영체제에서 해당 dll은 프로세스마다 같은 주소에 로딩되기 때문에 위 코드처럼 InjectDll.exe에 로딩된 주소를 사용해도 된다.

 

DLL 인젝션 기법은 이런 OS 핵심 DLL들은 자신만의 고유한 주소에 로딩되는 것을 보장해주는 Windows OS 특성을 이용한 것이다. (보안 취약점으로 작용하기도 한다.)

 

4. 대상 프로세스에 원격 스레드(Remote Thread)를 실행하기

  : notepad.exe가 LoadLibraryW() API를 호출하도록 명령하는 API가 존재하지 않는다. 그래서 다른 프로세스에게 스레드를 실행시켜주는 CreateRemoteThread() API를 사용한다. 해당 함수로 들어오는 파라미터의 값이 모두 대상 프로세스의 가상 메모리 주소이어야 한다. 그리고 대상이 되는 스레드의 주소를 LoadLibrary와 형태가 비슷한 ThreadProc로 지정한다. 결과적으로, 해당 프로세스가 LoadLibraryW()를 호출하도록 만들 수 있다.

 

3. 디버깅 방법

notepad.exe를 새로 실행시킨 후 ollydbg에 들어가 해당 프로세스를 attach 시킨다.

일시정지된 프로세스를 [F9]를 통해 실행시킨다. 그리고 해당 프로세스에 새로운 DLL이 로딩될 떄마다 해당 DLL의 EP에 멈추도록 [Options] - [Event] - [Pause on new module(DLL)]을 활성화한다. 마지막으로 InjectDll.exe를 실행시켜 DLL 인젝션을 성공시킨다. 그 이후 OllyDbg로 돌아가면 아래와 같은 화면을 만날 수 있다.

(만약 안나온다면 해당 dll 파일(myhack.dll)이 나올 때까지 [F9]를 누르면 된다.)

 

23.5 AppInit_DLLs

Windows 운영체제에서 기본으로 제공하는 AppInit_DLLs와 LoadAppInit_DLLs라는 이름의 레지스트리 항목이 있다. (사진 맨 아래 경로를 따라가면 사진과 같은 화면을 볼 수 있다..... 생각보다 안쪽에 존재한다.....)

AppInit_DLLs 항목에 인젝션을 원하는 DLL 경로 문자열을 쓰고 LoadAppInit_DLLs 항목(해당 항목은 기능의 사용 여부를 결정하는 것이다)의 값을 1로 변경한 후 재부팅하면, 실행되는 모든 프로세스에 해당 DLL을 인젝션해준다. 매우 간단하면서 강력한 기능이다. (정확히 말하자면, 모든 프로세스가 아닌 user32.dll을 로딩하는 프로세스에만 해당되는 사항이다.)

 

1. 예제 소스코드 분석(myhack2.dll)

// myhack2.cpp

#include "windows.h"
#include "tchar.h"

#define DEF_CMD  L"c:\\Program Files\\Internet Explorer\\iexplore.exe" 
#define DEF_ADDR L"http://www.naver.com"
#define DEF_DST_PROC L"notepad.exe"

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    TCHAR szCmd[MAX_PATH]  = {0,};
    TCHAR szPath[MAX_PATH] = {0,};
    TCHAR *p = NULL;
    STARTUPINFO si = {0,};
    PROCESS_INFORMATION pi = {0,};

    si.cb = sizeof(STARTUPINFO);
    si.dwFlags = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_HIDE;

    switch( fdwReason )
    {
    case DLL_PROCESS_ATTACH : 
        if( !GetModuleFileName( NULL, szPath, MAX_PATH ) )
            break;
   
        if( !(p = _tcsrchr(szPath, '\\')) )
            break;

        if( _tcsicmp(p+1, DEF_DST_PROC) )
            break;

        wsprintf(szCmd, L"%s %s", DEF_CMD, DEF_ADDR);
        if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd, 
                            NULL, NULL, FALSE, 
                            NORMAL_PRIORITY_CLASS, 
                            NULL, NULL, &si, &pi) )
            break;

        if( pi.hProcess != NULL )
            CloseHandle(pi.hProcess);

        break;
    }
   
    return TRUE;
}

현재 자신을 로딩한 프로세스의 이름이 'notepad.exe'와 같다면 IE를 숨김 모드로 실행해서 Naver 사이트에 접속하는 게 해당 소스코드의 내용이다.

2. 실습 예제 myhack2.dll

인젝션할 dll 파일이 있는 곳을 찾아 레지스트리 값을 변경하는 것이다. 레지스트리 편집기로 이동해 앞에서 보았던 2개의 값을 변경한다.

ApopInit_DLLs
LoadAppInit_DLLs

값을 위와 같이 변경하고 재부팅을 하면 아래와 같이 성공적으로 인젝션 된 것을 볼 수 있다. 해당 파일은 notepad.exe를 대상으로만 하는 파일이기 때문에 notepad를 제외한 다른 프로세스에겐 아무런 동작을 하지 않는다.(그저 강제로 로딩만 된 것이다.)

notepad를 실행하면 아래와 같이 IE가 숨김속성으로 실행되는 걸 확인할 수 있다.

 

23.6 SetWindowsHookEx()

DLL 인젝션을 위한 세 번째 방법은 메시지 후킹을 이용하는 것이다. SetWindowsHookEx() API를 이용해 메시지 훅을 설치하면 OS에서 hook procedure를 담고 있는 DLL을 프로세스에게 강제로 인젝션한다. 자세한 동작 원리는 21장에서 했다! 

 

 

출처 : 리버싱 핵심원리 (이승원, 인사이트)