Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 6개 있습니다.)
(시리즈 글이 5개 있습니다.)
DDK: 3. NT Legacy 드라이버를 이용하여 C#에서 Port 입출력
; https://www.sysnet.pe.kr/2/0/932

DDK: 5. NT Legacy 드라이버: 프로세스(EXE) 생성/제거 모니터링
; https://www.sysnet.pe.kr/2/0/937

DDK: 6. ZwTerminateProcess로 프로세스를 종료하는 Device Driver 프로그램
; https://www.sysnet.pe.kr/2/0/2904

DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제
; https://www.sysnet.pe.kr/2/0/12103

DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램
; https://www.sysnet.pe.kr/2/0/12104




커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램

아래의 글을 쓰다 보니,

User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
; https://www.sysnet.pe.kr/2/0/12102

커널 메모리 값을 읽기 위해 windbg의 로컬 커널 디버그 모드로 진입해야 하고, 다시 이것을 위해 UEFI Secure Boot도 꺼야 하고 "bcdedit -debug on"를 실행해 재부팅까지 해야 하는 불편함이 있습니다. 그래서 이런 불편함을 덜기 위해 ^^ 그냥 커널 메모리를 읽고 쓸 수 있는 device driver를 만들었습니다.

단순히 메모리를 읽고 쓰는 거라 예전에 만들어 둔,

NT Legacy 드라이버를 이용하여 C#에서 Port 입출력
; https://www.sysnet.pe.kr/2/0/932

소스 코드와 별반 다르지 않게 구현할 수 있었는데요. 게다가 Visual Studio 2019 + WDK의 도움으로 다음과 같이 금방 만들 수 있었습니다.

...[생략]...

NTSTATUS MajorDeviceControl(PDEVICE_OBJECT pDeviceObject, PIRP pIrp)
{
    PIO_STACK_LOCATION  irpSp;
    NTSTATUS            ntStatus = STATUS_SUCCESS;

    ULONG               inBufLength;   /* Input buffer length */
    ULONG               outBufLength;  /* Output buffer length */
    PVOID               ioBuffer;

    ULONG ptrSize = sizeof(void*);

    irpSp = IoGetCurrentIrpStackLocation(pIrp);

    inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
    outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;

    ioBuffer = pIrp->AssociatedIrp.SystemBuffer;
    pIrp->IoStatus.Information = 0; /* Output Buffer Size */

    PMEMORYIO_DEVICE_EXTENSION deviceExtension = (PMEMORYIO_DEVICE_EXTENSION)pDeviceObject->DeviceExtension;

    switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
    {
        case IOCTL_READ_MEMORY:
            if (inBufLength != ptrSize)
            {
                ntStatus = STATUS_BUFFER_TOO_SMALL;
            }
            else
            {
                PUCHAR ptr = BytesToPtr(ioBuffer);
                deviceExtension->Position = ptr;

                /*
                PUCHAR outBuffer = (PUCHAR)ioBuffer;
                for (ULONG i = 0; i < outBufLength; i++)
                {
                    outBuffer[i] = *(ptr + i);
                }
                */

                MM_COPY_ADDRESS address;
                address.VirtualAddress = ptr;

                SIZE_T numberOfBytesTransferred = 0;
                MmCopyMemory(ioBuffer, address, outBufLength, MM_COPY_MEMORY_VIRTUAL, &numberOfBytesTransferred);

                pIrp->IoStatus.Information = numberOfBytesTransferred;
                ntStatus = STATUS_SUCCESS;
            }
            break;

        ...[생략]...

        case IOCTL_WRITE_MEMORY:
            if (inBufLength == 0)
            {
                ntStatus = STATUS_BUFFER_TOO_SMALL;
            }
            else
            {
                PUCHAR ptr = deviceExtension->Position;

                /*
                PUCHAR inBuffer = (PUCHAR)ioBuffer;
                for (ULONG i = 0; i < inBufLength; i++)
                {
                    *(ptr + i) = inBuffer[i];
                }
                */

                MM_COPY_ADDRESS address;
                address.VirtualAddress = ioBuffer;

                SIZE_T numberOfBytesTransferred = 0;
                MmCopyMemory(ptr, address, inBufLength, MM_COPY_MEMORY_VIRTUAL, &numberOfBytesTransferred);
                pIrp->IoStatus.Information = numberOfBytesTransferred;

                ntStatus = STATUS_SUCCESS;
            }
            break;

        default:
            ntStatus = STATUS_UNSUCCESSFUL;
            break;
    }

    pIrp->IoStatus.Status = ntStatus;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return ntStatus;
}

...[생략]...

테스트도 해봐야겠지요. ^^ 클라이언트는 C#으로 DeviceIoControl을 DllImport로 연결하면 다음과 같이 쉽게 구현할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Net;

namespace MemoryIOLib
{
    class MemoryIOLib : IDisposable
    {
        const uint IOCTL_READ_MEMORY = 0x9c402410;
        const uint IOCTL_WRITE_MEMORY = 0x9c402414;
        const uint IOCTL_GETPOS_MEMORY = 0x9c402418;
        const uint IOCTL_SETPOS_MEMORY = 0x9c40241c;

        SafeFileHandle fileHandle;

        public MemoryIOLib()
        {
            InitializeDevice();
        }
            
        public bool InitializeDevice()
        {
            Dispose();

            fileHandle = Kernel32.CreateFile(@"\\.\KernelMemoryIO", NativeFileAccess.FILE_GENERIC_READ,
                NativeFileShare.NONE, IntPtr.Zero, NativeFileMode.OPEN_EXISTING, NativeFileFlag.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);

            if (fileHandle.IsInvalid == true)
            {
                return false;
            }

            return true;
        }

        public void Dispose()
        {
            if (fileHandle != null)
            {
                fileHandle.Close();
                fileHandle = null;
            }
        }

        public bool IsInitialized
        {
            get
            {
                return fileHandle != null && fileHandle.IsInvalid == false;
            }
        }

        public IntPtr Position
        {
            get
            {
                if (this.IsInitialized == false)
                {
                    return IntPtr.Zero;
                }

                byte[] addressBytes = new byte[IntPtr.Size];
                int pBytesReturned;

                if (Kernel32.DeviceIoControl(fileHandle, IOCTL_GETPOS_MEMORY,
                    null, 0, addressBytes, addressBytes.Length, out pBytesReturned, IntPtr.Zero) == true)
                {
                    if (IntPtr.Size == 8)
                    {
                        return new IntPtr(BitConverter.ToInt64(addressBytes, 0));
                    }

                    return new IntPtr(BitConverter.ToInt32(addressBytes, 0));
                }

                return IntPtr.Zero;
            }
            set
            {
                if (this.IsInitialized == false)
                {
                    return;
                }

                byte[] addressBytes = null;

                if (IntPtr.Size == 8)
                {
                    addressBytes = BitConverter.GetBytes(value.ToInt64());
                }
                else
                {
                    addressBytes = BitConverter.GetBytes(value.ToInt32());
                }

                int pBytesReturned;

                Kernel32.DeviceIoControl(fileHandle, IOCTL_SETPOS_MEMORY, addressBytes, addressBytes.Length, null, 9,
                    out pBytesReturned, IntPtr.Zero);
            }
        }

        internal int WriteMemory(IntPtr ptr, byte[] buffer)
        {
            if (this.IsInitialized == false)
            {
                return 0;
            }

            this.Position = ptr;
            int pBytesReturned;

            if (Kernel32.DeviceIoControl(fileHandle, IOCTL_WRITE_MEMORY, buffer, buffer.Length,
                null, 0, out pBytesReturned, IntPtr.Zero) == true)
            {
                return pBytesReturned;
            }

            return 0;
        }

        public int ReadMemory(IntPtr position, byte [] buffer)
        {
            if (this.IsInitialized == false)
            {
                return 0;
            }

            byte[] addressBytes = null;

            if (IntPtr.Size == 8)
            {
                addressBytes = BitConverter.GetBytes(position.ToInt64());
            }
            else
            {
                addressBytes = BitConverter.GetBytes(position.ToInt32());
            }

            int pBytesReturned;

            if (Kernel32.DeviceIoControl(fileHandle, IOCTL_READ_MEMORY, addressBytes, addressBytes.Length,
                buffer, buffer.Length,
                out pBytesReturned, IntPtr.Zero) == true)
            {
                return pBytesReturned;
            }

            return 0;
        }
    }
}

자, 그럼 다시 Process Explorer를 실행시키고,

Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회
; https://www.sysnet.pe.kr/2/0/12099

원하는 프로세스의 handle 목록 중에서 "Thread" 유형인 것을 찾아냅니다. 당연히 해당 핸들의 Object Address는 _ETHREAD 구조체의 주소일 것이고, 따라서 다음과 같이 코딩을 하면,

// Install-Package KernelStructOffset

using System;
using KernelStructOffset;

namespace MemoryIOLib
{
    class Program
    {
        static void Main(string[] args)
        {
            using (KernelMemoryIO memoryIO = new KernelMemoryIO())
            {
                if (memoryIO.IsInitialized == false)
                {
                    Console.WriteLine("Failed to open device");
                    return;
                }

                // 0xFFFF878274668080 from process explorer (Handles view pane: Ctrl+H)
                // It must be a Handle of Thead type.
                IntPtr ethreadPtr = new IntPtr(unchecked((long)0xFFFF878274668080));

                {
                    IntPtr clientIdPtr = ethreadPtr + 0x648;
                    byte[] buffer = new byte[16];

                    if (memoryIO.ReadMemory(clientIdPtr, buffer) != buffer.Length)
                    {
                        Console.WriteLine("failed to read");
                        return;
                    }

                    long value = BitConverter.ToInt64(buffer, 0);
                    Console.WriteLine("PID: " + value + "(" + value.ToString("x") + ")");
                    value = BitConverter.ToInt64(buffer, 8);
                    Console.WriteLine("TID: " + value + "(" + value.ToString("x") + ")");
                }
            }
        }
    }
}

/* 출력 결과

PID: 63740(f8fc)
TID: 72284(11a5c)

*/

_ETHREAD 구조체에 보관된 _CLIENT_ID 타입의 Cid 구조체 값을 접근할 수 있습니다. 즉, 사용자 모드(Ring 3) 프로그램에서 원하는 커널 메모리의 내용을 자유롭게 확인할 수 있는 것입니다.




이 글의 전체 소스 코드는 github에 올려 두었습니다.

stjeong/KernelMemoryIO
; https://github.com/stjeong/KernelMemoryIO

이 프로그램을 사용할 때 주의할 점이 있다면, 절대로 이렇게 만든 device driver를 "서명"까지 해서 제품에 포함하거나 공식적으로 배포하면 안 됩니다. 커널 메모리를 자유롭게 읽을 수 있게 만드는 것은 시스템에 심각한 보안 위협을 초래할 수 있으며 더군다나 서명까지 해서 배포하면 전 세계의 수많은 해커들이 여러분들이 배포한 sys 파일을 기반으로 더욱 나쁜 해악을 끼칠 수 있도록 도와주게 되는 것입니다.

설마 소스 코드도 없이 몰래 구현한 sys의 기능을 누가 알까...라고 안심하는 분이 있다면, 과거 capcom에서 자사의 제품에 배포한 capcom.sys에 사용자 정의 코드를 실행할 수 있는 것을 넣어두었던 것이 역공학으로 널리 ^^; 알려져,

Double KO! Capcom's Street Fighter V installs hidden rootkit on PCs
; https://www.theregister.co.uk/2016/09/23/capcom_street_fighter_v/

tandasat/ExploitCapcom
; https://github.com/tandasat/ExploitCapcom

사용되었던 사례를 간과해서는 안 될 것입니다.

그러니까,,, KernelMemoryIO는 공부나 테스트 용도로만 사용해야 합니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/9/2020]

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

비밀번호

댓글 작성자
 



2020-01-08 02시26분
[Lyn] 오옷... 요즘 올라오는 글을 보니 디버거 만드시는건가요 ...?
[guest]
2020-01-08 08시38분
@Lyn 아니... 그냥 심심해서... 요즘 딱히 뭘 해야 할지 모르겠어서 방황하는 시기입니다. ^^;
정성태
2020-07-09 11시16분
Introducing Kernel Data Protection, a new platform security technology for preventing data corruption
; https://www.microsoft.com/security/blog/2020/07/08/introducing-kernel-data-protection-a-new-platform-security-technology-for-preventing-data-corruption/
정성태
2020-08-26 10시37분
커널모드에서 유저 모드 메모리 접근하는 방법. #1
; https://killdos.tistory.com/12
정성태

... 91  92  93  94  95  96  97  98  99  [100]  101  102  103  104  105  ...
NoWriterDateCnt.TitleFile(s)
11430정성태1/10/201827720.NET Framework: 724. WPF + Direct2D 출력 C# 예제 [2]파일 다운로드1
11429정성태1/9/201818457개발 환경 구성: 348. ASP.NET Core 2.1 Preview 버전 적용 방법
11428정성태1/6/201821239개발 환경 구성: 347. WinForm 프로젝트를 WPF 프로젝트 유형으로 변경하는 방법파일 다운로드1
11427정성태1/5/201819254오류 유형: 445. vcpkg 빌드 오류 - Starting the CLR failed with HRESULT 80040153
11426정성태1/5/201828922오류 유형: 444. curl로 호출할 때 발생하는 오류 정리
11425정성태1/4/201819504개발 환경 구성: 346. ASP.NET Core Web Application을 IIS에서 호스팅하는 방법 (2)
11424정성태1/4/201819083개발 환경 구성: 345. ASP.NET Core 프로젝트를 명령행에서 빌드하는 방법
11423정성태1/3/201837330VC++: 123. 내가 만든 코드보다 OpenCV의 속도가 월등히 빠른 이유 [8]파일 다운로드2
11422정성태1/2/201827950.NET Framework: 723. C# - OpenCvSharp 사용 시 C/C++을 이용한 속도 향상 (for 루프 연산) [4]파일 다운로드1
11421정성태1/2/201819712오류 유형: 443. Visual Studio - nuget configuration is invalid
11420정성태12/30/201723850.NET Framework: 722. C# - Windows 10 운영체제의 데스크톱 앱에서 음성인식(SpeechRecognizer) 사용하는 방법 [3]파일 다운로드1
11419정성태12/23/201725999.NET Framework: 721. WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요? [2]파일 다운로드1
11418정성태12/23/201735770.NET Framework: 720. 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미 [2]파일 다운로드1
11417정성태12/22/201721618.NET Framework: 719. Task를 포함하는 async 메서드의 동작 방식 [2]
11416정성태12/21/201719289.NET Framework: 718. AsyncTaskMethodBuilder.Create() 메서드 동작 방식 [2]
11415정성태12/21/201720996.NET Framework: 717. Task를 포함하지 않는 async 메서드의 동작 방식 [6]
11414정성태12/21/201728156.NET Framework: 716. async 메서드의 void 반환 타입 사용에 대하여파일 다운로드2
11413정성태12/20/201722461개발 환경 구성: 344. 윈도우 10 - TTS 및 음성 인식을 위한 환경 설정
11412정성태12/20/201725071.NET Framework: 715. C# - Windows 10 운영체제의 데스크톱 앱에서 TTS(SpeechSynthesizer) 사용하는 방법 [1]파일 다운로드1
11411정성태12/20/201723376사물인터넷: 15. 라즈베리 파이용 C++ 프로젝트에 SSL Socket 적용
11410정성태12/20/201735657.NET Framework: 714. SSL Socket 예제 - C/C++ 서버, C# 클라이언트 [1]파일 다운로드1
11409정성태12/18/201741617VC++: 122. 오픈 소스 라이브러리를 쉽게 빌드해 주는 "C++ Package Manager for Windows: vcpkg" [7]
11408정성태12/18/201721285.NET Framework: 713. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV (2)파일 다운로드1
11407정성태12/18/201724167.NET Framework: 712. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV [1]파일 다운로드1
11406정성태12/17/201746547.NET Framework: 711. C# - OpenCvSharp의 Mat 데이터 조작 방법 [5]파일 다운로드1
11405정성태12/17/201742503.NET Framework: 710. C# - OpenCvSharp을 이용한 Webcam 영상 처리 + Direct2D [1]파일 다운로드1
... 91  92  93  94  95  96  97  98  99  [100]  101  102  103  104  105  ...