Socket 통신 Patching을 통한 후킹

안상준·2025년 6월 7일

Reversing

목록 보기
4/5
post-thumbnail

Patching을 직접 실습해 보았다. Pathching한 코드는 간단하게 소켓통신하는 코드에서 buffer를 조작하여 원하는 문자열로 조작 후 서버가 수신하도록 하였다.

Code

후킹한 코드는 socket 통신을 하는 코드에서 client의 코드를 패칭을 통해 후킹을 하였다. 패칭은 client에서 send를 호출하기전, 프로세스 메모리에서 "hello"라는 문자열을 찾고, 이를 "hacked"로 바꾸는 패칭을 하여 결과적으로 서버가 "hacked"라는 문자열을 수신하도록 하였다.
코드를 살펴보도록 하자.

client.c

// client.c
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>

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

int main() {
    WSADATA wsa;
    SOCKET s;
    struct sockaddr_in server;
    char buffer[1024] = "hello";

    DWORD pid = GetCurrentProcessId();
    printf("[*] My PID: %lu\n", pid);
    system("pause");

    WSAStartup(MAKEWORD(2,2), &wsa);

    s = socket(AF_INET, SOCK_STREAM, 0);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr("127.0.0.1");
    server.sin_port = htons(8888);

    connect(s, (struct sockaddr *)&server, sizeof(server));
    send(s, buffer, strlen(buffer), 0);

    closesocket(s);
    WSACleanup();
    return 0;
}

먼저 client 코드다. 컴파일은

gcc client.c -o client.exe -lws2_32

이렇게 옵션을 추가해 라이브러리를 명시해 주어야 한다.
buffer에 전송할 문자열을 저장해 주고, GetCurrentProcessId를 통해서 현재 실행중인 process의 id를 출력해 준다. 출력하는 이유는 프로세스 메모리에 패칭을 하기 때문에 프로세스의 id가 필요하다. 그 다음에 pause를 하여 patching 될 때까지 잠시 멈춰준다. 그 다음은 소켓을 연결하고 send를 실행한다.

server.c

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

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

int main() {
    WSADATA wsa;
    SOCKET s, new_socket;
    struct sockaddr_in server, client;
    char buffer[1024] = {0};
    int c;

    WSAStartup(MAKEWORD(2,2), &wsa);

    s = socket(AF_INET, SOCK_STREAM, 0);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons(8888);

    bind(s, (struct sockaddr *)&server, sizeof(server));
    listen(s, 3);
    printf("[*] Waiting for connection...\n");

    c = sizeof(struct sockaddr_in);
    new_socket = accept(s, (struct sockaddr *)&client, &c);
    printf("[*] Client connected.\n");

    recv(new_socket, buffer, sizeof(buffer), 0);
    printf("[*] Received: %s\n", buffer);

    closesocket(new_socket);
    closesocket(s);
    WSACleanup();
    system("pause");
    return 0;
}

서버의 코드다. 마찬가지로 컴파일은

gcc server.c -o server.exe -lws2_32

옵션을 명시해 주어야 한다.
코드는 송신측에서 보낸 패킷을 수신하여 buffer를 출력하는 식으로 이루어져 있다.

patcher.c

// patcher.c
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <string.h>

#define TARGET_STRING "hello"
#define REPLACE_STRING "hacked"

BOOL patch_buffer(DWORD pid) {
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProc) {
        printf("[-] Failed to open process.\n");
        return FALSE;
    }

    SYSTEM_INFO si;
    GetSystemInfo(&si);

    MEMORY_BASIC_INFORMATION mbi;
    BYTE *addr = 0;

    while (addr < si.lpMaximumApplicationAddress) {
        if (VirtualQueryEx(hProc, addr, &mbi, sizeof(mbi)) == 0)
            break;

        if ((mbi.State == MEM_COMMIT) && (mbi.Protect & (PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE))) {
            BYTE *buffer = malloc(mbi.RegionSize);
            SIZE_T bytesRead;

            if (ReadProcessMemory(hProc, addr, buffer, mbi.RegionSize, &bytesRead)) {
                for (SIZE_T i = 0; i < bytesRead - strlen(TARGET_STRING); i++) {
                    if (memcmp(buffer + i, TARGET_STRING, strlen(TARGET_STRING)) == 0) {
                        // found, patch it
                        SIZE_T written;
                        WriteProcessMemory(hProc, addr + i, REPLACE_STRING, strlen(REPLACE_STRING), &written);
                        printf("[+] Patched at address: %p\n", addr + i);
                        free(buffer);
                        CloseHandle(hProc);
                        return TRUE;
                    }
                }
            }

            free(buffer);
        }

        addr += mbi.RegionSize;
    }

    CloseHandle(hProc);
    return FALSE;
}

int main() {
    DWORD pid;
    printf("Enter target PID: ");
    scanf("%lu", &pid);

    if (patch_buffer(pid)) {
        printf("[+] Successfully patched.\n");
    } else {
        printf("[-] Patch failed.\n");
    }

    return 0;
}

핵심 부분인 patching을 하는 코드다. 먼저 입력받은 pid를 열어 준다. 그 다음 시스템 정보를 가져와 검색할 범위를 정한다.
VirtualQueryEx 는 지정된 프로세스의 가상 메모리 영역 정보를 가져오는 Windows API이다. addr 위치에 미모리 블록이 어떤 상태인지, 보호 속성은 어떤지 알려준다.
그 다음에 메모리를 검사한다. 보호 속성이 있는지 실제 사용 중인지 확인을 하여 WriteProcessMemory로 수정할 수 있는 구역을 검색한다.
그 다음은 "hello"라는 문자열이 있는 곳을 찾고 WriteProcessMemory를 실행해 준다.

Result


위에서 부터 server, client, patcher 이고, 먼저 실행후 patcher에 client의 pid를 입력해 준다. 이후 client에서 pause를 풀면

server에서 "hello"대신 "hacked"를 수신한 것을 볼 수 있다.

OllyDbg

OllyDbg를 이용해 자세하게 분석해 보자

먼저 client를 보면 스택에 6F6C6C6568의 값을 저장하는 것을 볼 수 있다.
68646C6C6F->"hello"가 된다. 즉 "hello"를 아스키코드 헥스값으로 스택에 저장을 하게 된다.
patcher를 통해 patching 한 후 살펴보자. 먼저 send함수에 break point를 걸고, patcher를 실행 후 pause를 풀면 된다.

break point가 걸린 상태에서 스택을 검사하면 send함수의 파라미터를 볼 수 있다. esp위치로 이동한 다음 ASCII로 덤프하여 확인해 보면 "hacked"라는 값이 있는 것을 확인할 수 있다.

개선법

과정을 보면 알 수 있듯이 한계점은 명확하다. buffer의 값을 알지 못하면 patching을 할 수 없다.
send함수를 후킹하여 buffer를 조작해 보자.

흐름

먼저 전체 흐름에 대해서 설명하도록 하겠다.
1. 후킹용 DLL 작성
- DLL에서 send함수 후킹을 수행
- send의 엔트리포인트에서 내가 만든 함수로 jump 하도록
2. DLL 인젝션
- CreateRemoteThread + LoadLibrary 방식으로 client.exe에 DLL을 주입
3. 결과
- send 실행 시 특정 위치로 jump
- 조작된 buffer로 send 실행

Code

injector

dll 파일을 삽입시키기 위한 코드이다.

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

BOOL InjectDLL(DWORD pid, const char* dllPath) {
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProc) return FALSE;

    LPVOID alloc = VirtualAllocEx(hProc, NULL, strlen(dllPath)+1, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProc, alloc, dllPath, strlen(dllPath)+1, NULL);

    HMODULE hKernel = GetModuleHandleA("kernel32.dll");
    FARPROC loadLib = GetProcAddress(hKernel, "LoadLibraryA");

    HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)loadLib, alloc, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);

    VirtualFreeEx(hProc, alloc, 0, MEM_RELEASE);
    CloseHandle(hProc);
    return TRUE;
}

int main() {
    DWORD pid;
    char dllPath[MAX_PATH] = "hook.dll";

    printf("Enter PID: ");
    scanf("%lu", &pid);

    if (InjectDLL(pid, dllPath)) {
        printf("[+] DLL injected!\n");
    } else {
        printf("[-] Injection failed.\n");
    }
    return 0;
}
  1. 먼저 pid를 입력 받고, OpenProcess를 실행
  2. 경로의 문자열 길이 만큼 프로세스 메모리에 공간을 할당 후, 경로를 메모리에 write
  3. LoadLibraryA의 주소를 가져와 CreateRemoteThread를 통해 dll파일 load

hook_send.dll

삽입할 dll파일이다.

#include <windows.h>
#include <winsock2.h>
#include <stdio.h>

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

typedef int (WINAPI *SEND)(SOCKET, const char *, int, int);
SEND original_send = NULL;
BYTE original_bytes[5];
BYTE patch_bytes[5];

int WINAPI hooked_send(SOCKET s, const char *buf, int len, int flags) {
    char new_buf[1024] = {0};
    strcpy(new_buf, "hacked");

    // 원본 함수 복원
    DWORD oldProtect;
    VirtualProtect((LPVOID)original_send, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy((void*)original_send, original_bytes, 5);
    VirtualProtect((LPVOID)original_send, 5, oldProtect, &oldProtect);

    // 원본 함수 호출
    int ret = original_send(s, new_buf, strlen(new_buf), flags);

    // 다시 후킹 적용
    VirtualProtect((LPVOID)original_send, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy((void*)original_send, patch_bytes, 5);
    VirtualProtect((LPVOID)original_send, 5, oldProtect, &oldProtect);

    char dbg[128];
    sprintf(dbg, "[hooked_send] ret = %d, sent: %s", ret, new_buf);
    OutputDebugStringA(dbg);

    return ret;
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason, LPVOID lpReserved) {
    if (ul_reason == DLL_PROCESS_ATTACH) {
        MessageBoxA(NULL, "DLL injected", "Status", MB_OK);

        HMODULE hMod = GetModuleHandleA("ws2_32.dll");
        FARPROC target = GetProcAddress(hMod, "send");
        original_send = (SEND)target;

        // 원본 5바이트 백업
        memcpy(original_bytes, (void*)target, 5);

        // 패치 바이트 생성
        DWORD offset = (DWORD)hooked_send - (DWORD)target - 5;
        patch_bytes[0] = 0xE9;
        memcpy(patch_bytes + 1, &offset, 4);

        // 후킹 적용
        DWORD oldProtect;
        VirtualProtect((LPVOID)target, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
        memcpy((void*)target, patch_bytes, 5);
        VirtualProtect((LPVOID)target, 5, oldProtect, &oldProtect);
    }
    return TRUE;
}

컴파일은

g++ -shared -o hook.dll hook_send.cpp -lws2_32

이렇게 해주었다.
코드의 흐름을 순서대로 설명하면
1. injector로 인해 실행되며, send함수를 찾아 entry point에서 5바이트를 백업
2. 패칭할 값과 패칭시킬 주소 구하기

offset = 목적지 주소 - 현재 주소 - 명령어 크기
offset = hooked_send - target - 5

jmp는 상대주소를 이용하여 jump

3. 구한 주소(hook_send)로 jump하는 코드 패칭
4. send함수 호출 시 hook_send로 이동, 백업을 이용하여 send함수 복원
5. 조작된 buffer를 이용하여 원본 send함수 호출
6. 다시 send함수를 후킹

먼저 5바이트를 백업하는 이유는 패칭할 코드가 5바이트이기 때문이다.
백업 후 inline-hooking을 통해 hook_send로 jump하는 명령어를 삽입한다.
send 호출 -> hook_send이 실행되며, 해당 함수에서 다시 후킹한 함수를 복원을 한다. 그 이유는 원본 send함수를 실행하여 조작된 buffer를 전송하기 때문이다.
이후 다시 함수를 후킹하여, 조작된 buffer를 전송할 수 있도록 한다.

결과

코드를 실행하며 생겼던 문제중에 hook_send가 재귀적으로 실행되는 문제가 있었다. send를 hook_send로 jump하도록 후킹하고, send를 실행하여 발생한 문제였다. 이는 백업과 복원을 통하여 문제를 해결하였다.

stack 조작 vs Inline Hooking

사실 처음 생각해본 방법은 send함수 호출시, stack을 조작하여 buffer를 조작하면 되지 않나 생각했었다.

stack 조작

  • 직접적이고 빠름
  • 함수 호출 오버헤드 없음
  • 배우 복잡하고 불안정
  • 컴파일러 최적화나 호출 규약변경 시 깨짐
  • 디버깅 극도로 어려움

Inline Hooking

  • 안전성과 가독성
  • 복잡한 로직 구현 가능
  • 여러 매개변수 동시 조작 가능
  • 약간의 성능 오버헤드

Inline Hooking이 더 안정적이고 쉬운 방법인 것 같다.

0개의 댓글