어셈블리 튜토리얼 (17) 64비트 api hooking(Trampoline)

4.5. api hooking(Trampoline)

Trampoline은 IAT 방식과 달리 많이 복잡해졌다. 32비트와 달라진점 에서 잠깐 설명했었듯이 64bit에서는 함수의 시작이 정형화되어있지 않다.

32bit MessageBoxA 함수

; 32bit stdcall 정형화된 함수시작 (5byte)
mov edi,edi
push ebp
mov ebp,esp

64bit MessageBoxA 함수

; 64bit fastcall 바로 구현이 시작되어 함수마다 모두 다르다.
sub rsp,38
xor r11d,r11d
cmp dword ptr ds:[7FFE1B2340B8],r11d
...

32bit에서는 5byte의 정형화된 함수시작코드를 가지는 반면 64bit에서는 바로 구현코드가 나오게된다. 때문에 잘라서 덮어쓸때 명령어가 잘리는 문제가 발생하게 된다.

예를 들어 위의 어셈블리코드를 바이트와 함께 다시보면

48 83 EC 38              | sub rsp,38
45 33 DB                 | xor r11d,r11d
44 39 1D DA 58 03 00     | cmp dword ptr ds:[7FFE1B2340B8],r11d

4byte 3byte 7byte 로 구성되어있다. 여기서 만약 5byte를 자른다면 두번째 명령어인 xor r11d,r11d 이 깨지게된다. 온전히 동작하는 단위로 자른다면 7byte로 잘라야한다.

이것은 기존의 32bit 소스에서 후킹한 함수뒤에 원래의 함수 시작부분(5byte)을 배치하여 자연스럽게 원래의 함수도 호출할 수 있게 처리한 것과도 관련을 갖는다.

때문에 기존의 방법처럼 동작하려면 후킹할 함수의 시작부분의 어셈블리코드를 분석해야하는 번거로움이 있다.

이를 해결하려면 다소 부하가 걸리더라도 후킹한 함수에서 매번 시작할때 원래 함수를 복원하고 종료부분에 다시 후킹을 거는 방법을 사용 할수도 있다. 이 방법은 추후에 설명하도록 하겠다.

먼저 기존의 방법대로 구현한 소스부터 살펴보자.

크게 두가지가 달라졌는데 첫번째는 위에서 설명한 5byte 대신 온전히 동작하는 단위를 입력해야 하는 부분. 두번째는 함수로 점프하는 stub 부분이 조금 복잡해졌다.

apihook_64.asm

option casemap:none
;option frame:auto

include D:\WinInc208\Include\windows.inc
include D:\WinInc208\Include\winnt.inc
;include D:\WinInc208\Include\user32.inc
;include D:\WinInc208\Include\kernel32.inc

includelib user32.lib
includelib kernel32.lib
includelib winmm.lib

LoadApiHook proto, lpszDll:qword, lpszProc:qword, lpTossProc:qword, lpTossJMP:qword, orgStubCount:qword
GetMsgProc proto, nCode:dword, wParam:WPARAM, lParam:LPARAM

MyMessageBoxA proto

.data
szUSER32        db 'USER32.DLL',0
szMessageBoxA       db 'MessageBoxTimeoutA',0

.data?
szVictim            byte 50 dup(?)
hCBTHook            qword ?
hGlobalModule       qword ?

.code
DllEntry proc hInstance:HINSTANCE, reason:DWORD, dwReserved:DWORD
    mov hInstance, rcx
    mov reason, edx
    mov dwReserved, r9d

    .if reason==DLL_PROCESS_ATTACH
        .if hGlobalModule==0
            push hInstance
            pop hGlobalModule
        .endif
        ;invoke MessageBox, 0, addr szVictim, addr szVictim, 0
        invoke GetModuleHandle, addr szVictim
        .if rax!=0
            invoke MessageBoxA, NULL, addr szVictim, addr szVictim, MB_OK

            invoke LoadApiHook, addr szUSER32, addr szMessageBoxA, addr MyMessageBoxA, addr MyMessageBoxAJMP, 15
        .endif
    .elseif reason==DLL_PROCESS_DETACH
        invoke GetModuleHandle, addr szVictim
    .endif

    mov rax, TRUE
    ret
DllEntry Endp

GetMsgProc proc nCode:dword, wParam:WPARAM, lParam:LPARAM
    mov nCode, ecx
    mov wParam, rdx
    mov lParam, r8

    invoke CallNextHookEx, hCBTHook, nCode, wParam, lParam
    ret
GetMsgProc endp

ProtectMemCopy proc lpSrc:qword, lpDst:qword, count:qword, isExecute:qword
    local dwOrgProtect:dword
    local mbi:MEMORY_BASIC_INFORMATION

    mov lpSrc, rcx
    mov lpDst, rdx
    mov count, r8
    mov isExecute, r9

    invoke VirtualQuery, lpDst, addr mbi, sizeof mbi
    mov esi, mbi.Protect
    and esi, not PAGE_READONLY
    and esi, not PAGE_EXECUTE_READ
    .if isExecute==0
        or esi, PAGE_READWRITE
    .else
        or esi, PAGE_EXECUTE_READWRITE
    .endif

    invoke VirtualProtect, lpDst, count, esi, addr dwOrgProtect

    mov rcx, count
    mov rsi, lpSrc
    mov rdi, lpDst
    rep movsb

    invoke VirtualProtect, lpDst, count, dwOrgProtect, addr dwOrgProtect
    xor rax, rax

    ret
ProtectMemCopy endp

LoadApiHook proc uses rsi rdi, lpszDll:qword, lpszProc:qword, lpTossProc:qword, lpTossJMP:qword, orgStubCount:qword
    local hModule:qword
    local lpOrgProc:qword

    local StubOrg[32]:byte
    local StubHook[32]:byte
    local OrgJmpStub[14]:byte

    mov lpszDll, rcx
    mov lpszProc, rdx
    mov lpTossProc, r8
    mov lpTossJMP, r9

    invoke GetModuleHandle, lpszDll
    mov hModule, rax
    .if rax==0
        jmp LOAD_HOOK_EXIT
    .endif

    invoke GetProcAddress, hModule, lpszProc
    mov lpOrgProc, rax
    .if rax==0
        jmp LOAD_HOOK_EXIT
    .endif

    ; 원본 stub 백업
    invoke ProtectMemCopy, lpOrgProc, addr StubOrg, orgStubCount, 0

    ; Hook 함수로 점프하는 stub
    mov rax, lpTossProc
    ;sub rax, lpOrgProc
    ;sub rax, 14

    ; push 000000h ; 68 DWORD
    ; mov [rsp+4], 000000h ; c7 44 24 04 DWORD
    ; ret ; c3
    lea rsi, StubHook
    mov byte ptr [rsi], 68h
    mov dword ptr [rsi + 1], eax
    mov byte ptr [rsi + 5], 0c7h
    mov byte ptr [rsi + 6], 44h
    mov byte ptr [rsi + 7], 24h
    mov byte ptr [rsi + 8], 04h
    shr rax, 32
    mov dword ptr [rsi + 9], eax
    mov byte ptr [rsi + 13], 0c3h

    add rsi, 14
    mov rcx, orgStubCount
    sub rcx, 14
@@:
    mov byte ptr [rsi], 90h
    inc esi
    loop @b

    ; hook stub으로 교체
    invoke ProtectMemCopy, addr StubHook, lpOrgProc, orgStubCount, 0

    ; 원본 stub 깔기
    invoke ProtectMemCopy, addr StubOrg, lpTossJMP, orgStubCount, 1

    ; 원본 함수로 점프하는 stub
    ; push 000000h ; 68 DWORD
    ; mov [rsp+4], 000000h ; c7 44 24 04 DWORD
    ; ret ; c3
    mov rax, lpOrgProc
    add rax, orgStubCount
    lea rsi, OrgJmpStub
    mov byte ptr [rsi], 68h
    mov dword ptr [rsi + 1], eax
    mov byte ptr [rsi + 5], 0c7h
    mov byte ptr [rsi + 6], 44h
    mov byte ptr [rsi + 7], 24h
    mov byte ptr [rsi + 8], 04h
    shr rax, 32
    mov dword ptr [rsi + 9], eax
    mov byte ptr [rsi + 13], 0c3h

    mov rsi, lpTossJMP
    add rsi, orgStubCount
    invoke ProtectMemCopy, addr OrgJmpStub, rsi, 14, 1

    xor rax, rax

LOAD_HOOK_EXIT:
    ret
LoadApiHook endp

; export functions
SetVictim proc lpszVictim:qword
    mov lpszVictim, rcx
    invoke lstrcpy, addr szVictim, lpszVictim
    ret
SetVictim endp

StartHook proc
    invoke SetWindowsHookEx, WH_CBT, addr GetMsgProc, hGlobalModule, NULL
    mov hCBTHook, rax
    ret
StartHook endp

EndHook proc
    .if hCBTHook!=0
        invoke UnhookWindowsHookEx, hCBTHook
    .endif
    ret
EndHook endp

; hook functions
MyMessageBoxA proc
    sub rsp, 38h
    call MyMessageBoxAJMP
    add rsp, 38h
    mov rax, IDCANCEL
    ret

MyMessageBoxA endp
MyMessageBoxAJMP:
    db 32 dup(90h) ;org stub
    db 14 dup(90h) ;jmp

end DllEntry

먼저 함수로 점프하는 stub 부분을 보도록 하겠다.

; Hook 함수로 점프하는 stub
; 원본 함수로 점프하는 stub

두 부분이다. 32bit에서는 jmp 00000000 jmp코드로 간단하게 처리했었다.

; 32bit 구현부분
    mov byte ptr [esi], 0E9h ; E9 00 00 00 00 ; jmp 00000000
    mov dword ptr [esi + 1], eax

64bit에서는 64bit 주소로 바로 jmp하는 명령어가 없다.

; 64bit 주소로 jmp를 지원하지 않는다.
jmp 0000000000000000

; 레지스터를 이용해야한다.
mov rax, 0000000000000000
jmp rax

이런식으로 레지스터를 이용해야 한다. 레지스터를 쓰지 않고 jmp하는 방법으로 return을 이용하기도 한다.

; jmp 00401000 와 같다.
push 00401000
ret

그런데 jmp와 마찬가지로 64bit값(qword) 는 push 할수 없다. 역시 레지스터를 이용해야한다.

; 64bit 주소값을 push할수없다.
push 00007FFE1B1FEAFB
ret

; 이런식으로 가능
mov rax, 00007FFE1B1FEAFB
push rax
ret

레지스터 없이 사용하기위해 하위 dword를 push 하고 상위 dword를 복사한다.

; push 00007FFE1B1FEAFB와 같다.
push 1B1FEAFB
mov [rsp+4], 00007FFE
ret

여기에선 이 방법을 이용한다. 이 코드는 14byte이다. 그러므로 32bit의 5byte와 달리 64bit에서는 14byte가 jmp구문이다.

    mov rax, lpTossProc

    ; push 000000h ; 68 DWORD
    ; mov [rsp+4], 000000h ; c7 44 24 04 DWORD
    ; ret ; c3
    ; 5byte + 8byte + 1byte = 14byte 이다.

    ; 위의 코드대로 jmp구문을 만드는 부분
    lea rsi, StubHook
    mov byte ptr [rsi], 68h
    mov dword ptr [rsi + 1], eax ; 하위 dword 를 복사
    mov byte ptr [rsi + 5], 0c7h
    mov byte ptr [rsi + 6], 44h
    mov byte ptr [rsi + 7], 24h
    mov byte ptr [rsi + 8], 04h
    shr rax, 32 ; 32bit를 오른쪽으로 shift하여 상위 4byte를 eax쪽으로 이동시킨다.
    mov dword ptr [rsi + 9], eax ; 상위 dword 를 복사
    mov byte ptr [rsi + 13], 0c3h

이렇게 14byte짜리 점프 구문을 생성한다.

LoadApiHook 함수가 달라졌다. orgStubCount 파라메터가 추가되었다. 위에서 설명한 온전히 동작하는 단위이다. 여기서는 15를 넘겨주고 있다.

후킹하려는 함수 시작부분이다.

48 8B C4                  | mov rax,rsp                             |
48 89 58 08               | mov qword ptr ds:[rax+8],rbx            |
48 89 68 18               | mov qword ptr ds:[rax+18],rbp           |
48 89 70 20               | mov qword ptr ds:[rax+20],rsi           |
...

jmp구문의 최소크기인 14byte를 자르면 마지막 구문이 깨지게 된다. 동작하는 단위는 15byte이다.

소스에서 MessageBoxA 대신 MessageBoxTimeoutA를 후킹하고 있다. MessageBoxTimeoutA는 MessageBoxA보다 파라메터가 훨씬 많은 함수로 사실 MessageBoxA 에서도 내부적으로는 이 함수를 이용한다. 따라서 MessageBoxTimeoutA를 후킹하면 자연스럽게 MessageBoxA를 후킹하는것과 같은 효과를 볼수 있다.

MessageBoxA는 현재의 방법에서는 후킹할 수 없다. MessageBoxA의 시작부분을 보면

48 83 EC 38              | sub rsp,38
45 33 DB                 | xor r11d,r11d
44 39 1D DA 58 03 00     | cmp dword ptr ds:[7FFE1B2340B8],r11d

일단 크기는 정확히 14byte로 자르는데에는 문제는 없다. 다만 마지막 구문이 문제가 되는데 DA 58 03 00 는 상대주소값이다. 현재의 위치에서의 상대주소값이기 때문에 다른 위치에서 실행 할 경우 값이 달라지게 된다. 그래서 이 부분을 후킹한 함수 뒤에 붙여서 실행주소가 달라질 경우 동작하지 않는다. 32bit와 달리 바로 구현부분이 나오기 때문에 발생하는 문제이다.

MyMessageBoxA proc
    sub rsp, 38h
    call MyMessageBoxAJMP
    add rsp, 38h
    mov rax, IDCANCEL
    ret

MessageBoxTimeoutA 의 파라메터는 7개이다. rsp를 56byte(38h)를 확보하고 호출한다.

MyMessageBoxAJMP:
    db 32 dup(90h) ;org stub
    db 14 dup(90h) ;jmp

원래 함수를 호출하기 위한 영역이다. 32bit에서는 5byte 5byte로 깔끔하게 떨어졌는데 64bit에서는 원래 함수 시작부분은 여유 크기로 32byte를 jmp 구문은 14byte로 처리하였다.

LoadApiHook가 실행되고나면 아래와같이 변한다.

MyMessageBoxAJMP:
; MessageBoxTimeoutA의 시작부분 15byte
mov rax,rsp
mov qword ptr ds:[rax+8],rbx
mov qword ptr ds:[rax+18],rbp
mov qword ptr ds:[rax+20],rsi
; 17byte가 nop으로 채워져있다        |
nop
nop
nop
.
.
nop
; MessageBoxTimeoutA의 15byte 이후 코드주소로 jmp구문 (14byte)
push 1B1FEB0Ah
mov [rsp+4], 00007FFEh
ret

MyMessageBoxAJMP를 call하면 자연스럽게 원래의 함수가 실행될 것이다.

다소 제약사항이 생기고 함수호출이 조금 번잡하지만 32bit Trampoline만 이해한다면 크게 달라진 부분은 없다.

injector_64 와 victim_64를 실행하여 테스트해보자.

다음은 좀더 유연한 Trampoline 방식을 간단히 알아보고 넘어가자. 이 방식은 후킹함수가 호출될때마다 원본 함수를 복구하고 다시 후킹하는 방식이기때문에 성능상으로는 떨어지게되지만 매번 함수시작부분 코드를 체크하지 않아도 되어 간편하다.

목차 이전글 어셈블리 튜토리얼 (16) 64비트 api hooking(IAT)