반응형

Global API Hooking 개념과 구현 방법에 대해서 알아보도록 하겠습니다.


<Fig - Global API Hooking>

본 내용은 이전 포스트에서 이어지는 내용입니다.

* 참고!
모든 소스 코드는 MS Visual C++ 2008 Express Edition 으로 개발 되었으며, Windows XP SP3 & Windows 7 환경에서 테스트 되었습니다.



Global API Hooking


Global API Hooking 이란 1) 현재 실행중인 모든 프로세스2) 앞으로 실행될 모든 프로세스에 대해서 API Hooking 을 시키는 것입니다.

지난번에 설명한 예제 프로그램(HideProc.exe, stealth.dll)은 global hooking 이 아닙니다. 위에서 설명한 2) 번 조건이 만족되지 않기 때문입니다.

HideProc.exe 를 실행 하여 notepad.exe 프로세스를 은폐시켜도 이후에 Process Explorer (혹은 task manager) 를 실행시키면 이들 프로세스에서는 notepad.exe 프로세스를 볼 수 있습니다.

그 이유는 HideProc.exe 실행 이후에 생성된 프로세스들에게는 stealth.dll 파일이 (자동으로) 인젝션 되지 않기 때문입니다.

아래 링크를 참조하여 직접 실습해 보세요.

API Hooking – ‘스텔스’ 프로세스 (2)


이러한 문제를 해결하기 위한 다양한 방법이 있을 수 있습니다.
그 중에서 또 다른 API 를 Hooking 하여 Global API Hooking 을 구현하는 방법에 대해서 설명 드리겠습니다.


 
Kernel32!CreateProcess() API


새로운 프로세스가 생성되려면 kernel32!CreateProcess() API 를 사용해야 합니다. WinExec(), ShellExecute(), system() 등의 API 도 내부적으로는 CreateProcess() 를 호출합니다.

BOOL WINAPI CreateProcess(
  __in_opt     LPCTSTR lpApplicationName,
  __inout_opt  LPTSTR lpCommandLine,
  __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in         BOOL bInheritHandles,
  __in         DWORD dwCreationFlags,
  __in_opt     LPVOID lpEnvironment,
  __in_opt     LPCTSTR lpCurrentDirectory,
  __in         LPSTARTUPINFO lpStartupInfo,
  __out        LPPROCESS_INFORMATION lpProcessInformation
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682425(VS.85).aspx

따라서 현재 실행중인 모든 프로세스에 stealth.dll 을 인젝션 하고, stealth.dll 에서 CreateProcess() API 를 후킹하면 이후 실행되는 프로세스에게도 자동으로 stealth.dll 을 인젝션 시키도록 만들 수 있습니다.

다시 설명 드리면 모든 프로세스는 부모 프로세스에서 (CreateProcess() 를 이용하여) 생성시켜주기 때문에 부모 프로세스의 CreateProcess() API 를 후킹하여 자식 프로세스에게 stealth.dll 을 인젝션 하도록 만들면 됩니다. (보통 부모 프로세스는 explorer.exe 가 될 것입니다.)

어떤가요? 좋은 아이디어 이지요?
이와 같이 Global API Hooking 의 개념은 어렵지 않습니다.

하지만 CreateProcess() API 를 후킹하면 아래와 같이 고려해야 할 사항들이 있습니다.

1) CreateProcess() API 를 후킹할 때는 kernel32!CreateProcessA(), kernel32!CreateProcessW() 두 개의 API 를 각각 후킹해야 합니다. (ASCII 버전과 UniCode 버전)

2) CreateProcessA(), CreateProcessW() 는 각각 내부적으로 CreateProcessInternalA(), CreateProcessInternalW() 를 호출합니다. 실제 MS 제품들 중에서 일부는 CreateProcessInternalA/W 를 직접 호출하기도 하지요. 따라서 좀 더 정확히 Global API Hooking 을 구현하기 위해서는 이 두 함수를 더 후킹해줘야 합니다.

3) 후킹 함수(NewCreateProcess) 는 원본 함수(CreateProcess) 를 호출 한 후 생성된 자식 프로세스에 대해서 API 를 후킹 해야 합니다. 따라서 아주 짧은 시간동안 자식 프로세스가 후킹 되지 않은 채로 실행될 수 있습니다.


많은 리버싱 선배님들에 의하여 kernel32!CreateProcess() 보다 더 후킹하기 좋은 함수가 발견 되었습니다.
바로 ntdll!ZwResumeThread() API 입니다.

NtResumeThread(
    IN    HANDLE    ThreadHandle,
    OUT   PULONG    SuspendCount OPTIONAL
);

* 유저 모드에서는 NtXXX 계열과 ZwXXX 계열은 동일합니다.

* 출처 : http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Thread/NtResumeThread.html


ZwResumeThread() 는 프로세스가 생성 된 후 메인 스레드 실행 직전에 호출되는 함수입니다. 따라서 이 함수 하나만 후킹하면 자식 프로세스의 코드가 하나도 실행되지 않은 상태에서 API 를 후킹시킬 수 있습니다.

단점은 ZwResumeThread() 는 undocumented API 라서 언제 바뀔지 알 수 없으며, 그만큼 안정성을 보장 할 수 없습니다. 따라서 ZwResumeThread() 같은 undocumented API 를 후킹 할 때는 OS 가 패치되면서 변경될 수 있다는 것을 항상 염두에 두어야 합니다. 하위 버전에서는 잘 되던 후킹이 최신 버전에서는 갑자기 안 되는 일이 많기 때문입니다.



실습


HideProc2.exe

stealth2.dll



* 참고
위 stealth2.dll 는 CreateProcess 후킹 버전입니다.
ZwResumeThread 후킹 버전을 원하시는 분께서는 따로 요청해 주시면 보내드리겠습니다.

실습을 간단히 하기 위해서 은폐 프로세스를 notepad.exe 로 고정하였습니다. 참고하시기 바랍니다.

#1. stealth2.dll 파일 -> %SYSTEM% 폴더에 복사


<Fig. 1>

실행 중인 모든 프로세스에 stealth2.dll 파일을 인젝션 시킬 예정입니다. 따라서 모든 프로세스에서 공통적으로 인식할 수 있는 path 인 %SYSTEM% 폴더에 stealth2.dll 파일을 복사합니다.

#2. HideProc2.exe –hide 실행


<Fig. 2>

기존 HideProc.exe 와 비교해서 실행 파라미터가 변경되었습니다. 은폐 프로세스 이름이notepad.exe 로 하드코딩 되어있습니다.
HideProc2.exe 를 –hide 옵션으로 실행시키면 이제부터 글로벌 후킹이 시작됩니다.

#3. ProcExp.exe & notepad.exe 실행

Process Explorer(혹은 작업관리자) 와 notepad 를 여러 개 실행 해 주세요.

 
<Fig. 3>

위 그림을 보시면 ProcExp.exe 와 notepad.exe 프로세스가 각각 2개씩 실행되고 있습니다.
하지만 ProcExp.exe 에서는 notepad.exe 프로세스가 은폐되어 있습니다.

추가로 ProcExp.exe 를 몇 개 더 실행해 보시기 바랍니다. 마찬가지로 새로 생성된 ProcExp.exe 프로세스에서도 notepad.exe 프로세스가 은폐되어서 보이지 않을 것입니다.

이것이 바로 Global API Hooking 의 효과입니다.

#4. HideProc2.exe –show 실행

Global API Hooking 을 해제 합니다.

 
<Fig. 4>

이제 Process Explorer(혹은 작업관리자) 에서 notepad.exe 프로세스가 보일 것입니다.



소스 코드


# HideProc2.cpp

HideProc2.cpp


HideProc2.cpp 는 기존 HideProc.cpp 에서 실행 파라미터를 줄인 것뿐이므로, 기존 설명을 참고하시면 되겠습니다.


# stealth2.cpp

stealth2.cpp


stealth2.cpp 는 기존 stealth.cpp 에서 은폐 프로세스 이름을 "notepad.exe" 로 하드 코딩 하였고, global hooking 을 위해서 CreateProcessA() API 와 CreateProcessW() API 를 후킹 하는 코드가 추가 되었습니다.


DllMain()

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // HideProc2.exe 프로세스에는 인젝션 되지 않도록 예외처리

    GetModuleFileName(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;
}

위 DllMain() 함수를 보시면 CreateProcessA, CreateProcessW 를 후킹하는 코드가 추가되었습니다.


NewCreateProcessA()

CreateProcessA() API 의 후킹 함수인 NewCreateProcessA() 코드를 살펴 보겠습니다. (NewCreateProcessW() 코드도 거의 동일합니다.)

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(GetModuleHandle("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;

}

코드는 매우 단순합니다.
일단 후킹을 풀고 원본 함수를 실행해서 생성된 자식 프로세스에 stealth2.dll 을 인젝션 시킵니다.
그 후 다음 실행을 위해서 다시 후킹해 줍니다.

제 글을 꾸준히 읽어오셨다면 쉽게 이해하실 수 있으실 겁니다.

한가지 눈여겨 볼 사항은 인젝션 함수인 InjectDll2() 입니다.
기존 InjectDll() 함수는 프로세스 ID (PID) 를 이용하여 프로세스 핸들을 얻어 인젝션 시키는 방법이었습니다. (OpenProcess() API 이용)

하지만 위의 경우는 CreateProcessA() API 를 호출하면서 자연스럽게 자식 프로세스의 핸들(lpProcessInformaiton->hProcess)을 얻을 수 있습니다. 이 내용도 같이 참고하시면 좋을 것 같습니다.



+---+

지금까지 Global API Hooking 에 대해서 알아보았습니다.

시스템 전체 프로세스에 대해서 후킹을 하는 기술이기 때문에 예상치 못한 에러가 발생할 수 있습니다. 따라서 사전에 꼼꼼한 테스트가 필요합니다. 그리고 undocumented API 를 후킹할 때는 현재 OS 버전에서 예상대로 동작하는지 반드시 확인하셔야 합니다.


다음번에는 API Hooking 시리즈의 마지막 테마인 Code Injection 기법에 대해서 설명드리도록 하겠습니다.

많이 기대해 주세요.

ReverseCore

위 글이 도움이 되셨다면 추천(VIEW ON) 부탁 드려요~
반응형
  1. 이전 댓글 더보기
반응형

실습 예제 프로그램의 소스를 분석하면서 "Code Patch" 방식의 API Hooking 에 대하여 알아 보도록 하겠습니다.

<stealth.cpp 코드 일부>


본 내용은 이전 포스트에서 이어지는 내용입니다.
API Hooking – '스텔스' 프로세스 (2)


* 참고!
모든 소스 코드는 MS Visual C++ 2008 Express Edition 으로 개발 되었으며, Windows XP SP3 환경에서 테스트 되었습니다.



HookProc.exe


HideProc.cpp


HookProc.exe 는 실행중인 모든 프로세스에 특정 DLL 파일을 Injection/Ejection 시켜주는 프로그램 입니다. 기존의 InjectDll.exe 프로그램에 모든 프로세스를 검사하는 기능을 추가한 거라고 생각하시면 됩니다.

대부분의 인젝션 코드는 기존의 InjectDll.cpp 와 같습니다. 아래 링크를 참고해 주세요.
DLL Injection – 프로세스에 침투하기 (2)


# InjectAllProcess()

새로 추가된 InjectAllProcess() 함수에 대해서 설명하겠습니다. 이 함수에서 실행중인 모든 프로세스를 검색하여 각각 DLL Injection/Ejection 을 수행합니다.

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 를 이용해서 시스템에 실행중인 모든 프로세스 리스트를 얻어내고 Process32First() 와 Process32Next() API 를 이용해서 PID 를 구합니다.

HANDLE WINAPI CreateToolhelp32Snapshot(
  __in  DWORD dwFlags,
  __in  DWORD th32ProcessID
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx

BOOL WINAPI Process32First(
  __in     HANDLE hSnapshot,
  __inout  LPPROCESSENTRY32 lppe
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms684834(VS.85).aspx

BOOL WINAPI Process32Next(
  __in   HANDLE hSnapshot,
  __out  LPPROCESSENTRY32 lppe
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms684836(VS.85).aspx


* 주의!
미리 HideProc.exe 프로세스의 권한(특권)을 상승시켜 놓아야 전체 프로세스의 리스트를 정확하게 얻을 수 있습니다. HideProc.cpp 에서는 main() 함수에서 미리 _EnableNTPrivilege() 함수를 호출하고 이 함수 내부에서 AdjustTokenPrivileges() API 를 이용해서 권한을 얻습니다.

PID 를 구했으면 모드(INJECTION / EJECTION)에 따라 InjectDll() / EjectDll() 함수를 호출합니다.

한가지 특이한 점은 PID 값이 100 보다 작다면, 그 프로세스는 작업하지 않고 그냥 통과합니다. 그 이유는 시스템의 안정성을 위해서 시스템 프로세스(PID = 0, 4, 8, … ) 에게는 DLL Injection 하지 않는 것입니다. (이런 PID 값은 Windows XP 에 대해서 경험적으로 얻은 값으로써 다른 Windows 버전에서는 시스템 프로세스의 PID 값이 달라 질 수 있습니다.)



stealth.dll


stealth.cpp


# SetProcName()

먼저 export 함수인 SetProcName() 을 살펴보겠습니다.

// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
    char g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

// export function
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
    strcpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

위 코드를 보시면 “.SHARE” 이름의 공유 메모리 섹션을 만들고 g_szProcName 버퍼를 생성합니다. 그리고 export 함수인 SetProcName() 에 의해서 은폐 시키고 싶은 프로세스 이름을 g_szProcName 에 저장시킵니다. (SetProcName() 함수는 HookProc.exe 에서 호출됩니다.)

* 참고
g_szProcName 버퍼를 공유 메모리 섹션에 만들면 stealth.dll 이 모든 프로세스에게 인젝션 될 때 간단하게 은폐 프로세스 이름을 공유 시킬 수 있는 장점이 있습니다. (향후 프로그램을 더 발전 시켜서 동적으로 은폐 프로세스를 다른 걸로 변경시킬 수도 있습니다.)



# DllMain()

그럼 DllMain() 함수를 살펴볼까요?

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    char            szCurProc[MAX_PATH] = {0,};
    char            *p = NULL;

    // #1. 예외처리
    // 현재 프로세스가 HookProc.exe 라면 후킹하지 않고 종료
    GetModuleFileName(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("ntdll.dll", "ZwQuerySystemInformation",
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
        break;

        // #3. API Unhooking
        case DLL_PROCESS_DETACH :
        unhook_by_code("ntdll.dll", "ZwQuerySystemInformation",
                       g_pOrgBytes);
        break;
    }

    return TRUE;
}

DllMain() 은 보시다시피 매우 간단합니다.

먼저 문자열 비교를 통해 프로세스 이름이 "HookProc.exe" 라면 API 후킹하지 않도록 예외처리를 해둡니다. 모든 프로세스를 정확히 검색하기 위해서 HookProc.exe 자신은 후킹 당하면 안되겠죠? (후킹 당하면 HookProc.exe 자신도 은폐 프로세스를 볼 수 없고, stealth.dll 을 인젝션 시킬 수 없기 때문입니다.)

그리고 DLL_PROCESS_ATTACH 이벤트에 hook_by_code() 함수로 API 를 후킹하고, DLL_PROCESS_DETACH 이벤트에 unhook_by_code() 함수로 API 후킹을 해제 시킵니다.


# hook_by_code()

코드 패치를 이용하여 API 를 후킹하는 hook_by_code() 함수 입니다.

BOOL hook_by_code(LPCTSTR szDllName, LPCTSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
    FARPROC pfnOrg;
    DWORD dwOldProtect, dwAddress;
    BYTE pBuf[5] = {0xE9, 0, };
    PBYTE pByte;

    // 후킹대상 API 주소를 구한다
    pfnOrg = (FARPROC)GetProcAddress(GetModuleHandle(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;
}

hook_by_code() 함수 파라미터 소개입니다.

LPCTSTR szDllName  : [IN] 후킹하려는 API 가 포함된 DLL 파일 이름
LPCTSTR szFuncName : [IN] 후킹하려는 API 이름
PROC pfnNew        : [IN] 사용자가 제공한 후킹함수 주소
PBYTE pOrgBytes    : [OUT] 원본 5 byte 를 저장시킬 버퍼 – 나중에 unhook 에서 사용됨

동작원리에서 설명 드렸듯이 hook_by_code() 함수의 기능은 원본 API 코드 시작부분의 5 byte 를 "JMP XXXX" 명령어로 변경하는 것입니다.

참고 API Hooking – '스텔스' 프로세스 (1)

소스 코드가 간단하여 주석을 보시면 대부분 쉽게 이해가 되는 내용입니다만, 중간의 점프 주소 계산 부분은 리버싱에서 매우 중요한 내용이기 때문에 자세히 살펴보도록 하겠습니다.

Intel x86 (IA-32) Instruction Format 에 따르면 JMP 명령어의 Op Code(Operation Code) 는 'E9' 입니다. 그리고 뒤에 4 byte 값이 이어집니다.

즉, JMP 명령어의 Op Code 는 "E9 XXXX" 형태가 됩니다.

문제는 XXXX 의 값이 JMP 할 절대 주소 값이 아니라, 현재 JMP 명령어 에서부터 JMP 할 위치까지의 상대 거리입니다. 이것은 아래의 공식으로 구할 수 있습니다.

XXXX = 점프할 주소 – 현재 명령어 주소 - 5

마지막에 5 를 더 빼주는 이유는 JMP 명령어 자체 길이 5 byte 를 보정해 주는 것입니다.

예를 들어 402000 주소의 JMP 명령어에서 401000 주소로 가고 싶을 때는 "E9 00104000" 이라고 쓰는 것이 아니고, 위 계산 공식대로 적용해서 XXXX 값을 구해야 합니다.

XXXX = 401000 – 402000 – 5 = FFFFEFFB

따라서 이 JMP 명령어의 Op Code 는 "E9 FBEFFFFF" 입니다.

OllyDbg 의 [Assemble] 또는 [Edit] 기능으로 확인해 보시기 바랍니다.


<Fig. 1>

* 참고 1
JMP 말고 short JMP 명령이 있습니다. 말 그대로 짧은 거리를 점프 할 때 쓰이는 명령어로 Op Code 는 "EB X" 입니다. (명령어 크기 2 byte)
OllyDbg 에서 'EB' 명령어도 테스트 해보시기 바랍니다.

* 참고 2
위와 같이 상대 거리 를 계산해서 JMP 명령어를 써주는 것이 좀 불편해 보일 수 있습니다.
물론 다른 명령어를 써서 절대 주소로 JMP 할 수 도 있습니다.

예1) PUSH + RET 
  68 00104000    PUSH 00401000
  C3             RETN

예2) MOV + JMP
  B8 00104000    MOV EAX, 00401000
  FFE0           JMP EAX


* 참고 3
32bit 주소 계산 하실 때 Windows 계산기는 좀 불편합니다.
다양한 기능이 있는 32bit Calculator v1.7 by cybult 를 추천합니다.

* 향후 x86 Op Code Map 을 해석하는 방법에 대해서 글을 올리도록 하겠습니다.


실제로 hook_by_code() 함수에 의해서 ZwQuerySystemInformation() API 가 후킹 되기 전/후의 모습을 디버거를 통하여 살펴 보도록 하겠습니다. (해당 프로세스는 procexp.exe 입니다.)

먼저 API 후킹 전의 ZwQuerySystemInformation() API 코드 입니다.

<Fig. 2>

ZwQuerySystemInformation() 주소는 7C93D92E 이며, 명령어 코드는 아래와 같습니다.

7C93D92E    B8 AD000000     MOV EAX, 0AD


이제 stealth.dll 이 인젝션 되면서 hook_by_code() 함수에 의해서 API 가 후킹된 이후의 코드입니다.

<Fig. 3>

ZwQuerySystemInformation() 코드 시작 명령어가 아래와 같이 변경 되었습니다. (정확히 5 byte)

7C93D92E    E9 ED376C93     JMP 10001120

10001120 주소는 바로 후킹 함수 NewZwQuerySystemInformation() 의 주소입니다.
그리고 E9 뒤의 4 byte (936C37ED) 값은 바로 위에서 설명 드린 계산 공식에 의해서 구할 수 있습니다. (한번씩 직접 해보시기 바랍니다.)


# unhook_by_code() 

후킹을 해제하는 unhook_by_code() 코드 입니다.

BOOL unhook_by_code(LPCTSTR szDllName, LPCTSTR szFuncName, PBYTE pOrgBytes)
{
    FARPROC pFunc;
    DWORD dwOldProtect;

    // API 주소 구한다
    pFunc = GetProcAddress(GetModuleHandle(szDllName), szFuncName);

    // 원래 코드 (5 byte)를 덮어쓰기 위해 메모리에 WRITE 속성 추가
    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    // Unhook
    memcpy(pFunc, pOrgBytes, 5);

    // 메모리 속성 복원
    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

Unhook 의 동작원리는 원래 코드의 5 byte 를 복원해 주는 것입니다.
코드가 단순하므로 설명은 주석으로 대체 합니다.


# NewZwQuerySystemInformation()

마지막으로 후킹함수 NewZwQuerySystemInformation() 에 대해서 살펴볼 시간입니다.

하지만 그전에 먼저 ntdll!ZwQuerySystemInfomation() 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, *PSYSTEM_PROCESS_INFORMATION;

* 출처 : http://msdn.microsoft.com/en-us/library/ms725506(VS.85).aspx

간단히 설명해서 SystemInformationClass 파라미터에 SystemProcessInformation (5) 으로 세팅하고 ZwQuerySystemInformation() API 를 호출하면 SystemInformation [inout] 파라미터에 SYSTEM_PROCESS_INFORMATION 구조체 단방향 연결 리스트(single linked list)의 시작 주소가 얻어집니다.

바로 이 구조체 연결 리스트에 실행중인 모든 프로세스들의 정보가 담겨 있습니다.

따라서 프로세스 은폐를 구현하려면 은폐하고 싶은 프로세스에 해당하는 리스트 멤버를 찾아서 리스트 연결을 끊어 버리면 됩니다.

아래 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("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);

    // original API 호출
    pFunc = GetProcAddress(GetModuleHandle("ntdll.dll"),
                           "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)
        {
            // wide character => multi byte 변환
            WideCharToMultiByte(CP_ACP, 0, (PWSTR)pCur->Reserved2[1],
                                -1, szProcName, MAX_PATH, NULL, NULL);

            // 프로세스 이름 비교
            // g_szProcName = 은폐 하려는 프로세스 이름
            // (=> SetProcName() 에서 세팅됨)

            if(!_strcmpi(szProcName, 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("ntdll.dll", "ZwQuerySystemInformation",
                 (PROC)NewZwQuerySystemInformation, g_pOrgBytes);

    return status;
}

위 NewZwQuerySystemInformation() 함수의 구조를 간단히 설명 드리면 아래와 같습니다.

- 언훅(unhook) ZwQuerySystemInformation()
- ZwQuerySystemInformation() 호출
- SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 은폐 프로세스 찾음
- 은폐 프로세스를 찾으면 리스트에서 제거
- 훅(hook) ZwQuerySystemInformation()


NewZwQuerySystemInformation() 코드의 중간쯤 while() 문을 보시면 SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 프로세스 이름(pCur->Reserved2[1])을 비교합니다. (프로세스 이름은 Unicode 문자열이기 때문에 간단히 ASCII 로 변경 후 strcmp_i() 함수를 사용했습니다.)

...
// wide character => multi byte 변환
WideCharToMultiByte(CP_ACP, 0, (PWSTR)pCur->Reserved2[1],
-1, szProcName, MAX_PATH, NULL, NULL);

// 프로세스 이름 비교
// g_szProcName = 은폐 하려는 프로세스 이름
// (=> SetProcName() 에서 세팅됨)

if(!_strcmpi(szProcName, g_szProcName))
{
...

함수의 동작 원리만 이해하시면 (주석과 함께) 코드를 보시는데 어려움이 없으실 걸로 생각합니다.


다음 번에는 Global API Hooking 에 대해서 배워보도록 하겠습니다.

API Hooking – '스텔스' 프로세스 (4)


ReverseCore

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

반응형
  1. 이전 댓글 더보기
반응형

특정 프로세스를 감추는 은폐(stealth) 기법에 대해 설명하고, 예제 파일을 통해서 실습을 해보도록 하겠습니다.

 

<photo : mashleymorgan on flickr>


아래 내용은 이전 포스트에서 이어지는 내용입니다.
API Hooking – '스텔스' 프로세스 (1)



프로세스 은폐(stealth) 동작 원리 (User Mode)


프로세스 은폐(stealth)에 관한 내용은 이미 많은 정보가 공개되어 있습니다.
그 중에서 유저 모드(User Mode)에서 가장 널리 사용되는 "ntdll!ZwQuerySystemInformation() API 후킹 방법"에 대해서 살펴보겠습니다.

* 커널 모드의 프로세스 은폐 기법은 (유저 모드에 비해) 더 강력하고, 더 고급 기법입니다. 향후 제 블로그에서 자세히 다루도록 하겠습니다.


#1. 프로세스 은폐(stealth) 개념

스텔스 전투기는 레이다에 포착되지 않기 위해서 각종 첨단 과학을 동원하여 전투기 자체를 (기존 전투기와 다르게) 완전히 새롭게 개발 하였습니다. 즉, 작업 대상이 스텔스 전투기 자신입니다.

반면에 은폐 프로세스의 개념은 이와는 정반대입니다.
특정 프로세스를 은폐시키기 위해서 나머지 모든 프로세스들의 메모리에 침투하여 API 를 후킹합니다. 즉, 작업 대상은 다른 프로세스입니다.

은폐 프로세스의 개념을 스텔스 전투기의 설명에 적용해보면 아래와 같습니다.

그냥 일반 전투기를 띄워 보낸 후 모든 레이다를 고장내면(조작하면), 그 일반 전투기는 그 순간부터 스텔스 전투기가 되는 것과 같은 이치입니다.

이것이 바로 은폐(stealth) 프로세스의 개념입니다.


#2. 관련 API

프로세스(Process)는 커널 객체이기 때문에 (유저 모드 프로그램은) API 를 통해서만 접근이 가능합니다. 일반적으로 유저 모드에서 프로세스를 검색하기 위한 API 는 아래와 같이 2 종류가 있습니다.

HANDLE WINAPI CreateToolhelp32Snapshot(
    DWORD    dwFlags,
    DWORD    th32ProcessID
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx

BOOL EnumProcesses(
    DWORD*   pProcessIds,
    DWORD    cb,
    DWORD*   pBytesReturned
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms682629(VS.85).aspx

위 2 가지 API 모두 내부적으로 ntdll!ZwQuerySystemInformation() API 를 호출합니다.

NTSTATUS ZwQuerySystemInformation(
    SYSTEM_INFORMATION_CLASS    SystemInformationClass,
    PVOID    SystemInformation,
    ULONG    SystemInformationLength,
    PULONG   ReturnLength
);

* 출처 : http://msdn.microsoft.com/en-us/library/ms725506(VS.85).aspx

ZwQuerySystemInformation() API 를 이용하여 실행중인 모든 프로세스들의 정보(구조체)를 연결 리스트 형태로 얻을 수 있습니다.

그 연결 리스트를 조작하면(리스트에서 빼내면) 해당 프로세스는 은폐 되는 것입니다.

따라서 유저 모드에서는 CreateToolhelp32Snapshot() 나 EnumProcess() API 를 후킹할 필요 없이  ZwQuerySystemInformation() API 하나만 후킹하면 확실하게 특정 프로세스를 은폐 시킬 수 있습니다.


#3. Global Hooking 개념

우리가 은폐하고자 하는 프로세스를 test.exe 라고 하겠습니다.
실행중인 ProcExp.exe (또는 taskmgr.exe) 프로세스의 ZwQuerySystemInformation() API 를 후킹 하면 ProcExp.exe 는 test.exe 를 찾지 못할 것입니다.

* ProcExp.exe = 프로세스 익스플로러, taskmgr.exe = 윈도우 작업 관리자

위와 같이 하면 ProcExp.exe(또는 taskmgr.exe) 프로세스 하나에 대해서 test.exe 가 은폐되었다고 말할 수 있습니다.

하지만 이 방법에는 두 가지 문제점 이 있습니다.

첫 번째 문제점은 후킹 대상 프로세스 개수 입니다.
프로세스 검색 유틸리티가 과연 이 두 가지뿐 일까요?
이들 외에도 수 많은 프로세스 검색 유틸리티가 있을 것이며, 사용자가 직접 만든 유틸리티도 있을 수 있습니다. 따라서 시스템에 실행중인 모든 프로세스를 후킹 해야만 내 프로세스가 은폐되었다고 확신할 수 있습니다.

두 번째 문제점은 새로 생성되는 프로세스입니다.
만약 사용자가 ProcExp.exe (또는 taskmgr.exe) 를 하나 더 실행하면 어떻게 될까요?
첫 번째 ProcExp.exe 프로스세는 이미 후킹이 되어 있으므로 test.exe 프로세스를 찾지 못하겠지만, 두 번째 실행된 ProcExp.exe 프로세스는 후킹 되지 않았으므로 test.exe 프로세스를 정상적으로 찾아 낼 것입니다.

이 두 가지 문제를 정리해 보면 우리는 test.exe 프로세스를 완전히 숨기기 위해서 시스템에 실행 중인 모든 프로세스의 ZwQuerySystemInformation() API 를 후킹해야 하며, 추가적으로 나중에 실행되는 모든 프로세스에 대해서도 똑 같이 후킹을 해줘야 합니다. (물론 자동으로 해줘야겠지요.)

이것이 바로 global hooking 의 개념입니다.
(이러한 global hooking 에 대해서는 뒷부분에서 따로 설명하도록 하겠습니다.)



실습


HideProc.exe

stealth.dll


HideProc.exe 는 실행중인 모든 프로세스에게 Stealth.dll 파일을 인젝션 시키는 역할을 합니다.
Stealth.dll 은 인젝션 된 프로세스의 ntdll!ZwQuerySystemInformation() API 를 후킹하는 역할을 합니다.

위 두 파일을 이용해서 notepad.exe 프로세스를 은폐시켜 보도록 하겠습니다.

* 참고!
위 실습 파일은 “global hooking – 새로운 프로세스”에 대한 대책이 없는 버전입니다.
따라서 HideProc.exe 실행 이후에 생성된 프로세스는 자동으로 후킹 되지 않습니다.
그에 대해서는 따로 설명 드릴 예정입니다. 참고하세요.



#1. notepad.exe, procexp.exe, taskmgr.exe 를 실행시켜 주세요.


#2. 아래와 같이 HideProc.exe 를 실행합니다.

<Fig. 1>

실행 파라미터에 대해서 간략히 설명 드리겠습니다.

-hide/-show : 은폐 시킬 때는 –hide, 은폐 해제 시에는 –show
process name : 은폐 시킬 프로세스 이름
dll path  : 인젝션 시킬 DLL 파일 경로


#3. 모든 프로세스에 stealth.dll 파일이 제대로 인젝션 되었는지 확인합니다.

Process Explorer 을 이용해서 stealth.dll 을 검색합니다.

<Fig. 2>

실행중인 모든 프로세스에 stealth.dll 파일이 인젝션 된 것을 확인 할 수 있습니다.

* 사실은 시스템 프로세스들(PID 0 & PID 4)에 대해서는 (시스템 안정성을 위해) 인젝션 시키지 않았습니다. (notepad.exe 프로세스 은폐에는 아무 상관 없습니다.)


#4. procexp.exe 와 taskmgr.exe 에서 notepad.exe 프로세스가 사라진걸 확인합니다.

<Fig. 3>

<Fig. 4>

위의 <Fig. 3> 과 <Fig. 4> 를 보시면 notepad.exe 프로세스가 분명히 실행 중이지만, procexp.exe 와 taskmgr.exe 에서 notepad.exe 프로세스가 사라진걸 확인하실 수 있습니다.

* 메모장의 윈도우가 보이기 때문에 프로세스가 완벽히 숨은 게 아니라고 생각하실 수 있겠습니다만, 우리의 목표는 프로세스 은폐이기 때문에 윈도우는 그냥 놔뒀습니다. 참고로 윈도우를 사라지게 하려면 SetWindowPos() API 등을 사용하시면 됩니다.


#5. notepad.exe 프로세스를 다시 보이도록 합니다.

HideProc.exe 를 –show 모드로 실행시킵니다. (stealth.dll 을 ejection 시켜줍니다.)

<Fig. 5>

procexp.exe 와 taskmgr.exe 에서 notepad.exe 프로세스가 정상적으로 보이는지 직접 확인해보시기 바랍니다.


다음 번에는 예제 소스 파일을 상세하게 분석해보도록 하겠습니다.

API Hooking – '스텔스' 프로세스 (3)


ReverseCore

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

반응형

+ Recent posts