Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 17개 있습니다.)
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: 900. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 네 번째 이야기(Monitor.Enter 후킹)
; https://www.sysnet.pe.kr/2/0/12165

.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

비밀번호

댓글 작성자
 




... 166  167  168  169  170  171  172  173  174  175  [176]  177  178  179  180  ...
NoWriterDateCnt.TitleFile(s)
600정성태10/9/200832259디버깅 기술: 18. TFS Team Build + Source Server = 소스 코드 디버깅 [3]
603정성태10/15/200824164    답변글 디버깅 기술: 18.1. 소스 서버 구성, 그 외의 이야기
599정성태10/5/200830090디버깅 기술: 17. TFS Team Build + Symbol Server [1]
598정성태10/3/200820090VS.NET IDE: 57. VS.NET 2008 - 다중 프로젝트에서 단일 SNK를 사용하는 방법
597정성태10/2/200818778Team Foundation Server: 25. VSTS 2008의 Build Explorer
596정성태10/2/200825542오류 유형: 58. WPF : 드롭다운 유형의 ComboBox가 펼쳐지지 않는 문제
595정성태10/1/200833093디버깅 기술: 16. Watson Bucket 정보를 이용한 CLR 응용 프로그램 예외 분석 [2]
594정성태9/22/200821123.NET Framework: 104. Win32Exception 클래스 소개
591정성태7/24/200817883오류 유형: 57. VS.NET 2008 TFC - 체크인 시에 비프 음과 함께 정지되는 현상
592정성태7/28/200817882    답변글 오류 유형: 57.1. VS.NET 2008 TFC - 체크인 시에 비프 음과 함께 정지되는 현상 [1]
590정성태7/20/200823662.NET Framework: 103. WPF - ControlTemplate을 코드에서 다뤄보기 [1]
589정성태6/17/200820555.NET Framework: 102. COM 개체의 이벤트를 구독하는 코드 제작 [1]
588정성태6/13/200822415VC++: 35. COM 이벤트에서 반환값을 가진 콜백 정의
587정성태6/10/200827157VS.NET IDE: 56. C#에서 아쉬운 __DATE__, __TIME__ 매크로 [2]
586정성태6/4/200824789오류 유형: 56. WPF 디자이너 - The string was not recognized as a valid DateTime [2]
585정성태6/4/200832934.NET Framework: 101. WPF - ActiveX 컨트롤 호스팅하는 방법 [2]
582정성태5/16/200824783오류 유형: 55. Windowless ActiveX controls are not supported
580정성태4/24/200823881VC++: 34. 64비트 윈도우즈에서의 이벤트 후킹
579정성태4/24/200823684VC++: 33. 변환 후의 RGS 파일 내용을 얻는 방법
577정성태4/16/200824574.NET Framework: 100. XML Serializer를 이용한 값 복사 [5]
575정성태4/7/200821722오류 유형: 54. TFS Source Control - 명령을 사용할 수 없음 [2]
574정성태3/31/200820014오류 유형: 53. TFS 연결 오류 - The workspace [...] exists on computer [...]
573정성태3/25/200823746Windows: 31. TS Web Access와 UAC [1]
570정성태3/17/200823089오류 유형: 52. TFS 연결 오류 - TF31001 [2]
569정성태3/16/200824032Team Foundation Server: 24. TFS 2008로 마이그레이션 (2) [2]
566정성태2/28/200825183.NET Framework: 99. AppDomain.GetEntryAssembly()를 우회하는 방법파일 다운로드1
... 166  167  168  169  170  171  172  173  174  175  [176]  177  178  179  180  ...