ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리버싱 핵심원리 study] 33장 '스텔스' 프로세스
    Reverse Engineering 2021. 1. 26. 21:43

    API 코드 패치를 이용한 API 후킹 방법에 대해 실습한다. 32장에서 사용한 IAT 후킹 기법은 API가 프로세스의 IAT에 존재하지 않을 경우 후킹이 불가능한 반면 API 코드 패치 기법은 그런 제약이 없어 가장 널리 사용되는 방법이다. 또한 이번 실습에서 나아가 모든 프로세스를 후킹 하는 글로벌 후킹(Global Hooking)과 보다 효율적인 핫 패치 방법에 대해 알아본다.

     

    1. API 코드 패치 동작 원리

    이전 IAT 후킹 방식에서는 프로세스의 특정 IAT 값을 조작해서 후킹을 진행하였다. 코드 패치 방식은 실제 API 코드의 시작 5 byte 값을 JMP XXXXXXXX(XXXXXXXX는 사용자가 새롭게 정의한 함수)로 변경하여 후킹 함수로 제어를 넘기는 방식이다. 실습에서는 후킹하려는 대상 프로세스에 stealth.dll 파일을 인젝션 한 후, ntdll.ZwQuerySystemInformation() API를 후킹 하여 원하는 동작을 하도록 조작한다. 큰 흐름은 다음과 같다.

     

    (1) ZwQuerySystemInformation() API가 호출된다.

    (2) 이때, ZwQuerySystemInformation() API의 시작 5byte가 후킹 함수로 JMP 하라는 명령어로 바뀌어 있으므로 후킹 함수로 점프한다. 후킹함수에서는 unhook() 함수에 의해 ZwQuerySystemInformation() API가 원래대로 코드가 복원된다.

    (3) 후킹 함수에서 ZwQuerySystemInformation() API(복원된 API)를 호출한다.

    (4) 원하는 조작을 한 후, hook() 함수에 의해 다시 ZwQuerySystemInformation() API의 시작 5 byte 값을 조작한다.

    (5) 후킹함수가 종료되면 retn 명령어에 의해 (1)의 ZwQuerySystemInformation() API가 호출되었던 위치로 되돌아간다.

     

    2. 프로세스 은폐

    실습에서는 프로세스를 은폐하기 위해 ZwQuerySystemInformation() API를 후킹 한다. 해당 API를 이용하면 현재 실행 중인 모든 프로세스의 정보를 연결 리스트 형태로 얻을 수 있다. 이 연결 리스트를 조작하여 리스트 목록에서 은폐를 원하는 프로세스를 빼내면 해당 프로세스가 은폐되는 것이다. 즉 다른 프로세스들이 특정 프로세스를 인식하지 못하는 상황이 된다. 내가 투명인간이 되지 못한다면 다른 사람들의 눈을 가려버리는(?) 그런 전략이라고 보면 될 것 같다. 실습 #1에서는 이와 같은 방법을 적용한다. 그러나 ZwQuerySystemInformation() API만을 후킹 해서는 이후에 실행되는 프로세스들에 대해서는 적용이 안된다는 문제점이 있는데, 이를 해결하기 위해서 실습 #2, #3에서는 글로벌 API 후킹 기법을 사용한다.

    (모든 실습은 64bit 환경에서는 작동하지 않아서 virtual box를 이용해 windwos 7 32bit 환경에서 진행하였다.)

     

    3. 실습 #1 (HideProc.exe, stealth.dll)

    예제 파일로 HideProc.exe와 stealth.dll이 있다. HideProc.exe는 모든 프로세스에 stealth.dll을 인젝션해주는 프로그램이다. stealth.dll은 인젝션 된 모든 프로세스에 대해 ntdll.ZwQuerySystemInformation() API를 후킹 하는 역할을 한다. 위 두 파일로 notepad.exe를 은폐하는 실습을 진행해보자.

     

    우선 notepad.exe, ProcessExplorer, TaskMgr 프로세스를 실행한 후, cmd 창에 아래와 같은 명령어를 입력하여 stealth.dll은 현재 실행 중인 모든 프로세스에 인젝션 해보자.

     

    프로그램 실행 직후

    위와 같이 Process Explorer와 작업 관리자에 notepad.exe가 탐색이 안되는 것을 확인할 수 있다. Process Explorer에서 stealth.dll을 한 번 검색해보자.

     

    stealth.dll 인젝션

    stealth.dll이 실행중인 프로세스들에 인젝션 되어 있는 것을 확인할 수 있다. 특히 procexp.exe와 taskmgr.exe에 stealth.dll이 인젝션 되어서 해당 프로세스에서는 notepad.exe를 인식하지 못한다. 이제 다시 아래와 같은 명령어를 입력하여 stealth.dll을 이젝션해보자.

     

    stealth.dll 이젝션

     

    stealth.dll 이젝션 후

    위와 같이 Process Explorer와 taskmgr.exe에서 정상적으로 notepad.exe를 다시 인식하는 것을 확인할 수 있다.

     

    4. 소스코드 분석

    (1) HideProc.cpp

    HideProc.cpp에서는 실행 중인 모든 프로세스에 특정 DLL을 인젝션 시켜주는 기능을 한다. 

     

    InjectAllProcess()

    BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
    {
    	DWORD                   dwPID = 0;
    	HANDLE                  hSnapShot = INVALID_HANDLE_VALUE;
    	PROCESSENTRY32          pe;
    
    	// Get the snapshot of the system
    	pe.dwSize = sizeof( PROCESSENTRY32 );
    	hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );
    
    	// find process
    	Process32First(hSnapShot, &pe);
    	do
    	{
    		dwPID = pe.th32ProcessID;
    
            // 시스템의 안정성을 위해서
            // PID 가 100 보다 작은 시스템 프로세스에 대해서는
            // DLL Injection 을 수행하지 않는다.
    		if( dwPID < 100 )
    			continue;
    
            if( nMode == INJECTION_MODE )
    		    InjectDll(dwPID, szDllPath);
            else
                EjectDll(dwPID, szDllPath);
    	}
    	while( Process32Next(hSnapShot, &pe) );
    
    	CloseHandle(hSnapShot);
    
    	return TRUE;
    }

    CreateToolhelp32Snapshot() API를 이용해서 현재 실행 중인 모든 프로세스 리스트를 얻어낸 후, while loop 안에서 PID가 100보다 크거나 같은 프로세스에 대하여 szDllPath의 dll을 인젝션 해주는 기능을 한다. 책에서 windows 7 기준 100 이하의 PID를 갖는 프로세스들은 시스템 프로세스로, 안정성을 위해 인젝션을 하지 않는다고 한다.

     

     

    (2) stealth.cpp

    실제로 API 후킹을 담당하는 dll의 소스코드다.

     

    SetProcName()

    // global variable (in sharing memory)
    #pragma comment(linker, "/SECTION:.SHARE,RWS")
    #pragma data_seg(".SHARE")
        TCHAR g_szProcName[MAX_PATH] = {0,};
    #pragma data_seg()
    
    // export function
    #ifdef __cplusplus
    extern "C" {
    #endif
    __declspec(dllexport) void SetProcName(LPCTSTR szProcName)
    {
        _tcscpy_s(g_szProcName, szProcName);
    }
    #ifdef __cplusplus
    }
    #endif

    SetProcName() 함수는 생성된 공유 메모리 섹션인 .SHARE 영역의 g_szProcName 버퍼에 매개변수 szProcName을 복사하는 기능을 한다. (HideProc.exe에서 호출) 공유 메모리 섹션을 만듦으로써 모든 프로세스에게 stealth.dll이 인젝션 될 때 간단하게 은폐 프로세스 이름을 공유시켜준다.

     

     

    DllMain()

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
    {
        char            szCurProc[MAX_PATH] = {0,};
        char            *p = NULL;
    
        // #1. 예외처리
        // 현재 프로세스가 HookProc.exe 라면 후킹하지 않고 종료
        GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
        p = strrchr(szCurProc, '\\');
        if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") )
            return TRUE;
    
        switch( fdwReason )
        {
            // #2. API Hooking
            case DLL_PROCESS_ATTACH : 
            hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                         (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
            break;
    
            // #3. API Unhooking 
            case DLL_PROCESS_DETACH :
            unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                           g_pOrgBytes);
            break;
        }
    
        return TRUE;
    }

    모든 프로세스들에게 stealth.dll이 인젝션 되었을 때 #1에 의해 만약 대상 프로세스가 "stealth.dll"인 경우 return TRUE를 통해 예외 처리하고, 그렇지 않은 경우 #2에 의해 훅, #3에 의해 언훅을 한다.

     

     

    hook_by_code()

    BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
    {
        FARPROC pfnOrg;
        DWORD dwOldProtect, dwAddress;
        BYTE pBuf[5] = {0xE9, 0, };
        PBYTE pByte;
    
        // 후킹 대상 API 주소를 구한다
        pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
        pByte = (PBYTE)pfnOrg;
    
        // 만약 이미 후킹 되어 있다면 return FALSE
        if( pByte[0] == 0xE9 )
            return FALSE;
    
        // 5 byte 패치를 위하여 메모리에 WRITE 속성 추가
        VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    
        // 기존 코드 (5 byte) 백업
        memcpy(pOrgBytes, pfnOrg, 5);
    
        // JMP 주소 계산 (E9 XXXX)
        // => XXXX = pfnNew - pfnOrg - 5
        dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
        memcpy(&pBuf[1], &dwAddress, 4);
    
        // Hook - 5 byte 패치 (JMP XXXX)
        memcpy(pfnOrg, pBuf, 5);
    
        // 메모리 속성 복원
        VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);
        
        return TRUE;
    }

    각 매개변수는 다음과 같다.

    szDllName : 후킹 하려는 API가 포함된 DLL 이름 (실습에서는 ZwQuerySystemInformation() API가 포함된 ntdll.dll)

    szFuncName : 후킹하려는 API (실습에서는 ZwQuerySystemInformation() API) 주소

    pfnNew : 후킹 함수 (실습에서는 NewZwQuerySystemInformation() 함수 주소

    pOrgBytes : 원본 코드를 저장하기 위한 byte 버퍼

     

    위 코드는 후킹 하려는 API의 앞 5byte를 원하는 함수의 주소로 점프하게끔 바꿔주는 코드다.

     

     

    unhook_by_code()

    BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)
    {
        FARPROC pFunc;
        DWORD dwOldProtect;
        PBYTE pByte;
    
        // API 주소 구한다
        pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
        pByte = (PBYTE)pFunc;
    
        // 만약 이미 언후킹 되어 있다면 return FALSE
        if( pByte[0] != 0xE9 )
            return FALSE;
    
        // 원래 코드(5 byte)를 덮어쓰기 위해 메모리에 WRITE 속성 추가
        VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    
        // Unhook
        memcpy(pFunc, pOrgBytes, 5);
    
        // 메모리 속성 복원
        VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
    
        return TRUE;
    }

    hook_by_code()와 기본 원리는 동일하다. 후킹 된 함수를 복원하기 위해 저장했던 byte 버퍼(pOrgBytes)로 대체하는 기능을 수행한다.

     

     

    NewZwQuerySystemInformation()

    후킹 함수인 NewZwQuerySystemInformation 코드를 살펴보기 전에, 먼저 ZwQuerySystemInformation() API를 살펴보자. 해당 API는 아래와 같은 구조를 갖는다.

    NTSTATUS WINAPI ZwQuerySystemInformation(
      _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,
      _Inout_   PVOID                    SystemInformation,
      _In_      ULONG                    SystemInformationLength,
      _Out_opt_ PULONG                   ReturnLength
    );
    
    typedef struct _SYSTEM_PROCESS_INFORMATION {
        ULONG NextEntryOffset;
        ULONG NumberOfThreads;
        BYTE Reserved1[48];
        PVOID Reserved2[3];
        HANDLE UniqueProcessId;
        PVOID Reserved3;
        ULONG HandleCount;
        BYTE Reserved4[4];
        PVOID Reserved5[11];
        SIZE_T PeakPagefileUsage;
        SIZE_T PrivatePageCount;
        LARGE_INTEGER Reserved6[6];
    } SYSTEM_PROCESS_INFORMATION;

    API의 SystemInformationClass 파라미터를 SystemProcessInformation(enum 값 5)로 세팅하고 API를 호출하면, SystemInformation 파라미터네 SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트의 시작 부분이 저장된다. 이 정보를 이용해서 은폐를 원하는 프로세스를 리스트에서 제거해주는 기능을 NewZwQuerySystemInformation() 함수에 구현해주면 된다.

     

    NTSTATUS WINAPI NewZwQuerySystemInformation(
                    SYSTEM_INFORMATION_CLASS SystemInformationClass, 
                    PVOID SystemInformation, 
                    ULONG SystemInformationLength, 
                    PULONG ReturnLength)
    {
        NTSTATUS status;
        FARPROC pFunc;
        PSYSTEM_PROCESS_INFORMATION pCur, pPrev;
        char szProcName[MAX_PATH] = {0,};
        
        // 작업 전에 unhook
        unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes);
    
        // original API 호출
        pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL), 
                               DEF_ZWQUERYSYSTEMINFORMATION);
        status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)
                  (SystemInformationClass, SystemInformation, 
                  SystemInformationLength, ReturnLength);
    
        if( status != STATUS_SUCCESS )
            goto __NTQUERYSYSTEMINFORMATION_END;
    
        // SystemProcessInformation 인 경우만 작업함
        if( SystemInformationClass == SystemProcessInformation )
        {
            // SYSTEM_PROCESS_INFORMATION 타입 캐스팅
            // pCur 는 single linked list 의 head
            pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
    
            while(TRUE)
            {
                // 프로세스 이름 비교
                // g_szProcName = 은폐하려는 프로세스 이름
                // (=> SetProcName() 에서 세팅됨)
                if(pCur->Reserved2[1] != NULL)
                {
                    if(!_tcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName))
                    {
                        // 연결 리스트에서 은폐 프로세스 제거
                        if(pCur->NextEntryOffset == 0)
                            pPrev->NextEntryOffset = 0;
                        else
                            pPrev->NextEntryOffset += pCur->NextEntryOffset;
                    }
                    else		
                        pPrev = pCur;
                }
    
                if(pCur->NextEntryOffset == 0)
                    break;
    
                // 연결 리스트의 다음 항목
                pCur = (PSYSTEM_PROCESS_INFORMATION)
                        ((ULONG)pCur + pCur->NextEntryOffset);
            }
        }
    
    __NTQUERYSYSTEMINFORMATION_END:
    
        // 함수 종료 전에 다시 API Hooking
        hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
    
        return status;
    }

    기본적으로는 다음과 같은 아래와 같은 흐름이다.

    (1) unhook_by_code를 호출하여서 ZwQuerySystemInformation() API를 복원한다.

    (2) 복원된 ZwQuerySystemInformation() API를 호출하여서 실행 중인 프로세스 연결 리스트를 얻어낸다.

    (3) 프로세스 리스트로부터 은폐시킬 프로세스를 찾은 후, 해당 프로세스를 리스트에서 제거한다.

    (4) hook_by_code를 호출하여 다시 ZwQuerySystemInformation() API에 훅을 설치한다.

     

    5. 글로벌 API 후킹

    앞에서 진행한 방법으로는 새롭게 생성되는 프로세스는 은폐 프로세스를 인식한다는 문제점이 있으며 마찬가지로 은폐 프로세스를 하나 더 생성하면(ex. notepad.exe 하나 더 생성) 더 이상 은폐 기능을 하지 못하게 된다는 문제점이 있다. 이를 위해서 실습 #2에서는 아래와 같은 API를 추가적으로 후킹 한다.

     

    - CreateProcessA() API & CreateProcessW() API

    위 두 API는 kernel32 library에 있는 API로 프로세스가 생성될 때 호출하는 API다. 두 API를 후킹함으로써 새롭게 프로세스가 생성되어도 은폐 프로세스가 은폐를 유지할 수 있도록 해준다.

     

    6. 실습 #2

    stealth2.dll 파일을 모든 프로세스가 인식할 수 있는 SYSTEM32 폴더에 넣고, 아래와 같은 명령어를 실행해보자.

    stealth2.dll 인젝션

    이후 notepad.exe를 추가적으로 마구 생성해보고, Process Explorer로 확인해보자.

    인젝션 후 Process Explorer

    3개 정도 notepad.exe를 실행시키고 Process Explorer를 확인해봤지만, notepad.exe는 보이지 않는다. 이제 아래와 같은 명령어를 입력하여 stealth2.dll을 프로세스들에게서 이젝션하자.

     

    stealth2.dll 이젝션
    이젝션 후 Process Explorer

    위와 같이 stealth2.dll을 이젝션하자, 실행시켰던 notepad.exe 프로세스가 인식되는 것을 확인할 수 있다.

     

    7. 소스코드 분석

    (1) HideProc2.cpp

    (HideProc.cpp와 유사. 함수 파라미터 개수만 줄어들었다.)

     

    (2) stealth2.cpp

    stealth2.cpp는 stealth.cpp와는 다르게 은폐 대상 프로세스가 notepad.exe로 고정되었고, 추가적으로 CreateProcessA() API와 CreateProcessW() API를 후킹 하였다.

     

    DllMain()

    BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
    {
        char            szCurProc[MAX_PATH] = {0,};
        char            *p = NULL;
    
        // HideProc2.exe 프로세스에는 인젝션 되지 않도록 예외처리
        GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
        p = strrchr(szCurProc, '\\');
        if( (p != NULL) && !_stricmp(p+1, "HideProc2.exe") )
            return TRUE;
    
        // change privilege
        SetPrivilege(SE_DEBUG_NAME, TRUE);
    
        switch( fdwReason )
        {
            case DLL_PROCESS_ATTACH : 
                // hook
                hook_by_code("kernel32.dll", "CreateProcessA", 
                             (PROC)NewCreateProcessA, g_pOrgCPA);
                hook_by_code("kernel32.dll", "CreateProcessW", 
                             (PROC)NewCreateProcessW, g_pOrgCPW);
                hook_by_code("ntdll.dll", "ZwQuerySystemInformation", 
                             (PROC)NewZwQuerySystemInformation, g_pOrgZwQSI);
                break;
    
            case DLL_PROCESS_DETACH :
                // unhook
                unhook_by_code("kernel32.dll", "CreateProcessA", 
                               g_pOrgCPA);
                unhook_by_code("kernel32.dll", "CreateProcessW", 
                               g_pOrgCPW);
                unhook_by_code("ntdll.dll", "ZwQuerySystemInformation", 
                               g_pOrgZwQSI);
                break;
        }
    
        return TRUE;
    }

    hook_by_code / unhook_by_code 부분에 CreateProcess() API 후킹/언후킹이 추가되었다.

     

     

    NewCreateProcessA()

    BOOL WINAPI NewCreateProcessA(
        LPCTSTR lpApplicationName,
        LPTSTR lpCommandLine,
        LPSECURITY_ATTRIBUTES lpProcessAttributes,
        LPSECURITY_ATTRIBUTES lpThreadAttributes,
        BOOL bInheritHandles,
        DWORD dwCreationFlags,
        LPVOID lpEnvironment,
        LPCTSTR lpCurrentDirectory,
        LPSTARTUPINFO lpStartupInfo,
        LPPROCESS_INFORMATION lpProcessInformation
    )
    {
        BOOL bRet;
        FARPROC pFunc;
    
        // unhook
        unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);
    
        // original API 호출
        pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
        bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
                                         lpCommandLine,
                                         lpProcessAttributes,
                                         lpThreadAttributes,
                                         bInheritHandles,
                                         dwCreationFlags,
                                         lpEnvironment,
                                         lpCurrentDirectory,
                                         lpStartupInfo,
                                         lpProcessInformation);
    
        // 생성된 자식 프로세스에 stealth2.dll 을 인젝션 시킴
        if( bRet )
            InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);
    
        // hook
        hook_by_code("kernel32.dll", "CreateProcessA", 
                     (PROC)NewCreateProcessA, g_pOrgCPA);
    
        return bRet;
    }

    매개변수가 많아 복잡해 보이지만 흐름은 단순하다. 우선 unhook_by_code를 통해 CreateProcessA를 복원시킨 후, CreateProcessA() API를 호출하여 프로세스를 생성한다. 생성에 성공한다면 InjectDll2 함수를 통해 stealth2.dll을 인젝션 해주고 다시 hook_by_code를 호출하여 훅을 설치한다. NewCreateProcessW() 함수도 마찬가지다.

     

    8. 핫 패치 방식의 API 후킹

    앞의 글로벌 후킹 방식은 비록 은폐 목적을 달성했지만, 다음과 같은 문제점들이 존재한다.

     

    - 원본 API를 정상적으로 호출하기 위해 unhook, 후킹 함수가 끝나기 전에 hook을 해야 하는 오버헤드 존재

    - 멀티 스레드 환경에서 한 스레드가 hook/unhook을 하는 도중 다른 스레드가 접근할 수도 있음

     

    위와 같은 문제점으로 인해 보다 안정적으로 사용하는 방법인 핫 패치(Hot Patch) 방법이 존재한다. 일반적으로 API의 시작 코드 부분은 다음과 같다.

     

    ...

    NOP

    NOP

    NOP

    NOP

    NOP

    MOV EDI, EDI

    ...

     

    API의 시작이 MOV EDI, EDI(2 byte) 명령어로 시작하며, 그 앞에는 5개의 NOP(총 5byte)이 존재한다. 총 7 byte의 아무 의미 없는 명령어들이 나열되어 있는 것을 확인할 수 있는데 이를 이용하여 후킹을 구현할 수 있다. 다음과 같은 어셈블리 코드를 비교해보자.

     

    후킹 전

     

    후킹 후

    위 두 코드는 MessageBox() API의 코드 수정 전/후이다. 후킹을 했을 경우 ①에 의해 771F192B(원래는 NOP)로 점프하고, 점프한 곳에서는 원하는 후킹 함수 주소(후킹 함수가 10001000 주소에 있다고 가정한 상황)로 점프하게 된다. 이렇게 코드를 작성하게 되면 별도로 hook/unhook을 위해 API 코드를 매번 수정할 필요가 없으며, 원본 함수를 호출하기 위해서는 단순히 ①의 다음 코드 위치인 771F1932로 점프하도록 코드를 작성하면 된다. 굉장히 편리한 기능이지만 모든 API가 NOP 5개 + MOV EDI, EDI 형태의 어셈블리 코드 형태를 띠고 있지는 않기 때문에, 핫 패치를 이용하려면 사전에 후킹 하려는 API가 조건을 만족하는지 확인해야 한다.

     

    9. 실습 #3 - stealth3dll

    다음과 같은 명령어를 입력해 stealth3.dll을 인젝션 해보자.

    stealth3.dll 인젝션
    인젝션 후 Process Explorer

    마찬가지로 notepad.exe 프로세스가 은폐된 것을 확인할 수 있다.

     

    10. 소스코드 분석

    - stealth3.cpp

    핫 패치 관련 기능만 추가되었고 나머지는 stealth2.cpp와 유사하다.

     

    hook_by_hotpatch()

    BOOL hook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew)
    {
    	FARPROC pFunc;
    	DWORD dwOldProtect, dwAddress;
    	BYTE pBuf[5] = { 0xE9, 0, };
        BYTE pBuf2[2] = { 0xEB, 0xF9 };
    	PBYTE pByte;
    
    	pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    	pByte = (PBYTE)pFunc;
    	if( pByte[0] == 0xEB )
    		return FALSE;
    
    	VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    
        // 1. NOP (0x90)
    	dwAddress = (DWORD)pfnNew - (DWORD)pFunc;
    	memcpy(&pBuf[1], &dwAddress, 4);
    	memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);
        
        // 2. MOV EDI, EDI (0x8BFF)
        memcpy(pFunc, pBuf2, 2);
    
    	VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);
    
    	return TRUE;
    }

    우선은 5개의 NOP 중 첫 번째 NOP을 JMP XXXXXXXX (후킹 함수 주소)로 변경해야 한다. 해당 부분이 첫 번째 memcpy(1. NOP)에서 구현되었다. JMP SHORT 명령어는 두 번째 memcpy(2. MOV EDI, EDI)에서 구현되었다. 해당 부분은 첫 번째 JMP 명령어의 주소로 점프하게 만들어준다.

     

     

    unhook_by_hotpatch()

    BOOL unhook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName)
    {
        FARPROC pFunc;
        DWORD dwOldProtect;
        PBYTE pByte;
        BYTE pBuf[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
        BYTE pBuf2[2] = { 0x8B, 0xFF };
    
    
        pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
        pByte = (PBYTE)pFunc;
        if( pByte[0] != 0xEB )
            return FALSE;
    
        VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    
        // 1. NOP (0x90)
        memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);
        
        // 2. MOV EDI, EDI (0x8BFF)
        memcpy(pFunc, pBuf2, 2);
    
        VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
    
        return TRUE;
    }

    기존에 바꿨던 NOP / MOV EDI, EDI 부분을 원래대로 복원시키는 기능을 한다. 이제 핫 패치 기능을 적용했을 때, 후킹 함수 내부는 어떻게 달라지는지 예시로 NewCreateProcessA() 함수를 분석해보자.

     

     

    NewCreateProcessA()

    BOOL WINAPI NewCreateProcessA(
        LPCTSTR lpApplicationName,
        LPTSTR lpCommandLine,
        LPSECURITY_ATTRIBUTES lpProcessAttributes,
        LPSECURITY_ATTRIBUTES lpThreadAttributes,
        BOOL bInheritHandles,
        DWORD dwCreationFlags,
        LPVOID lpEnvironment,
        LPCTSTR lpCurrentDirectory,
        LPSTARTUPINFO lpStartupInfo,
        LPPROCESS_INFORMATION lpProcessInformation
    )
    {
        BOOL bRet;
        FARPROC pFunc;
    
        // original API 호출
        pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
        pFunc = (FARPROC)((DWORD)pFunc + 2);
        bRet = ((PFCREATEPROCESSA)pFunc)(lpApplicationName,
                                         lpCommandLine,
                                         lpProcessAttributes,
                                         lpThreadAttributes,
                                         bInheritHandles,
                                         dwCreationFlags,
                                         lpEnvironment,
                                         lpCurrentDirectory,
                                         lpStartupInfo,
                                         lpProcessInformation);
    
        // 생성된 자식 프로세스에 stealth3.dll 을 인젝션 시킴
        if( bRet )
            InjectDll2(lpProcessInformation->hProcess, STR_MODULE_NAME);
    
        return bRet;
    }

    stealth2.cpp와는 다르게 hook / unhook 하는 코드가 없어진 것을 확인할 수 있다. 또한 기존 함수와 다르게 pFunc에 + 2만큼 해주어 원본 API를 호출하기 위해서 후킹 부분인 JMP SHORT 명령어를 건너뛰어서 호출한다.

     

    11. Comment

    너무 어려운 실습이었다. 4장 자체가 생소한 API들이 많이 나오고 하다 보니 전체적으로 난이도가 높은 것 같다. 예전에 처음 간단한 크랙미 파일을 디버깅할 때 한 줄씩 코드를 분석한 적이 있었는데 mov edi, edi를 보고 아무 의미 없는 코드 같은데 무슨 기능이 있을까 고민한 적이 있었는데 이번 챕터를 공부하면서 어느 정도 도움이 된 것 같다.

    반응형

    댓글

Designed by Tistory.