当学习主动安全认证专家(OSCE) 认证时,我花费了大量的时间用于研究如何编写自定义shellcode。本文是我计划要发布的一系列博文里的第一篇,这个系列详细介绍我在准备认证过程中所学习到的技术。本文的重点是描述如何找到EIP/RIP以及找到后做什么。OSCE专注于32位系统,作为继续学习的一部分,我会研究并记录下在64位系统上的方法。这超出了OSCE的需求,但这是我继续学习过程的一部分。
在本文中,我不会介绍到所有的可能找到EIP 和RIP 的方法。我会介绍一些我熟悉并已经研究过的方法。如果你想找到更多的方法,我鼓励你开始你自己的研究并开始做自己的学习总结。在这里你看到的是我的学习总结,我希望它会在某些方面对你有帮助。我知道这些主题多年来已经被多次提到,也没有新的东西,但重点不是提出新的东西,而是在学习总结中提升自己,并且还可以帮助到刚开始学习的人。
从使用x86汇编(32位)指令定位EIP 的方法开始。有两种方法可以用,根据你可能会遇到的情况和限制,两种方法可能都有用。两种方法都可以找到EIP 。我要介绍的第一种方法比第二种方法小一个字节。两种方法都可以达到完全相同的目的,存储EIP 的值到EAX 寄存器中。两种方法都没有空(0x00)字节。之所以使用一种而不是另一种,是因为您可能会遇到大小或字符限制。
该方法最先由Aaron Adams在漏洞开发(Vulnerability Development)邮件列表 中提出.他使用的这种方法利用x87浮点单元(FPU)寄存器来获取EIP 值。以下的基础汇编代码将EIP 的值存储到EAX 寄存器中。
执行fldz 指令来激活FPU寄存器。该指令将常数值+0.0压入FPU 寄存器栈(ST(0))。在这个过程中,FPU 寄存器被初始化,当前的EIP 值被存储到FPU指令指针偏移(FIP)寄存器中。图 1 描述了FPU 寄存器的结构。该表拷贝自《Intel(r) 64 和 IA-32 架构软件开发者手册,卷1:基础架构 PDF》.
图1:内存中保护模式x87 FPU状态图片(32位格式)
下一条指令存储FPU寄存器在特殊的地址。因此使用fnstenv 指令将EIP 的值存储在便宜0x0C(12)的FIP 寄存器中。ESP-0x0C 的目标是固定的,所以FIP 的值会存储在当前ESP 地址处。接下来,该值会从栈中弹出,并存储到EAX 寄存器中。由于在将EIP存储到FPU的FIP 寄存器和弹到EAX 的这段时间已经执行了几个字节的指令,所以需要调整该值来表示当前的EIP的值。用AL 寄存器加0x7(7)完成此操作。AL 寄存器被用来避免使用空字节。
要测试可以使用nasm 来汇编代码:
随后插入字节到C 中:
使用MingW来编译代码:
运行最终的PE文件在你喜欢的调试器中,并看它是如何运行的。你需要将.data 节标记为可执行。你可以使用调试器来实现或者使用如LordPE这样的工具。如果你不标记 .data 节为可执行,你会在执行你的shellcode时发生访问违例。我需要在PE文件中搜索call eax 指令,并设置断点,第一个调用到EAX 的会是shellcode。如果一切顺利,EAX 会在执行add al,7 指令时指向EIP,如图 2 。
图 2:EAX指向EIP
** 方法1的替代——使用减法
感谢来自@TheColonial 的建议,对EAX 寄存器使用减法而不是add al 指令可以再一些情况下避免错误,我添加了该替代方法。他正确的指出的问题,是通过对AL 进行加法运算,如果加法导致了进位,EAX 就会指向错误的地址。例如,如果AL 寄存器包含大于或等于0xF9 的任何数值,加上0x07 后,进位的1会被丢弃掉。例如,如果EAX 是0x001234F9 ,我们把0x07 加到AL 寄存器(0xF9 )后,结果会是0x00123400 而不是我们需要的0x00123500 。
为了避免这个问题,并且避免空字节,可以让EAX 减去一个负值。基本的数学运算,减去一个负值等于加上它(的绝对值)。简单却有效。正确的代码如下,还避开了空字节,并且只比之前大了1字节:
在图 9 中我们可以看出建议的代码正常运行并且会比原来的代码更加可靠。
图 9 EAX指向EIP
第二种方法仅比第一种方法长一个字节,也完成了将EIP 的值存储到EAX 中的目标。我从 Phrack issue 62,Phile 7 标题为History and Advances in Windows Shellcode 中学到的这种方法。这篇文章由SK Chong所写。为了获得EIP 这种方法使用了一系列的跳转和调用,最终EIP的值被存储在EAX 中。SK Chong文章的原始文件已经找不到了,但是,我找到了其中的一些。在他的例子中,它使用了db 项来硬编码了那些跳转和调用。我要提供的版本读起来更容易并且以及更容易使用nasm 汇编。
上面的代码执行了跳转到label2 ,一个call指令调用了label1 。当call被执行时,返回地址,即下一条指令的地址,被压入栈中。执行,随后跳转到getEIP ,会弹出返回地址到EAX 。不要小题大作(No muss, no fuss.)。
你可以使用测试方法 1的测试方法来测试方法2.当执行完,结果看起来如图 3 。
图 3:EAX指向EIP
查看是否32位架构中运行的方法在64位世界中是否运行是一个好的开始。如果只需做些微修改,为什么还要重新造轮子?
使用FPU指令可以运行,因为指令在64位下仍然生效。首先,找出未对32位模式运行下的指令进行修改或微小修改发生的情况。要做到这点,我使用x64dbg并使其执行notepad.exe ,运行直到命中EntryPoing 。使用Assemble 指令,我手动替换了开始的一些指令位32-bit的指令。这样做的结果如图 4 。这里有一些需要注意。首先,67 :出现在fnstenv 指令前。根据Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z 章节2.1.1,67h 前缀是地址大小覆写。这一定会出现的,因为我之前使用的是ESP 寄存器而不是RSP 。第二,我不能使用POP EAX 指令,我需要使用POP RAX 。图 4:使用FPU寻找RIP,尝试1
运行该序列会部分成功(见图5 )。当加法执行时,RAX指向RIP之前的指令。这是必须使用67h 地址大小前缀的原因。使用0x08 替代0x07 就能很容易的使之平衡。继续,自己尝试下,看看它是否有效。图 5:使用FPU指令寻找RIP,尝试1的结果
既然已经证明了使用FPU寄存器来获取RIP的值时可行的。我的下一步是看看是否能够写一些汇编代码,可以在调试器中手工输入命令可以使之平衡。经过一些调整后,这是我想出的代码:
当使用Nasm 汇编时,上面指令中的代码结果如图 6 。为了测试那些指令,我对用x64dbg 打开的64位进程的第一条指令做二进制粘贴。如你所看到的,在add rax,0x07 指令前有了新的48h 前缀。根据Intel® 64 and IA-32 Architectures Software Developer’s Manual, Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4 章节3.7.2.1,这是一个REX前缀 ,允许指令寻址8位寄存器。汇编器可能够以这种方式丢到空字节。而且,由于当使用RSP 而不是ESP 时,对于fnstenv 指令,地址大小前缀是非必须的,所以,RAX 加上0x07 字节的结果是RAX 包含了RIP 的地址。图 6:使用FPU'指令寻找RIP,终结
在开始测试该方法前,我想做些微小的修改。我想有必要将EAX 替换成RAX 。这种跳转和调用和手工干扰到调试器中有一些不同,所以我选择先开始编写一些汇编代码。下面的代码就是我选择用来最开始尝试的:
执行了使用汇编的代码覆盖的notepad的入口点的结果并可以在图 7 中看到。这种方法依旧没有空字节,长度上仅仅10个字节。图 7:使用跳转和调用寻找RIP,终结
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)