Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

실행 시에 메서드 가로채기 - 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://docs.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;
}




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





[최초 등록일: ]
[최종 수정일: 2/24/2020 ]

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

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12211정성태4/27/2020588개발 환경 구성: 486. WSL에서 Makefile로 공개된 리눅스 환경의 C/C++ 소스 코드 빌드
12210정성태4/20/2020819.NET Framework: 903. .NET Framework의 Strong-named 어셈블리 바인딩 (1) - app.config을 이용한 바인딩 리디렉션 [1]파일 다운로드1
12209정성태4/13/2020487오류 유형: 614. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우 (2)
12208정성태4/12/2020596Linux: 29. 리눅스 환경에서 C/C++ 프로그램이 Segmentation fault 에러가 발생한 경우
12207정성태4/2/2020553스크립트: 19. Windows PowerShell의 NonInteractive 모드
12206정성태4/2/2020649오류 유형: 613. 파일 잠금이 바로 안 풀린다면? - The process cannot access the file '...' because it is being used by another process.
12205정성태4/2/2020507스크립트: 18. Powershell에서는 cmd.exe의 명령어를 지원하진 않습니다.
12204정성태4/1/2020428스크립트: 17. Powershell 명령어에 ';' (semi-colon) 문자가 포함된 경우
12203정성태3/18/2020767오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/20201021개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/2020614오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token".
12200정성태3/18/2020962VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/2020475오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/2020602오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/2020692VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기
12196정성태3/17/2020607오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/17/2020759.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/2020735오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/2020636개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행
12192정성태3/14/2020788개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/20201235개발 환경 구성: 483. docker - OracleXE 컨테이너 실행 [1]
12190정성태3/14/2020439오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/20201217개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태9/24/2020648Windows: 169. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/2020500오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/2020609오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...