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

비밀번호

댓글 작성자
 




1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13843정성태12/13/20244381오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20244522디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20244855오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20244428오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20244856오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20244595오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20245051디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20244610디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20245055오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20245283Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
13833정성태11/29/20244965개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20244900Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/20244381Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20245181개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20245137스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20244439개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20245091Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20245320닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20245650Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20244742Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20245287개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20245150개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20245079Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20245234VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20244477Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20245260Windows: 272. Windows 11 24H2 - sudo 추가
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...