본문 바로가기

리버싱!

x64 Hooking 공부(메모장을 통한)

// dllmain.cpp : DLL 애플리케이션의 진입점을 정의합니다.
#include "pch.h"
#include<stdio.h>
#include<Windows.h>
#pragma pack(push,1)//구조체 데이터 정렬크기를 1바이트로 정렬, 구조체 변수간에 공간이 있을수있기에 1바이트로 할당하는것같다.
struct GOGO_NAM {
    BYTE opcode1;
    DWORD lpTarget1;
    DWORD opcode2;
    DWORD lpTarget2;
    BYTE opcode3;
};
#pragma pack(pop)//위의 설정 이전상태로 돌림

DWORD WINAPI Hook64();
DWORD WINAPI Unhook64();
GOGO_NAM orgFP;
BOOL Hooked = FALSE;

INT WINAPI NewWriteFile(
    _In_        HANDLE       hFile,
    _In_        LPCVOID      lpBuffer,
    _In_        DWORD        nNumberOfBytesToWrite,
    _Out_opt_   LPDWORD      lpNumberOfBytesWritten,
    _Inout_opt_ LPOVERLAPPED lpOverlapped) {
    MessageBoxA(NULL, (LPCSTR)lpBuffer, "hooking api call success", MB_OK);
    for (int i = 0; i < 10; i++) {
        MessageBoxA(NULL, "내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈", "hooking api call success", MB_OK);
    }
    Unhook64();
    BOOL ret = WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
    Hook64();
    return ret;
}

DWORD WINAPI Hook64() {
    if (Hooked)
        return 0; // Already Hooked 
                  // Get address of target function 
    LPVOID lpOrgFunc = NULL;
    if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile")) == NULL)
        return -1; // Check KernelBase-Redirect 
    if (*(SHORT*)lpOrgFunc == 0x25FF) {
        if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile")) == NULL)
            return -1;
    } // Inline Hook 
      // Backup old protect 
    DWORD dwOldProtect;
    if (VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), PAGE_EXECUTE_READWRITE, &dwOldProtect) == NULL)
        return -1;
    memcpy_s(&orgFP, sizeof(GOGO_NAM), lpOrgFunc, sizeof(GOGO_NAM));
    GOGO_NAM newFuncObj;
    newFuncObj.opcode1 = 0x68; // Push ???? 
    newFuncObj.lpTarget1 = (DWORD)((DWORD64)(&NewWriteFile) & 0xFFFFFFFF);
    newFuncObj.opcode2 = 0x042444C7; // MOV [ESP+4], ???? 
    newFuncObj.lpTarget2 = (DWORD)((DWORD64)(&NewWriteFile) >> 32);
    newFuncObj.opcode3 = 0xC3; // RET 
    memcpy_s(lpOrgFunc, sizeof(GOGO_NAM), &newFuncObj, sizeof(GOGO_NAM)); // Replacing 
                                                                          // Rollback protection 
    VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), dwOldProtect, NULL);
    Hooked = TRUE;
    return 0;
}

DWORD WINAPI Unhook64() {
    if (!Hooked)
        return 0; // Not Hooked 
    LPVOID lpOrgFunc = NULL;
    if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile")) == NULL)
        return -1; // Check KernelBase-Redirect 
    if (*(SHORT*)lpOrgFunc == 0x25FF) {
        if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile")) == NULL)
            return -1;
    } // Inline Hook 
      // Backup old protect 
    DWORD dwOldProtect;
    if (VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), PAGE_EXECUTE_READWRITE, &dwOldProtect) == NULL)
        return -1; // reset org FP 
    memcpy_s(lpOrgFunc, sizeof(GOGO_NAM), &orgFP, sizeof(GOGO_NAM)); // Rollback protection 
    VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), dwOldProtect, NULL);
    Hooked = FALSE;
    return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Hook64();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

후킹의 역할을 하는 dll이다. 이 dll을 프로세스에 넣는 방법은 해당링크 참고

https://nameng.tistory.com/117

 

DLL Injection 공부

https://github.com/gud425/DLL_Injection 백업용 github DLL_Injection made by nam 메모장이 켜질때마다 DLL을 삽입하여 내가 원하는 작동을 하도록 실행하는 프로그램 소스해석 DLL 소스 DWORD WINAPI ThreadPr..

nameng.tistory.com

후킹 dll을 차근차근 부분별로 분석해보자

 

#pragma pack(push,1)//구조체 데이터 정렬크기를 1바이트로 정렬, 구조체 변수간에 공간이 있을수있기에 1바이트로 할당하는것같다.
struct GOGO_NAM {
    BYTE opcode1;
    DWORD lpTarget1;
    DWORD opcode2;
    DWORD lpTarget2;
    BYTE opcode3;
};
#pragma pack(pop)//위의 설정 이전상태로 돌림

#progma pack->각 변수간의 할당공간의 크기를 1바이트로 정렬한다. padding을 없애려는 의도같다.

struct 구조체->후킹할 어셈블리 명령어가 들어갈 공간을 할당

 

INT WINAPI NewWriteFile(
    _In_        HANDLE       hFile,
    _In_        LPCVOID      lpBuffer,
    _In_        DWORD        nNumberOfBytesToWrite,
    _Out_opt_   LPDWORD      lpNumberOfBytesWritten,
    _Inout_opt_ LPOVERLAPPED lpOverlapped) {
    MessageBoxA(NULL, (LPCSTR)lpBuffer, "hooking api call success", MB_OK);
    for (int i = 0; i < 10; i++) {
        MessageBoxA(NULL, "내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈내돈", "hooking api call success", MB_OK);
    }
    Unhook64();
    BOOL ret = WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, lpNumberOfBytesWritten, lpOverlapped);
    Hook64();
    return ret;
}

wrtiefile api를 덮어쓸 함수인 newwritefile, 실제 writefile api가 실행되기전에 실행되므로 동일한 인자값을 만들어주면 똑같기때문에 그대로 써먹을수있다.

원하는 동작을 한 후 14바이트 공간으로 인해 프로그램이 깨지지 않도록 unhook을 한 후 실제 동작을 하는것을 알수있다. 해당소스에서는 ret변수에 진짜 writefile동작을 해주고 return 값을 받는 모습을 알수있다.

이후 다시 후킹을 진행해 후킹상태를 유지해준다.

DWORD WINAPI Hook64() {
    if (Hooked)
        return 0; // Already Hooked 
                  // Get address of target function 
    LPVOID lpOrgFunc = NULL;
    if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile")) == NULL)
        return -1; // Check KernelBase-Redirect 
    if (*(SHORT*)lpOrgFunc == 0x25FF) {
        if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile")) == NULL)
            return -1;
    } // Inline Hook 
      // Backup old protect 
    DWORD dwOldProtect;
    if (VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), PAGE_EXECUTE_READWRITE, &dwOldProtect) == NULL)
        return -1;
    memcpy_s(&orgFP, sizeof(GOGO_NAM), lpOrgFunc, sizeof(GOGO_NAM));
    GOGO_NAM newFuncObj;
    newFuncObj.opcode1 = 0x68; // Push ???? 
    newFuncObj.lpTarget1 = (DWORD)((DWORD64)(&NewWriteFile) & 0xFFFFFFFF);
    newFuncObj.opcode2 = 0x042444C7; // MOV [ESP+4], ???? 
    newFuncObj.lpTarget2 = (DWORD)((DWORD64)(&NewWriteFile) >> 32);
    newFuncObj.opcode3 = 0xC3; // RET 
    memcpy_s(lpOrgFunc, sizeof(GOGO_NAM), &newFuncObj, sizeof(GOGO_NAM)); // Replacing 
                                                                          // Rollback protection 
    VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), dwOldProtect, NULL);
    Hooked = TRUE;
    return 0;
}

if getprocaddress ->요즘 운영체제에서는 kernel32.dll에서 소스가 바로 나오지 않고 kernelbase.dll로 리다이렉트 되어 나오는 경우가 있다. 모든 api가 리다이렉트가 되는것은 아니므로 조건문을 통해 구별해준다. 이때 구별하기 위해 0x25FF값을 이용하게 되는데 이는 리다이렉트 될때 Absolute Jump를 하면서 앞 2바이트가 0x25FF가 되기때문이다.

 

첫번째 VirtualProtect-> 메모리 영역에 쓰기 권한을 줘야 overwrite를 할 수 있기에 virtualprotect를 통해 실제 api주소에 14바이트 영역만큼 읽고쓰기권한을 준다. 권한 원상복구용으로 dwoldprotect에 저장해준다.

 

memcpy_s =>orgFP에 실제 writefile의 메모리값을 14바이트 복사 ->나중에 unhook하면서 원상복구할때 사용

 

구조체 생성 및 각각의 opcode값은 주석 참고

newwritefile & 0xFFFFFFFF은 4바이트만 추출하기 위함-> PUSH (4바이트 값)

newwritefile >>32는 상위 32비트만뽑음

 

memcpy_s -> 실제 writefile주소에 우리가 만들어준 14바이트 공간 만큼 덮어써줌

VirtualProtect->실제 writefile주소의 권한을 다시 돌려놓음

 

DWORD WINAPI Unhook64() {
    if (!Hooked)
        return 0; // Not Hooked 
    LPVOID lpOrgFunc = NULL;
    if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile")) == NULL)
        return -1; // Check KernelBase-Redirect 
    if (*(SHORT*)lpOrgFunc == 0x25FF) {
        if ((lpOrgFunc = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile")) == NULL)
            return -1;
    } // Inline Hook 
      // Backup old protect 
    DWORD dwOldProtect;
    if (VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), PAGE_EXECUTE_READWRITE, &dwOldProtect) == NULL)
        return -1; // reset org FP 
    memcpy_s(lpOrgFunc, sizeof(GOGO_NAM), &orgFP, sizeof(GOGO_NAM)); // Rollback protection 
    VirtualProtect(lpOrgFunc, sizeof(GOGO_NAM), dwOldProtect, NULL);
    Hooked = FALSE;
    return 0;
}

대부분 Hooking부분과 동일

memcpy_s-> 실제 writefile의 14바이트를 백업해둔 writefile의 메모리값으로 바꿔줌

 

소스가 길어서 어려워보이나 실제로 분석해보면 별거없다.

 

실제 writefile의 부분으로 앞의 14바이트가 후킹되어 뒤에 많은내용이 있지만 ret로 넘어가는모습

 

14바이트의 후킹코드가 박히는 구간 7FFA4D9110E6의 내가만든 NewWriteFile의 주소를 가지고 연산을 한다.

0xFFFFFFFF와 and연산을하여 하위 4바이트를 넣고 NewWriteFile과 shift right 32를 통해 상위바이트를 뽑게된다. 64비트 주소에서 0xFFFFFFFF로 하위 4바이트 추출 SHIFT RIGHT 32를 통해 4바이트를 움직여줌으로써 상위 바이트 4바이트가 추출이 된다.

이는 첫번째 사진의 PUSH (하위 4바이트) MOV dword ptr [rsp+4],(상위 4바이트->앞부분은 0000이므로 7FFA만 입력)

이과정은 64비트에서 RSP+8부분에 하위 4바이트, RSP+4부분에 상위 4바이트를 넣어줌으로써 NewWriteFile로 JMP를 하게 된다.

비어있는 스택에서 

PUSH 를 하게 되면 RSP-8(스택은 높은주소부터 낮은주소 이므로)후 RSP가 가르키고 있는위치에 값을 넣는다.

mov [rsp+4]를 한모습

이후 ret과정으로

POP RIP를 통해 기존의 RSP가 가르키고 있던 값을 RIP에 저장하고 RSP+8을 하게 된다.

이후 JMP RIP를 하므로 가르키고 있는 [high addr+low addr]->우리가 만든 writefile 주소로 점프를 하게 된다.

 

32비트 후킹코드도 여기에

https://5kyc1ad.tistory.com/354