Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification)

Patch Guard가 보호한다는,

Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례
; https://www.sysnet.pe.kr/2/0/12110

ActiveProcessLinks를 변조해 실제로 프로세스를 숨겨보겠습니다. ^^ 사실 방법은 이미 대충 설명했습니다.

지난 글에서,

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

KernelMemoryIO 드라이버를 이용해 _ETHREAD와 _EPROCESS에 접근하는 방법을 다뤘습니다. 물론, _ETHREAD의 값을 알기 위해 Process Explorer로 Object Address를 구하긴 했지만, 사실 이것도 핸들을 열람하기만 하면 되므로,

C# - 프로세스의 모든 핸들을 열람
; https://www.sysnet.pe.kr/2/0/12080

위에서 만들어 둔 WindowsHandleInfo 타입을 이용해 다음과 같이 현재 프로세스의 스레드로부터 _ETHREAD 주소를 구할 수 있습니다.

// Install-Package KernelStructOffset

using (WindowsHandleInfo whi = new WindowsHandleInfo())
{
    for (int i = 0; i < whi.HandleCount; i++)
    {
        var she = whi[i];

        if (she.OwnerPid != processId)
        {
            continue;
        }

        string objName = she.GetName(out string handleTypeName);
        if (handleTypeName == "Thread")
        {
            // she.ObjectPointer
        }
    }
}

따라서, 이런 것들을 종합해 지난 "커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램" 글의 예제를 다음과 같이 구현할 수 있습니다.

// Install-Package KernelStructOffset

using KernelStructOffset;
using System;
using System.Diagnostics;

namespace HideProcess
{
    class Program
    {
        // Prerequisite:
        //  Register and start "KernelMemoryIO" kernel driver
        //  https://github.com/stjeong/KernelMemoryIO/tree/master/KernelMemoryIO
        //
        // sc create "KernelMemoryIO" binPath= "D:\Debug\KernelMemoryIO.sys" type= kernel start= demand
        // sc delete "KernelMemoryIO"
        // net start KernelMemoryIO
        // net stop KernelMemoryIO

        static void Main(string[] args)
        {
            int processId = Process.GetCurrentProcess().Id;
            Console.WriteLine($"ThisPID: {processId}");

            IntPtr ethreadPtr = GetEThread();
            if (ethreadPtr == IntPtr.Zero)
            {
                Console.WriteLine("THREAD handle not found");
                return;
            }

            Console.WriteLine($"_ETHREAD address: {ethreadPtr.ToInt64():x}");
            Console.WriteLine();

            var ethreadOffset = DbgOffset.Get("_ETHREAD");

            using (KernelMemoryIO memoryIO = new KernelMemoryIO())
            {
                if (memoryIO.IsInitialized == false)
                {
                    Console.WriteLine("Failed to open device");
                    return;
                }

                {
                    /*
                        2.2.2.16.2.1 CLIENT_ID
                        ; https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsts/a11e7129-685b-4535-8d37-21d4596ac057

                         typedef struct _CLIENT_ID {
                           HANDLE UniqueProcess;
                           HANDLE UniqueThread;
                         } CLIENT_ID;
                    */

                    // +0x648 Cid : _CLIENT_ID
                    IntPtr clientIdPtr = ethreadOffset.GetPointer(ethreadPtr, "Cid"); // windows 10 1909 == 0x648;
                    _CLIENT_ID cid = memoryIO.ReadMemory<_CLIENT_ID>(clientIdPtr);

                    Console.WriteLine($"PID: {cid.Pid} ({cid.Pid:x})");
                    Console.WriteLine($"TID: {cid.Tid} ({cid.Tid:x})");
                }
            }
        }

        private static IntPtr GetEThread()
        {
            int processId = Process.GetCurrentProcess().Id;

            using (WindowsHandleInfo whi = new WindowsHandleInfo())
            {
                for (int i = 0; i < whi.HandleCount; i++)
                {
                    var she = whi[i];

                    if (she.OwnerPid != processId)
                    {
                        continue;
                    }

                    string objName = she.GetName(out string handleTypeName);
                    if (handleTypeName == "Thread")
                    {
                        return she.ObjectPointer;
                    }
                }
            }

            return IntPtr.Zero;
        }
    }
}




본론으로 돌아와서, 프로세스를 숨기는 것도 지난번에 소개한 DLL 숨기는 방법과 유사합니다.

C# - PEB를 조작해 로드된 DLL을 숨기는 방법
; https://www.sysnet.pe.kr/2/0/12105

윈도우는 현재 실행 중인 프로세스에 대한 목록을 내부적으로 커널 영역에서 이중 연결 리스트로 관리하고 있으며 따라서 이 연결을 끊어주면 프로세스는 숨게 되는 것입니다. 지난 글에 소개한 링크들이 바로 그와 관련해 설명하고 있습니다.

ActiveProcessLinks---hide/src/hide.cpp
; https://github.com/irp/ActiveProcessLinks---hide/blob/master/src/hide.cpp

AntiForensics techniques : Process hiding in Kernel Mod
; https://www.cert-devoteam.fr/en/antiforensics-techniques-process-hiding-in-kernel-mode/

여기서 우리가 다뤄야 할 연결 리스트는 _EPROCESS의 ActiveProcessLinks이므로,

0:007> dt _EPROCESS
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : Ptr64 Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY
    ...[생략]...

DbgOffset을 이용해 다음과 같이 제법 우아하게 ^^ 구할 수 있습니다.

{
    // +0x220 Process : Ptr64 _KPROCESS
    IntPtr processPtr = kthreadOffset.GetPointer(ethreadPtr, "Process");
    IntPtr eprocessPtr = memoryIO.ReadMemory(processPtr);
    IntPtr activeProcessLinksPtr = eprocessOffset.GetPointer(eprocessPtr, "ActiveProcessLinks");

    _LIST_ENTRY entry = memoryIO.ReadMemory<_LIST_ENTRY>(activeProcessLinksPtr);
    Console.WriteLine($"entry.Flink: {entry.Flink.ToInt64():x}");
    Console.WriteLine($"entry.Blink: {entry.Blink.ToInt64():x}");
}

/* 출력 결과
dll.Flink: ffff850956caa370
dll.Blink: ffff850953e46370
*/

여기까지 구했으면, 이제 남은 작업은 커널 메모리 공간에서의 연결 리스트를 끊고,

private unsafe static IntPtr Unlink(KernelMemoryIO memoryIO, IntPtr linkPtr)
{
    _LIST_ENTRY entry = memoryIO.ReadMemory<_LIST_ENTRY>(linkPtr);

    IntPtr pNext = entry.Flink;
    IntPtr pPrev = entry.Blink;

    memoryIO.WriteMemory<IntPtr>(pNext + sizeof(IntPtr) /* pNext.Blink */, pPrev);
    memoryIO.WriteMemory<IntPtr>(pPrev + 0              /* pPrev.Flink */, pNext);

    memoryIO.WriteMemory<IntPtr>(linkPtr + sizeof(IntPtr) /* linkPtr.Blink */, linkPtr);
    memoryIO.WriteMemory<IntPtr>(linkPtr + 0              /* linkPtr.Flink */, linkPtr);

    return linkPtr;
}

IntPtr deletedEntry = Unlink(memoryIO, activeProcessLinksPtr);

다시 이어붙이는 작업을 구현하면 됩니다.

RestoreLink(memoryIO, deletedEntry);

private unsafe static void RestoreLink(KernelMemoryIO memoryIO, IntPtr deletedLink)
{
    if (deletedLink == IntPtr.Zero)
    {
        return;
    }

    // WindowsHandleInfo를 이용해 다른 프로세스의 ethreadPtr을 찾아 ActiveProcessLinks를 반환
    IntPtr baseLink = GetActiveProcessLinksFromAnotherProcess(memoryIO);
    if (baseLink == IntPtr.Zero)
    {
        Console.WriteLine("Can't find an appropriate ActiveProcessLinks");
        return;
    }

    Console.WriteLine($"Restore {deletedLink.ToInt64():x} to {baseLink.ToInt64():x}");
    _LIST_ENTRY baseEntry = memoryIO.ReadMemory<_LIST_ENTRY>(baseLink);

    // 다른 프로세스의 ActiveProcessLinks에 숨겨두었던 프로세스의 ActiveProcessLinks를 연결
    IntPtr nextItem = baseEntry.Flink;

    memoryIO.WriteMemory<IntPtr>(deletedLink + 0              /* deletedLink.Flink */, nextItem);
    memoryIO.WriteMemory<IntPtr>(deletedLink + sizeof(IntPtr) /* deletedLink.Blink */, baseLink);

    memoryIO.WriteMemory<IntPtr>(baseLink + 0               /* baseLink.Flink */, deletedLink);
    memoryIO.WriteMemory<IntPtr>(nextItem + sizeof(IntPtr)  /* nextItem.Blink */, deletedLink);
}

와~~~ ^^ 이제 Unlink 호출 후 Console.ReadLine()을 찍어 프로세스가 없어진 것을 (작업 관리자에서) 확인하고,

hide_process_2.png

다시 RestoreLink를 호출해 복원할 수 있습니다.

참고로, 이 글의 완전한 소스 코드는 다음의 github에 올려 두었습니다.

DotNetSamples/WinConsole/Debugger/HideProcess/
; https://github.com/stjeong/DotNetSamples/tree/master/WinConsole/Debugger/HideProcess




Process Explorer로 HideProcess.exe를 테스트해 보면 재미있는 사실을 하나 알 수 있습니다. 이 프로그램을 cmd.exe를 통해 실행하는 경우, 자식 프로세스인 conhost.exe에는 HideProcess.exe에 대해 Process 핸들을 하나 열게 됩니다. 그렇다면 그 Process 핸들은 해당 프로세스가 사라진 다음 어떻게 되는 걸까요?

hide_process_1.png

보는 바와 같이 "<Non-existent Process>"로 나옵니다.

하지만, Object Address도 있고 Pid도 구해 오는 걸로 봐서 EPROCESS 자료 구조에 대한 접근은 여전히 할 수 있는 걸로 나옵니다. 사실 EPROCESS에도 파일명을 구할 수 있는 필드가 있는데,

lkd> dt _EPROCESS
...[생략]...
+0x448 ImageFilePointer : Ptr64 _FILE_OBJECT
+0x450 ImageFileName    : [15] UChar
...[생략]...

Process Explorer는 저 필드를 사용해 프로세스 이름을 보여주지는 않는 것입니다.

그리고 프로세스를 숨겨 둔 상태를 일정 시간 지속하면 "Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례" 글에 설명한 대로 BSOD가 발생하게 됩니다. 반면, 숨겨두었다가 다시 복원을 하는 경우 ActiveProcessLinks의 순서가 바뀌었지만 BSOD는 발생하지 않는 걸로 봐서 순서까지는 체크하지 않는 것으로 보입니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/12/2024]

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

비밀번호

댓글 작성자
 



2020-02-20 11시39분
정성태
2020-08-13 07시06분
[윈공부] 안녕하세요. 좋은 글 잘 읽고 있습니다.
다름이 아니라 아래 부분에서 무조건 FALSE 로 반환이 되는데요.
if (memoryIO.IsInitialized == false)

이 부분에 대해서 제가 놓친 부분이나 참고할만한 내용이 있으면 힌트 부탁드립니다^^
감사합니다.
[guest]
2020-08-13 10시32분
위의 소스 코드에 정리해 둔 "KernelMemoryIO" 디바이스 드라이버를 등록하셨나요?
정성태
2020-08-14 09시54분
[윈공부] https://www.sysnet.pe.kr/2/0/12104
위 글을 참고하면 될까요? 저는 단순히 이 예제의 HideProcess 코드만 따와서 실행만 해봤습니다^^;;

아래 소스를 다운로드 받아서 컴파일 후 실행해볼께요.
https://github.com/stjeong/KernelMemoryIO


답변 감사합니다~!!!
[guest]
2020-08-14 10시12분
넵, 그 device driver가 필요합니다. ^^
정성태
2020-08-14 12시25분
[윈공부] 죄송한데요... 질문 하나만 더 드릴께요ㅠㅠ;
Device driver does not install on any devices, use primitive driver if this is intended.

이렇게 에러가 나오는데.. inf 파일을 어떻게 수정해야 할까요?
사실 일반 개발잔데 이쪽 분야는 지식이 전무하다보니 하나하나 물어보게 되네요.
감사합니다.
[guest]
2020-08-14 01시11분
해당 드라이버는 INF를 이용한 등록이 아니기 때문에 그걸 수정해야 할 필요는 없습니다. 빌드만 잘 돼서 .sys 파일이 나왔다면 sc를 이용해 등록/시작만 하면 됩니다. 이 글의 C# 소스 코드에서 "// Prerequisite:" 주석을 참고하세요.
정성태
2020-08-14 02시02분
[윈공부] Windows 10 Pro, Visual Studio 2019 Community 환경입니다.
컴파일을 하면... 아래와 같은 출력 메시지가 나와요...
1>------ 빌드 시작: 프로젝트: KernelMemoryIO, 구성: Debug x64 ------
1>Building 'KernelMemoryIO' with toolset 'WindowsKernelModeDriver10.0' and the 'Desktop' target platform.
1>Stamping x64\Debug\KernelMemoryIO.inf
1>Stamping [Version] section with DriverVer=08/14/2020,13.59.25.778
1>C:\Users\ngmas\Desktop\KernelMemoryIO-master\KernelMemoryIO-master\KernelMemoryIO\KernelMemoryIO.inf : error 1297: Device driver does not install on any devices, use primitive driver if this is intended.
1>C:\Users\ngmas\Desktop\KernelMemoryIO-master\KernelMemoryIO-master\KernelMemoryIO\KernelMemoryIO.inf(5-5): warning 1324: [Version] section should specify PnpLockdown=1.
1>"KernelMemoryIO.vcxproj" 프로젝트를 빌드했습니다. - 실패
========== 빌드: 성공 0, 실패 1, 최신 0, 생략 0 ==========

어디가 문제인지를 구글 검색해봐도... 잘 모르겠습니다.
도움 부탁드립니다.
계속 시간 뺏게 되어 죄송합니다.
[guest]
2020-08-14 02시51분
아래의 글을 참고하세요. (아무래도 최근 WDK부터 바뀐 듯합니다.)

Error 1297 #2067
; https://github.com/MicrosoftDocs/windows-driver-docs/issues/2067
정성태
2020-08-14 03시12분
[윈공부] 감사합니다. 관련 내용들을 검색하다가 찾게 되었습니다.
;[Manufacturer]
;%ManufacturerName%=Standard,NT$ARCH$

;[Standard.NT$ARCH$]
[DefaultInstall.NT(ARCH)]

이렇게 변경 후 빌드가 되었습니다.
이제 좀 더 공부해보도록 하겠습니다!!
감사드립니다^^
[guest]
2020-09-28 01시17분
[질문자] 안녕하세요 혹시 패치가드 끄고 1607버전에서 드라이버컴파일후 로드시키는거까진 성공했습니다
hide process실행하면 thispid와 ethpead address 잘나오는데 pid,tid가 둘다 0 (0)으로 뜹니다
혹시 버전이 달라 offset이 달라서 그런건가요??
[guest]
2020-09-28 11시58분
아래의 글에 덧글과 같은 문제인지 확인해 보세요

C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법
; https://www.sysnet.pe.kr/2/0/12098
정성태
2022-02-09 10시50분
[학생] 선생님 프로그램 끝까지 잘 실행해서 작동이 되는 것 확인했는데 글 마지막 부분에 (그리고 프로세스를 숨겨 둔 상태를 일정 시간 지속하면 "Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례" 글에 설명한 대로 BSOD가 발생하게 됩니다. )
블루 스크린이 뜨더군요. 이 부분은 해결이 불가능 한 건가요??
[guest]
2022-02-10 12시17분
그 부분은 해결할 수 없습니다. 해당 기능은 윈도우의 보안 목적으로 생긴 것이고 의도된 것입니다. 그것을 막으려면 Patch Guard를 무력화시켜야 합니다.

다음의 덧글에도 썼듯이,

https://www.sysnet.pe.kr/2/0/12110#14396

패치가드를 무력화시키는 것이 아주 없는 것도 아니지만, 그런 것들 역시 보안을 뚫는 것이라서 권장할 수는 없습니다.
정성태
2022-12-14 03시05분
[학생] 공부가 많이 되었습니다.. 어떻게 감지해서 BSOD를 발생시키는지, 몇분의 인터벌로 검사하는지 디버깅도 해보고 싶은데 실력이 한참 모자라네요
[guest]
2023-03-27 09시34분
오호~~~ 아래의 글에 재미있는 사실이 있군요. ^^

ZeroMemoryEx/Chaos-Rootkit
; https://github.com/ZeroMemoryEx/Chaos-Rootkit

Note: After removing a node from a PLIST_ENTRY, we should set the pointer to NULL to ensure that the nodes before and after the removed node are no longer pointing to it. This can prevent potential null pointer dereferences and avoid BSODs.

DKOM으로 실행 프로그램을 숨기면 BSOD가 발생하는데, 위의 글에서는 그렇게 끊은 프로세스의 Flink와 Blink에 null을 대입해 두면 BSOD가 발생하지 않는다고 합니다. 정말 그런지, 나중에 한번 테스트해봐야겠습니다. ^^

또한, System 권한의 프로세스가 가진 Token을 일반 프로세스의 Token에 덮어 써 권한 상승을 하는 방법도 설명하고 있습니다. ^^
정성태

... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13222정성태1/20/20233934개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/20234167Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234316오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20233880Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/20233811VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/20234406디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/20234658디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/20236182Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법 [1]
13214정성태1/10/20235742.NET Framework: 2087. .NET 6부터 SourceGenerator와 통합된 System.Text.Json [1]파일 다운로드1
13213정성태1/9/20235271오류 유형: 836. docker 이미지 빌드 시 "RUN apt install ..." 명령어가 실패하는 이유
13212정성태1/8/20235031기타: 85. 단정도/배정도 부동 소수점의 정밀도(Precision)에 따른 형변환 손실
13211정성태1/6/20235113웹: 42. (https가 아닌) http 다운로드를 막는 웹 브라우저
13210정성태1/5/20234134Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
13209정성태1/4/20234039Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
13208정성태1/3/20233983.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법파일 다운로드1
13207정성태12/26/20224287.NET Framework: 2085. C# - gpedit.msc의 "User Rights Assignment" 특권을 코드로 설정/해제하는 방법파일 다운로드1
13206정성태12/24/20224495.NET Framework: 2084. C# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법 [3]파일 다운로드1
13205정성태12/24/20224888.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)파일 다운로드1
13204정성태12/22/20224169.NET Framework: 2082. C# - (LSA_UNICODE_STRING 예제로) CustomMarshaler 사용법파일 다운로드1
13203정성태12/22/20224329.NET Framework: 2081. C# Interop 예제 - (LSA_UNICODE_STRING 예제로) 구조체를 C++에 전달하는 방법파일 다운로드1
13202정성태12/21/20224712기타: 84. 직렬화로 설명하는 Little/Big Endian파일 다운로드1
13201정성태12/20/20225331오류 유형: 835. PyCharm 사용 시 C 드라이브 용량 부족
13200정성태12/19/20224206오류 유형: 834. 이벤트 로그 - SSL Certificate Settings created by an admin process for endpoint
13199정성태12/19/20224493개발 환경 구성: 656. Internal Network 유형의 스위치로 공유한 Hyper-V의 VM과 호스트가 통신이 안 되는 경우
13198정성태12/18/20224373.NET Framework: 2080. C# - Microsoft.XmlSerializer.Generator 처리 없이 XmlSerializer 생성자를 예외 없이 사용하고 싶다면?파일 다운로드1
13197정성태12/17/20224308.NET Framework: 2079. .NET Core/5+ 환경에서 XmlSerializer 사용 시 System.IO.FileNotFoundException 예외 발생하는 경우파일 다운로드1
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...