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

회사 업무랑 관계가 있다 보니, "가로채기" 기술에 대해서는 늘 관심이 있습니다. 비록, 몇 가지 제약으로 인해 채택할 수는 없었지만 기술 검토 과정 중에 알게 된 재미있는 글을 하나 소개해 드립니다.

CLR Injection: Runtime Method Replacer
; http://www.codeproject.com/KB/dotnet/CLRMethodInjection.aspx

그나저나, "가로 채기" 기술은 개발자라면 누구나 다 관심있는 것 같습니다. 커널 드라이버도 그렇고, 일반 사용자 모드 응용 프로그램도 마찬가지죠. 심지어 "버퍼 오버런"도 일종의 가로채기 기술이니. ^^;

이번 글의 주제인 "CLR Injection: Runtime Method Replacer"를 실행해 보신 분은 아시겠지만, 크게 2가지 오류가 있습니다.

  • 현재의 .NET 3.5 x64에서 DynamicMethod 치환 오류
  • .NET 4.0에서 DynamicMethod 치환 오류

저는 위의 2가지 문제를 개선한 코드를 알아볼 텐데요. 사실, 이전에 써놓은 아래의 2가지 글은 이 문제를 해결하기 위해 windbg를 사용하면서 정리가 된 것들입니다. 시간이 허락된다면, 아래의 2가지 글을 가볍게 읽고 시작하는 것도 좋겠습니다. ^^

windbg로 확인하는 .NET CLR 메서드 
; https://www.sysnet.pe.kr/2/0/940

windbg로 확인하는 .NET CLR LCG 메서드(DynamicMethod) 
; https://www.sysnet.pe.kr/2/0/941




"CLR Injection: Runtime Method Replacer" 글이 쓰인 것은 2009년 6월인데 그 당시까지만 해도 정상작동했을지 모르지만, .NET 3.5 x64 및 .NET 4.0에서는 더 이상 동작하지 않게 되어 버렸습니다.

그러하니, 문제가 되는 소스 코드들을 하나씩 살펴보겠습니다. ^^

private static IntPtr GetDynamicMethodAddress(MethodBase method)
{
    unsafe
    {
        RuntimeMethodHandle handle = GetDynamicMethodRuntimeHandle(method);
        byte* ptr = (byte*)handle.Value.ToPointer();
        if (...[.NET 2.0 SP2 이상]...)
        {
            RuntimeHelpers.PrepareMethod(handle);
            if (IntPtr.Size == 8)
            {
                // x64 오류
                ulong* address = (ulong*)ptr;
                address = (ulong*)*(address + 5);
                return new IntPtr(address + 12);
            }
            else
            {
                // x86 정상동작
                uint* address = (uint*)ptr;
                address = (uint*)*(address + 5);
                return new IntPtr(address + 12);
            }
        }
        else // .NET 2.0 SP2 
        {
        ...[생략]...                    
        }

    }
}

이런 ~~~ ^^ 불안정한 offset 값 조정이군요. 여기를 정리하기에 앞서 다시 내부에서 호출되는 GetDynamicMethodRuntimeHandle 메서드 먼저 살펴보겠습니다.

private static RuntimeMethodHandle GetDynamicMethodRuntimeHandle(MethodBase method)
{
    if (method is DynamicMethod)
    {
        FieldInfo fieldInfo = typeof(DynamicMethod).GetField("m_method",BindingFlags.NonPublic|BindingFlags.Instance);
        RuntimeMethodHandle handle = ((RuntimeMethodHandle)fieldInfo.GetValue(method));
                
        return handle;
    }
    return method.MethodHandle;
}

아하~~~ m_method에서 구해내는군요. 이전에 제가 썼던 글에서처럼 위의 코드는 .NET 4.0에서 더 이상 동작하지 않습니다. 그래서 다음과 같이 수정해 주어야 합니다.

private static RuntimeMethodHandle GetDynamicMethodRuntimeHandle(MethodBase method)
{
    RuntimeMethodHandle handle;

    if (Environment.Version.Major == 4)
    {
        MethodInfo getMethodDescriptorInfo = typeof(DynamicMethod).GetMethod("GetMethodDescriptor",
                BindingFlags.NonPublic | BindingFlags.Instance);
        handle = (RuntimeMethodHandle)getMethodDescriptorInfo.Invoke(method, null);
    }
    else
    {
        FieldInfo fieldInfo = typeof(DynamicMethod).GetField("m_method", BindingFlags.NonPublic | BindingFlags.Instance);
        handle = ((RuntimeMethodHandle)fieldInfo.GetValue(method));
    }
                
    return handle;
}

그럼 이제 다시 GetDynamicMethodAddress의 옵셋 조정으로 돌아가서 살펴보겠습니다.

+5/+12의 옵셋 조정으로 구하는 값이 도대체 무엇인지 알아야겠습니다. 사실 지금까지 알아본 바에 의하면 이 값이 RuntimeMethodHandle.GetFunctionPointer()가 반환하는 값일 거라는 것이 제 생각인데요. 맞는지 볼까요? .NET 3.5 x86에서 정상동작을 했으니 환경설정을 그렇게 바꾸고 ReplaceMethod의 최종 *d 값과 RuntimeMethodHandle.GetFunctionPointer() 반환값을 비교해 보면 될 것입니다.


public static void ReplaceMethod(IntPtr srcAdr, MethodBase dest)
{
    IntPtr destAdr = GetMethodAddress(dest);
    unsafe
    {
        if (IntPtr.Size == 8)
        {
            // x64...
        }
        else
        {
            uint* d = (uint*)destAdr.ToPointer();
            *d = *((uint*)srcAdr.ToPointer());
            Console.WriteLine("Pointer == " + (*d).ToString("x"));
        }
    }
}


private static IntPtr GetDynamicMethodAddress(MethodBase method)
{
    unsafe
    {
        RuntimeMethodHandle handle = GetDynamicMethodRuntimeHandle(method);
        byte* ptr = (byte*)handle.Value.ToPointer();

        if (IsNet20Sp2OrGreater())
        {
            RuntimeHelpers.PrepareMethod(handle);

            IntPtr pFunction = handle.GetFunctionPointer();
            Console.WriteLine("pFunction == " + pFunction.ToString("x"));

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

출력 결과는 역시 동일합니다. ^^

pFunction == 4900a8
Pointer == 4900a8

그 외에도, windbg로 연결해서 확인해 보면 결국 "!dumpmd [RuntimeMethodHandle 값]"의 "CodeAddr" 값과 일치한다는 것을 알 수 있습니다. 오호~~~ 여지없이 RuntimeMethodHandle.GetFunctionPointer 값으로도 대체된다는 것을 의미합니다.

따라서, 어렵게 옵셋 조정할 필요 없이 다음과 같이 매우 안정적으로 메서드 호출 한 번으로 끝낼 수 있습니다. (아마도, 이런 방식은 앞으로의 패치에 대해서도 좀 더 안정적으로 동작할 수 있을 것입니다.)

private static IntPtr GetDynamicMethodAddress(MethodBase method)
{
    unsafe
    {
        RuntimeMethodHandle handle = GetDynamicMethodRuntimeHandle(method);
        byte* ptr = (byte*)handle.Value.ToPointer();
        if (IsNet20Sp2OrGreater())
        {
            RuntimeHelpers.PrepareMethod(handle);

            return handle.GetFunctionPointer();
            /* 이후 주석 처리된 코드는 더 이상 필요하지 않음.
            if (IntPtr.Size == 8)
            {
                ulong* address = (ulong*)ptr;
                address = (ulong*)*(address + 5);
                return new IntPtr(address + 12);
            }
            else
            {
                uint* address = (uint*)ptr;
                address = (uint*)*(address + 5);
                return new IntPtr(address + 12);
            }
            */
        }
        ... [생략]...

다음으로 변경해야 할 곳은, ReplaceMethod에서 갈아치우려는 메서드가 DynamicMethod인 경우 GetFunctionPointer()로 구해진 값을 static 형 메서드와는 구분해서 다음과 같이 대입을 해줘야 합니다. (주의!!! 여기에서 일컫는 static 은 "DynamicMethod"의 반대 의미로 사용된 것일 뿐 instance 메서드와 대비되는 static 메서드를 지칭하는 것이 아닙니다.)

public static void ReplaceMethod(IntPtr srcAdr, MethodBase dest, bool isDynamicSource)
{
    IntPtr destAdr = GetMethodAddress(dest);
    unsafe
    {
        if (IntPtr.Size == 8)
        {
            ulong* d = (ulong*)destAdr.ToPointer();

            if (isDynamicSource == true)
            {
                *d = (ulong)srcAdr.ToInt64();
            }
            else
            {
                *d = *((ulong*)srcAdr.ToPointer());
            }
        }
        else
        {
            uint* d = (uint*)destAdr.ToPointer();

            if (isDynamicSource == true)
            {
                *d = (uint)srcAdr.ToInt32();
            }
            else
            {
                *d = *((uint*)srcAdr.ToPointer());
            }        
        }
    }
}

마지막으로, 그다지 중요도는 없는 .NET 버전 체크 메서드인 IsNet20Sp2OrGreater에 다음과 같이 추가를 해줍니다.

private static bool IsNet20Sp2OrGreater()
{
    if (Environment.Version.Major == 4)
    {
        return true;
    }

    return Environment.Version.Major == FrameworkVersions.Net20SP2.Major &&
        Environment.Version.MinorRevision >= FrameworkVersions.Net20SP2.MinorRevision;
}

자, 여기까지 변경하고 .NET 3.5 x64 / .NET 4.0에서 실행을 하면 정상적으로 정적 메서드의 주소를 동적으로 생성된 메서드의 주소로 치환해서 실행이 되는 것을 확인할 수 있습니다.

오~~~ 멋지죠? ^^

[그림 1: .NET 4.0 / .NET 3.5 x64에서 실행]
replace_clr_method_1.png

첨부한 소스 코드는 위의 변경 사항이 반영되었습니다.

** 참고로, 원본 소스를 가능한 바꾸지 않으려고 해서 위와 같이 설명을 했는데, 원래는 2번째 첨부된 소스 코드로 변경되어야 하는 것이 맞습니다.



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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2011-03-31 11시01분
MSIL Injection: Rewrite a non dynamic method at runtime
; https://learn.microsoft.com/en-us/archive/blogs/zelmalki/msil-injection-rewrite-a-non-dynamic-method-at-runtime
; https://stackoverflow.com/questions/2436082/msil-inspection

위의 방법은, 일단 JIT 컴파일 된 이후에는 효력이 없다는 것!
정성태
2012-02-16 07시13분
[바람나그네] 아.. 감사합니다.
원글에서 부터 찾아서, 여기로 왔습니다.
한국 분이 쓰셨다니 더 좋네요. ^^ 잘 사용하겠습니다.
혹시 문제가 있다면 알려드릴께요. ^^
[guest]
2012-02-16 07시14분
[바람나그네] 아. 두가지 방법 중에 어떤 것이 더 좋나요?
두 글 다 읽었는데...... 장단점은 모르겠네요.
[guest]
2012-02-16 10시35분
어떤 2가지 방법인가요? codeproject에 있는 것과 MSIL Injection인가요?
정성태
2012-02-17 08시09분
[바람나그네] 네.. codeproject와 MSIL injection은 구현 방식이 다른 것 같아서요.
그리고, 첫 번째 방법은 시도해보니 제 코드에서는 동작하지 않네요.
"위의 방법은, 일단 JIT 컴파일 된 이후에는 효력이 없다는 것!" 이 의미가 codeproject 에 나온 방법은 한계가 있다는 뜻인가요?
[guest]
2012-02-17 11시07분
제가 위의 댓글에서 MSIL Injection은 JIT 컴파일된 이후에는 효력이 없다고 적었는데요. 반면에 codeproject 것은 JIT 컴파일 후에야 동작합니다. (그래서 codeproject 것은 일부러 JIT 컴파일을 시킵니다.)
정성태
2012-02-19 11시47분
[바람나그네] 아.. 친절한 답변 감사합니다. ^^
시도해보겠습니다.
[guest]

... 91  92  93  94  95  96  97  98  99  100  101  [102]  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11382정성태12/4/201721872오류 유형: 436. System.Data.SqlClient.SqlException (0x80131904): Connection Timeout Expired 예외 발생 시 "[Pre-Login] initialization=48; handshake=1944;" 값의 의미
11381정성태11/30/201718259.NET Framework: 702. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법(두 번째 이야기)파일 다운로드1
11380정성태11/30/201718308디버깅 기술: 109. windbg - (x64에서의 인자 값 추적을 이용한) Thread.Abort 시 대상이 되는 스레드를 식별하는 방법
11379정성태11/30/201719040오류 유형: 435. System.Web.HttpException - Session state has created a session id, but cannot save it because the response was already flushed by the application.
11378정성태11/29/201720494.NET Framework: 701. 한글이 포함된 바이트 배열을 나눈 경우 한글이 깨지지 않도록 다시 조합하는 방법 [1]파일 다운로드1
11377정성태11/29/201719793.NET Framework: 700. CommonOpenFileDialog 사용 시 사용자가 선택한 파일 목록을 구하는 방법 [3]파일 다운로드1
11376정성태11/28/201724123VS.NET IDE: 123. Visual Studio 편집기의 \r\n (crlf) 개행을 \n으로 폴더 단위로 설정하는 방법
11375정성태11/28/201718945오류 유형: 434. Visual Studio로 ASP.NET 디버깅 중 System.Web.HttpException - Could not load type 오류
11374정성태11/27/201724037사물인터넷: 14. 라즈베리 파이 - (윈도우의 NT 서비스처럼) 부팅 시 시작하는 프로그램 설정 [1]
11373정성태11/27/201723025오류 유형: 433. Raspberry Pi/Windows 다중 플랫폼 지원 컴파일 관련 오류 기록
11372정성태11/25/201726075사물인터넷: 13. 윈도우즈 사용자를 위한 라즈베리 파이 제로 W 모델을 설정하는 방법 [4]
11371정성태11/25/201719682오류 유형: 432. Hyper-V 가상 스위치 생성 시 Failed to connect Ethernet switch port 0x80070002 오류 발생
11370정성태11/25/201719581오류 유형: 431. Hyper-V의 Virtual Switch 생성 시 "External network" 목록에 특정 네트워크 어댑터 항목이 없는 경우
11369정성태11/25/201721700사물인터넷: 12. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드 및 마우스로 쓰는 방법 (절대 좌표, 상대 좌표, 휠) [1]
11368정성태11/25/201727287.NET Framework: 699. UDP 브로드캐스트 주소 255.255.255.255와 192.168.0.255의 차이점과 이를 고려한 C# UDP 서버/클라이언트 예제 [2]파일 다운로드1
11367정성태11/25/201727351개발 환경 구성: 337. 윈도우 운영체제의 route 명령어 사용법
11366정성태11/25/201719011오류 유형: 430. 이벤트 로그 - Cryptographic Services failed while processing the OnIdentity() call in the System Writer Object.
11365정성태11/25/201721276오류 유형: 429. 이벤트 로그 - User Policy could not be updated successfully
11364정성태11/24/201723180사물인터넷: 11. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 (절대 좌표) [2]
11363정성태11/23/201723112사물인터넷: 10. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 (두 번째 이야기)
11362정성태11/22/201719660오류 유형: 428. 윈도우 업데이트 KB4048953 - 0x800705b4 [2]
11361정성태11/22/201722430오류 유형: 427. 이벤트 로그 - Filter Manager failed to attach to volume '\Device\HarddiskVolume??' 0xC03A001C
11360정성태11/22/201722239오류 유형: 426. 이벤트 로그 - The kernel power manager has initiated a shutdown transition.
11359정성태11/16/201721730오류 유형: 425. 윈도우 10 Version 1709 (OS Build 16299.64) 업그레이드 시 발생한 문제 2가지
11358정성태11/15/201726501사물인터넷: 9. Visual Studio 2017에서 Raspberry Pi C++ 응용 프로그램 제작 [1]
11357정성태11/15/201726979개발 환경 구성: 336. 윈도우 10 Bash 쉘에서 C++ 컴파일하는 방법
... 91  92  93  94  95  96  97  98  99  100  101  [102]  103  104  105  ...