실행 시에 메서드 가로채기 - 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에서 실행]
첨부한 소스 코드는 위의 변경 사항이 반영되었습니다.
** 참고로, 원본 소스를 가능한 바꾸지 않으려고 해서 위와 같이 설명을 했는데, 원래는
2번째 첨부된 소스 코드로 변경되어야 하는 것이 맞습니다.
[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]