Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일
 

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://docs.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는 발생하지 않는 걸로 봐서 순서까지는 체크하지 않는 것으로 보입니다.




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

[연관 글]





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

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

비밀번호

댓글 쓴 사람
 



2020-02-20 11시39분
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12203정성태3/18/202064오류 유형: 612. warning: 'C:\ProgramData/Git/config' has a dubious owner: '...'.
12202정성태3/18/202087개발 환경 구성: 486. .NET Framework 프로젝트를 위한 GitLab CI/CD Runner 구성
12201정성태3/18/202032오류 유형: 611. git-credential-manager.exe: Using credentials for username "Personal Access Token".
12200정성태3/18/2020122VS.NET IDE: 145. NuGet + Github 라이브러리 디버깅 관련 옵션 3가지 - "Enable Just My Code" / "Enable Source Link support" / "Suppress JIT optimization on module load (Managed only)"
12199정성태3/17/202035오류 유형: 610. C# - CodeDomProvider 사용 시 Unhandled Exception: System.IO.DirectoryNotFoundException: Could not find a part of the path '...\f2_6uod0.tmp'.
12198정성태3/17/202031오류 유형: 609. SQL 서버 접속 시 "Cannot open user default database. Login failed."
12197정성태3/17/2020120VS.NET IDE: 144. .NET Core 콘솔 응용 프로그램을 배포(publish) 시 docker image 자동 생성 - 두 번째 이야기
12196정성태3/17/202057오류 유형: 608. The ServicedComponent being invoked is not correctly configured (Use regsvcs to re-register).
12195정성태3/17/2020169.NET Framework: 902. C# - 프로세스의 모든 핸들을 열람 - 세 번째 이야기
12194정성태3/16/202040오류 유형: 607. PostgreSQL - Npgsql.NpgsqlException: sorry, too many clients already
12193정성태3/16/202073개발 환경 구성: 485. docker - SAP Adaptive Server Enterprise 컨테이너 실행
12192정성태3/14/202086개발 환경 구성: 484. docker - Sybase Anywhere 16 컨테이너 실행
12191정성태3/14/2020125개발 환경 구성: 483. docker - OracleXE 컨테이너 실행
12190정성태3/14/202044오류 유형: 606. Docker Desktop 업그레이드 시 "The process cannot access the file 'C:\Program Files\Docker\Docker\resources\dockerd.exe' because it is being used by another process."
12189정성태3/13/202085개발 환경 구성: 482. Facebook OAuth 처리 시 상태 정보 전달 방법과 "유효한 OAuth 리디렉션 URI" 설정 규칙
12188정성태3/14/202047Windows: 167. 부팅 시점에 실행되는 chkdsk 결과를 확인하는 방법
12187정성태3/12/202034오류 유형: 605. NtpClient was unable to set a manual peer to use as a time source because of duplicate error on '...'.
12186정성태3/12/202052오류 유형: 604. The SysVol Permissions for one or more GPOs on this domain controller and not in sync with the permissions for the GPOs on the Baseline domain controller.
12185정성태3/11/202052오류 유형: 603. The browser service was unable to retrieve a list of servers from the browser master...
12184정성태3/11/202053오류 유형: 602. Automatic certificate enrollment for local system failed (0x800706ba) The RPC server is unavailable.
12183정성태3/12/202052오류 유형: 601. Warning: DsGetDcName returned information for \\[...], when we were trying to reach [...].
12182정성태3/11/2020117.NET Framework: 901. C# Windows Forms - Vista/7 이후의 Progress Bar 업데이트가 느린 문제파일 다운로드1
12181정성태3/11/2020144기타: 76. 재현 가능한 최소한의 예제 프로젝트란? - 두 번째 예제파일 다운로드1
12180정성태3/10/202046오류 유형: 600. "Docker Desktop for Windows" - EXPOSE 포트가 LISTENING 되지 않는 문제
12179정성태3/10/202069개발 환경 구성: 481. docker - PostgreSQL 컨테이너 실행
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...