Microsoft MVP성태의 닷넷 이야기
닷넷: 2330. C# - 실행 시에 메서드 가로채기 (.NET 5 ~ .NET 8) [링크 복사], [링크+제목 복사],
조회: 1015
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일

(시리즈 글이 3개 있습니다.)
닷넷: 2329. C# - 실행 시에 메서드 가로채기 (.NET Framework 4.8)
; https://www.sysnet.pe.kr/2/0/13909

닷넷: 2330. C# - 실행 시에 메서드 가로채기 (.NET 5 ~ .NET 8)
; https://www.sysnet.pe.kr/2/0/13912

닷넷: 2331. C# - 실행 시에 메서드 가로채기 (.NET 9)
; https://www.sysnet.pe.kr/2/0/13915




C# - 실행 시에 메서드 가로채기 (.NET 5 ~ .NET 8)

(이번 글의 코드는 .NET 5, 6, 7, 8에서만 테스트되었습니다.)




지난 글에서 .NET Framework 환경에 대해 글을 썼는데요,

C# - 실행 시에 메서드 가로채기 (.NET Framework 4.8)
; https://www.sysnet.pe.kr/2/0/13909

이번에는 (테스트하기 귀찮으므로 .NET Core 1.x ~ 3.x는 제외하고) .NET 8로 한정해서 설명해 보겠습니다. (미리 언급하자면, .NET 9에서는 이 코드가 동작하지 않습니다.)

일단, 시작으로 지난 글의 소스 코드를 그대로 .NET 8 프로젝트로 가져와 실행해 보면 가로채기가 전혀 안 되는 것을 볼 수 있습니다. .NET Framework의 경우에는 메서드 호출을 "call [...]"로 JIT 컴파일 전/후 모두 간접 주소를 이용했는데요, .NET Core의 경우에는 "call ..." 또는 "jmp ..."로 offset 형식의 호출로 변경되면서 방식이 달라졌기 때문입니다. 게다가, MethodDesc에 보관했던 Jitted Address 값을 매번 사용하는 방식이 아닌, 최초 한 번만 그 값을 가져다가 재사용하므로 이후 호출에서는 MethodDesc로부터 구하지 않습니다. 이로 인해 가로채기가 되려면 반드시 "JIT 컴파일 전"에 이뤄져야 합니다.

따라서, 지난 글의 소스 코드에서 oldMethod 인자의 PrepareMethod 호출을 제거하고,

using System;
using System.Reflection;
using System.Runtime.CompilerServices;

public class ReplaceMethod
{
    public unsafe static void Replace(MethodBase oldMethod, MethodInfo newMethod)
    {
        // RuntimeHelpers.PrepareMethod(oldMethod.MethodHandle); // 사실 이 코드는 .NET Framework에서도 필요 없는 작업이었습니다.
        RuntimeHelpers.PrepareMethod(newMethod.MethodHandle);

        IntPtr oldMethodPos = IntPtr.Zero;

        if (oldMethod.IsConstructor && oldMethod.GetParameters().Length == 0)
        {
            oldMethodPos = FromMethodTable(oldMethod);
        }
        else
        {
            oldMethodPos = FromMethodDesc(oldMethod);
        }

        if (oldMethodPos == IntPtr.Zero)
        {
            throw new InvalidOperationException("Failed to get method position.");
        }

        IntPtr newFuncAddr = newMethod.MethodHandle.GetFunctionPointer();
        
        if (IntPtr.Size == 8)
        {
            ulong* ptr = (ulong*)oldMethodPos;
            *ptr = (ulong)newFuncAddr.ToInt64();
        }
        else
        {
            uint* ptr = (uint*)oldMethodPos;
            *ptr = (uint)newFuncAddr.ToInt32();
        }
    }

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

해당 메서드를 사용하기 전에 가로채기를 완료하면 됩니다.

// 가로채기를 먼저 한 다음,
{
    MethodInfo instanceMethod = typeof(TestClass).GetMethod("TestMethod");
    MethodInfo newInstanceMethod = typeof(Program).GetMethod("MyMethod");
    ReplaceMethod.Replace(instanceMethod, newInstanceMethod);
}

{
    MethodBase ctorMethod = typeof(TestClass).GetConstructor(new Type[] { });
    MethodInfo newCtorMethod = typeof(Program).GetMethod("MyCtor");
    ReplaceMethod.Replace(ctorMethod, newCtorMethod);
}

// 메서드를 사용
TestClass testClass = new TestClass();
testClass.TestMethod();}

만약, 저 순서를 바꾸면 가로채기가 안 됩니다.




하지만, 저렇게 했는데도 여전히 "매개변수 없는 생성자"의 호출이 가로채기가 안 되는 것을 볼 수 있습니다. 재미있게도, .NET 5 ~ 8의 경우 (.NET Framework과는 달리) "매개변수 없는 생성자"도 MethodDesc의 구조체에 함수 포인터를 보관하는 방식으로 사용하기 때문입니다.

따라서, 그 부분의 소스 코드도 다음과 같이 변경해 주면 됩니다.

public unsafe static void Replace(MethodBase oldMethod, MethodInfo newMethod)
{
    RuntimeHelpers.PrepareMethod(newMethod.MethodHandle);

    IntPtr oldMethodPos = IntPtr.Zero;

#if !NET5_0_OR_GREATER
    if (oldMethod.IsConstructor && oldMethod.GetParameters().Length == 0)
    {
        oldMethodPos = FromMethodTable(oldMethod);
    }
    else
#endif
    {
        oldMethodPos = FromMethodDesc(oldMethod);
    }

    if (oldMethodPos == IntPtr.Zero)
    {
        throw new InvalidOperationException("Failed to get method position.");
    }

    IntPtr newFuncAddr = newMethod.MethodHandle.GetFunctionPointer();
        
    if (IntPtr.Size == 8)
    {
        ulong* ptr = (ulong*)oldMethodPos;
        *ptr = (ulong)newFuncAddr.ToInt64();
    }
    else
    {
        uint* ptr = (uint*)oldMethodPos;
        *ptr = (uint)newFuncAddr.ToInt32();
    }
}

이제 다시 (x86/x64 상관없이) 실행해 보면,

using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

internal class Program
{
    static void Main(string[] args)
    {
        // 아래의 코드를 주석 해제하면, 가로채기가 안 됩니다.
        //{
        //    TestClass testClass = new TestClass();
        //    testClass.TestMethod();
        //}

        {
            MethodInfo? instanceMethod = typeof(TestClass).GetMethod("TestMethod");
            MethodInfo? newInstanceMethod = typeof(Program).GetMethod("MyMethod");

            ReplaceMethod.Replace(instanceMethod, newInstanceMethod);
        }

        {
            MethodBase? ctorMethod = typeof(TestClass).GetConstructor(new Type[] { });
            MethodInfo? newCtorMethod = typeof(Program).GetMethod("MyCtor");

            ReplaceMethod.Replace(ctorMethod, newCtorMethod);
        }

        {
            TestClass testClass = new TestClass();
            testClass.TestMethod();
        }
    }

    public void MyMethod()
    {
        Console.WriteLine("MyMethod called! (replaced)");
    }

    public void MyCtor()
    {
        Console.WriteLine("MyCtor called! (replaced)");
    }
}

public class TestClass
{
    string _name = "TestClass";

    public TestClass()
    {
        Console.WriteLine("TestClass constructor");
    }

    public TestClass(string name)
    {
        Console.WriteLine("TestClass constructor2");
        _name = name;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void TestMethod()
    {
        Console.WriteLine("Instance TestMethod");
    }

    public static void TestMethod2()
    {
        Console.WriteLine("Static TestMethod2");
    }
}

화면에는, 정상적으로 가로채기가 된 결과가 나옵니다.

MyCtor called! (replaced)
MyMethod called! (replaced)

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




만약, 한 번이라도 실행된 적이 있는 메서드에 대해 가로채기를 하고 싶다면 2가지 정도의 방법을 고려할 수 있습니다.

첫 번째로, 호출이 되는 "jmp ..." 명령어의 주소를 읽어 가로챌 대상 메서드의 주소로 offset 값을 계산하여 덮어쓰는 방법입니다. 물론, (원하는) 모든 호출 측 코드를 가로채야 하는 한다면 이는 매우 귀찮은 방법입니다.

두 번째로, "jmp ..." 대상 위치가 되는 Jitted Address의 초입 부분을 trampoline 기법으로 덮어쓰는 방법입니다. (이 과정에서 SharpDisasm을 쓰면 편리합니다.)

방법이야 그렇지만, 2가지 모두 이번 글의 방식에 비해서는 매우 귀찮은 작업임에는 틀림없습니다.




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







[최초 등록일: ]
[최종 수정일: 4/9/2025]

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

비밀번호

댓글 작성자
 




... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...
NoWriterDateCnt.TitleFile(s)
396정성태2/13/200730013오류 유형: 18. "Automatic Updates" 서비스 CPU 100% 점유 현상
393정성태11/8/200619546오류 유형: 17. Unable to start debugging - The binding handle is invalid.
371정성태10/23/200618717오류 유형: 16. STS Communication failed.
370정성태11/12/200622608.NET Framework: 75. Windows CardSpace 이야기 (이 글의 내용은 재작성되어질 예정입니다.)
375정성태10/25/200624611    답변글 .NET Framework: 75.1. 개인 발행 카드에 대한 Microsoft 예제 실습(이 글의 내용은 재작성되어질 예정입니다.)
376정성태10/27/200624203    답변글 .NET Framework: 75.2. "Windows CardSpace"와 "인증서 서비스"의 만남(이 글의 내용은 재작성되어질 예정입니다.)
377정성태10/26/200623853    답변글 .NET Framework: 75.3. Managed Card 발행에 대한 Microsoft 예제 실습 (1) - CardWriter (이 글의 내용은 재작성되어질 예정입니다.)
385정성태11/6/200626482    답변글 .NET Framework: 75.4. Managed Card 발행에 대한 Microsoft 예제 실습 (2) - STS 구현 (이 글의 내용은 재작성되어질 예정입니다.) [7]
387정성태11/2/200627261    답변글 .NET Framework: 75.5. Windows CardSpace와 SYSNET 사이트의 만남 (이 글의 내용은 재작성되어질 예정입니다.) [1]
397정성태11/11/200624771    답변글 .NET Framework: 75.6. CardWriter.csproj와 함께 알아보는 인증서 식별 방법(이 글의 내용은 재작성되어질 예정입니다.)
398정성태11/12/200623294    답변글 .NET Framework: 75.7. 카드에 암호 거는 방법(이 글의 내용은 재작성되어질 예정입니다.)
399정성태11/12/200625549    답변글 .NET Framework: 75.8. 인증서/스마트 카드에 기반한 Managed Card - STS 구현(이 글의 내용은 재작성되어질 예정입니다.) [5]
369정성태10/22/200621046오류 유형: 15. 자동 업데이트 실패
367정성태10/22/200636825Windows: 3. IIS 7.0 다중 바인딩 설정하는 방법 [1]
365정성태10/21/200620524Windows: 2. 서버(build 5600)에 IIS 7.0 서비스와 .NET 3.0 설치 방법
359정성태10/17/200616675오류 유형: 14. VS.NET 빌드 오류 - FxCopCmd.exe returned error code 65.
358정성태10/17/200621876오류 유형: 13. WSE 3.0 서비스 관련 WSE101 오류 / Destination Unreachable
357정성태12/1/200624131.NET Framework: 74. WCF 이야기 [4]
378정성태10/28/200628915    답변글 .NET Framework: 74.1. WCF와 WSE 3.0의 활용 [4]파일 다운로드1
379정성태11/3/200627865    답변글 .NET Framework: 74.2. WCF로 구현하는 .NET Remoting [4]파일 다운로드1
380정성태10/28/200626781    답변글 .NET Framework: 74.3. 웹 서비스와 닷넷 리모팅으로써의 WCF 구현파일 다운로드1
381정성태10/28/200629189    답변글 .NET Framework: 74.4. WCF 서비스 참조 추가 메뉴 [2]
382정성태10/28/200635264    답변글 .NET Framework: 74.5. WCF 서비스를 IIS에서 호스팅하는 방법파일 다운로드1
383정성태10/28/200630034    답변글 .NET Framework: 74.6. IIS 6.0: 다중 Endpoint 제공파일 다운로드1
384정성태10/28/200626925    답변글 .NET Framework: 74.7. IIS 7.0: 다중 Endpoint 제공
389정성태11/11/200629730    답변글 .NET Framework: 74.8. WCF에 SSL 적용 (1) - Httpcfg.exe 도구를 이용한 SSL 설정
... 181  182  [183]  184  185  186  187  188  189  190  191  192  193  194  195  ...