进程线程篇——线程切换(上)

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?系统调用篇学会了吗?练习做完了吗?没有的话就不要继续了。


🔒 华丽的分割线 🔒


练习及参考

本次答案均为参考,可以与我的答案不一致,但必须成功通过。代码在折叠区,而思考原因解释将会在文章内部讲解。

1️⃣ 断链进程结构体,实现隐藏,并思考为什么断链进程为什么还能够执行。

🔒 点击查看应用代码 🔒
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作码:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define _Show CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideProcess"

HANDLE g_Device;

int main(int argc, char* argv[])
{
   //获取驱动链接对象句柄
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
   if (g_Device==INVALID_HANDLE_VALUE)
   {
      puts("访问驱动符号链接失败!");
      goto endproc;
   }

   DWORD pid;
   DWORD outBuffer;
   DWORD m[2];
   DWORD re;

   puts("请输入需要隐藏程序的 pid :");
   scanf("%d",&pid);

   if (pid)
   {
      if (DeviceIoControl(g_Device,Hide,&pid,sizeof(DWORD), outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("隐藏命令发送成功,请查看程序是否隐藏……");
      }  
      puts("检查任务管理器,按任意键恢复隐藏!");  
      m[0]=GetCurrentProcessId();
      m[1]=outBuffer;
      if (DeviceIoControl(g_Device,_Show,m,sizeof(DWORD)*2, outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("恢复命令发送成功,请查看程序是否显示……");
      }
   }
   else
   {
      puts("pid 非法!");
   }

endproc:

    CloseHandle(g_Device);
    system("pause");
    return 0;
}

🔒 点击查看驱动代码 🔒
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

#define DEVICE_NAME L"\\Device\\HideProcess"
#define SYMBOL_LINK L"\\??\\HideProcess"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define Show CTL_CODE(FILE_DEVICE_UNKNOWN,0x900,METHOD_BUFFERED,FILE_ANY_ACCESS)

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteSymbolicLink(&SymbolLink);
    IoDeleteDevice(DriverObject->DeviceObject); //假设 DriverEntry 函数中的 dobj 变量声明为全局的,为什么不使用这个删除设备对象
    DbgPrint("卸载成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead[2];
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    LIST_ENTRY* lethis;
    LIST_ENTRY* fle;
    LIST_ENTRY* ble;
    PEPROCESS eProcess;

    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(uRead, pIoBuffer, 4);
            PsLookupProcessByProcessId(uRead[0], &eProcess);

            RtlMoveMemory(pIoBuffer, &eProcess, 4);
            pIrp->IoStatus.Information = 4;

            lethis = (LIST_ENTRY*)((ULONG)eProcess + 0x88);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            DbgPrint("断链成功!!!");
            break;
        case Show:

            RtlMoveMemory(uRead, pIoBuffer, 8);
            PsLookupProcessByProcessId(uRead[0], &eProcess);     //第一个成员:pid

            lethis = (LIST_ENTRY*)((ULONG)eProcess + 0x88);  //自身的pid
            ble= (LIST_ENTRY*)((ULONG)uRead[1] + 0x88);

            fle = lethis->Flink;
           
            lethis->Flink = ble;
            ble->Blink = lethis;

            ble->Flink = fle;
            fle->Blink = ble;

            DbgPrint("恢复成功!!!");
            pIrp->IoStatus.Information = 0;
            break;

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

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    PDEVICE_OBJECT pdobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pdobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("加载成功!!!");

    return STATUS_SUCCESS;
}
🔒 点击查看答案 🔒


  先给个效果图来看看:

  注释中有一个问题:假设DriverEntry函数中的dobj变量声明为全局的,为什么不使用这个删除设备对象。你可以试试,答案是不可以,会导致蓝屏,原因这个东西是使用的分页内存,而这个卸载驱动的线程是不能使用分页内存的。


2️⃣ 使用DebugPort清零实现反调试。

🔒 点击查看应用代码 🔒
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作码:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideProcess"

HANDLE g_Device;

int main(int argc, char* argv[])
{
   //获取驱动链接对象句柄
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
   if (g_Device==INVALID_HANDLE_VALUE)
   {
      puts("访问驱动符号链接失败!");
      goto endproc;
   }

   DWORD pid;
   DWORD outBuffer;
   DWORD re;
   
   puts("请输入需要反调试保护程序的 pid :");
   scanf("%d",&pid);
   
   if (pid)
   {
      if (DeviceIoControl(g_Device,Hide,&pid,sizeof(DWORD), &outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("反调试保护命令发送成功……");
      }  
   }
   else
   {
      puts("pid 非法!");
   }
   
endproc:
   
   CloseHandle(g_Device);
   system("pause");
   return 0;
}
}
🔒 点击查看驱动代码 🔒
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

ULONG* pDebugPort=NULL;
HANDLE hThread;

#define DEVICE_NAME L"\\Device\\HideProcess"
#define SYMBOL_LINK L"\\??\\HideProcess"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)

VOID HideThread(_In_ PVOID StartContext)
{
    while (1)
    {
      *pDebugPort = 0;
    }
}

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteDevice(DriverObject);
    IoDeleteSymbolicLink(&SymbolLink);
    DbgPrint("卸载成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead;
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    PEPROCESS eProcess;

    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(&uRead, pIoBuffer, 4);
            PsLookupProcessByProcessId(uRead, &eProcess);

            if (pDebugPort)
                break;

            pDebugPort = (ULONG*)((ULONG)eProcess + 0xbc);

            PsCreateSystemThread(&hThread, GENERIC_ALL, NULL, NULL, NULL, HideThread, NULL);

            DbgPrint("断链成功!!!");
            break;
    }
    pIrp->IoStatus.Information = 0;
    pIrp->IoStatus.Status = STATUS_SUCCESS;    
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    DEVICE_OBJECT dobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &dobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("加载成功!!!");

    return STATUS_SUCCESS;
}

3️⃣ 如何判断一个进程是否为GUI线程。

🔒 点击查看答案 🔒


  我预先在虚拟机开启一个DebugView,然后在WinDbug开始输入指令,遍历进程:

kd> !process 0 0
****NT ACTIVE PROCESS DUMP****
Failed to get VadRoot
PROCESS 89c258c0  SessionId: 0  Cid: 0758    Peb: 7ffd3000  ParentCid: 05f4
    DirBase: 13ac01a0  ObjectTable: e1d63088  HandleCount:  57.
    Image: Dbgview.exe

  然后再看看这个进程信息:

kd> !process 89c258c0
Failed to get VadRoot
PROCESS 89c258c0  SessionId: 0  Cid: 0758    Peb: 7ffd3000  ParentCid: 05f4
    DirBase: 13ac01a0  ObjectTable: e1d63088  HandleCount:  57.
    Image: Dbgview.exe
    VadRoot 00000000 Vads 0 Clone 0 Private 192. Modified 7. Locked 0.
    DeviceMap e1638c88
    Token                             e176a7d0
    ElapsedTime                       00:00:05.476
    UserTime                          00:00:00.010
    KernelTime                        00:00:00.010
    QuotaPoolUsage[PagedPool]         0
    QuotaPoolUsage[NonPagedPool]      0
    Working Set Sizes (now,min,max)  (974, 50, 345) (3896KB, 200KB, 1380KB)
    PeakWorkingSetSize                974
    VirtualSize                       30 Mb
    PeakVirtualSize                   32 Mb
    PageFaultCount                    1030
    MemoryPriority                    FOREGROUND
    BasePriority                      8
    CommitCharge                      392

THREAD 898b0020 Cid 0758.0760 Teb: 7ffdf000 Win32Thread: e1e6d2c8 WAIT: (UserRequest) UserMode Non-Alertable
89b0b6a8 SynchronizationEvent
89b43930 SynchronizationEvent
Not impersonating
DeviceMap e1638c88
Owning Process 00000000 Image:
Attached Process 89c258c0 Image: Dbgview.exe
Wait Start TickCount 6228 Ticks: 1 (0:00:00:00.010)
Context Switch Count 225 IdealProcessor: 0 LargeStack
UserTime 00:00:00.000
KernelTime 00:00:00.010
Win32 Start Address 0x00413487
Stack Init b8138000 Current b813795c Base b8138000 Limit b8132000 Call 00000000
Priority 9 BasePriority 8 PriorityDecrement 0 IoPriority 0 PagePriority 0
ChildEBP RetAddr
b8137974 80501cd6 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
b8137980 804faae2 nt!KiSwapThread+0x46 (FPO: [0,0,0])
b81379b8 805b7418 nt!KeWaitForMultipleObjects+0x284 (FPO: [Non-Fpo])
b8137d48 8053e638 nt!NtWaitForMultipleObjects+0x2a2 (FPO: [Non-Fpo])

b8137d48 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b8137d64)

0012fc84 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

THREAD 89cca928 Cid 0758.0764 Teb: 7ffde000 Win32Thread: 00000000 WAIT: (DelayExecution) UserMode Alertable
89ccaa18 NotificationTimer
Not impersonating
DeviceMap e1638c88
Owning Process 00000000 Image:
Attached Process 89c258c0 Image: Dbgview.exe
Wait Start TickCount 6228 Ticks: 1 (0:00:00:00.010)
Context Switch Count 13 IdealProcessor: 0
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x0043261b
Stack Init b873e000 Current b873dcbc Base b873e000 Limit b873b000 Call 00000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 0 PagePriority 0
ChildEBP RetAddr
b873dcd4 80501cd6 nt!KiSwapContext+0x2e (FPO: [Uses EBP] [0,0,4])
b873dce0 804fa79f nt!KiSwapThread+0x46 (FPO: [0,0,0])
b873dd0c 8060db19 nt!KeDelayExecutionThread+0x1c9 (FPO: [Non-Fpo])
b873dd54 8053e638 nt!NtDelayExecution+0x87 (FPO: [Non-Fpo])

b873dd54 7c92e4f4 (T) nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ b873dd64)

00d4ff68 00000000 (T) ntdll!KiFastSystemCallRet (FPO: [0,0,0])

  可以里面有两个进程,其中有一个是GUI线程,一个为普通线程,那么是如何知道的呢?我们查看它们的ServiceTable

kd> dt _KTHREAD 898b0020
ntdll!_KTHREAD

+0x0e0 ServiceTable : 0x80553f60 Void

kd> dt _KTHREAD 89cca928
ntdll!_KTHREAD

+0x0e0 ServiceTable : 0x80553fa0 Void

kd> dd KeServiceDescriptorTable
80553fa0 80502b8c 00000000 0000011c 80503000
80553fb0 00000000 00000000 00000000 00000000
80553fc0 00000000 00000000 00000000 00000000
80553fd0 00000000 00000000 00000000 00000000

kd> dd KeServiceDescriptorTableShadow
80553f60 80502b8c 00000000 0000011c 80503000
80553f70 bf999b80 00000000 0000029b bf99a890
80553f80 00000000 00000000 00000000 00000000
80553f90 00000000 00000000 00000000 00000000

  KeServiceDescriptorTableShadowKeServiceDescriptorTable没有的有关GUI线程相关的服务表,是不是很容易判断出来哪个是GUI线程了吗?是KeServiceDescriptorTableShadowGUI线程,是KeServiceDescriptorTable则为普通线程。


4️⃣ 断链线程结构体,实现隐藏,并思考为什么断链线程为什么还能够执行。

🔒 点击查看应用代码 🔒
#include "stdafx.h"
#include <windows.h>
#include <winioctl.h>
#include <stdlib.h>

//操作码:0x0-0x7FF 被保留,0x800-0xFFF 可用
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define SYMBOL_LINK_NAME L"\\\\.\\HideThread"

HANDLE g_Device;

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
   int i=0;
   while (1)
   {
      printf("\n%d:线程老子还活着!!!\n",i++);
      Sleep(1500);
   }
   return 0;
}

int main(int argc, char* argv[])
{
   //获取驱动链接对象句柄
   g_Device=CreateFileW(SYMBOL_LINK_NAME,GENERIC_READ GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0   ;
   if (g_Device!=INVALID_HANDLE_VALUE)
   {
      DWORD tid;
      DWORD re;
      DWORD outBuffer;

      HANDLE hthread = CreateThread(NULL,NULL   (LPTHREAD_START_ROUTINE)ThreadProc,NULL,NULL,&tid);
      CloseHandle(hthread);

      if (hthread==INVALID_HANDLE_VALUE)
      {
         puts("线程创建失败!!!");
         goto endproc;
      }  
      system("pause");

      if (DeviceIoControl(g_Device,Hide,&tid,sizeof(DWORD), outBuffer,sizeof(DWORD),&re,NULL))
      {
         puts("隐藏命令正在发送,请查看线程数是否减少……");
      }  
      system("pause");
   }
   else
   {
      puts("访问驱动符号链接失败!");
   }

endproc:

   CloseHandle(g_Device);
   system("pause");
   return 0;
}
🔒 点击查看驱动代码 🔒
#include <ntifs.h>
#include <ntddk.h>

UNICODE_STRING Devicename;
UNICODE_STRING SymbolLink;

#define DEVICE_NAME L"\\Device\\HideThread"
#define SYMBOL_LINK L"\\??\\HideThread"
#define Hide CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
    IoDeleteSymbolicLink(&SymbolLink);
    IoDeleteDevice(DriverObject->DeviceObject);
    DbgPrint("卸载成功!!!");
}

NTSTATUS DispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
    
    NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
    PIO_STACK_LOCATION pIrqlStack;
    ULONG uIoControlCode;
    PVOID pIoBuffer;
    ULONG uInLength;
    ULONG uOutLength;
    ULONG uRead;
    ULONG uWrite;

    pIrqlStack = IoGetCurrentIrpStackLocation(pIrp);
    uIoControlCode = pIrqlStack->Parameters.DeviceIoControl.IoControlCode;

    pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
    uInLength = pIrqlStack->Parameters.DeviceIoControl.InputBufferLength;
    uOutLength = pIrqlStack->Parameters.DeviceIoControl.OutputBufferLength;

    LIST_ENTRY* lethis;
    LIST_ENTRY* fle;
    LIST_ENTRY* ble;
    PETHREAD eThread;
    
    switch (uIoControlCode)
    {
        case Hide:
            RtlMoveMemory(&uRead, pIoBuffer,4);         
            PsLookupThreadByThreadId(uRead, &eThread);

            lethis = (LIST_ENTRY*)((ULONG)eThread + 0x1B0);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            lethis = (LIST_ENTRY*)((ULONG)eThread + 0x22C);
            fle = lethis->Flink;
            ble = lethis->Blink;

            fle->Blink = ble;
            ble->Flink = fle;

            DbgPrint("断链成功!!!");
            break;
    }

    pIrp->IoStatus.Information = 0;
    pIrp->IoStatus.Status = STATUS_SUCCESS;    
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

NTSTATUS DefaultDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
   pIrp->IoStatus.Information = 0;
   pIrp->IoStatus.Status = STATUS_SUCCESS;
   IoCompleteRequest(pIrp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{

    DriverObject->DriverUnload = UnloadDriver;

    UNICODE_STRING Devicename;
    RtlInitUnicodeString(&Devicename, DEVICE_NAME);

    DEVICE_OBJECT dobj;
    IoCreateDevice(DriverObject, 0, &Devicename, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &dobj);
    RtlInitUnicodeString(&SymbolLink, SYMBOL_LINK);
    IoCreateSymbolicLink(&SymbolLink, &Devicename);

    DriverObject->Flags |= DO_BUFFERED_IO;

    DriverObject->MajorFunction[IRP_MJ_CREATE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DefaultDispatchFunction;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchFunction;

    DbgPrint("加载成功!!!");

    return STATUS_SUCCESS;
}
🔒 点击查看答案 🔒


  先给个效果图来看看:

  可以看出,隐藏线程后有一个后遗症,那个进程即使线程数是0也是还在运行,且无法用任务管理器结束,虽然能点击结束程序按钮,但无效。


等待链表与调度链表

  进程结构体EPROCESS链表,里面有两处圈着当前进程所有的线程。对进程断链,程序可以正常运行,原因是CPU执行与调度是基于线程的,进程断链只是影响一些遍历系统进程的API,并不会影响程序执行。对线程断链也是一样的,断链后在Windbg或者OD中无法看到被断掉的线程,但并不影响其执行,原因是CPU调度线程的时候压根不用这个链表。
  线程有3种状态:就绪(ready)、等待(wait)、运行(running)。
  正在运行中的线程存储在KPCR中,就绪和等待的线程全在另外的33个链表中。一个等待链表,32个就绪链表。这些链表都使用了_KTHREAD + 0x060这个位置。这个一个位置,两个名字,如下所示。也就是说,线程在某一时刻,只能属于其中一个圈。

   +0x060 WaitListEntry    : _LIST_ENTRY
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY

等待链表

  线程调用了Sleep或者WaitForSingleObject等函数时,就挂到一个链表之中,它是等待链表。学习等待链表之前,我们需要知道一个全局变量:

kd> dd KiWaitListHead
80553d88  89b59540 89bb0300 00000010 00000000
80553d98  b6de6b92 a787ea13 01000013 ffdff980
80553da8  ffdff980 80500df0 00000000 00006029
80553db8  00000000 00000000 80553dc0 80553dc0
80553dc8  00000000 00000000 80553dd0 80553dd0
80553dd8  00000000 00000000 00000000 89da7da8
80553de8  00000000 00000000 00040001 00000000
80553df8  89da7e18 89da7e18 00000001 00000000

  这个全局变量存储的是双向链表,我们测试一下:

kd> dt _ETHREAD 89b59540-60
ntdll!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x1c0 CreateTime       : _LARGE_INTEGER 0x0ebf1cd6`795268e0
   +0x1c0 NestedFaultCount : 0y00
   +0x1c0 ApcNeeded        : 0y0
   +0x1c8 ExitTime         : _LARGE_INTEGER 0x89b596a8`89b596a8
   +0x1c8 LpcReplyChain    : _LIST_ENTRY [ 0x89b596a8 - 0x89b596a8 ]
   +0x1c8 KeyedWaitChain   : _LIST_ENTRY [ 0x89b596a8 - 0x89b596a8 ]
   +0x1d0 ExitStatus       : 0n0
   +0x1d0 OfsChain         : (null) 
   +0x1d4 PostBlockList    : _LIST_ENTRY [ 0xe1828d30 - 0xe198e730 ]
   +0x1dc TerminationPort  : 0xe18e94d8 _TERMINATION_PORT
   +0x1dc ReaperLink       : 0xe18e94d8 _ETHREAD
   +0x1dc KeyedWaitValue   : 0xe18e94d8 Void
   +0x1e0 ActiveTimerListLock : 0
   +0x1e4 ActiveTimerListHead : _LIST_ENTRY [ 0x89b596c4 - 0x89b596c4 ]
   +0x1ec Cid              : _CLIENT_ID
   +0x1f4 LpcReplySemaphore : _KSEMAPHORE
   +0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
   +0x208 LpcReplyMessage  : (null) 
   +0x208 LpcWaitingOnPort : (null) 
   +0x20c ImpersonationInfo : (null) 
   +0x210 IrpList          : _LIST_ENTRY [ 0x89b596f0 - 0x89b596f0 ]
   +0x218 TopLevelIrp      : 0
   +0x21c DeviceToVerify   : (null) 
   +0x220 ThreadsProcess   : 0x89c2cd40 _EPROCESS
   +0x224 StartAddress     : 0x7c8106e9 Void
   +0x228 Win32StartAddress : 0x7c930230 Void
   +0x228 LpcReceivedMessageId : 0x7c930230
   +0x22c ThreadListEntry  : _LIST_ENTRY [ 0x89c2ced0 - 0x89aeafd4 ]
   +0x234 RundownProtect   : _EX_RUNDOWN_REF
   +0x238 ThreadLock       : _EX_PUSH_LOCK
   +0x23c LpcReplyMessageId : 0
   +0x240 ReadClusterSize  : 7
   +0x244 GrantedAccess    : 0x1f03ff
   +0x248 CrossThreadFlags : 0
   +0x248 Terminated       : 0y0
   +0x248 DeadThread       : 0y0
   +0x248 HideFromDebugger : 0y0
   +0x248 ActiveImpersonationInfo : 0y0
   +0x248 SystemThread     : 0y0
   +0x248 HardErrorsAreDisabled : 0y0
   +0x248 BreakOnTermination : 0y0
   +0x248 SkipCreationMsg  : 0y0
   +0x248 SkipTerminationMsg : 0y0
   +0x24c SameThreadPassiveFlags : 0
   +0x24c ActiveExWorker   : 0y0
   +0x24c ExWorkerCanWaitUser : 0y0
   +0x24c MemoryMaker      : 0y0
   +0x250 SameThreadApcFlags : 0
   +0x250 LpcReceivedMsgIdValid : 0y0
   +0x250 LpcExitThreadCalled : 0y0
   +0x250 AddressSpaceOwner : 0y0
   +0x254 ForwardClusterOnly : 0 ''
   +0x255 DisablePageFaultClustering : 0 ''

  为什么拿到的值还要减去0x60,是因为它串在这个结构体的偏移位置,需要减去偏移才是真正线程结构体的头部,我们还可以通过它其中的成员找到它所属的进程:

kd> dt _EPROCESS 0x89c2cd40 
ntdll!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x06c ProcessLock      : _EX_PUSH_LOCK
   +0x070 CreateTime       : _LARGE_INTEGER 0x01d7e39a`7e928c10
   +0x078 ExitTime         : _LARGE_INTEGER 0x0
   +0x080 RundownProtect   : _EX_RUNDOWN_REF
   +0x084 UniqueProcessId  : 0x00000374 Void
   +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x89b12e28 - 0x89c3e7a0 ]
   +0x090 QuotaUsage       : [3] 0x1a30
   +0x09c QuotaPeak        : [3] 0x1b20
   +0x0a8 CommitCharge     : 0x316
   +0x0ac PeakVirtualSize  : 0x3fc6000
   +0x0b0 VirtualSize      : 0x3f86000
   +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0x89b12e54 - 0x89c3e7cc ]
   +0x0bc DebugPort        : (null) 
   +0x0c0 ExceptionPort    : 0xe12aa720 Void
   +0x0c4 ObjectTable      : 0xe172c078 _HANDLE_TABLE
   +0x0c8 Token            : _EX_FAST_REF
   +0x0cc WorkingSetLock   : _FAST_MUTEX
   +0x0ec WorkingSetPage   : 0x14b84
   +0x0f0 AddressCreationLock : _FAST_MUTEX
   +0x110 HyperSpaceLock   : 0
   +0x114 ForkInProgress   : (null) 
   +0x118 HardwareTrigger  : 0
   +0x11c VadRoot          : 0x89caf290 Void
   +0x120 VadHint          : 0x89caf290 Void
   +0x124 CloneRoot        : (null) 
   +0x128 NumberOfPrivatePages : 0x14c
   +0x12c NumberOfLockedPages : 0
   +0x130 Win32Process     : 0xe179b368 Void
   +0x134 Job              : (null) 
   +0x138 SectionObject    : 0xe177e0b0 Void
   +0x13c SectionBaseAddress : 0x01000000 Void
   +0x140 QuotaBlock       : 0x8055b200 _EPROCESS_QUOTA_BLOCK
   +0x144 WorkingSetWatch  : (null) 
   +0x148 Win32WindowStation : 0x00000038 Void
   +0x14c InheritedFromUniqueProcessId : 0x00000290 Void
   +0x150 LdtInformation   : (null) 
   +0x154 VadFreeHint      : (null) 
   +0x158 VdmObjects       : (null) 
   +0x15c DeviceMap        : 0xe1005450 Void
   +0x160 PhysicalVadList  : _LIST_ENTRY [ 0x89c2cea0 - 0x89c2cea0 ]
   +0x168 PageDirectoryPte : _HARDWARE_PTE_X86
   +0x168 Filler           : 0
   +0x170 Session          : 0xbadce000 Void
   +0x174 ImageFileName    : [16]  "svchost.exe"
   +0x184 JobLinks         : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x18c LockedPagesList  : (null) 
   +0x190 ThreadListHead   : _LIST_ENTRY [ 0x89c2cc44 - 0x89b5970c ]
   +0x198 SecurityPort     : (null) 
   +0x19c PaeTop           : 0xbaf190e0 Void
   +0x1a0 ActiveThreads    : 0x14
   +0x1a4 GrantedAccess    : 0x1f0fff
   +0x1a8 DefaultHardErrorProcessing : 0
   +0x1ac LastThreadExitStatus : 0n0
   +0x1b0 Peb              : 0x7ffda000 _PEB
   +0x1b4 PrefetchTrace    : _EX_FAST_REF
   +0x1b8 ReadOperationCount : _LARGE_INTEGER 0x3e
   +0x1c0 WriteOperationCount : _LARGE_INTEGER 0x9
   +0x1c8 OtherOperationCount : _LARGE_INTEGER 0x195
   +0x1d0 ReadTransferCount : _LARGE_INTEGER 0x3864a
   +0x1d8 WriteTransferCount : _LARGE_INTEGER 0x1c4
   +0x1e0 OtherTransferCount : _LARGE_INTEGER 0x7506
   +0x1e8 CommitChargeLimit : 0
   +0x1ec CommitChargePeak : 0x16d4
   +0x1f0 AweInfo          : (null) 
   +0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
   +0x1f8 Vm               : _MMSUPPORT
   +0x238 LastFaultCount   : 0
   +0x23c ModifiedPageCount : 1
   +0x240 NumberOfVads     : 0x80
   +0x244 JobStatus        : 0
   +0x248 Flags            : 0xd2800
   +0x248 CreateReported   : 0y0
   +0x248 NoDebugInherit   : 0y0
   +0x248 ProcessExiting   : 0y0
   +0x248 ProcessDelete    : 0y0
   +0x248 Wow64SplitPages  : 0y0
   +0x248 VmDeleted        : 0y0
   +0x248 OutswapEnabled   : 0y0
   +0x248 Outswapped       : 0y0
   +0x248 ForkFailed       : 0y0
   +0x248 HasPhysicalVad   : 0y0
   +0x248 AddressSpaceInitialized : 0y10
   +0x248 SetTimerResolution : 0y0
   +0x248 BreakOnTermination : 0y1
   +0x248 SessionCreationUnderway : 0y0
   +0x248 WriteWatch       : 0y0
   +0x248 ProcessInSession : 0y1
   +0x248 OverrideAddressSpace : 0y0
   +0x248 HasAddressSpace  : 0y1
   +0x248 LaunchPrefetched : 0y1
   +0x248 InjectInpageErrors : 0y0
   +0x248 VmTopDown        : 0y0
   +0x248 Unused3          : 0y0
   +0x248 Unused4          : 0y0
   +0x248 VdmAllowed       : 0y0
   +0x248 Unused           : 0y00000 (0)
   +0x248 Unused1          : 0y0
   +0x248 Unused2          : 0y0
   +0x24c ExitStatus       : 0n259
   +0x250 NextPageColor    : 0xbd35
   +0x252 SubSystemMinorVersion : 0 ''
   +0x253 SubSystemMajorVersion : 0x4 ''
   +0x252 SubSystemVersion : 0x400
   +0x254 PriorityClass    : 0x2 ''
   +0x255 WorkingSetAcquiredUnsafe : 0 ''
   +0x258 Cookie           : 0x7f42570f

  我们很轻松地找到了,这个进程属于svchost.exe这个程序。

调度链表

  调度链表有32个圈,就是优先级是0-31,0为最低优先级,31为最高,默认优先级一般是8。改变优先级就是从一个圈里面卸下来挂到另外一个圈上,这32个圈是正在调度中的线程,包括正在运行的和准备运行的。比如:只有一个CPU但有10个线程在运行,那么某一时刻,正在运行的线程在KPCR中,其他9个在这32个圈中。
  既然有32个链表,就要有32个链表头,我们来看一下:

kd> dd KiDispatcherReadyListHead L50
80554820  80554820 80554820 80554828 80554828
80554830  80554830 80554830 80554838 80554838
80554840  80554840 80554840 80554848 80554848
80554850  80554850 80554850 80554858 80554858
80554860  80554860 80554860 80554868 80554868
80554870  80554870 80554870 80554878 80554878
80554880  80554880 80554880 80554888 80554888
80554890  80554890 80554890 80554898 80554898
805548a0  805548a0 805548a0 805548a8 805548a8
805548b0  805548b0 805548b0 805548b8 805548b8
805548c0  805548c0 805548c0 805548c8 805548c8
805548d0  805548d0 805548d0 805548d8 805548d8
805548e0  805548e0 805548e0 805548e8 805548e8
805548f0  805548f0 805548f0 805548f8 805548f8
80554900  80554900 80554900 80554908 80554908
80554910  80554910 80554910 80554918 80554918
80554920  00000000 00000000 00000000 00000000
80554930  00000000 00000000 00000000 00000000
80554940  00000000 00000000 00000000 00000000
80554950  00000000 e1006000 00000000 00000000

  其中每一个成员都是一个双向链表。如果你心细地发现。现在每个成员的地址都和当前地址一样,前结点和后结点一样,这就是说明当前没有在调度链表的线程。这是因为我们中断操作系统调试输入命令的时候,它会把操作系统的所有线程挂起,所以都在等待链表中。如果你真把线程结构体从上面几个摘掉的话,这线程真的就跑不起来了。由此可知,线程是永远没法隐藏的。
  不同的Windows版本略有不同,XP只有一共33个圈,也就是说上面这个数组只有一个,多核也只有一个。Win7也是一样的只有一个圈,如果是64位的,那就有64个圈。而服务器版本KiWaitListHead整个系统只有一个,但KiDispatcherReadyListHead这个数组有几个CPU就有几组。

模拟线程切换

  在正式学习Windows线程切换,我们需要读懂一份代码,点击 此蓝奏云链接 下载,密码为hwso,可以直接用VC6.0直接打开。

关键结构体

  我们来看一下模拟线程的结构体:

typedef struct 
{
   char* name;      //线程名
   int Flags;      //线程状态
   int SleepMillsecondDot;      //休眠时间   
   void* initialStack;      //线程堆栈起始位置
   void* StackLimit;      //线程堆栈界限
   void* KernelStack;      //线程堆栈当前位置,也就是ESP 
   void* lpParameter;      //线程函数的参数
   void(*func)(void* lpParameter);      //线程函数
}GMThread_t;

  跟线程切换相关的最关键的参数是如下几个项目:

   void* initialStack;      //线程堆栈起始位置
   void* StackLimit;      //线程堆栈界限
   void* KernelStack;      //线程堆栈当前位置,也就是ESP

模拟调度链表

  线程切换有调度链表,我们是如何处理的呢,我们可以看到如下代码:

//线程的列表
GMThread_t GMThreadList[MAXGMTHREAD] = {NULL, 0};

  所谓创建线程,就是创建一个结构体,并且挂到这个数组中此时的线程状态为创建。我们看到主函数用行代码实现创建线程:

   RegisterGMThread("Thread1", Thread1, NULL);

  跟进去看一下,具体函数内容如下:

//将一个函数注册为单独线程执行
int RegisterGMThread(char* name, void(*func)(void*lpParameter), void* lpParameter)
{
   int i;
   for (i = 1; GMThreadList[i].name; i++) 
   {
      if (0 == _stricmp(GMThreadList[i].name, name)) 
      {
         break;
      }
   }
   initGMThread(&GMThreadList[i], name, func, lpParameter);
   return (i & 0x55AA0000);
}

  可以看到,它是查找有没有线程,如果有就用initGMThread初始化这个结构体,跟进去看看:

//初始化线程的信息
void initGMThread(GMThread_t* GMThreadp, char* name, void (*func)(void* lpParameter), void* lpParameter)
{
   unsigned char* StackPages;
   unsigned int* StackDWordParam;
   GMThreadp->Flags = GMTHREAD_CREATE;
   GMThreadp->name = name;
   GMThreadp->func = func;
   GMThreadp->lpParameter = lpParameter;
   StackPages = (unsigned char*)VirtualAlloc(NULL, GMTHREADSTACKSIZE, MEM_COMMIT, PAGE_READWRITE);
   ZeroMemory(StackPages, GMTHREADSTACKSIZE);
   GMThreadp->initialStack = StackPages + GMTHREADSTACKSIZE;
   StackDWordParam = (unsigned int* GMThreadp->initialStack;
   //入栈
   PushStack(&StackDWordParam, (unsigned int)GMThreadp   ;        //startup 函数所需要的参数
   PushStack(&StackDWordParam, (unsigned int)0);        /   你好奇这里为什么放0,简单来说是为了平衡堆栈,其次是因为调   startup是要参数的,pop startup->eip 后 esp也就是这里,   函数后会把 mov ebp,esp ,然后 ebp+8 就是函数默认的参数   置。
   PushStack(&StackDWordParam, (unsigned int GMThreadStartup);
   PushStack(&StackDWordParam, (unsigned int)5);    //push  ebp
   PushStack(&StackDWordParam, (unsigned int)7);    //push  edi
   PushStack(&StackDWordParam, (unsigned int)6);    //push  esi
   PushStack(&StackDWordParam, (unsigned int)3);    //push  ebx
   PushStack(&StackDWordParam, (unsigned int)2);    //push  ecx
   PushStack(&StackDWordParam, (unsigned int)1);    //push  edx
   PushStack(&StackDWordParam, (unsigned int)0);    //push  eax
   //当前线程的栈顶
   GMThreadp->KernelStack = StackDWordParam;
   GMThreadp->Flags = GMTHREAD_READY;
   return;
}

  我们可以看到,它首先初始化所谓的线程结构体,挂到数组中,分配一个内存作为堆栈,然后进行一系列的堆栈操作。

初始化线程堆栈

  initGMThread这个函数里面有一系列的PushStack,其实这个就是我们所谓的初始化线程堆栈操作,示意图如下:

模拟线程切换

  这个就是我们模拟线程切换的核心,我们看一下代码:

//切换线程
__declspec(naked) void SwitchContext(GMThread_t* SrcGMThreadp, GMThread_t* DstGMThreadp)
{
   __asm
      {
         push ebp
         mov ebp, esp
         push edi
         push esi
         push ebx
         push ecx
         push edx
         push eax

         mov esi, SrcGMThreadp
         mov edi, DstGMThreadp
         mov [esi+GMThread_t.KernelStack], esp
         //经典线程切换,另外一个线程复活
         mov esp, [edi+GMThread_t.KernelStack]        
         pop eax  //esp在上面已经切换到新的线程栈中,这个栈       pop eax,拿到的就是保存的esp(初始化的esp/运行时esp)
         pop edx
         pop ecx
         pop ebx
         pop esi
         pop edi
         pop ebp
         ret   //把栈顶的值弹到eip中,在这里弹出的就是startup的地址到eip中
      }
}

  从上面的代码看出,上面的代码把我们定义堆栈的值挨个压入,然后把新线程的堆栈的值依次替换,然后把新堆栈的值弹回给寄存器继续执行,这就是所谓了线程切换。那么我们看看是谁调用了这个函数:

//这个函数会让出cpu,从队列里重新选择一个线程执行
void Scheduling(void)
{
   int i;
   int TickCount;
   GMThread_t* SrcGMThreadp;
   GMThread_t* DstGMThreadp;
   TickCount = GetTickCount();
   SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
   DstGMThreadp = &GMThreadList[0]; 
   for (i = 1; GMThreadList[i].name; i++) {
      if (GMThreadList[i].Flags & GMTHREAD_SLEEP) {
         if (TickCount > GMThreadList[i].SleepMillsecondDot) {
            GMThreadList[i].Flags = GMTHREAD_READY;
         }
      }
      if (GMThreadList[i].Flags & GMTHREAD_READY) {
         DstGMThreadp = &GMThreadList[i];
         break;
      }
   }  
   CurrentThreadIndex = DstGMThreadp - GMThreadList;
   SwitchContext(SrcGMThreadp, DstGMThreadp);
   return;
}

  我们再看看是谁调用这个函数:

void GMSleep(int MilliSeconds)
{
   GMThread_t* GMThreadp;
   GMThreadp = &GMThreadList[CurrentThreadIndex];
   if (GMThreadp->Flags != 0) {
      GMThreadp->Flags = GMTHREAD_SLEEP;
      GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds;
   }

   Scheduling();
   return;
}

  而这个函数又是线程主动调用的:

void Thread1(void*) {
    while(1){
        printf("Thread1\n");
        GMSleep(500);
    }
}

  综上可知:线程不是被动切换的,而是主动让出CPU。线程切换并没有使用TSS来保存寄存器,而是使用堆栈。线程切换的过程就是堆栈切换的过程。
  我们可以看一下效果,由于线程是自己模拟的,所以在任务管理器看到只是一个线程,也就是操作系统帮我们创建的主线程:

线程切换

  之前我们介绍了模拟Windows线程切换,在这个项目里面我们介绍了一个重要的函数:SwitchContext,只要调用这个函数,就会导致线程切换。Windows也有类似的函数:KiSwapContext,只要调用这个函数,就会触发线程切换。这个函数请自行分析,有关线程切换的部分将会在下一篇进行揭晓。

下一篇

  进程线程篇——线程切换(下)

posted @ 2021-12-04 23:30  寂静的羽夏  阅读(1997)  评论(2编辑  收藏  举报