성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
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> 지난 글에서,<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를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12150'>https://www.sysnet.pe.kr/2/0/12150</a> </pre> <br /> Win32 API를 C# 메서드로 가로채는 방법을 Trampoline 기법을 이용해 설명했는데요. 여기서 x86에서의 "<a target='tab' href='https://www.sysnet.pe.kr/2/0/1231'>Win32 API 후킹 - Trampoline API Hooking</a>" 글과 비교해 미흡한 점이 하나 있다면 바로 "원본 함수"에 대한 호출 구현이 안 되었다는 점입니다.<br /> <br /> 예를 들어, SleepEx와 대체 메서드의 주솟값이 각각 다음과 같은 경우,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > sleepPtr == 7ffcf22e6890 ptrBodyReplaceMethod == 7ffc7cf50c10 </pre> <br /> SleepEx의 원본 코드가,<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: 00007FFCF22E6890 <span style='color: blue; font-weight: bold'>89 54 24 10</span> mov dword ptr [rsp+10h],edx 00007FFCF22E6894 <span style='color: blue; font-weight: bold'>4C 8B DC</span> mov r11,rsp 00007FFCF22E6897 <span style='color: blue; font-weight: bold'>53</span> push rbx 00007FFCF22E6898 <span style='color: blue; font-weight: bold'>56</span> push rsi 00007FFCF22E6899 <span style='color: blue; font-weight: bold'>57</span> push rdi 00007FFCF22E689A <span style='color: blue; font-weight: bold'>48 81</span> EC 80 00 00 00 sub rsp,80h 00007FFCF22E68A1 8b DA mov ebx,edx 00007FFCF22E68A3 8b F9 mov edi,ecx ...[생략]... </pre> <br /> 아래와 같이 대체 메서드의 JMP 코드로 바뀐 것까지만 구현한 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 00007FFCF22E6890 <span style='color: blue; font-weight: bold'>48 B8 10 0C F5 7C FC 7F</span> 00 00 mov rax,7FFC7CF50C10h 00007FFCF22E689A <span style='color: blue; font-weight: bold'>FF E0</span> jmp rax 00007FFCF22E689C EC in al,dx 00007FFCF22E689D 80 00 00 add byte ptr [rax],0 00007FFCF22E68A0 00 8B DA 8B F9 49 add byte ptr [rbx+49F98BDAh],cl 00007FFCF22E68A6 C7 43 98 48 00 00 00 mov dword ptr [rbx-68h],48h 00007FFCF22E68AD C7 44 24 38 01 00 00 00 mov dword ptr [rsp+38h],1 00007FFCF22E68B5 33 C0 xor eax,eax ...[생략]... </pre> <br /> 여기서 유의할 것은, MOV/JMP의 12바이트 패치로 인해 덮어써지는 영역이 기존 코드의 Opcode 단위에 꼭 맞지는 않다는 것입니다. 즉, 기존의 바이트들 중 마지막 명령어인 sub rsp, 80h의 중간까지,<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'>89 54 24 10 4C 8B DC 53 56 57 48 81</span> EC 80 00 00 00 // sub rsp, 80h </pre> <br /> 12바이트가 덧씌워졌기 때문에 이후의 명령어는 새로운 Opcode 단위로 인식이 되어 젼혀 다른 in, add, add, mov... 등의 명령어로 바뀝니다<br /> <br /> <hr style='width: 50%' /><br /> <br /> 결국, 원본 함수를 다시 호출하고 싶다면 기존 12바이트의 내용만 보관해서는 안 되고 그 12바이트가 온전하게 영향을 주는 opcode까지 모두 보관해야 합니다. 즉, 위의 경우에는 "sub rsp, 80h" 명령어 전체까지 포함해서 총 17바이트의 명령어를 보관해 두어야 하는 것입니다.<br /> <br /> 이를 위해서는 당연히 기계어 코드를 해석할 수 있어야 하고, 따라서 SharpDisasm과 같은 라이브러리를 활용해 다음과 같은 식으로 12바이트가 덮어쓰게 될 영역을 포함한 명령어를 모두 알아내는 코드가 필요합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 이 코드에서는 maxBytes == 12바이트 // codeAddress == SleepEx 함수 주소 private byte[] GetOldCode(IntPtr codeAddress, int maxBytes) { SharpDisasm.ArchitectureMode mode = (IntPtr.Size == 8) ? SharpDisasm.ArchitectureMode.x86_64 : SharpDisasm.ArchitectureMode.x86_32; List<byte> entranceCodes = new List<byte>(); int totalLen = 0; using (var disasm = new SharpDisasm.Disassembler(codeAddress, maxBytes + NativeMethods.MaxLengthOpCode, mode)) { foreach (var insn in disasm.Disassemble()) { for (int i = 0; i < insn.Length; i++) { entranceCodes.Add(codeAddress.ReadByte(totalLen + i)); } totalLen += insn.Length; if (totalLen >= maxBytes) { return entranceCodes.ToArray(); // 보관은 sub rsp, 80h의 온전한 명령어를 포함한 17바이트 } } } return null; } </pre> <br /> 이후 이것을 EXECUTE_READWRITE 권한을 갖는 별도의 메모리를 VirtualAlloc으로 할당받아 보관해 두는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0000000001CB0000 89 54 24 10 mov dword ptr [rsp+10h],edx 0000000001CB0004 4C 8B DC mov r11,rsp 0000000001CB0007 53 push rbx 0000000001CB0008 56 push rsi 0000000001CB0009 57 push rdi 0000000001CB000A 48 81 EC 80 00 00 00 sub rsp,80h ...[이하 쓰레기 영역]... </pre> <br /> 이러한 처리와 함께 "sub rsp, 80h" 명령어 실행 후에는 당연히 원본 SleepEx의,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 00007FFCF22E6890 48 B8 10 0C F5 7C FC 7F 00 00 mov rax,7FFC7CF50C10h 00007FFCF22E689A FF E0 jmp rax 00007FFCF22E689C EC in al,dx 00007FFCF22E689D 80 00 00 add byte ptr [rax],0 <span style='color: blue; font-weight: bold'>00007FFCF22E68A</span>0 00 <span style='color: blue; font-weight: bold'>8B DA</span> 8B F9 49 add byte ptr [rbx+49F98BDAh],cl 00007FFCF22E68A6 C7 43 98 48 00 00 00 mov dword ptr [rbx-68h],48h 00007FFCF22E68AD C7 44 24 38 01 00 00 00 mov dword ptr [rsp+38h],1 00007FFCF22E68B5 33 C0 xor eax,eax ...[생략]... </pre> <br /> 00007FFCF22E68A1 주소로 (여기서 "8B DA"는 다시 예전의 "mov ebx,edx"로써 해석되기 시작하므로) 점프해 들어가야 합니다. 그렇다면 VirtualAlloc에 쓴 코드의 뒷부분에 다음과 같은 식의 코드를 덧붙이면 되는 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0000000001CB0000 89 54 24 10 mov dword ptr [rsp+10h],edx 0000000001CB0004 4C 8B DC mov r11,rsp 0000000001CB0007 53 push rbx 0000000001CB0008 56 push rsi 0000000001CB0009 57 push rdi 0000000001CB000A 48 81 EC 80 00 00 00 sub rsp,80h <span style='color: blue; font-weight: bold'>0000000001CB0011 48 B8 A1 68 2E F2 FC 7F 00 00 mov rax,7FFCF22E68A1h 0000000001CB001B FF E0 jmp rax</span> </pre> <br /> 이렇게 기존 SleepEx 함수의 일부와 원래 함수로 점프하는 코드를 포함한 영역을 가리키는 delegate를 반환하도록 TrampolinePatch 타입에 메서드 하나를 추가해 주면,<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 { // ...[생략]... public T GetOriginalFunc() { List<byte> newJump = new List<byte>(); newJump.AddRange(_oldCode); _originalCode = new <a target='tab' href='https://github.com/stjeong/DotNetSamples/blob/master/WinConsole/PEFormat/DetourFunc/Trampoline/MachineCodeGen.cs'>MachineCodeGen</a><T>(); IntPtr fromAddress = _originalCode.Alloc(newJump.Count + NativeMethods.<a target='tab' href='https://www.sysnet.pe.kr/2/0/12147'>MaxLengthOpCode</a> * 2); byte[] jumpCode = GetJumpToCode(fromAddress, newJump.Count, _fromMethodAddress + _oldCode.Length); newJump.AddRange(jumpCode); return _originalCode.GetFunc(newJump.ToArray()) as T; } // ...[생략]... } </pre> <br /> 구현은 대충 마무리가 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <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.9 // 소스 코드: 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 /> 다음과 같은 식으로 SleepEx 함수를 .NET 메서드로 대체 및 원본 함수를 부를 수 있는 코드를 작성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using DetourFunc; using DetourFunc.Clr; using System; using System.Runtime.InteropServices; namespace ConsoleApp1 { public delegate void SleepExDelegate(int milliseconds, bool bAlertable); class Program { [DllImport("kernel32.dll")] public static extern void SleepEx(int milliseconds, bool bAlertable); <span style='color: blue; font-weight: bold'>static SleepExDelegate s_originalSleepExFunc;</span> static void Main(string[] _) { IntPtr <span style='color: blue; font-weight: bold'>sleepPtr</span> = MethodReplacer.GetExportFunctionAddress(<span style='color: blue; font-weight: bold'>"kernel32.dll", "SleepEx"</span>, out var _); IntPtr <span style='color: blue; font-weight: bold'>ptrBodyReplaceMethod</span>; { SleepExDelegate action = <span style='color: blue; font-weight: bold'>Replaced_TestMethod</span>; MethodDesc mdReplaceMethod = MethodDesc.ReadFromMethodInfo(action.Method); ptrBodyReplaceMethod = mdReplaceMethod.GetNativeFunctionPointer(); } Console.WriteLine($"Address to be patched: {sleepPtr.ToInt64():x}"); Console.WriteLine($"With this address: {ptrBodyReplaceMethod.ToInt64():x}"); using (var item = new TrampolinePatch<SleepExDelegate>()) { if (item.<span style='color: blue; font-weight: bold'>JumpPatch(sleepPtr, ptrBodyReplaceMethod)</span> == true) { <span style='color: blue; font-weight: bold'>s_originalSleepExFunc = item.GetOriginalFunc();</span> SleepEx(3000, false); } } Console.WriteLine("Press any key to exit..."); Console.ReadLine(); } <span style='color: blue; font-weight: bold'>public static void Replaced_TestMethod(int milliseconds, bool bAlertable) { Console.WriteLine($"Replaced_TestMethod called: milliseconds = {milliseconds}, bAlertable = {bAlertable}"); s_originalSleepExFunc?.Invoke(milliseconds, bAlertable); }</span> } } </pre> <br /> 위의 코드를 수행하면 SleepEx(3000, false) 호출은 우선 Replaced_TestMethod로 대체되고, 그 안에서 기존 함수의 진입 코드를 가리키는 s_originalSleepExFunc 델리게이트를 호출함으로써 원래의 SleepEx 함수까지 실행이 됩니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1558&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <a name='x86_cc'></a> <br /> 참고로, Win23 API를 C# 메서드로 대체하는 것은 x64 환경에서만 가능합니다. x86인 경우에는, Win32 API의 호출 규약이 StdCall인 반면, C# 메서드는 fastcall과 유사한 <a target='tab' href='https://www.sysnet.pe.kr/2/0/11873#tag33'>__clrcall</a>을 따르므로 인자 전달에 문제가 생깁니다.<br /> <br /> 실제로 위의 예제를 x86으로 컴파일하면 Replaced_TestMethod 안에서 Console.WriteLine으로 출력한 인자의 값이 정상적으로 나오지 않게 됩니다. (그 순간의 ecx, edx 레지스터의 값이 출력되므로!)<br /> <br /> 따라서, 굳이 x86에서 동작시키고 싶다면 인자 전달을 포기하고 아예 새롭게 호출하거나,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public static void Replaced_TestMethod(int milliseconds, bool bAlertable) { Console.WriteLine($"Replaced_TestMethod called: milliseconds = {milliseconds}, bAlertable = {bAlertable}"); s_originalSleepExFunc?.Invoke(<span style='color: blue; font-weight: bold'>3000, false</span>); } </pre> <br /> 아니면 C# 메서드를 호출 규약을 맞춰 DllExport시킨,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/11052'>https://www.sysnet.pe.kr/2/0/11052</a> C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12118'>https://www.sysnet.pe.kr/2/0/12118</a> C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12120'>https://www.sysnet.pe.kr/2/0/12120</a> </pre> <br /> 함수로 우회시켜야만 합니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
2207
(왼쪽의 숫자를 입력해야 합니다.)