본문 바로가기

공부/리버싱 핵심원리

메모장 WriteFile 함수 후킹

1. 기술 맵 디버그 테크닉

디버그(Debug) 기법을 이용한 API 후킹기술을 이용하여 notepad. exe 프로세스의 kernel32! WriteFile() API를 후킹한다.


2. 디버거 설명

용어 정리

 ① 디버거(Debugger) · 디버깅을 위해 사용하는 프로그램

 ② 디버기(Debuggee) - 디버깅 당하는 target 프로그램 

 

디버거 기능: 디버기 명령을 하나씩 수행, 레지스터와 메모리에 대한 모든 접근 권한을 갖는다. 

 

디버거 동작 원리

 ① 디버거 프로세스로 등록

 ② OS는 디버기에서 디버그 이벤트가 발생하면 디버기의 실행을 멈추고 해당 이벤트를 디버거에게 통보

 ③ 디버거는 해당 이벤트를 처리

 ④ 디버거는 디버기의 실행을 재개

 

디버그 이벤트 (Debug Event): 9가지

① CREATE_PROCESS_DEBUG_EVENT

② CREATE_THREAD_DEBUG_EVENT

③ EXCEPTION_DEBUG_EVENT → 디버깅 관련 이벤트

④ EXIT_PROCESS_DEBUG_EVENT

⑤ EXIT_THREAD_DEBUG_EVENT

⑥ LOAD_DLL_DEBUG_EVENT

⑦ OUTPUT_ DEBUG_STRING_ EVENT

⑧ UNLOAD_DLL_DEBUG_ EVENT

⑨ RIP_EVENT

 

 

3. 작업 순서

① 디버거 프로그램을 작성/실행하여, 후킹을 원하는 프로세스에 attach하여 해당 프로세스를 디버기로 설정한다.

② 디버거에서 후킹 하려는 API 함수의 시작 주소의 첫 바이트를 0xCC로 변경(BP설정)한다.

③  디버기를 수행한다.

④ 해당 API 함수가 호출되면 제어는 디버거에게 넘어 온다.

⑤ 디버기에 대하여 원하는 작업을 수행한다.

⑥ 언혹: OxCC를 원래대로 복원시킨다 (API 함수의 정상 수행을 위하여)

⑦ 해당 API 함수가 정상 실행된다.

 

 

 

4. 실습

① notepad.exe 실행 → PID 확인

② hookdbg. exe 실행

③ notepad.exe (메모장)에 글 입력 → 저장

④ hookdbg. exe 콘솔의 결과 확인

⑤ 저장한 메모장 파일을 다시 열어서 내용을 확인

    : 소문자로 입력한 것이 모두 대문자로 변경되어 파일에 저장됨

 

 

5.1 디버거 소스코드 분석: hookdbg.cpp - main()

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

    if (argc != 2)
    {
        printf("\nUSAGE : hookdbg.exe <pid>\n");
        return 1;
    }
    
    // attach 프로세스
    dwPID = atoi(argv[1]); // argv[1]: 디버기(notepad.exe)의 PID
    if (!DebugActiveProcess(dwPID)) // 디버거(hookdbg 프로세스)를 디버기할 프로세스(notepad)에 attach시켜서 디버깅을 시작할 수 있게 한다.
    {
        printf("DebugActiveProcess(%d) failed!!!\n"
               "Error Code = %d\n", dwPID, GetLastError());
        return 1;
    }
    
    // 디버거 루프
    DebugLoop();

    return 0;
}

 

 

5.2 디버거 소스코드 분석: hookdbg.cpp - DebugLoop()

void DebugLoop()
{
    DEBUG_EVENT de;
    DWORD dwContinueStatus;

    while (WaitForDebugEvent(&de, INFINITE)) // Debuggee 로부터 event가 발생할 때까지 기다림 > 발생한 이벤트 정보는 de에 저장
    {
        dwContinueStatus = DBG_CONTINUE;

        if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) // CREATE_PROCESS_DEBUG_EVENT: 디버거가 attach될 때 발생
        { // Debuggee 프로세스 생성 혹은 attach 이벤트
            OnCreateProcessDebugEvent(&de);
        }
        else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode) // EXCEPTION_DEBUG_EVENT: 디버기 수행 중 BP 명령이 수행되면 발생
        { // 예외 이벤트
            if (OnExceptionDebugEvent(&de))
                continue;
        }
        else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode) // EXIT_PROCESS_DEBUG_EVENT: 디버기가 종료될 때 발생하는 이벤트
        { // Debuggee 프로세스 종료 이벤트 > Debugger도 종료
            break;
        }
        
        // 이벤트 처리 후, Debuggee의 실행을 재개시킴 (dwContinueStatus == DBG_CONTINUE)
        ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
    }
}

 

5.3 디버거 소스코드 분석: hookdbg.cpp - OnExceptionDebugEvent()

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
    CONTEXT ctx;
    PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
    PCHAR lpFileName = NULL;
    DWORD dwAddrOfFileName, i;
    HANDLE hFile;
    DWORD dwFileSize, dwBytesRead, dwBytesWritten;

    if (EXCEPTION_BREAKPOINT == per->ExceptionCode) // 전송된 '이벤트/예외' == BP Exception인 경우만 처리
    {
        if (g_pfCreateFile == per->ExceptionAddress) // Event를 발생시킨 주소(=BP주소)가 WriteFile() API함수의 시작 주소인지 확인
        {
            // 1) Unhook: 0xCC로 덮어쓴 부분을 WriteFile()API 코드의 원래 byte로 되돌림
            if (!WriteProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chOrgByte, sizeof(BYTE), NULL)) {
                printf("WriteProcessMemory failed with error %d\n", GetLastError());
                return FALSE;
            }

            // 2) Thread Context 구하기: CONTEXT ctx;
            ctx.ContextFlags = CONTEXT_FULL;
            if (!GetThreadContext(g_cpdi.hThread, &ctx)) {
                printf("GetThreadContext failed with error %d\n", GetLastError());
                return FALSE;
            }

            // 3) WriteFile()의 파라미터 2, 3(IpBuffer, numberOfBytesToWrite) 값 구하기 > 스택 확인
            if (!ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x4), &dwAddrOfFileName, sizeof(DWORD), NULL)) {
                printf("ReadProcessMemory failed with error %d\n", GetLastError());
                return FALSE;
            }
            
            lpFileName = (PCHAR)malloc(MAX_PATH);
            if (!lpFileName) {
                printf("Memory allocation failed\n");
                return FALSE;
            }
            memset(lpFileName, 0, MAX_PATH);

            if (!ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfFileName, lpFileName, MAX_PATH, NULL)) {
                printf("ReadProcessMemory failed with error %d\n", GetLastError());
                free(lpFileName);
                return FALSE;
            }

            printf("\n### original file name ###\n%s\n", lpFileName);

            hFile = CreateFileA(lpFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
            if (hFile != INVALID_HANDLE_VALUE)
            {
                // 4) 임시 버퍼 할당 후, WriteFile()의 버퍼를 임시 버퍼에 복사
                dwFileSize = GetFileSize(hFile, NULL);
                PCHAR lpFileBuffer = (PCHAR)malloc(dwFileSize + 1);
                if (!lpFileBuffer) {
                    printf("Memory allocation failed\n");
                    CloseHandle(hFile);
                    free(lpFileName);
                    return FALSE;
                }
                memset(lpFileBuffer, 0, dwFileSize + 1);

                if (!ReadFile(hFile, lpFileBuffer, dwFileSize, &dwBytesRead, NULL)) {
                    printf("ReadFile failed with error %d\n", GetLastError());
                    free(lpFileBuffer);
                    CloseHandle(hFile);
                    free(lpFileName);
                    return FALSE;
                }

                printf("\n### original file content ###\n%s\n", lpFileBuffer);

                // 5) 소문자 > 대문자 변환
                for (i = 0; i < dwBytesRead; i++) {
                    if (0x61 <= lpFileBuffer[i] && lpFileBuffer[i] <= 0x7A)
                        lpFileBuffer[i] -= 0x20;
                }

                printf("\n### converted file content ###\n%s\n", lpFileBuffer);
                
                 // 6) 변환된 버퍼를 WriteFile() 버퍼로 복사한 후, 임시 버퍼를 해제
                SetFilePointer(hFile, 0, NULL, FILE_BEGIN);

                if (!WriteFile(hFile, lpFileBuffer, dwBytesRead, &dwBytesWritten, NULL)) {
                    printf("WriteFile failed with error %d\n", GetLastError());
                }

                CloseHandle(hFile);
                free(lpFileBuffer);
            }
            else {
                printf("CreateFileA failed with error %d\n", GetLastError());
            }

            free(lpFileName);
            
            // 7) Thread Context의 EIP를 WriteFile() 시작으로 변경: 현재는 WriteFile()+1만큼 지나왔음
            ctx.Eip = (DWORD)g_pfCreateFile;
            if (!SetThreadContext(g_cpdi.hThread, &ctx)) {
                printf("SetThreadContext failed with error %d\n", GetLastError());
                return FALSE;
            }

            // 8) Debuggee 프로세스 실행 재개 및 Sleep(0)
            if (!ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE)) {
                printf("ContinueDebugEvent failed with error %d\n", GetLastError());
                return FALSE;
            }
            Sleep(0);

            // 9) API Hook을 다시 설정하고 True를 return하여 작업을 마친다.
            if (!WriteProcessMemory(g_cpdi.hProcess, g_pfCreateFile, &g_chINT3, sizeof(BYTE), NULL)) {
                printf("WriteProcessMemory failed with error %d\n", GetLastError());
                return FALSE;
            }

            return TRUE; // 디버거는 다음 이벤트를 기다림
        }
    }

    return FALSE; // 이벤트 처리 종료 > 디버기 실행 재기
}

 

 

'공부 > 리버싱 핵심원리' 카테고리의 다른 글

DLL Injection&코드 인젝션  (0) 2024.05.22
DLL 인젝션  (0) 2024.05.21
Windows 메시지 후킹  (0) 2024.05.21
인라인 패치 실습  (0) 2024.05.14
UPX 언패킹  (0) 2024.05.12