실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기
지난 글에,
실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선
; https://www.sysnet.pe.kr/2/0/942
codeproject의 글 하나를 소개했는데요.
CLR Injection: Runtime Method Replacer
; http://www.codeproject.com/KB/dotnet/CLRMethodInjection.aspx
우선, "
실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선" 글에서의 코드를 정리해 DetourFunc 프로젝트에 반영했으니,
C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)
; https://www.sysnet.pe.kr/2/0/12132
이를 사용하면 다음과 같이 일반 닷넷 메서드의 호출을 가로챌 수 있습니다.
// Install-Package DetourFunc -Version 1.0.7
using DetourFunc;
using System;
class Program
{
static void Main(string[] _)
{
Action<bool> oldAction = TestMethod;
Action<bool> newAction = NewMethod;
Console.WriteLine($"oldFunc == {oldAction.Method.MethodHandle.GetFunctionPointer().ToInt64():x}");
Console.WriteLine($"newFunc == {newAction.Method.MethodHandle.GetFunctionPointer().ToInt64():x}");
Console.WriteLine();
TestMethod(true);
NetMethodReplacer.ReplaceMethod(oldAction.Method, newAction.Method);
TestMethod(true);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void TestMethod(bool showMessage)
{
if (showMessage == true)
{
Console.WriteLine("TestMethod");
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void NewMethod(bool showMessage)
{
if (showMessage == true)
{
Console.WriteLine("NewMethod");
}
}
}
/* 출력 결과
oldFunc == 7ffe48320488
newFunc == 7ffe48320490
TestMethod
NewMethod
*/
보는 바와 같이 TestMethod의 동작이 NewMethod로 치환되었습니다.
그런데 새롭게 바뀐 JIT 컴파일 방식으로 인해,
Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12133
Runtime Method Replacer의 동작에 결함이 생기게 되었습니다. 이것을 간단하게 다음과 같이 재현할 수 있습니다.
for (int i = 0; i < 10000; i++)
{
TestMethod(false);
}
NetMethodReplacer.ReplaceMethod(oldAction.Method, newAction.Method);
TestMethod(true);
/* 출력 결과
TestMethod
*/
그러니까, NetMethodReplacer.ReplaceMethod 호출 이전에 Fixup Precode가 call에서 jmp로 바뀌도록 TestMethod를 충분히 불러주면 이후 ReplaceMethod를 해도 효력이 없는 것입니다. 그 이유를 간단하게 정리해 보면, NetMethodReplacer.ReplaceMethod 메서드는 대상 코드를 치환하기 위해 MethodDesc의 8바이트 위치에 값을 써 PreStubWorker 단계에서 MethodDesc::GetMethodEntryPoint 함수가 그 값을 이용할 수 있게 만드는데, Fixup Precode가 Method의 Body로 향하는 jmp 문으로 일단 바뀌게 되면 이후부터는 MethodDesc의 8바이트 위치에 값을 써도 아무런 영향을 주지 못하기 때문입니다.
따라서, NetMethodReplacer.ReplaceMethod 메서드를 이용한다면 대상 메서드의 어셈블리가 로드되는 - 즉, 메서드들이 호출되지 않았을 - 초기 시점에 안전하게 치환 작업을 마무리해야만 합니다.
그나저나... 혹시 몇 번의 호출만에 call이 jmp로 바뀌게 되는 걸까요? 예전에 테스트했을 때는,
.NET Core 2.1 - Tiered Compilation 도입
; https://www.sysnet.pe.kr/2/0/11539
30번 정도였는데 이번에도 비슷할지... 다음과 같은 식으로 코드를 만들어 검증할 수 있습니다.
using System;
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
Action action = TestMethod;
IntPtr oldPtr = action.Method.MethodHandle.GetFunctionPointer();
byte oldOPCode = Marshal.ReadByte(oldPtr); // call로 시작하므로 0xe8
int count = 0;
while (true)
{
TestMethod();
count++;
if (oldOPCode != Marshal.ReadByte(oldPtr)) // jmp는 0xe9
{
break;
}
}
Console.WriteLine(count);
}
static void TestMethod()
{
Console.WriteLine("TEST");
}
}
/* 출력 결과
TEST
TEST
2
*/
그렇습니다. 단 두 번째의 호출에서 call에서 jmp 문으로 바뀝니다. 그러니까, 마이크로소프트는 단 한 번만 호출되는 메서드의 수가 적지 않은 비율을 차지한다는... 통계를 가지고 있는 듯하군요. ^^
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]