编写Shellcode:寻找EIP/RIP
2020-01-31 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:70 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:sudozhange

当学习主动安全认证专家(OSCE)链接:https://www.offensive-security.com/ctp-osce/)认证时,我花费了大量的时间用于研究如何编写自定义shellcode。本文是我计划要发布的一系列博文里的第一篇,这个系列详细介绍我在准备认证过程中所学习到的技术。

本文的重点是描述如何找到EIP/RIP以及找到后做什么。OSCE专注于32位系统,作为继续学习的一部分,我会研究并记录下在64位系统上的方法。这超出了OSCE的需求,但这是我继续学习过程的一部分。

在本文中,我不会介绍到所有的可能找到EIP和RIP的方法。我会介绍一些我熟悉并已经研究过的方法。如果你想找到更多的方法,我鼓励你开始你自己的研究并开始做自己的学习总结。在这里你看到的是我的学习总结,我希望它会在某些方面对你有帮助。

我知道这些主题多年来已经被多次提到,也没有新的东西,但重点不是提出新的东西,而是在学习总结中提升自己,并且还可以帮助到刚开始学习的人。

寻找EIP:32位汇编方法
从使用x86汇编(32位)指令定位EIP的方法开始。有两种方法可以用,根据你可能会遇到的情况和限制,两种方法可能都有用。两种方法都可以找到EIP。
我要介绍的第一种方法比第二种方法小一个字节。两种方法都可以达到完全相同的目的,存储EIP的值到EAX寄存器中。两种方法都没有空(0x00)字节。之所以使用一种而不是另一种,是因为您可能会遇到大小或字符限制。

>>>>

方法1:使用FPU指令

该方法最先由Aaron Adams在漏洞开发(Vulnerability Development)邮件列表中提出.他使用的这种方法利用x87浮点单元(FPU)寄存器来获取EIP值。以下的基础汇编代码将EIP的值存储到EAX寄存器中。
[SECTION .text]
 
BITS 32
 
global _start
 
_start:
    fldz
    fnstenv [esp-0x0C]
    pop eax
    add al, 0x07
执行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来汇编代码:
nasm method1.asm -o method1.asm
随后插入字节到C中:
char code[] = "\xD9\xEE\xD9\x74\x24\xF4\x58\x04\x07";
 
int main(int argc, char **argv)
{
   int (*func)();
   func = (int (*)()) code;
   (int)(*func)();
}
使用MingW来编译代码:
i686-w64-mingw32-gcc-win32 method1.c -o method1.exe -fno-stack-protector -no-pie -m32
运行最终的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字节:
[SECTION .text]
 
BITS 32
 
global _start
 
_start:
    fldz
    fnstenv [esp-0x0C]
    pop eax
    sub eax, -0x07
在图 9中我们可以看出建议的代码正常运行并且会比原来的代码更加可靠。
 

图 9 EAX指向EIP
>>>>方法2:使用跳转和调用
第二种方法仅比第一种方法长一个字节,也完成了将EIP的值存储到EAX中的目标。我从 Phrack issue 62,Phile 7标题为History and Advances in Windows Shellcode中学到的这种方法。
这篇文章由SK Chong所写。为了获得EIP这种方法使用了一系列的跳转和调用,最终EIP的值被存储在EAX中。
SK Chong文章的原始文件已经找不到了,但是,我找到了其中的一些。在他的例子中,它使用了db项来硬编码了那些跳转和调用。我要提供的版本读起来更容易并且以及更容易使用nasm汇编。
[SECTION .text]
 
BITS 32
 
global _start
 
_start:
    jmp label2
    label1:
        jmp getEIP
    label2:
        call label1
    getEIP:
        pop eax
上面的代码执行了跳转到label2,一个call指令调用了label1。当call被执行时,返回地址,即下一条指令的地址,被压入栈中。执行,随后跳转到getEIP,会弹出返回地址到EAX。不要小题大作(No muss, no fuss.)。

测试

你可以使用测试方法 1的测试方法来测试方法2.当执行完,结果看起来如图 3。
 

图 3:EAX指向EIP
寻找RIP:64位汇编方法
查看是否32位架构中运行的方法在64位世界中是否运行是一个好的开始。如果只需做些微修改,为什么还要重新造轮子?

>>>>

方法1:使用FPU指令

尝试1

使用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的值是可行的。我的下一步是看看是否能够写一些汇编代码,可以在调试器中手工输入命令可以使之平衡。经过一些调整后,这是我想出的代码:
SECTION .text]
 
BITS 64
 
global _start
 
_start:
    fldz
    fnstenv [rsp-0x0C]
    pop rax
    add rax, 0x07
当使用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,终结

>>>>

方法2:使用跳转和调用

在开始测试该方法前,我想做些微小的修改。我想有必要将EAX替换成RAX。这种跳转和调用和手工干扰到调试器中有一些不同,所以我选择先开始编写一些汇编代码。下面的代码就是我选择用来最开始尝试的:
[SECTION .text]
 
BITS 64
 
global _start
 
_start:
    jmp label2
    label1:
        jmp getEIP
    label2:
        call label1
    getEIP:
        pop rax
执行了使用汇编的代码覆盖的notepad的入口点的结果并可以在图 7中看到。这种方法依旧没有空字节,长度上仅仅10个字节。

图 7:使用跳转和调用寻找RIP,终结

>>>>

方法3:加载RIP的有效地址

在64位架构上获取了两种方法后,我想看看是否有更好的方式。当搜索时,我想到了一篇由 Booze | Allen | Hamilton发表的文章,详细的描述了由Rapid7 在Metasploit中实现的方法,该方法使用LEA指令来获取RIP。
经过一番尝试,在使用Nasm命令行时,我想出了下面的代码,可以复现Metasploit使用的方法:
[SECTION .text]
 
BITS 64
 
default rel
 
global _start
 
_start:
    lea rax, [_start]
当使用Nasm并使用一下标记汇编时,结果代码没有空字节,并仅仅7字节长。-O0标记告诉Nasm不要执行优化。如果你没有使用该标记,操作码会包含空值。自己尝试,并看看是如何功能做的:
nasm -O0 method3.asm -o method3.bin
代码和结果可以在图 8中看到。
图 8:使用加载有效地址(LEA)指令寻找RIP,终结
总结
我已经详细描述了几种根据目标架构寻找EIP或RAX的方法。如果希望执行其他与shellcode交互的操作,动态获取这些值是非常重要的。
由于ASLR无法动态查找EIP/RIP,所以解码,复制或者其他修改你的代码的操作是不可能的。在之后的文章,我会继续以此为基础。
本系列的最后一篇文章将会编写一个完全自定义的shellcode,使用Intel x64汇编中的scatch编写。
 
感谢您花费时间阅读我的博客!我希望你能够从中学习并可能启发你自己学习更多。
原文链接:
https://blog.xenoscr.net/Finding-EIP/

- End -

看雪ID:sudozhange

https://bbs.pediy.com/user-703263.htm 

*本文由看雪翻译小组 sudozhange 编译,lipss校对。

推荐文章++++

CVE-2017-11882理论以及实战样本分析

恶意代码分析之 RC4 算法学习

CVE-2017-0101-Win32k提权分析笔记

ROPEmporium全解

实战栈溢出漏洞

好书推荐


公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]
“阅读原文”一起来充电吧!

文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458303081&idx=1&sn=b40b9e8510cbddf31b7acb5fc791973f&chksm=b1818ae386f603f549b3f610704b364b781c786ca99ca366ed8118f9a46c59f1e0bc2750cec4#rd
如有侵权请联系:admin#unsafe.sh