写在前面

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

  看此教程之前,问几个问题,


🔒 华丽的分割线 🔒


练习及参考

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

  

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

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

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

  

等待链表与调度链表

  进程结构体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,其实这个就是我们所谓的初始化线程堆栈操作,示意图如下:

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

模拟线程切换

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

//切换线程
__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来保存寄存器,而是使用堆栈。线程切换的过程就是堆栈切换的过程。
  我们可以看一下效果,由于线程是自己模拟的,所以在任务管理器看到只是一个线程,也就是操作系统帮我们创建的主线程:

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

线程切换

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

下一篇

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

12-05 02:24