通过驱动接触win内核
2022-12-9 22:29:0 Author: bbs.pediy.com(查看原文) 阅读量:11 收藏

通过驱动接触win内核

2022-12-9 22:29 4811

windows I/O 模型 主要概念

整个结构会很复杂,比如处理IRP部分:
2girpeg

模型中我们最需要关注这三个概念:
pimpmypid_winkernel_device_objects_model

  1. I/O 请求使用 IRP 从用户空间发送到驱动程序
  2. I/O 管理器(I/O manager)为所有内核模式驱动程序提供一致的接口。
  3. 此 I/O 管理器为每个已安装和加载的驱动程序创建一个驱动程序对象( driver object)。

DRIVER_OBJECT包含许多驱动程序标准例程的入口点的存储。同样重要的是要注意这一点:当 I/O 管理器处理 IRP 时,它会将当前驱动程序的DRIVER_OBJECT内存地址提供给名为 DriverEntry 的主函数。更详细的描述参考链接1。

内核中需要了解的驱动结构和组件

了解一些重要的结构能帮助我们快速构建需要的payload

IRP structure

它是 Input/Output Request Packet 的简称,在WDM.H定义了标准的NT结构。如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

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;

        _IORING_OBJECT        *IoRing;

        struct _IORING_OBJECT *IoRing;

      };

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

我们重点关注两个元素:

  1. UserBuffer 字段 :这是从驱动程序返回的数据将被传输到客户端的地方
  2. CurrentStackLocation: 这是驱动程序将访问从客户端发送的数据的位置。

IO_STACK_LOCATION 结构

该结构体有上面提到的 CurrentStackLocation指定,其结构可以从这查看https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_io_stack_location

我们需要关注的两个结构体是:

  1. MajorFunction: 它是要执行的 I/O 操作的类型。在后面的示例中,将使用 DeviceIoControl API,它通过调用 IRP_MJ_DEVICE_CONTROL 主要函数来访问驱动程序。
  2. Parameters: 实际就是一堆win api,用来检索用户输入或者数据输入的所在位置。

DEVICE_OBJECT 结构

该结构体位于 IO_STACK_LOCATION结构中,标识驱动程序处理 I/O 请求的逻辑、虚拟或物理设备。

  1. DeviceType:标识设备类型。生成 IOCTL 控制代码时,此信息非常重要
  2. DriverObject: 指向 DRIVER_OBJECT 的指针,该指针表示输入到 DriverEntry 例程的驱动程序的加载图像。此成员由 I/O 管理器在成功调用 IoCreateDevice API 时设置。

DRIVER_OBJECT

把上面的结构体组好到一起,就是DRIVER_OBJECT

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

typedef struct _DRIVER_OBJECT {

  CSHORT             Type;

  CSHORT             Size;

  PDEVICE_OBJECT     DeviceObject;

  ULONG              Flags;

  PVOID              DriverStart;

  ULONG              DriverSize;

  PVOID              DriverSection;

  PDRIVER_EXTENSION  DriverExtension;

  UNICODE_STRING     DriverName;

  PUNICODE_STRING    HardwareDatabase;

  PFAST_IO_DISPATCH  FastIoDispatch;

  PDRIVER_INITIALIZE DriverInit;

  PDRIVER_STARTIO    DriverStartIo;

  PDRIVER_UNLOAD     DriverUnload;

  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

} DRIVER_OBJECT, *PDRIVER_OBJECT;

  1. DeviceObject : 由 IoCreateDevice 创建
  2. DriverStart :包含内核中的驱动程序内存位置
  3. DriverName: 字面意思
  4. MajorFunction: 调度表,由驱动程序的 DispatchXxx 例程的入口点数组组成

每个驱动程序都包含主要功能代码(major function codes),这些代码告诉驱动程序应执行哪些操作来满足 I/O 请求。所有驱动程序必须至少支持:

  • IRP_MJ_CREATE
  • IRP_MJ_CLOSE
  • IRP_MJ_DEVICE_CONTROL

实例

下面将给一个实际的例子,直接上demo。

核心功能将被放置在vikingdrv2DeviceControl例程中,该例程有两个参数:

  • DEVICE_OBJECT 指针
  • IRP 指针

首先把IO_STACK_LOCATION 指针存储到栈上,该结构用来接受用户发来的信息:

  1. 我们首先验证IOCTL编号
  2. 如果IOCTL是已知值,我们继续
  3. 最终我们将有效负载存储在数据变量中

大致结构如下:

1

2

3

4

5

6

7

8

9

NTSTATUS vikingdrv2DeviceControl(PDEVICE_OBJECT, PIRP Irp) {

        auto stack = IoGetCurrentIrpStackLocation(Irp);

        switch (stack->Parameters.DeviceIoControl.IoControlCode) {

            case IOCTL_number1: {

                auto data = (ThreadData*)stack->Parameters.DeviceIoControl.Type3InputBuffer;

            }

            ...

        }

    }

然后我们定义名为 DriverEntry 的主函数。如前所述,此函数获取指向DRIVER_OBJECT结构的指针。为了知道在提供IRP_MJ代码时必须执行哪个函数,驱动程序必须定义调度例程(dispatch routine)。这里最重要的是使其能够处理IRP_MJ_DEVICE_CONTROL消息:让它指向 vikingdrv2DeviceControl函数。

1

2

3

4

5

6

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {

        //调度例程 dispatch routine

        DriverObject->MajorFunction[IRP_MJ_CREATE] = vikingdrv2CreateClose;

        DriverObject->MajorFunction[IRP_MJ_CLOSE] = vikingdrv2CreateClose;

        DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = vikingdrv2DeviceControl;

    }

这就是一个简单基础的驱动实例~,确实很基础,但是能用。

写一个driver

参考自https://leanpub.com/windowskernelprogramming,写一个简单的驱动程序,需要完成三个功能:

  1. 等待 IRP
  2. 当 IRP有一个完整且正确的 IOCTL 结构的时候,展示windows版本信息。
  3. 更改给定线程的优先级(<=>用户进程)

内核方面

第一步,准备驱动和外界交互部分

驱动程序客户端和驱动程序本身必须具有“通用”说话方式。DeviceIoControl 函数将控制代码直接发送到指定的设备驱动程序,使设备执行相应的操作。这个函数有三个重要的部分:

  • 控制代码(在本例中,我们选择 IOCTL 0x800)
  • 包含我们数据的输入缓冲区(对本例中是 ThreadData,线程信息)
  • 输出缓存

1

2

3

4

5

6

7

0x800, METHOD_NEITHER, FILE_ANY_ACCESS)

struct ThreadData {

    ULONG ThreadId;

    int Priority;

};

第二步,准备内核代码处理驱动客户端发来请求

创建入口点:
DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath)

设置调度例程来处理IRP_MJ_DEVICE_CONTROL/驱动程序对象:我们将函数命名为vikingdrv2DeviceControl
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = vikingdrv2DeviceControl;

提供device namesymlink name名称,然后创建设备对象,以便客户端可以访问驱动程序并打开文件系统句柄。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\vikingdrv2"); // 驱动名

    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\vikingdrv2"); // symlink

    PDEVICE_OBJECT DeviceObject;

    NTSTATUS status = IoCreateDevice(

        DriverObject,        // 自定义的驱动对象

        0,                   

        & devName,            //  device name,

        FILE_DEVICE_UNKNOWN,    // device type,

        0,                    // characteristics flags,

        FALSE,               

        & DeviceObject        // 输出指针位置

    );

    if (!NT_SUCCESS(status)) {

        KdPrint(("Failed to create device object (0x%08X)\n", status));

        return status;

    }

现在我们有一个指向设备对象的指针,通过提供符号链接使用户模式调用方可以访问它。

1

2

3

4

5

6

status = IoCreateSymbolicLink(&symLink, &devName);

    if (!NT_SUCCESS(status)) {

        KdPrint(("Failed to create symbolic link (0x%08X)\n", status));

        IoDeleteDevice(DeviceObject);

        return status;

    }

第三步 驱动核心功能

前面是准备处理请求部分,下面是如何处理它。

首先,我们必须找到我们的堆栈位置(从驱动程序的角度来看),并确认我们的客户端/用户给了我们一个我们能够处理的 IOCTL。

1

2

3

4

NTSTATUS vikingdrv2DeviceControl(PDEVICE_OBJECT, PIRP Irp) {

    auto stack = IoGetCurrentIrpStackLocation(Irp); // IO_STACK_LOCATION*

      switch (stack->Parameters.DeviceIoControl.IoControlCode) {

          case IOCTL_PRIORITY_BOOSTER_SET_PRIORITY: {

然后我们处理缓冲区以检索客户端准备的 ThreadData 结构
auto data = (ThreadData*)stack->Parameters.DeviceIoControl.Type3InputBuffer;

最终,我们会处理客户提供的数据。这里我们修改进程的线程优先级:

1

2

3

4

5

// 通过pid找线程,

    PETHREAD Thread;

    status = PsLookupThreadByThreadId(ULongToHandle(data->ThreadId), &Thread);

    // 设定新权限

    KeSetPriorityThread((PKTHREAD)Thread, data->Priority);

客户端方面

在内核部分:驱动程序正在等待请求。现在客户端代码呢?

首先,让我们创建一个处理用户提供的参数的 main 函数。

1

2

3

4

5

int main(int argc, const char* argv[]) {

        if (argc < 3) {

            printf("Usage: Booster <threadid> <priority>\n");

            return 0;

        }

然后使用符号链接打开设备的句柄。

1

2

3

4

5

6

7

8

9

HANDLE hDevice = CreateFile(L"\\\\.\\vikingdrv2", GENERIC_WRITE,

        FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);

    if (hDevice == INVALID_HANDLE_VALUE)

        return Error("Failed to open device");

    ThreadData data;

    data.ThreadId = atoi(argv[1]);    // 第一个参数

    data.Priority = atoi(argv[2]);    // 第二个参数

现在,调用 DeviceIoControl 并在之后关闭设备句柄。

1

2

3

4

5

6

7

8

9

10

11

12

DWORD returned;

BOOL success = DeviceIoControl(hDevice,

    IOCTL_PRIORITY_BOOSTER_SET_PRIORITY,// control code

    &data, sizeof(data),                // 输入缓存和大小

    nullptr, 0,                            // 输出缓存和大小

    &returned, nullptr);

if (success)

    printf("Priority change succeeded!\n");

else

    Error("Priority change failed!");

CloseHandle(hDevice);

编译测试

编译此驱动程序会生成一个文件.sys该文件可以作为服务安装:
sc create viking_drv2 type= kernel binpath= C:\viking_driver2.sys

然后禁用签名验证,如果驱动程序已签名,则启用测试签名模式并禁用完整性检查。

1

2

3

bcdedit -debug on

bcdedit.exe -set TESTSIGNING ON

bcdedit.exe /set nointegritychecks on

重启并启动服务。

  • 启动进程资源管理器
  • 启动CMD.exe
  • 启动服务/驱动程序
  • 标识线程 ID
  • 使用驱动程序客户端修改线程优先级:从 8 到 25

这个例子参考自Windows Kernel Programming。肯定会运行的,但是问题是目的是提权,而不是修改线程优先级。

优化

寻找线程内存环境:

1

2

PsLookupProcessByProcessId((HANDLE)*pid, &process);

PsLookupProcessByProcessId((HANDLE)4, &system_process);

PsLookupProcessByProcessId API 结束时,名为 processsystem_processPEPROCESS 结构可用,并且包含获取有关请求的 PID(以及系统 pid 编号 4)的信息所需的所有内容。

获取进程 token:

1

2

targetToken = PsReferencePrimaryToken(process);

systemToken = PsReferencePrimaryToken(system_process);

上面所有操作完成之后,提权需要重新写一个客户端,先中止进程:
FindAndReplaceMember((PDWORD_PTR)process, (DWORD_PTR)targetToken, (DWORD_PTR)systemToken, MaxExpectedEprocessSize);

但是不用再启用vs,直接使用ps代替调用:

1

2

3

4

5

6

7

8

9

10

$myPID = [int]$args[0]

$driverName =  "\\.\vikingdrv2sym"

$hDevice = [KGETSYSTEMCLIENT]::CreateFile($driverName, [System.IO.FileAccess]::ReadWrite,

[System.IO.FileShare]::ReadWrite, [System.IntPtr]::Zero, 0x3, 0x40000080, [System.IntPtr]::Zero)

[KGETSYSTEMCLIENT]::DeviceIoControl($hDevice, $IOCTL_DRV_QUERY_PROPERTY,

[ref]$myPID, [System.Runtime.InteropServices.Marshal]::SizeOf($myPID), $null, 0, [ref]0, [System.IntPtr]::Zero)|Out-null

看看效果

参考链接

  1. Example I/O Request - The Details
  2. ns-wdm-_irp
  3. Kernel/Windows Driver Model
  4. Understanding the Windows I/O System (Mark E. Russinovich, Kate Chase and Alex Ionescu)
  5. Writing Dispatch Routines
  6. Pavel’s book
  7. 使用PSL fuzzing
  8. pimp-my-pid

[2022冬季班]《安卓高级研修班(网课)》月薪两万班招生中~


文章来源: https://bbs.pediy.com/thread-275480.htm
如有侵权请联系:admin#unsafe.sh