Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 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




실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)

지난 글에서,

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

Fixup Precode가 일단 call에서 jmp로 바뀐 이후에는 메서드 가로채기를 할 수 없었던 것을 다뤘습니다. 당연히 이 제약 사항은 Trampoline 방식을 이용한 후킹이라면 극복할 수 있습니다.




IL 코드를 trampoline으로 후킹하려면 우선 어떤 지점에서 JMP 패치를 할 것인지를 결정해야 합니다. 이를 위해 JIT 컴파일 전/후를 살펴보면,

Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12133

[그림 출처: https://www.cnblogs.com/zkweb/p/7746222.html]
jit_before_after.jpg

"Before JIT"와 "After JIT"에서도 변하지 않는 "Fixup Precode"와 "Native Code"의 위치를 후보로 선정할 수 있습니다. 그럼 둘 중에서도 어느 곳이 좋을까요? 우선 "Fixup Preocde"는 JIT 이전과 첫 호출까지는 call 호출로 동작을 하다가 두 번째 호출 이후부터는 "jmp"로 바뀝니다. 즉, 안전하게 trampoline 패치를 하려면 두 번째 호출이 될 때까지 기다려야 한다는 성가신 제약이 있습니다. 따라서 적절한 trampoline 패치 위치는 "Native Code" 영역이 더 낫습니다.




"Native Code" 주소를 구하는 가장 안전한 방법은 GetFunctionPointer입니다.

RuntimeMethodHandle.GetFunctionPointer Method
; https://learn.microsoft.com/en-us/dotnet/api/system.runtimemethodhandle.getfunctionpointer?view=netframework-4.8

그리고 이에 대한 것도 이미 다음의 글에서 다룬 적이 있습니다.

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

따라서, 일반적인 닷넷 메서드에 대해 JIT 이후 GetFunctionPointer를 호출하는 경우 다음과 같이 2가지 경우로 나뉘는 결과를 얻게 됩니다.

  1. Visual Studio + F5 디버깅: Fixup Precode 위치 반환
  2. 그 외의 경우: Native Code 위치 반환

여기서 Fixup Precode는 jmp 코드로 구현되는데, (다음 기회에 설명하겠지만) 아쉽게도 아직 우리가 만든 TrampolinePatch 타입은 jmp 코드를 가진 경우 "가로채기"는 지원하지만 그것의 원본 메서드에 대한 호출을 지원하지 못합니다. 따라서 현재 단계에서는 2번의 경우로 집중할 텐데 이를 위해 마찬가지로 SharpDisasm를 이용해 GetFunctionPointer로 반환한 곳의 코드가 jmp인 경우 그 대상의 주소를 한 번 더 구하도록 다음과 같은 식의 메서드를 마련했습니다.

// https://github.com/stjeong/DotNetSamples/blob/master/WinConsole/PEFormat/DetourFunc/ClrType/MethodDesc.cs
public IntPtr GetNativeFunctionPointer()
{
    if (HasStableEntryPoint() == false)
    {
        return IntPtr.Zero;
    }

    IntPtr ptrEntry = GetFunctionPointer();
    if (ptrEntry == IntPtr.Zero)
    {
        return IntPtr.Zero;
    }

    SharpDisasm.ArchitectureMode mode = (IntPtr.Size == 8) ? SharpDisasm.ArchitectureMode.x86_64 : SharpDisasm.ArchitectureMode.x86_32;
    SharpDisasm.Disassembler.Translator.IncludeAddress = false;
    SharpDisasm.Disassembler.Translator.IncludeBinary = false;

    {
        byte[] buf = ptrEntry.ReadBytes(NativeMethods.MaxLengthOpCode);
        var disasm = new SharpDisasm.Disassembler(buf, mode, (ulong)ptrEntry.ToInt64());

        Instruction inst = disasm.Disassemble().First();
        if (inst.Mnemonic == SharpDisasm.Udis86.ud_mnemonic_code.UD_Ijmp)
        {
            // Visual Studio + F5 Debug = Always point to "Fixup Precode"
            long address = (long)inst.PC + inst.Operands[0].Value;
            return new IntPtr(address);
        }
        else
        {
            return ptrEntry;
        }
    }
}

그럼 이걸로 준비가 끝났군요. ^^ 이제 위의 모든 것들이 반영된 DetourFunc 라이브러리를 참조 추가하고,

Install-Package DetourFunc -Version 1.0.9

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

다음과 같이 지난 예제를 바꿀 수 있습니다.

using DetourFunc;
using DetourFunc.Clr;
using System;

namespace ConsoleApp1
{
    public delegate void TestMethodDelegate();

    class Program
    {
        static TestMethodDelegate s_originalMethod;

        static void Main(string[] _)
        {
            for (int i = 0; i < 10; i ++)
            {
                TestMethod();
            }

            IntPtr ptrBodyTestMethod;

            {
                TestMethodDelegate action = TestMethod;
                MethodDesc mdTestMethod = MethodDesc.ReadFromMethodInfo(action.Method);
                ptrBodyTestMethod = mdTestMethod.GetNativeFunctionPointer();
            }

            IntPtr ptrBodyReplaceMethod;

            {
                TestMethodDelegate action2 = Replaced_TestMethod;
                MethodDesc mdReplaceMethod = MethodDesc.ReadFromMethodInfo(action2.Method);
                ptrBodyReplaceMethod = mdReplaceMethod.GetNativeFunctionPointer();
            }

            Console.WriteLine($"Address to be patched: {ptrBodyTestMethod.ToInt64():x}");
            Console.WriteLine($"With this address: {ptrBodyReplaceMethod.ToInt64():x}");
            Console.WriteLine();

            using (var item = new TrampolinePatch<TestMethodDelegate>())
            {
                if (item.JumpPatch(ptrBodyTestMethod, ptrBodyReplaceMethod) == true)
                {
                    s_originalMethod = item.GetOriginalFunc();
                }

                Console.WriteLine("[After trampoline]");
                TestMethod();
            }

            Console.WriteLine();
            Console.WriteLine("[Revert to original method]");
            TestMethod();

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }

        public static void TestMethod()
        {
            Console.WriteLine("TestMethod called!");
        }

        public static void Replaced_TestMethod()
        {
            Console.WriteLine("Replaced_TestMethod called!");
            s_originalMethod?.Invoke();
        }
    }
}

/* 출력 결과
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
TestMethod called!
Address to be patched: 13b0b80
With this address: 60760e0

[After trampoline]
Replaced_TestMethod called!
TestMethod called!

[Revert to original method]
TestMethod called!
Press any key to exit...
*/

보는 바와 같이 "실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기" 글에서 설명한 제약 사항을 trampoline으로는 해결할 수 있었다는 것 외에도, "원본 메서드"까지도 호출할 수 있다는 장점이 있습니다.

또한 "C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)" 글과 비교해서는, Win32 메서드의 경우 닷넷 메서드로 우회할 때는 x86인 경우 호출 규약(Calling Convention)의 문제로 인해 원본 메서드를 호출할 수 없었지만, "닷넷 메서드"를 "닷넷 메서드"로 우회하는 경우에는 x86/x64 모두 호출 규약이 일치하므로 플랫폼에 따른 제약도 없습니다.

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




이쯤에서 MOV/JMP의 조합으로 처리하던 JumpPatch 메서드를 "JMP rel32"로 개선할 가치가 있게 됩니다. JIT 컴파일러의 특성상 런타임에 생성된 "Native Code"들이 +/- 2GB 주소 범위 내에 있을 확률이 높기 때문에 그런 경우라면 12바이트의 MOV/JMP 대신 5바이트의 JMP로 처리할 수 있는 경우가 많을 것이기 때문입니다.

코드 변환은 TrampolinePatch 클래스의 GetJumpToCode 메서드만 다음과 같은 정도로 개선하는 수준에서 끝납니다.

// https://github.com/stjeong/DotNetSamples/blob/master/WinConsole/PEFormat/DetourFunc/Trampoline/TrampolinePatch.cs
byte[] GetJumpToCode(IntPtr fromAddress, int prologueLengthOnFromAddress, IntPtr toAddress)
{
    long offset = toAddress.ToInt64() - fromAddress.ToInt64();

    if (IntPtr.Size == 8)
    {
        if (Math.Abs(offset) > (Int32.MaxValue - NativeMethods.MaxLengthOpCode * 10))
        {
            byte[] longJumpToBytes = _longJumpTemplate.ToArray();
            byte[] buf8 = BitConverter.GetBytes(toAddress.ToInt64());
            Array.Copy(buf8, 0, longJumpToBytes, 2, IntPtr.Size);
            return longJumpToBytes;
        }
    }

    byte[] shortJumpToBytes = _shortJumpTemplate.ToArray();
    byte[] buf4 = BitConverter.GetBytes(offset - (prologueLengthOnFromAddress + shortJumpToBytes.Length));
    Array.Copy(buf4, 0, shortJumpToBytes, 1, 4);
    return shortJumpToBytes;
}




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/9/2024]

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

비밀번호

댓글 작성자
 




... 61  [62]  63  64  65  66  67  68  69  70  71  72  73  74  75  ...
NoWriterDateCnt.TitleFile(s)
12097정성태1/2/20209822디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태12/30/201911778디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작 [1]
12095정성태12/27/201913244VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/201911404.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/201911468.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/201910918디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/201912414디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/201911125.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점 [1]파일 다운로드1
12089정성태12/23/201911709디버깅 기술: 146. gflags와 _CrtIsMemoryBlock을 이용한 Heap 메모리 손상 여부 체크
12088정성태12/23/201910659Linux: 28. Linux - 윈도우의 "Run as different user" 기능을 shell에서 실행하는 방법
12087정성태12/21/201911131디버깅 기술: 145. windbg/sos - Dictionary의 entries 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
12086정성태12/20/201913159디버깅 기술: 144. windbg - Marshal.FreeHGlobal에서 발생한 덤프 분석 사례
12085정성태12/20/201910917오류 유형: 586. iisreset - The data is invalid. (2147942413, 8007000d) 오류 발생 - 두 번째 이야기 [1]
12084정성태12/19/201911521디버깅 기술: 143. windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py) [1]
12083정성태12/17/201912783Linux: 27. linux - lldb를 이용한 .NET Core 응용 프로그램의 메모리 덤프 분석 방법 [2]
12082정성태12/17/201912615오류 유형: 585. lsof: WARNING: can't stat() fuse.gvfsd-fuse file system
12081정성태12/16/201914395개발 환경 구성: 465. 로컬 PC에서 개발 중인 ASP.NET Core 웹 응용 프로그램을 다른 PC에서도 접근하는 방법 [5]
12080정성태12/16/201912266.NET Framework: 870. C# - 프로세스의 모든 핸들을 열람
12079정성태12/13/201913570오류 유형: 584. 원격 데스크톱(rdp) 환경에서 다중 또는 고용량 파일 복사 시 "Unspecified error" 오류 발생
12078정성태12/13/201913460Linux: 26. .NET Core 응용 프로그램을 위한 메모리 덤프 방법 [3]
12077정성태12/13/201913056Linux: 25. 자주 실행할 명령어 또는 초기 환경을 "~/.bashrc" 파일에 등록
12076정성태12/12/201911228디버깅 기술: 142. Linux - lldb 환경에서 sos 확장 명령어를 이용한 닷넷 프로세스 디버깅 - 배포 방법에 따른 차이
12075정성태12/11/201912079디버깅 기술: 141. Linux - lldb 환경에서 sos 확장 명령어를 이용한 닷넷 프로세스 디버깅
12074정성태12/10/201911684디버깅 기술: 140. windbg/Visual Studio - 값이 변경된 경우를 위한 정지점(BP) 설정(Data Breakpoint)
12073정성태12/10/201913496Linux: 24. Linux/C# - 실행 파일이 아닌 스크립트 형식의 명령어를 Process.Start로 실행하는 방법
12072정성태12/9/201910926오류 유형: 583. iisreset 수행 시 "No such interface supported" 오류
... 61  [62]  63  64  65  66  67  68  69  70  71  72  73  74  75  ...