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

비밀번호

댓글 작성자
 




1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13558정성태2/18/20242151닷넷: 2217. C# - 최댓값이 1인 SemaphoreSlim 보다 Mutex 또는 lock(obj)를 선택하는 것이 나은 이유
13557정성태2/18/20241903Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241948Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20242102닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241850VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241934닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241883닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242079닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/20242164Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/20242494개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/20242321개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/20242069개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/20241924Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20241845닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20241856오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20241852Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20241886오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20241932VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20242060Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20242305개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20242394닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/20242458닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/20242565닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242403닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242238닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242450닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...