Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 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를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)

지난 글에서,

C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법
; https://www.sysnet.pe.kr/2/0/12150

Win32 API를 C# 메서드로 가로채는 방법을 Trampoline 기법을 이용해 설명했는데요. 여기서 x86에서의 "Win32 API 후킹 - Trampoline API Hooking" 글과 비교해 미흡한 점이 하나 있다면 바로 "원본 함수"에 대한 호출 구현이 안 되었다는 점입니다.

예를 들어, SleepEx와 대체 메서드의 주솟값이 각각 다음과 같은 경우,

sleepPtr == 7ffcf22e6890
ptrBodyReplaceMethod == 7ffc7cf50c10

SleepEx의 원본 코드가,

KERNELBASE!SleepEx:
00007FFCF22E6890 89 54 24 10          mov         dword ptr [rsp+10h],edx  
00007FFCF22E6894 4C 8B DC             mov         r11,rsp  
00007FFCF22E6897 53                   push        rbx  
00007FFCF22E6898 56                   push        rsi  
00007FFCF22E6899 57                   push        rdi  
00007FFCF22E689A 48 81 EC 80 00 00 00 sub         rsp,80h  
00007FFCF22E68A1 8b DA                mov         ebx,edx
00007FFCF22E68A3 8b F9                mov         edi,ecx
...[생략]...

아래와 같이 대체 메서드의 JMP 코드로 바뀐 것까지만 구현한 것입니다.

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  
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  
...[생략]...

여기서 유의할 것은, MOV/JMP의 12바이트 패치로 인해 덮어써지는 영역이 기존 코드의 Opcode 단위에 꼭 맞지는 않다는 것입니다. 즉, 기존의 바이트들 중 마지막 명령어인 sub rsp, 80h의 중간까지,

89 54 24 10
4C 8B DC
53
56
57
48 81 EC 80 00 00 00  // sub rsp, 80h

12바이트가 덧씌워졌기 때문에 이후의 명령어는 새로운 Opcode 단위로 인식이 되어 젼혀 다른 in, add, add, mov... 등의 명령어로 바뀝니다




결국, 원본 함수를 다시 호출하고 싶다면 기존 12바이트의 내용만 보관해서는 안 되고 그 12바이트가 온전하게 영향을 주는 opcode까지 모두 보관해야 합니다. 즉, 위의 경우에는 "sub rsp, 80h" 명령어 전체까지 포함해서 총 17바이트의 명령어를 보관해 두어야 하는 것입니다.

이를 위해서는 당연히 기계어 코드를 해석할 수 있어야 하고, 따라서 SharpDisasm과 같은 라이브러리를 활용해 다음과 같은 식으로 12바이트가 덮어쓰게 될 영역을 포함한 명령어를 모두 알아내는 코드가 필요합니다.

// 이 코드에서는 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;
}

이후 이것을 EXECUTE_READWRITE 권한을 갖는 별도의 메모리를 VirtualAlloc으로 할당받아 보관해 두는데,

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  
...[이하 쓰레기 영역]...

이러한 처리와 함께 "sub rsp, 80h" 명령어 실행 후에는 당연히 원본 SleepEx의,

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  
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  
...[생략]...

00007FFCF22E68A1 주소로 (여기서 "8B DA"는 다시 예전의 "mov ebx,edx"로써 해석되기 시작하므로) 점프해 들어가야 합니다. 그렇다면 VirtualAlloc에 쓴 코드의 뒷부분에 다음과 같은 식의 코드를 덧붙이면 되는 것입니다.

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  
0000000001CB0011 48 B8 A1 68 2E F2 FC 7F 00 00 mov         rax,7FFCF22E68A1h  
0000000001CB001B FF E0                jmp         rax  

이렇게 기존 SleepEx 함수의 일부와 원래 함수로 점프하는 코드를 포함한 영역을 가리키는 delegate를 반환하도록 TrampolinePatch 타입에 메서드 하나를 추가해 주면,

public sealed class TrampolinePatch<T> : IDisposable where T : Delegate
{
    // ...[생략]...

    public T GetOriginalFunc()
    {
        List<byte> newJump = new List<byte>();
        newJump.AddRange(_oldCode);

        _originalCode = new MachineCodeGen<T>();
        IntPtr fromAddress = _originalCode.Alloc(newJump.Count + NativeMethods.MaxLengthOpCode * 2);

        byte[] jumpCode = GetJumpToCode(fromAddress, newJump.Count, _fromMethodAddress + _oldCode.Length);
        newJump.AddRange(jumpCode);

        return _originalCode.GetFunc(newJump.ToArray()) as T;
    }
    // ...[생략]...
}

구현은 대충 마무리가 됩니다.




이 모든 것을 종합해 DetourFunc 라이브러리를 NuGet에 올렸으니,

Install-Package DetourFunc -Version 1.0.9

// 소스 코드: github - https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/PEFormat/DetourFunc

다음과 같은 식으로 SleepEx 함수를 .NET 메서드로 대체 및 원본 함수를 부를 수 있는 코드를 작성할 수 있습니다.

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);

        static SleepExDelegate s_originalSleepExFunc;
        static void Main(string[] _)
        {
            IntPtr sleepPtr = MethodReplacer.GetExportFunctionAddress("kernel32.dll", "SleepEx", out var _);

            IntPtr ptrBodyReplaceMethod;

            {
                SleepExDelegate action = Replaced_TestMethod;
                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.JumpPatch(sleepPtr, ptrBodyReplaceMethod) == true)
                {
                    s_originalSleepExFunc = item.GetOriginalFunc();
                    SleepEx(3000, false);
                }
            }

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }

        public static void Replaced_TestMethod(int milliseconds, bool bAlertable)
        {
            Console.WriteLine($"Replaced_TestMethod called: milliseconds = {milliseconds}, bAlertable = {bAlertable}");

            s_originalSleepExFunc?.Invoke(milliseconds, bAlertable);
        }
    }
}

위의 코드를 수행하면 SleepEx(3000, false) 호출은 우선 Replaced_TestMethod로 대체되고, 그 안에서 기존 함수의 진입 코드를 가리키는 s_originalSleepExFunc 델리게이트를 호출함으로써 원래의 SleepEx 함수까지 실행이 됩니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




참고로, Win23 API를 C# 메서드로 대체하는 것은 x64 환경에서만 가능합니다. x86인 경우에는, Win32 API의 호출 규약이 StdCall인 반면, C# 메서드는 fastcall과 유사한 __clrcall을 따르므로 인자 전달에 문제가 생깁니다.

실제로 위의 예제를 x86으로 컴파일하면 Replaced_TestMethod 안에서 Console.WriteLine으로 출력한 인자의 값이 정상적으로 나오지 않게 됩니다. (그 순간의 ecx, edx 레지스터의 값이 출력되므로!)

따라서, 굳이 x86에서 동작시키고 싶다면 인자 전달을 포기하고 아예 새롭게 호출하거나,

public static void Replaced_TestMethod(int milliseconds, bool bAlertable)
{
    Console.WriteLine($"Replaced_TestMethod called: milliseconds = {milliseconds}, bAlertable = {bAlertable}");
    s_originalSleepExFunc?.Invoke(3000, false);
}

아니면 C# 메서드를 호출 규약을 맞춰 DllExport시킨,

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법
; https://www.sysnet.pe.kr/2/0/11052

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12118

C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)
; https://www.sysnet.pe.kr/2/0/12120

함수로 우회시켜야만 합니다.




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

[연관 글]






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

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

비밀번호

댓글 작성자
 




... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11255정성태7/18/201724282디버깅 기술: 89. Win32 Debug CRT Heap Internals의 0xBAADF00D 표시 재현 [1]파일 다운로드3
11254정성태7/17/201720685개발 환경 구성: 322. "Visual Studio Emulator for Android" 에뮬레이터를 "Android Studio"와 함께 쓰는 방법
11253정성태7/17/201721298Math: 21. "Coding the Matrix" 문제 2.5.1 풀이 [1]파일 다운로드1
11252정성태7/13/201719074오류 유형: 411. RTVS 또는 PTVS 실행 시 Could not load type 'Microsoft.VisualStudio.InteractiveWindow.Shell.IVsInteractiveWindowFactory2'
11251정성태7/13/201718501디버깅 기술: 88. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 (2)
11250정성태7/13/201722162디버깅 기술: 87. windbg 분석 - webengine4.dll의 MgdExplicitFlush에서 발생한 System.AccessViolationException의 crash 문제 [1]
11249정성태7/12/201719875오류 유형: 410. LoadLibrary("[...].dll") failed - The specified procedure could not be found.
11248정성태7/12/201726427오류 유형: 409. pip install pefile - 'cp949' codec can't decode byte 0xe2 in position 208687: illegal multibyte sequence
11247정성태7/12/201720744오류 유형: 408. SqlConnection 객체 생성 시 무한 대기 문제파일 다운로드1
11246정성태7/11/201718790VS.NET IDE: 118. Visual Studio - 다중 폴더에 포함된 파일들에 대한 "Copy to Output Directory"를 한 번에 설정하는 방법
11245정성태7/10/201724556개발 환경 구성: 321. Visual Studio Emulator for Android 소개 [2]
11244정성태7/10/201724733오류 유형: 407. Visual Studio에서 ASP.NET Core 실행할 때 dotnet.exe 프로세스의 -532462766 오류 발생 [1]
11243정성태7/10/201721516.NET Framework: 666. dotnet.exe - 윈도우 운영체제에서의 .NET Core 버전 찾기 규칙
11242정성태7/8/201721072제니퍼 .NET: 27. 제니퍼 닷넷 적용 사례 (7) - 노후된 스토리지 장비로 인한 웹 서비스 Hang (멈춤) 현상
11241정성태7/8/201719728오류 유형: 406. Xamarin 빌드 에러 XA5209, APT0000
11240정성태7/7/201723491.NET Framework: 665. ClickOnce를 웹 브라우저를 이용하지 않고 쿼리 문자열을 전달하면서 실행하는 방법 [3]파일 다운로드1
11239정성태7/6/201724170.NET Framework: 664. Protocol Handler - 웹 브라우저에서 데스크톱 응용 프로그램을 실행하는 방법 [5]파일 다운로드1
11238정성태7/6/201721656오류 유형: 405. NT 서비스 시작 시 "Error 1067: The process terminated unexpectedly." 오류 발생 [2]
11237정성태7/5/201723331.NET Framework: 663. C# - PDB 파일 경로를 PE 파일로부터 얻는 방법파일 다운로드1
11236정성태7/4/201727059.NET Framework: 662. C# - VHD/VHDX 가상 디스크를 마운트하지 않고 파일을 복사하는 방법파일 다운로드1
11235정성태6/29/201721249Math: 20. Matlab/Octave로 Gram-Schmidt 정규 직교 집합 구하는 방법
11234정성태6/29/201718762오류 유형: 404. SharePoint 2013 설치 과정에서 "The username is invalid The account must be a valid domain account" 오류 발생
11233정성태6/28/201718640오류 유형: 403. SharePoint Server 2013을 Windows Server 2016에 설치할 때 .NET 4.5 설치 오류 발생
11232정성태6/28/201719548Windows: 144. Windows Server 2016에 Windows Identity Extensions을 설치하는 방법
11231정성태6/28/201719501디버깅 기술: 86. windbg의 mscordacwks DLL 로드 문제 - 세 번째 이야기 [1]
11230정성태6/28/201718904제니퍼 .NET: 26. 제니퍼 닷넷 적용 사례 (6) - 잦은 Recycle 문제
... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...