성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
[정성태] 아쉽게도, 커뮤니티는 아니고 개인 블로그입니다. ^^
[정성태] 질문이 잘 이해가 안 됩니다. 우선, 해당 소스코드에서 ILis...
[양승조
] var대신 dinamic으로 선언해서 해결은 했습니다. 맞는 해...
[양승조
] 또 막혔습니다. ㅠㅠ var list = props[i].Ge...
[양승조
] 아. 감사합니다. 어제는 안됐던것 같은데....정신을 차려야겠네...
[정성태] "props[i].GetValue(props[i])" 코드에서 ...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법</h1> <p> 예전에, C#으로 Win32 API를 후킹하는 방법에 대해 설명한 적이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12132'>https://www.sysnet.pe.kr/2/0/12132</a> </pre> <br /> 위의 방법은 Win32 API의 Export 테이블에 있는 jmp 주솟값을 패치하는 간단한 절차였는데요, 이것을 지난 글에 설명한 trampoline 기법으로 대체해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > x64 환경에서 구현하는 다양한 Trampoline 기법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12148'>https://www.sysnet.pe.kr/2/0/12148</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/2/0/12153'>다음번에 설명</a>하겠지만) 일단 예제의 단순함을 위해 이번에는 (Sleep 대신) <a target='tab' href='https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex'>SleepEx</a>를 가로채는 경우로 진행하겠습니다. 우선, <a target='tab' href='https://www.sysnet.pe.kr/2/0/12132'>지난 글</a>에서 EAT 테이블로부터 구한 jmp 코드가 위치한 주솟값은 사실 <a target='tab' href='https://docs.microsoft.com/en-us/cpp/build/getprocaddress'>GetProcAddress</a>로도 구할 수 있습니다. 따라서 아래의 코드를,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > PEImage img = PEImage.FromLoadedModule("kernel32.dll"); foreach (var efi in img.EnumerateExportFunctions()) { if (efi.Name == "SleepEx") { IntPtr funcAddr = img.BaseAddress + (int)efi.RvaAddress; } } </pre> <br /> 간단하게 다음의 코드로 대체할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IntPtr modulePtr = NativeMethods.LoadLibrary("kernel32.dll"); if (modulePtr != IntPtr.Zero) { IntPtr funcAddr = NativeMethods.GetProcAddress(modulePtr, "SleepEx"); } </pre> <br /> 이렇게 구한 funcAddr(예를 들어, 7ffc49751fd0)를 disassemble시키면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > KERNEL32!SleepEx: 00007ffc`49751fd0 ff25226b0500 <span style='color: blue; font-weight: bold'>jmp</span> qword ptr [KERNEL32!_imp_SleepEx (<span style='color: blue; font-weight: bold'>00007ffc`497a8af8</span>)] ...이후 10번의 0xcc... (16byte 정렬) ... KERNEL32!_imp_SleepEx: <span style='color: blue; font-weight: bold'>00007ffc`497a8af8 00007ffc49446890</span> </pre> <br /> SleepEx 함수로 갈 수 있는 주솟값(00007ffc49446890)을 "00007ffc497a8af8"로부터 구할 수 있고, 그 대상이 되는 곳은 SleepEx의 함수 본체가 위치합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>KERNELBASE!SleepEx:</span> <span style='color: blue; font-weight: bold'>00007ffc`49446890</span> 89542410 mov dword ptr [rsp+10h],edx 00007ffc`49446894 4c8bdc mov r11,rsp 00007ffc`49446897 53 push rbx 00007ffc`49446898 56 push rsi 00007ffc`49446899 57 push rdi ...[생략]... </pre> <br /> 정리해 보면, trampoline 패치를 할 수 있는 후보군은 1) GetProcAddress가 가리킨 위치와 2) 그곳에서 jmp해 들어가는 대상 메서드의 본체가 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 여기서는, GetProcAddress가 아닌 jmp로 들어가는 대상 메서드의 본체에서 trampoline 처리를 해보겠습니다. 그러려면, jmp 대상이 되는 주소를 계산해야 하고 이것은 이미 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12132'>C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)</a> 글에서 다룬 적이 있습니다. <a target='tab' href='https://www.nuget.org/packages/DetourFunc/'>DetourFunc 라이브러리</a>에서 이를 정리해 GetExportFunctionAddress 메서드로 제공하니 SleepEx API의 Body 주소를 다음과 같이 구할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > IntPtr sleepPtr = MethodReplacer.GetExportFunctionAddress("kernel32.dll", "SleepEx", out var _); </pre> <br /> 그다음, SleepEx를 대신해 호출될 C# 메서드도 정의하고 그것의 주소를 받아옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public delegate void SleepExDelegate(int milliseconds, bool bAlertable); static void Main(string[] _) { // ...[생략]... SleepExDelegate action = Replaced_TestMethod; MethodDesc mdReplaceMethod = <a target='tab' href='https://www.sysnet.pe.kr/2/0/12142'>MethodDesc</a>.ReadFromMethodInfo(action.Method); <span style='color: blue; font-weight: bold'>IntPtr ptrBodyReplaceMethod</span> = mdReplaceMethod.GetNativeFunctionPointer(); } <span style='color: blue; font-weight: bold'>public static void Replaced_TestMethod(int milliseconds, bool bAlertable) { Console.WriteLine("Replaced_TestMethod called!"); }</span> </pre> <br /> 자... 그럼 재료가 준비되었군요 ^^ 이제 SleepEx body 코드에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > KERNELBASE!SleepEx: 00007ffc`49446890 <span style='color: blue; font-weight: bold'>89542410</span> mov dword ptr [rsp+10h],edx 00007ffc`49446894 <span style='color: blue; font-weight: bold'>4c8bdc</span> mov r11,rsp 00007ffc`49446897 <span style='color: blue; font-weight: bold'>53</span> push rbx 00007ffc`49446898 <span style='color: blue; font-weight: bold'>56</span> push rsi 00007ffc`49446899 <span style='color: blue; font-weight: bold'>57</span> push rdi 00007ffc`4944689a <span style='color: blue; font-weight: bold'>4881</span>ec80000000 sub rsp,80h ...[생략]... </pre> <br /> 최초의 12바이트 코드(89 54 24 10...로 시작하는 바이트)를 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12148#type2'>지난 글에 설명한 2번 방식</a>으로 12바이트의 jmp 코드로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] GetJumpToCode(IntPtr valueAddress) { // 48 B8 00 00 00 00 00 00 00 00 mov rax,0000000000000000h // FF E0 jmp rax byte[] _longJumpToBytes = new byte[] { <span style='color: blue; font-weight: bold'>0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0</span> }; byte[] buf = BitConverter.GetBytes(valueAddress); Array.Copy(buf, 0, _longJumpToBytes, 2, IntPtr.Size); return _longJumpToBytes; } </pre> <br /> 덮어쓰면 됩니다. 마찬가지로 그 코드 역시 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12132'>C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)</a> 글에서 만들어 두었으니 이를 재활용해서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > byte[] OverwriteCode(IntPtr codeAddress, byte[] code) { byte[] oldCode = new byte[code.Length]; // mov/jmp로 덮어써질 영역의 원본 바이트 코드를 보관 for (int i = 0; i < code.Length; i++) { oldCode[i] = codeAddress.ReadByte(i); } ProcessAccessRights rights = ProcessAccessRights.PROCESS_VM_OPERATION | ProcessAccessRights.PROCESS_VM_READ | ProcessAccessRights.PROCESS_VM_WRITE; PageAccessRights dwOldProtect = PageAccessRights.NONE; IntPtr hHandle = IntPtr.Zero; try { int pid = Process.GetCurrentProcess().Id; hHandle = NativeMethods.OpenProcess(rights, false, pid); if (hHandle == IntPtr.Zero) { return null; } if (NativeMethods.VirtualProtectEx(hHandle, codeAddress, new UIntPtr((uint)IntPtr.Size), PageAccessRights.PAGE_EXECUTE_READWRITE, out dwOldProtect) == false) { return null; } codeAddress.WriteBytes(code); NativeMethods.FlushInstructionCache(hHandle, codeAddress, new UIntPtr((uint)code.Length)); return oldCode; } finally { if (dwOldProtect != PageAccessRights.NONE) { NativeMethods.VirtualProtectEx(hHandle, codeAddress, new UIntPtr((uint)IntPtr.Size), dwOldProtect, out PageAccessRights _); } if (hHandle != IntPtr.Zero) { NativeMethods.CloseHandle(hHandle); } } } </pre> <br /> 최종적으로 JumpPatch라는 메서드를 만들 수 있게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public sealed class TrampolinePatch<T> : IDisposable where T : Delegate { byte[] _oldCode; IntPtr _funcAddress; <span style='color: blue; font-weight: bold'>public void JumpPatch(IntPtr codeAddress, IntPtr valueAddress)</span> { byte[] code = GetJumpToCode(codeAddress, valueAddress); _oldCode = OverwriteCode(codeAddress, code); if (_oldCode.Length == code.Length) { _funcAddress = codeAddress; } } // ...[생략]... } </pre> <br /> 이 모든 것을 종합한 DetourFunc 라이브러리를 NuGet에 올렸으니, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Install-Package DetourFunc -Version 1.0.8 // 소스 코드: github - <a target='tab' href='https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc'>https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc</a> </pre> <br /> 다음과 같이 간단하게 사용할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using (var item = new TrampolinePatch<SleepExDelegate>()) { item.<span style='color: blue; font-weight: bold'>JumpPatch</span>(sleepPtr, ptrBodyReplaceMethod); // 가로채기가 되었으므로 Sleep 호출이 되지 않고 화면에 "Replaced_TestMethod called!" 문자열 출력 <span style='color: blue; font-weight: bold'>SleepEx</span>(3000, false); } // Dispose 시점에 예전 코드로 복원 </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1557&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 물론, 가장 좋은 방법은 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12132'>지난 글에서 설명한 KERNEL32!_imp_SleepEx의 점프 위치를 수정</a>하는 것이 좋습니다. 왜냐하면 8바이트로 정렬된 위치이면서 CPU 워드의 크기이기 때문에 race condition 등의 문제가 없어 보다 더 안전하기 때문입니다.<br /> <br /> 참고로, 위의 코드는 상업용 수준의 안정성을 갖추진 않았는데 이와 관련해서는 다음의 글을 읽어보실 것을 권합니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 전략 서신: API 후킹을 할 때에 생각해야 할 것들… ; <a target='tab' href='http://www.jiniya.net/wp/archives/11276'>http://www.jiniya.net/wp/archives/11276</a> </pre> <br /> 그러니까, TrampolinePatch 코드는 실습 차원에서 만든 것일 뿐 높은 안정성이 요구되는 현업 프로그램에서 사용하는 것은 권장하지 않습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 마지막으로, 그냥 올리는 것으로... 아래는 SleepEx의 코드 전문입니다.<br /> <br /> <pre style='height: 400px; margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > KERNELBASE!SleepEx: 00007ffd`2d056890 89542410 mov dword ptr [rsp+10h],edx 00007ffd`2d056894 4c8bdc mov r11,rsp 00007ffd`2d056897 53 push rbx 00007ffd`2d056898 56 push rsi 00007ffd`2d056899 57 push rdi 00007ffd`2d05689a 4881ec80000000 sub rsp,80h 00007ffd`2d0568a1 8bda mov ebx,edx 00007ffd`2d0568a3 8bf9 mov edi,ecx 00007ffd`2d0568a5 49c7439848000000 mov qword ptr [r11-68h],48h 00007ffd`2d0568ad c744243801000000 mov dword ptr [rsp+38h],1 00007ffd`2d0568b5 33c0 xor eax,eax 00007ffd`2d0568b7 498943a8 mov qword ptr [r11-58h],rax 00007ffd`2d0568bb 498943b0 mov qword ptr [r11-50h],rax 00007ffd`2d0568bf 498943b8 mov qword ptr [r11-48h],rax 00007ffd`2d0568c3 498943c0 mov qword ptr [r11-40h],rax 00007ffd`2d0568c7 498943c8 mov qword ptr [r11-38h],rax 00007ffd`2d0568cb 498943d0 mov qword ptr [r11-30h],rax 00007ffd`2d0568cf 498943d8 mov qword ptr [r11-28h],rax 00007ffd`2d0568d3 498d4b20 lea rcx,[r11+20h] 00007ffd`2d0568d7 48ff150a0d1600 call qword ptr [KERNELBASE!_imp_RtlGetCurrentUmsThread (00007ffd`2d1b75e8)] 00007ffd`2d0568de 0f1f440000 nop dword ptr [rax+rax] 00007ffd`2d0568e3 898424b0000000 mov dword ptr [rsp+0B0h],eax 00007ffd`2d0568ea 85c0 test eax,eax 00007ffd`2d0568ec 0f89a6ec0500 jns KERNELBASE!SleepEx+0x5ed08 (00007ffd`2d0b5598) 00007ffd`2d0568f2 33f6 xor esi,esi 00007ffd`2d0568f4 488b9424b8000000 mov rdx,qword ptr [rsp+0B8h] 00007ffd`2d0568fc 85db test ebx,ebx 00007ffd`2d0568fe 0f8592000000 jne KERNELBASE!SleepEx+0x106 (00007ffd`2d056996) 00007ffd`2d056904 83ffff cmp edi,0FFFFFFFFh 00007ffd`2d056907 7443 je KERNELBASE!SleepEx+0xbc (00007ffd`2d05694c) 00007ffd`2d056909 4869cf10270000 imul rcx,rdi,2710h 00007ffd`2d056910 48894c2420 mov qword ptr [rsp+20h],rcx 00007ffd`2d056915 48f7d9 neg rcx 00007ffd`2d056918 48894c2420 mov qword ptr [rsp+20h],rcx 00007ffd`2d05691d 4885d2 test rdx,rdx 00007ffd`2d056920 7538 jne KERNELBASE!SleepEx+0xca (00007ffd`2d05695a) 00007ffd`2d056922 488d542420 lea rdx,[rsp+20h] 00007ffd`2d056927 0fb6cb movzx ecx,bl 00007ffd`2d05692a 48ff155f0b1600 call qword ptr [KERNELBASE!_imp_NtDelayExecution (00007ffd`2d1b7490)] 00007ffd`2d056931 0f1f440000 nop dword ptr [rax+rax] 00007ffd`2d056936 8bf8 mov edi,eax 00007ffd`2d056938 898424b0000000 mov dword ptr [rsp+0B0h],eax 00007ffd`2d05693f 85db test ebx,ebx 00007ffd`2d056941 7428 je KERNELBASE!SleepEx+0xdb (00007ffd`2d05696b) 00007ffd`2d056943 3d01010000 cmp eax,101h 00007ffd`2d056948 7521 jne KERNELBASE!SleepEx+0xdb (00007ffd`2d05696b) 00007ffd`2d05694a ebd6 jmp KERNELBASE!SleepEx+0x92 (00007ffd`2d056922) 00007ffd`2d05694c 89742420 mov dword ptr [rsp+20h],esi 00007ffd`2d056950 c744242400000080 mov dword ptr [rsp+24h],80000000h 00007ffd`2d056958 ebc3 jmp KERNELBASE!SleepEx+0x8d (00007ffd`2d05691d) 00007ffd`2d05695a 8b82f0040000 mov eax,dword ptr [rdx+4F0h] 00007ffd`2d056960 83c840 or eax,40h 00007ffd`2d056963 8982f0040000 mov dword ptr [rdx+4F0h],eax 00007ffd`2d056969 ebb7 jmp KERNELBASE!SleepEx+0x92 (00007ffd`2d056922) 00007ffd`2d05696b 488b8c24b8000000 mov rcx,qword ptr [rsp+0B8h] 00007ffd`2d056973 4885c9 test rcx,rcx 00007ffd`2d056976 0f854bec0500 jne KERNELBASE!SleepEx+0x5ed37 (00007ffd`2d0b55c7) 00007ffd`2d05697c 85db test ebx,ebx 00007ffd`2d05697e 7536 jne KERNELBASE!SleepEx+0x126 (00007ffd`2d0569b6) 00007ffd`2d056980 b8c0000000 mov eax,0C0h 00007ffd`2d056985 3bf8 cmp edi,eax 00007ffd`2d056987 7402 je KERNELBASE!SleepEx+0xfb (00007ffd`2d05698b) 00007ffd`2d056989 8bc6 mov eax,esi 00007ffd`2d05698b 4881c480000000 add rsp,80h 00007ffd`2d056992 5f pop rdi 00007ffd`2d056993 5e pop rsi 00007ffd`2d056994 5b pop rbx 00007ffd`2d056995 c3 ret 00007ffd`2d056996 33d2 xor edx,edx 00007ffd`2d056998 488d4c2430 lea rcx,[rsp+30h] 00007ffd`2d05699d 48ff15ec091600 call qword ptr [KERNELBASE!_imp_RtlActivateActivationContextUnsafeFast (00007ffd`2d1b7390)] 00007ffd`2d0569a4 0f1f440000 nop dword ptr [rax+rax] 00007ffd`2d0569a9 488b9424b8000000 mov rdx,qword ptr [rsp+0B8h] 00007ffd`2d0569b1 e94effffff jmp KERNELBASE!SleepEx+0x74 (00007ffd`2d056904) 00007ffd`2d0569b6 488d4c2430 lea rcx,[rsp+30h] 00007ffd`2d0569bb 48ff15d6091600 call qword ptr [KERNELBASE!_imp_RtlDeactivateActivationContextUnsafeFast (00007ffd`2d1b7398)] 00007ffd`2d0569c2 0f1f440000 nop dword ptr [rax+rax] 00007ffd`2d0569c7 ebb7 jmp KERNELBASE!SleepEx+0xf0 (00007ffd`2d056980) 00007ffd`2d0569c9 cc int 3 00007ffd`2d0569ca cc int 3 00007ffd`2d0569cb cc int 3 00007ffd`2d0569cc cc int 3 00007ffd`2d0569cd cc int 3 00007ffd`2d0569ce cc int 3 00007ffd`2d0569cf cc int 3 </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2090
(왼쪽의 숫자를 입력해야 합니다.)