Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 16개 있습니다.)
VC++: 36. Detours 라이브러리를 이용한 Win32 API - Sleep 호출 가로채기
; https://www.sysnet.pe.kr/2/0/631

.NET Framework: 187. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선
; https://www.sysnet.pe.kr/2/0/942

디버깅 기술: 40. 상황별 GetFunctionPointer 반환값 정리 - x86
; https://www.sysnet.pe.kr/2/0/1027

VC++: 56. Win32 API 후킹 - Trampoline API Hooking
; https://www.sysnet.pe.kr/2/0/1231

VC++: 57. 웹 브라우저에서 Flash만 빼고 다른 ActiveX를 차단할 수 있을까?
; https://www.sysnet.pe.kr/2/0/1232

VC++: 58. API Hooking - 64비트를 고려해야 한다면? EasyHook!
; https://www.sysnet.pe.kr/2/0/1242

개발 환경 구성: 419. MIT 라이선스로 무료 공개된 Detours API 후킹 라이브러리
; https://www.sysnet.pe.kr/2/0/11764

.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)
; https://www.sysnet.pe.kr/2/0/12132

.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64
; https://www.sysnet.pe.kr/2/0/12143

.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12144

디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법
; https://www.sysnet.pe.kr/2/0/12148

.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법
; https://www.sysnet.pe.kr/2/0/12150

.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)
; https://www.sysnet.pe.kr/2/0/12151

.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)
; https://www.sysnet.pe.kr/2/0/12152

.NET Framework: 898. Trampoline을 이용한 후킹의 한계
; https://www.sysnet.pe.kr/2/0/12153

.NET Framework: 968. C# 9.0의 Function pointer를 이용한 함수 주소 구하는 방법
; https://www.sysnet.pe.kr/2/0/12409




C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법

예전에, C#으로 Win32 API를 후킹하는 방법에 대해 설명한 적이 있습니다.

C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)
; https://www.sysnet.pe.kr/2/0/12132

위의 방법은 Win32 API의 Export 테이블에 있는 jmp 주솟값을 패치하는 간단한 절차였는데요, 이것을 지난 글에 설명한 trampoline 기법으로 대체해 보겠습니다.

x64 환경에서 구현하는 다양한 Trampoline 기법
; https://www.sysnet.pe.kr/2/0/12148




(다음번에 설명하겠지만) 일단 예제의 단순함을 위해 이번에는 (Sleep 대신) SleepEx를 가로채는 경우로 진행하겠습니다. 우선, 지난 글에서 EAT 테이블로부터 구한 jmp 코드가 위치한 주솟값은 사실 GetProcAddress로도 구할 수 있습니다. 따라서 아래의 코드를,

PEImage img = PEImage.FromLoadedModule("kernel32.dll");
foreach (var efi in img.EnumerateExportFunctions())
{
    if (efi.Name == "SleepEx")
    {
        IntPtr funcAddr = img.BaseAddress + (int)efi.RvaAddress;
    }
}

간단하게 다음의 코드로 대체할 수 있습니다.

IntPtr modulePtr = NativeMethods.LoadLibrary("kernel32.dll");
if (modulePtr != IntPtr.Zero)
{
    IntPtr funcAddr = NativeMethods.GetProcAddress(modulePtr, "SleepEx");
}

이렇게 구한 funcAddr(예를 들어, 7ffc49751fd0)를 disassemble시키면,

KERNEL32!SleepEx:
00007ffc`49751fd0 ff25226b0500    jmp     qword ptr [KERNEL32!_imp_SleepEx (00007ffc`497a8af8)]
...이후 10번의 0xcc... (16byte 정렬)
...
KERNEL32!_imp_SleepEx:
00007ffc`497a8af8 00007ffc49446890

SleepEx 함수로 갈 수 있는 주솟값(00007ffc49446890)을 "00007ffc497a8af8"로부터 구할 수 있고, 그 대상이 되는 곳은 SleepEx의 함수 본체가 위치합니다.

KERNELBASE!SleepEx:
00007ffc`49446890 89542410        mov     dword ptr [rsp+10h],edx
00007ffc`49446894 4c8bdc          mov     r11,rsp
00007ffc`49446897 53              push    rbx
00007ffc`49446898 56              push    rsi
00007ffc`49446899 57              push    rdi
...[생략]...

정리해 보면, trampoline 패치를 할 수 있는 후보군은 1) GetProcAddress가 가리킨 위치와 2) 그곳에서 jmp해 들어가는 대상 메서드의 본체가 됩니다.




여기서는, GetProcAddress가 아닌 jmp로 들어가는 대상 메서드의 본체에서 trampoline 처리를 해보겠습니다. 그러려면, jmp 대상이 되는 주소를 계산해야 하고 이것은 이미 C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) 글에서 다룬 적이 있습니다. DetourFunc 라이브러리에서 이를 정리해 GetExportFunctionAddress 메서드로 제공하니 SleepEx API의 Body 주소를 다음과 같이 구할 수 있습니다.

IntPtr sleepPtr = MethodReplacer.GetExportFunctionAddress("kernel32.dll", "SleepEx", out var _);

그다음, SleepEx를 대신해 호출될 C# 메서드도 정의하고 그것의 주소를 받아옵니다.

public delegate void SleepExDelegate(int milliseconds, bool bAlertable);

static void Main(string[] _)
{
    // ...[생략]...

    SleepExDelegate action = Replaced_TestMethod;
    MethodDesc mdReplaceMethod = MethodDesc.ReadFromMethodInfo(action.Method);
    IntPtr ptrBodyReplaceMethod = mdReplaceMethod.GetNativeFunctionPointer();
}

public static void Replaced_TestMethod(int milliseconds, bool bAlertable)
{
    Console.WriteLine("Replaced_TestMethod called!");
}

자... 그럼 재료가 준비되었군요 ^^ 이제 SleepEx body 코드에,

KERNELBASE!SleepEx:
00007ffc`49446890 89542410        mov     dword ptr [rsp+10h],edx
00007ffc`49446894 4c8bdc          mov     r11,rsp
00007ffc`49446897 53              push    rbx
00007ffc`49446898 56              push    rsi
00007ffc`49446899 57              push    rdi
00007ffc`4944689a 4881ec80000000  sub     rsp,80h
...[생략]...

최초의 12바이트 코드(89 54 24 10...로 시작하는 바이트)를 지난 글에 설명한 2번 방식으로 12바이트의 jmp 코드로,

byte[] GetJumpToCode(IntPtr valueAddress)
{
    // 48 B8 00 00 00 00 00 00 00 00 mov rax,0000000000000000h
    // FF E0                         jmp rax 
    byte[] _longJumpToBytes = new byte[]
    {
        0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0xFF, 0xE0
    };

    byte[] buf = BitConverter.GetBytes(valueAddress);
    Array.Copy(buf, 0, _longJumpToBytes, 2, IntPtr.Size);
    return _longJumpToBytes;
}

덮어쓰면 됩니다. 마찬가지로 그 코드 역시 C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) 글에서 만들어 두었으니 이를 재활용해서,

byte[] OverwriteCode(IntPtr codeAddress, byte[] code)
{
    byte[] oldCode = new byte[code.Length];

    // mov/jmp로 덮어써질 영역의 원본 바이트 코드를 보관
    for (int i = 0; i < code.Length; i++)
    {
        oldCode[i] = codeAddress.ReadByte(i);
    }

    ProcessAccessRights rights = ProcessAccessRights.PROCESS_VM_OPERATION | ProcessAccessRights.PROCESS_VM_READ | ProcessAccessRights.PROCESS_VM_WRITE;
    PageAccessRights dwOldProtect = PageAccessRights.NONE;
    IntPtr hHandle = IntPtr.Zero;

    try
    {
        int pid = Process.GetCurrentProcess().Id;

        hHandle = NativeMethods.OpenProcess(rights, false, pid);
        if (hHandle == IntPtr.Zero)
        {
            return null;
        }

        if (NativeMethods.VirtualProtectEx(hHandle, codeAddress, new UIntPtr((uint)IntPtr.Size), PageAccessRights.PAGE_EXECUTE_READWRITE, out dwOldProtect) == false)
        {
            return null;
        }

        codeAddress.WriteBytes(code);

        NativeMethods.FlushInstructionCache(hHandle, codeAddress, new UIntPtr((uint)code.Length));
        return oldCode;
    }
    finally
    {
        if (dwOldProtect != PageAccessRights.NONE)
        {
            NativeMethods.VirtualProtectEx(hHandle, codeAddress, new UIntPtr((uint)IntPtr.Size), dwOldProtect, out PageAccessRights _);
        }

        if (hHandle != IntPtr.Zero)
        {
            NativeMethods.CloseHandle(hHandle);
        }
    }
}

최종적으로 JumpPatch라는 메서드를 만들 수 있게 됩니다.

public sealed class TrampolinePatch<T> : IDisposable where T : Delegate
{
    byte[] _oldCode;
    IntPtr _funcAddress;

    public void JumpPatch(IntPtr codeAddress, IntPtr valueAddress)
    {
        byte[] code = GetJumpToCode(codeAddress, valueAddress);
        _oldCode = OverwriteCode(codeAddress, code);

        if (_oldCode.Length == code.Length)
        {
            _funcAddress = codeAddress;
        }
    }

    // ...[생략]...
}

이 모든 것을 종합한 DetourFunc 라이브러리를 NuGet에 올렸으니,

Install-Package DetourFunc -Version 1.0.8

// 소스 코드: github - https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc

다음과 같이 간단하게 사용할 수 있습니다.

using (var item = new TrampolinePatch<SleepExDelegate>())
{
    item.JumpPatch(sleepPtr, ptrBodyReplaceMethod);

    // 가로채기가 되었으므로 Sleep 호출이 되지 않고 화면에 "Replaced_TestMethod called!" 문자열 출력
    SleepEx(3000, false); 

} // Dispose 시점에 예전 코드로 복원

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




물론, 가장 좋은 방법은 지난 글에서 설명한 KERNEL32!_imp_SleepEx의 점프 위치를 수정하는 것이 좋습니다. 왜냐하면 8바이트로 정렬된 위치이면서 CPU 워드의 크기이기 때문에 race condition 등의 문제가 없어 보다 더 안전하기 때문입니다.

참고로, 위의 코드는 상업용 수준의 안정성을 갖추진 않았는데 이와 관련해서는 다음의 글을 읽어보실 것을 권합니다. ^^

전략 서신: API 후킹을 할 때에 생각해야 할 것들…
; http://www.jiniya.net/wp/archives/11276

그러니까, TrampolinePatch 코드는 실습 차원에서 만든 것일 뿐 높은 안정성이 요구되는 현업 프로그램에서 사용하는 것은 권장하지 않습니다.




마지막으로, 그냥 올리는 것으로... 아래는 SleepEx의 코드 전문입니다.

KERNELBASE!SleepEx:
00007ffd`2d056890 89542410        mov     dword ptr [rsp+10h],edx
00007ffd`2d056894 4c8bdc          mov     r11,rsp
00007ffd`2d056897 53              push    rbx
00007ffd`2d056898 56              push    rsi
00007ffd`2d056899 57              push    rdi
00007ffd`2d05689a 4881ec80000000  sub     rsp,80h
00007ffd`2d0568a1 8bda            mov     ebx,edx
00007ffd`2d0568a3 8bf9            mov     edi,ecx
00007ffd`2d0568a5 49c7439848000000 mov     qword ptr [r11-68h],48h
00007ffd`2d0568ad c744243801000000 mov     dword ptr [rsp+38h],1
00007ffd`2d0568b5 33c0            xor     eax,eax
00007ffd`2d0568b7 498943a8        mov     qword ptr [r11-58h],rax
00007ffd`2d0568bb 498943b0        mov     qword ptr [r11-50h],rax
00007ffd`2d0568bf 498943b8        mov     qword ptr [r11-48h],rax
00007ffd`2d0568c3 498943c0        mov     qword ptr [r11-40h],rax
00007ffd`2d0568c7 498943c8        mov     qword ptr [r11-38h],rax
00007ffd`2d0568cb 498943d0        mov     qword ptr [r11-30h],rax
00007ffd`2d0568cf 498943d8        mov     qword ptr [r11-28h],rax
00007ffd`2d0568d3 498d4b20        lea     rcx,[r11+20h]
00007ffd`2d0568d7 48ff150a0d1600  call    qword ptr [KERNELBASE!_imp_RtlGetCurrentUmsThread (00007ffd`2d1b75e8)]
00007ffd`2d0568de 0f1f440000      nop     dword ptr [rax+rax]
00007ffd`2d0568e3 898424b0000000  mov     dword ptr [rsp+0B0h],eax
00007ffd`2d0568ea 85c0            test    eax,eax
00007ffd`2d0568ec 0f89a6ec0500    jns     KERNELBASE!SleepEx+0x5ed08 (00007ffd`2d0b5598)
00007ffd`2d0568f2 33f6            xor     esi,esi
00007ffd`2d0568f4 488b9424b8000000 mov     rdx,qword ptr [rsp+0B8h]
00007ffd`2d0568fc 85db            test    ebx,ebx
00007ffd`2d0568fe 0f8592000000    jne     KERNELBASE!SleepEx+0x106 (00007ffd`2d056996)
00007ffd`2d056904 83ffff          cmp     edi,0FFFFFFFFh
00007ffd`2d056907 7443            je      KERNELBASE!SleepEx+0xbc (00007ffd`2d05694c)
00007ffd`2d056909 4869cf10270000  imul    rcx,rdi,2710h
00007ffd`2d056910 48894c2420      mov     qword ptr [rsp+20h],rcx
00007ffd`2d056915 48f7d9          neg     rcx
00007ffd`2d056918 48894c2420      mov     qword ptr [rsp+20h],rcx
00007ffd`2d05691d 4885d2          test    rdx,rdx
00007ffd`2d056920 7538            jne     KERNELBASE!SleepEx+0xca (00007ffd`2d05695a)
00007ffd`2d056922 488d542420      lea     rdx,[rsp+20h]
00007ffd`2d056927 0fb6cb          movzx   ecx,bl
00007ffd`2d05692a 48ff155f0b1600  call    qword ptr [KERNELBASE!_imp_NtDelayExecution (00007ffd`2d1b7490)]
00007ffd`2d056931 0f1f440000      nop     dword ptr [rax+rax]
00007ffd`2d056936 8bf8            mov     edi,eax
00007ffd`2d056938 898424b0000000  mov     dword ptr [rsp+0B0h],eax
00007ffd`2d05693f 85db            test    ebx,ebx
00007ffd`2d056941 7428            je      KERNELBASE!SleepEx+0xdb (00007ffd`2d05696b)
00007ffd`2d056943 3d01010000      cmp     eax,101h
00007ffd`2d056948 7521            jne     KERNELBASE!SleepEx+0xdb (00007ffd`2d05696b)
00007ffd`2d05694a ebd6            jmp     KERNELBASE!SleepEx+0x92 (00007ffd`2d056922)
00007ffd`2d05694c 89742420        mov     dword ptr [rsp+20h],esi
00007ffd`2d056950 c744242400000080 mov     dword ptr [rsp+24h],80000000h
00007ffd`2d056958 ebc3            jmp     KERNELBASE!SleepEx+0x8d (00007ffd`2d05691d)
00007ffd`2d05695a 8b82f0040000    mov     eax,dword ptr [rdx+4F0h]
00007ffd`2d056960 83c840          or      eax,40h
00007ffd`2d056963 8982f0040000    mov     dword ptr [rdx+4F0h],eax
00007ffd`2d056969 ebb7            jmp     KERNELBASE!SleepEx+0x92 (00007ffd`2d056922)
00007ffd`2d05696b 488b8c24b8000000 mov     rcx,qword ptr [rsp+0B8h]
00007ffd`2d056973 4885c9          test    rcx,rcx
00007ffd`2d056976 0f854bec0500    jne     KERNELBASE!SleepEx+0x5ed37 (00007ffd`2d0b55c7)
00007ffd`2d05697c 85db            test    ebx,ebx
00007ffd`2d05697e 7536            jne     KERNELBASE!SleepEx+0x126 (00007ffd`2d0569b6)
00007ffd`2d056980 b8c0000000      mov     eax,0C0h
00007ffd`2d056985 3bf8            cmp     edi,eax
00007ffd`2d056987 7402            je      KERNELBASE!SleepEx+0xfb (00007ffd`2d05698b)
00007ffd`2d056989 8bc6            mov     eax,esi
00007ffd`2d05698b 4881c480000000  add     rsp,80h
00007ffd`2d056992 5f              pop     rdi
00007ffd`2d056993 5e              pop     rsi
00007ffd`2d056994 5b              pop     rbx
00007ffd`2d056995 c3              ret
00007ffd`2d056996 33d2            xor     edx,edx
00007ffd`2d056998 488d4c2430      lea     rcx,[rsp+30h]
00007ffd`2d05699d 48ff15ec091600  call    qword ptr [KERNELBASE!_imp_RtlActivateActivationContextUnsafeFast (00007ffd`2d1b7390)]
00007ffd`2d0569a4 0f1f440000      nop     dword ptr [rax+rax]
00007ffd`2d0569a9 488b9424b8000000 mov     rdx,qword ptr [rsp+0B8h]
00007ffd`2d0569b1 e94effffff      jmp     KERNELBASE!SleepEx+0x74 (00007ffd`2d056904)
00007ffd`2d0569b6 488d4c2430      lea     rcx,[rsp+30h]
00007ffd`2d0569bb 48ff15d6091600  call    qword ptr [KERNELBASE!_imp_RtlDeactivateActivationContextUnsafeFast (00007ffd`2d1b7398)]
00007ffd`2d0569c2 0f1f440000      nop     dword ptr [rax+rax]
00007ffd`2d0569c7 ebb7            jmp     KERNELBASE!SleepEx+0xf0 (00007ffd`2d056980)
00007ffd`2d0569c9 cc              int     3
00007ffd`2d0569ca cc              int     3
00007ffd`2d0569cb cc              int     3
00007ffd`2d0569cc cc              int     3
00007ffd`2d0569cd cc              int     3
00007ffd`2d0569ce cc              int     3
00007ffd`2d0569cf cc              int     3




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 2/24/2020]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2020-08-25 01시13분
Massaging your CLR: Preventing Environment.Exit in In-Process .NET Assemblies
; https://www.mdsec.co.uk/2020/08/massaging-your-clr-preventing-environment-exit-in-in-process-net-assemblies/
정성태

... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13071정성태6/9/20227246스크립트: 39. Python에서 cx_Oracle 환경 구성
13070정성태6/8/20227069오류 유형: 813. Windows 11에서 입력 포커스가 바뀌는 문제 [1]
13069정성태5/26/20229295.NET Framework: 2019. C# - .NET에서 제공하는 3가지 Timer 비교 [2]
13068정성태5/24/20227749.NET Framework: 2018. C# - 일정 크기를 할당하는 동안 GC를 (가능한) 멈추는 방법 [1]파일 다운로드1
13067정성태5/23/20227117Windows: 206. Outlook - 1년 이상 지난 메일이 기본적으로 안 보이는 문제
13066정성태5/23/20226414Windows: 205. Windows 11 - Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
13065정성태5/20/20227084.NET Framework: 2017. C# - Windows I/O Ring 소개 [2]파일 다운로드1
13064정성태5/18/20226650.NET Framework: 2016. C# - JIT 컴파일러의 인라인 메서드 처리 유무
13063정성태5/18/20227070.NET Framework: 2015. C# - 인라인 메서드(inline methods)
13062정성태5/17/20227861.NET Framework: 2014. C# - async/await 그리고 스레드 (4) 비동기 I/O 재현파일 다운로드1
13061정성태5/16/20226672.NET Framework: 2013. C# - FILE_FLAG_OVERLAPPED가 적용된 파일의 읽기/쓰기 시 Position 관리파일 다운로드1
13060정성태5/15/20229115.NET Framework: 2012. C# - async/await 그리고 스레드 (3) Task.Delay 재현파일 다운로드1
13059정성태5/14/20227616.NET Framework: 2011. C# - CLR ThreadPool의 I/O 스레드에 작업을 맡기는 방법 [1]파일 다운로드1
13058정성태5/13/20227548.NET Framework: 2010. C# - ThreadPool.SetMaxThreads 사용법
13057정성태5/12/20229221오류 유형: 812. 파이썬 - ImportError: cannot import name ...
13056정성태5/12/20226388.NET Framework: 2009. C# - async/await 그리고 스레드 (2) MyTask의 호출 흐름 [2]파일 다운로드1
13055정성태5/11/20229281.NET Framework: 2008. C# - async/await 그리고 스레드 (1) MyTask로 재현 [11]파일 다운로드1
13054정성태5/11/20226802.NET Framework: 2007. C# - 10진수 숫자를 담은 문자열을 숫자로 변환하는 방법 [11]파일 다운로드1
13053정성태5/10/20226436.NET Framework: 2006. C# - GC.KeepAlive 메서드의 역할
13052정성태5/9/20226445.NET Framework: 2005. C# - 생성한 참조 개체가 언제 GC의 정리 대상이 될까요?
13051정성태5/8/20226385.NET Framework: 2004. C# XingAPI - ACF 검색 결과로 구한 CSV 파일을 통해 퀀트 종목 찾기파일 다운로드1
13050정성태5/6/20226400.NET Framework: 2003. C# - COM 개체의 이벤트 핸들러에서 발생하는 예외에 대한 CLR의 특별 대우파일 다운로드1
13049정성태5/6/20225374오류 유형: 811. GoLand - Error: Cannot find package
13048정성태5/6/20226500오류 유형: 810. "ASUS TUF GAMING B550M-PLUS (WI-FI)" 모델에서 블루투스 장치가 인식이 안 되는 문제
13047정성태5/6/20226485오류 유형: 809. Speech Recognition could not start
13046정성태5/5/20226777.NET Framework: 2002. C# XingAPI - ACF 파일을 이용한 퀀트 종목 찾기(t1857)
... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...