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

(시리즈 글이 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




C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)

예전에 C++를 이용한 방법을 소개했는데,

Detours 라이브러리를 이용한 Win32 API - Sleep 호출 가로채기
; https://www.sysnet.pe.kr/2/0/631

Win32 API 후킹 - Trampoline API Hooking
; https://www.sysnet.pe.kr/2/0/1231

이번에는 순수 C#만을 이용해 호출을 가로채 보겠습니다. ^^




코드를 작성하기 전에, Win32 API를 제공하는 측에서 어떤 식으로 동작하는지 잠시 살펴보겠습니다. 우선, API를 담고 있는 DLL이 시스템에 로딩될 때의 주소가 가변적이므로 해당 주소를 직접 "지정해서" 호출할 수 없다는 점을 알아야 합니다. 즉 아래와 같은 식으로,

// TestFunc 함수의 주소가 0x5000이라고 가정.

call 0x5000

컴파일러는 직접 "0x5000"을 가정하고 기계어 코드를 생성할 수 없습니다. 왜냐하면 DLL은 0x10000000에도 로드될 수 있고 0x10050000에도 로드될 수 있기 때문에 애당초 저런 식으로 주소를 확정할 수가 없는 것입니다. 이런 문제를 해결하기 위해 컴파일러는 다음과 같은 식으로 기계어 코드를 생성해 둡니다.

// kernel32.dll Sleep 함수의 EAT에 등록된 주소가 가리키는 위치의 기계어 코드

00007ffb`5dc1a0d0 48ff25c1d20500  jmp     qword ptr [KERNEL32!_imp_Sleep (00007ffb`5dc77398)]
                               // rex.W jmp QWORD PTR [rip+0x0005d2c1]
                               // 48 ff 25            c1 d2 05 00

이와 함께 운영체제의 DLL Loader는 jmp 코드의 operand에 전달되는 주소에 있는 값을 로딩 시에 패치해 주므로 정상적으로 구현 코드가 있는 곳으로 jmp할 수 있는 것입니다. 이 때문에, 해당 함수를 호출하는 측의 모든 코드를 패치할 필요 없이, 위의 jmp가 참조하는 "주솟값"을 알아내고 그 주소에 담긴 "코드의 위치"를 우리가 만든 메서드의 주소로 치환하면 되는 것입니다.




이론에 맞춰 구현하는 것은 예전에 만들어 둔 WindowsPE 라이브러리에 Win32 DLL의 Export 함수 목록을 구하는 EnumerateExportFunctions에서 시작할 수 있습니다.

C# - 로딩된 Native DLL의 export 함수 목록 출력
; https://www.sysnet.pe.kr/2/0/12093

이를 이용해 kernel32.dll의 "실제 Sleep 함수로 jmp하는 코드"의 주솟값을 알아낼 수 있습니다.

// Install-Package WindowsPE

PEImage img = PEImage.FromLoadedModule("kernel32.dll");
foreach (var efi in img.EnumerateExportFunctions())
{
    if (efi.Name == "Sleep")
    {
        IntPtr funcAddr = img.BaseAddress + (int)efi.RvaAddress;
        // 예를 들어 funcAddr == 7ffb5dc1a0d0
    }
}

windbg를 이용해 funcAddr의 주소를 덤프해 보면,

0:006> u 0x7ffb5dc1a0d0
Unmanaged code
00007ffb`5dc1a0d0 48ff25c1d20500  jmp     qword ptr [KERNEL32!_imp_Sleep (00007ffb`5dc77398)]
00007ffb`5dc1a0d7 cc              int     3

jmp의 operand 값으로 00000000`0005d2c1에 위치한 주솟값, 좀 더 정확히는 "jmp 명령어의 다음 주소 + 0x0005d2c1"의 주솟값인 00007ffb`5dc77398에 _imp_Sleep 함수의 코드 시작 주소가 담겨 있는 것을 확인할 수 있습니다.

자, 그럼 기계어 코드인 48ff25c1d20500으로부터 0x0005d2c1을 분리해서 구하면 되는데, 어림짐작으로 0x48(rex prefix) 0xff 0x25(absolute indirect jmp)이고 이후 4바이트가 0x0005d2c1의 역순이기 때문에 쉽게 추출해낼 수 있습니다. 또는 SharpDisasm을 이용해 정석적인 방법을 쓸 수도 있습니다.

byte[] code = img.ReadBytes(efi.RvaAddress, 1024);

var disasm = new SharpDisasm.Disassembler(code, mode, (ulong)funcAddr.ToInt64(), true);

foreach (var insn in disasm.Disassemble().Take(5))
{
    switch (insn.Mnemonic)
    {
        case SharpDisasm.Udis86.ud_mnemonic_code.UD_Ijmp:
            if (insn.Operands.Length == 1)
            {
                long value = insn.Operands[0].Value;
                IntPtr jumpPtr = new IntPtr((long)insn.PC + value);
                IntPtr targetPtr = new IntPtr(jumpPtr.ReadInt64());
                Console.WriteLine($"Jump to 0x{targetPtr.ToInt64():x} at 0x{funcAddr.ToInt64():x}");
                return targetPtr;
            }
            break;
    }
}

다 끝났군요. ^^ 남은 작업은 가로채기 해서 대신 수행할 메서드의 주소를 "jumpPtr"의 위치에 써 주면 됩니다. 이 작업은 다음과 같은 식으로 간단하게 해결할 수 있습니다.

/*
public delegate void SleepDelegate(int milliseconds);

static SleepDelegate _org_Sleep;
public static void ManagedSleep(int milliseconds)
{
    Console.WriteLine("ManagedSleep called: " + milliseconds);
    _org_Sleep?.Invoke(milliseconds);
}
*/

SleepDelegate func = ManagedSleep;
IntPtr proxyFuncAddr = Marshal.GetFunctionPointerForDelegate(func);

jumpPtr.WriteInt64(proxyFuncAddr.ToInt64()); // WriteInt64는 확장 메서드

여기서 jumpPtr이 가리키는 곳의 메모리 섹션은 쓰기가 금지되어 있으므로 Win32 API 후킹 - Trampoline API Hooking에 따라 메모리 속성을 변경해야 합니다.

IntPtr hHandle = NativeMethods.OpenProcess(rights, false, pid);
if (NativeMethods.VirtualProtectEx(hHandle, address, new UIntPtr((uint)IntPtr.Size), PageAccessRights.PAGE_EXECUTE_READWRITE, out dwOldProtect) == true)
{
    if (IntPtr.Size == 4)
    {
        address.WriteInt32(value.ToInt32());
    }
    else
    {
        address.WriteInt64(value.ToInt64());
    }

    NativeMethods.FlushInstructionCache(hHandle, address, new UIntPtr((uint)IntPtr.Size));
}




좀 복잡한가요? ^^ 그래서 위에서 설명한 내용을 정리해 다음의 github 프로젝트 및 NuGet에 올렸으니,

DotNetSamples/WinConsole/PEFormat/DetourFunc/
; https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc

Install-Package DetourFunc -Version 1.0.0

다음과 같이 간단하게 사용할 수 있습니다.

using DetourFunc;
using System;
using System.Runtime.InteropServices;

public delegate void SleepDelegate(int milliseconds);

class Program
{
    [DllImport("kernel32.dll")]
    public static extern void Sleep(int milliseconds);

    static void Main(string[] _)
    {
        using (var managed = MethodReplacer.Win32FuncWithManagedFunc<SleepDelegate>("kernel32.dll", "Sleep", ManagedSleep, out _org_Sleep))
        {
            Console.WriteLine(DateTime.Now + ": before - Sleep");
            Sleep(5 * 1000);
            Console.WriteLine(DateTime.Now + ": after - Sleep");
        }
    }

    static SleepDelegate _org_Sleep;
    public static void ManagedSleep(int milliseconds)
    {
        Console.WriteLine("ManagedSleep called: " + milliseconds);
        _org_Sleep?.Invoke(milliseconds);
    }
}

/* 출력 결과
2020-01-28 오후 11:24:45: before - Sleep
ManagedSleep called: 5000
2020-01-28 오후 11:24:50: after - Sleep
*/

기존 Sleep 함수의 주소까지 _org_Sleep에 반환해 주기 때문에 대신 수행하도록 지정된 ManagedSleep에서 "_org_Sleep" 함수도 호출하고 있습니다.

(이 글의 첨부 파일은 DetourFunc 라이브러리를 이용한 예제 코드를 포함합니다.)




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







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

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

비밀번호

댓글 작성자
 



2024-07-19 02시43분
정성태

... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...
NoWriterDateCnt.TitleFile(s)
1303정성태6/26/201227404개발 환경 구성: 152. sysnet DB를 SQL Azure 데이터베이스로 마이그레이션
1302정성태6/25/201229455개발 환경 구성: 151. Azure 웹 사이트에 사용자 도메인 네임 연결하는 방법
1301정성태6/20/201225766오류 유형: 156. KB2667402 윈도우 업데이트 실패 및 마이크로소프트 Answers 웹 사이트 대응
1300정성태6/20/201231785.NET Framework: 329. C# - Rabin-Miller 소수 생성방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 [1]파일 다운로드2
1299정성태6/18/201232897제니퍼 .NET: 21. 제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석 [2]파일 다운로드2
1298정성태6/14/201234410VS.NET IDE: 72. Visual Studio에서 pfx 파일로 서명한 경우, 암호는 어디에 저장될까? [2]
1297정성태6/12/201231056VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법파일 다운로드1
1296정성태6/5/201227699.NET Framework: 328. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 - 두 번째 이야기 [4]파일 다운로드1
1295정성태6/5/201225085.NET Framework: 327. RSAParameters와 System.Numerics.BigInteger 이야기파일 다운로드1
1294정성태5/27/201248544.NET Framework: 326. 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 [7]파일 다운로드2
1293정성태5/24/201229776.NET Framework: 325. System.Drawing.Bitmap 데이터를 Parallel.For로 처리하는 방법 [2]파일 다운로드1
1292정성태5/24/201223755.NET Framework: 324. First-chance exception에 대해 조건에 따라 디버거가 멈추게 할 수는 없을까? [1]파일 다운로드1
1291정성태5/23/201230283VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [2]
1290정성태5/18/201235083.NET Framework: 323. 관리자 권한이 필요한 작업을 COM+에 대행 [7]파일 다운로드1
1289정성태5/17/201239242.NET Framework: 322. regsvcs.exe로 어셈블리 등록 시 시스템 변경 사항 [5]파일 다운로드2
1288정성태5/17/201226467.NET Framework: 321. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (3) - Type Library파일 다운로드1
1287정성태5/17/201229303.NET Framework: 320. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (2) - .NET 4.0 + .NET 2.0 [2]
1286정성태5/17/201238233.NET Framework: 319. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU [5]
1285정성태5/16/201233267.NET Framework: 318. gacutil.exe로 어셈블리 등록 시 시스템 변경 사항파일 다운로드1
1284정성태5/15/201225704오류 유형: 155. Windows Phone 연결 상태에서 DRIVER POWER STATE FAILURE 블루 스크린 뜨는 현상
1283정성태5/12/201233316.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [1]파일 다운로드1
1282정성태5/12/201226110Phone: 6. Windows Phone 7 Silverlight에서 Google Map 사용하는 방법 [3]파일 다운로드1
1281정성태5/9/201233197.NET Framework: 316. WPF/Silverlight의 그래픽 단위와 Anti-aliasing 처리를 이해하자 [1]파일 다운로드1
1280정성태5/9/201226159오류 유형: 154. Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, ...'.
1279정성태5/9/201224919.NET Framework: 315. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 [1]파일 다운로드1
1278정성태5/8/201226151오류 유형: 153. Visual Studio 디버깅 - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...