如何只知道线程句柄进入线程函数

逆向工程 ollydbg 调试 线
2021-06-18 22:04:31

我如何进入线程函数(以便我可以逐步跟踪),只知道线程的句柄。我使用 OllyDbg 进行跟踪,线程是通过 API 创建的ZwCreateProcess()但是,我看到的有关此 API 的文档不包含创建标志和指向它将执行的已定义函数的指针,而我都需要这些。

有没有办法进入线程函数,只知道线程句柄?另外,除了CreateThread()and之外,还有其他方法可以创建挂起的线程CreateRemoteThread()吗?

3个回答

有没有办法进入线程函数,只知道线程句柄?

是的,这是一个 2 步过程。

步骤 1 - 将线程句柄转换为线程 ID

Process Explorer的菜单栏中,检查以下内容:

  • 查看显示下窗格
  • 视图下窗格视图手柄
  • 查看选择列...处理选项卡 → 选中所有复选框

接下来,在 Process Explorer 的进程列表中选择您的目标进程。然后,您将在下方窗格中看到该进程的句柄列表,包括线程句柄。找到与您的目标句柄关联的线程 ID。对于下面的示例,线程句柄0x228与线程 ID 相关联3000

处理 ID

尽管在 Process Explorer 中句柄值以十六进制显示,但线程 ID 以十进制显示。因此3000,十进制的线程 ID等于0xBB8十六进制的线程 ID

第 2 步 - 查找线程 ID 的 EIP

在 OllyDbg 的菜单栏中,选择ViewThreads右键单击Ident与您在步骤 1 中找到的线程 ID 对应的线程(0xBB8在下面的示例中),然后选择Show registers

线程

这将显示EIP该线程的当前值,即该线程恢复后要执行的下一条指令:

寄存器

替代步骤 2 - 查找线程 ID 的 EIP

如果目标线程在挂起状态下创建并且尚未恢复,则该线程将不会出现在 OllyDbg 的线程窗口中。在这种情况下,您可以使用LiveKd通过发出 LiveKd 命令来查找线程的起始地址!thread -t <thread ID in hexadecimal>

kd> !thread -t BB8
Cid handle table at 88e01108 with 944 entries in use

THREAD 86B4E548  Cid 169c.0bb8  Teb: 7ffdb000 Win32Thread: 00000000 WAIT: (Suspended) KernelMode Non-Alertable
SuspendCount 1
FreezeCount 1
    86b4ec28  Semaphore Limit 0x2
Not impersonating
DeviceMap                 9a70f9e8
Owning Process            86b4cd40       Image:         wordpad.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      21829348       Ticks: 1299 (0:00:00:20.264)
Context Switch Count      1              IdealProcessor: 0
UserTime                  00:00:00.000
KernelTime                00:00:00.000
Win32 Start Address 0x002cb23d
Stack Init 8b777ed0 Current 8b777a40 Base 8b778000 Limit 8b775000 Call 0
Priority 8 BasePriority 8 UnusualBoost 0 ForegroundBoost 0 IoPriority 2 PagePriority 5
ChildEBP RetAddr  Args to Child
8b777a58 82a88d3d 85807a60 00000000 82b35d20 nt!KiSwapContext+0x26 (FPO: [Uses EBP] [0,0,4])
8b777a90 82a87b9b 85807b20 85807a60 85807c28 nt!KiSwapThread+0x266
8b777ab8 82a8158f 85807a60 85807b20 00000000 nt!KiCommitThreadWait+0x1df
8b777b34 82abbfd9 85807c28 00000005 00000000 nt!KeWaitForSingleObject+0x393
8b777b4c 82abbaf4 00000000 00000000 00000000 nt!KiSuspendThread+0x18 (FPO: [3,0,0])
8b777b90 82e2390f 00000000 00000000 00000000 nt!KiDeliverApc+0x17f
8b777bb0 82e23b29 00000001 00000000 00000000 hal!HalpDispatchSoftwareInterrupt+0x49 (FPO: [Non-Fpo])
8b777bc8 82e23ba9 00000000 00000000 8b777c20 hal!HalpCheckForSoftwareInterrupt+0x83 (FPO: [Non-Fpo])
8b777bd8 82c6450d b553bcc6 00000000 00000000 hal!KfLowerIrql+0x61 (FPO: [Non-Fpo])
8b777c20 82abb559 00000000 778870d8 00000001 nt!PspUserThreadStartup+0x14
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x19

您可以Win32 Start Address 0x002cb23d在上面的输出中看到,这是挂起线程的起始地址。

另外,除了CreateThread()and之外,还有其他方法可以创建挂起的线程 CreateRemoteThread()吗?

是的,您可以致电ntdll!NtCreateThread()ntdll!NtCreateThreadEx()

根据提供的信息,我怀疑这里使用的方法是进程的动态分叉,或者因为它也知道进程挖空。这个想法是在挂起状态下执行一些任意进程,并用另一个可执行映像的内容替换它的“胆量”。一般来说,这个想法可以实现如下(基于Tan Chew Keong 的动态分叉,2004):

  1. 使用CreateProcess带有CREATE_SUSPENDED参数API从任何 EXE 文件创建挂起的进程。(称之为第一个 EXE)。
  2. 调用GetThreadContextAPI 获取挂起进程的寄存器值(线程上下文)。挂起进程的EBX 寄存器指向进程的PEB。EAX 寄存器包含进程的入口点(第一个 EXE)。
  3. 从其PEB中获取挂起进程的基地址,即在[EBX+8]
  4. 将第二个 EXE 加载到内存中(使用 ReadFile)并手动执行必要的对齐。如果文件对齐与内存对齐不同,这是必需的
  5. 如果第二个EXE与挂起进程的基地址相同,并且其image-size<=挂起进程的image-size,只需使用该WriteProcessMemory函数将第二个EXE的image写入挂起进程的内存空间暂停进程,从基地址开始。
  6. 否则,使用ZwUnmapViewOfSection(由 ntdll.dll 导出)取消映射第一个 EXE 的映像,并使用 VirtualAllocEx 为挂起进程的内存空间内的第二个 EXE 分配足够的内存。VirtualAllocExAPI必须与第二EXE的基地址提供给确保Windows会给我们记忆中所需要的区域。接下来,将第二个 EXE 的映像复制到挂起进程的内存空间中,从分配的地址开始(使用WriteProcessMemory)。
  7. 如果取消映射操作失败但第二个 EXE 是可重定位的(即有一个重定位表),则在任何位置的挂起进程中为第二个 EXE 分配足够的内存。根据分配的内存地址对第二个 EXE 执行手动重定位。接下来,将重定位的 EXE 复制到挂起进程的内存空间中,从分配的地址开始(使用WriteProcessMemory)。
  8. 在 [EBX+8] 处将第二个 EXE 的基地址修补到挂起进程的 PEB 中。
  9. 将线程上下文的 EAX 设置为第二个 EXE 的入口点。
  10. 使用 SetThreadContext API 修改挂起进程的线程上下文。
  11. 使用 ResumeThread API 恢复暂停进程的执行。

所以,为了回答你的问题,我首先建议验证这确实发生了。其次,如果是这样,您可以执行以下操作来中断新创建的线程入口点。建议的方法不是唯一的方法,但考虑到您在 RE/可执行分析方面的经验相对较少,恕我直言很容易做到:

  • 检查PoC以真正全面了解整个过程。

    1. 将 BP 放在SetThreadContext 上第二个参数是CTX变量,它保存线程上下文的各个方面,也是线程函数的地址。
    2. 简而言之,您需要检查CTX.eax以获取线程函数的地址。例如0x00402030,您在那里找到的地址。
    3. 下载并安装ProcessHacker,并在它的帮助下检查挂起进程的地址空间。打开线程函数所属的内存页-右键进程->属性->内存例如页面0x00400000
    4. ProcessHacker 将向您显示内存页面以及该页面的本地偏移量。您将需要转到 offset 0x2030
    5. 0x20300xEBFE(记住前面的字节)在偏移处修补内存,这会使线程无限循环 - jmp 0x00402030
    6. 现在,恢复父进程并将 的新实例附加Olly到现在已经运行的挂起进程。转到 EP 并修补回原始字节。
    7. 祝分析顺利。

我希望这是可以理解的,否则请询问,我会澄清。

如果线程被创建挂起第一次调用恢复线程,ollydbg 将不会在其中显示该线程,thread window也不会在堆栈窗口中提供该线程的堆栈跟踪(ALT+K)->Right Click->Thread ->Checkmark

在这种情况下,您可以在以下 apis 上设置断点

跟进基于 xp sp3 布局(您可能需要在较新的操作系统中进行一些调整)

1) ntdll!NtContinue / ZwContinue  (can be found without symbol )    
2) ntdll!ZwRegisterThreadTerminateport (can find this too without symbol)    
3) CsrNewThread  (needs symbol)   
4) BaseThreadStartThunk ditto    
5) BaseThreadStart  ditto     

恢复线程将在这些 api 中的任何一个上中断

如果上破度Zw / NT继续遵循context->eip从堆栈[ESP + 4] + 0xb8] 并设置在EIP断点发现(该地址通常会BaseThreadStartThunk which is directly identifiable if you have public symbols loaded),并打 ,一旦你在BaseThreadStartThunk单步F8几次,直到您进行间接调用, call dword ptr ds:[R32 + CONST] 这是您的 ThreadProc

如果它在ZwRegisterTerminatePort上损坏,您只需执行单步操作(您将返回 csrNewThread,您可以在其中找到符号是否可用)并保持 f8ing,直到您到达上述间接调用以登陆 ThreadProc

其他api都包含在上面,可以减少单步的数量

ntdll!ZwRegisterThreadTerminatePort 是 ThreadProc 之前最接近的中断

需要一个 ctrl+f9 (execute until return) and 3 f8 (single step)

如果 windbg / livekd 是一个选项,则此脚本可以在 ResumeThread 调用中断时检索 eip,无论是首次恢复还是后续恢复

.foreach /ps 8 /pS 0n19 (place { !process 0 4 ${$arg1} } ) {.printf "ETHREAD = %x\n", place ; r? $t0 =  (((nt!_ETHREAD *) @@masm( place ))->Tcb) ; r? $t1 = @$t0.StackLimit;r? $t2 = @$t0.InitialStack;.foreach (vlace {s -[1]d @$t1 @$t2 0x23 0x23 } ) {dt nt!_KTRAP_FRAME DbgEip Eip  @@masm(${vlace}-34) }}