* 本文原创作者:与非门salome,本文属FreeBuf原创奖励计划,未经许可禁止转载
首先,在windows10下编写一个具有一定安全机制但又存在漏洞的程序作为实验。接着通过一定的分析,编写出能够成功反弹shell功能的漏洞程序。
值得说明一下的是,本次的实验环境是Windows10 x64版本,漏洞程序为32位程序,关闭gs代码生成选项(以便去除干扰文章重点rop的因素),开启DEP (数据执行保护)。
首先需要说明几个概念以及本文的意义。DEP基本可以理解为让能够执行代码的内存区域执行指令,而不具有执行特性的内存区无法执行指令(硬件DEP主要通过页表项的特殊位来指示),如果在不可执行的区域执行了这些指令,那么便会抛出异常。
当然在Windows中支持硬件DEP的话,其也是可以被配置的。配置选项有AlwaysOn,AlwaysOff,OptIn,OptOut 。在MSDN的详细说明如下图。
我们尝试将普通的shellcode(jmp esp地址写入函数返回地址达到控制),确实我们转入了shellcode的布置空间,但是我们执行第一个熟悉的shellcode,我们便无法执行了,理所当然,我们的程序抛出了异常。
由于上述安全机制的存在,那么我们就不能直接把shellcode放在栈种控制程序的流程。所以我们需要一些手段。
本文中将会展示2类方法绕过DEP,但其核心都是ROP(返回导向编程的思想)。
先不急,我们来看漏洞实验程序。由于这是一个漏洞程序,我们直接看存在栈溢出漏洞的程序,非常简单,一个有限空间的栈上分配的数组允许使用者无限制的用户输入。
同时看到,这里的坏字节是x00,所以我们的漏洞利用代码不能存在x00的字节。这里我们开始一种漏洞利用程序的分析及编写。
攻击姿势1:
既然我们的栈中的数据不能被作为指令来被执行,但是我们可以把像ntdll.dll,kernel32.dll中我们需要的指令地址溢出并填充到栈中,ntdll.dll等模块中的多个组合的指令便就是我们的shellcode。
其中ROP的形式为 retn结尾,以便能够在栈中依次执行rop 链中的每个gadget。
现在我们期望通过溢出布置WinExec来完成calc.exe的执行。我们的大致从低地址到高地址栈空间将会是如下。
WinExec ExitThread 指向calc.exe位置 0×01010101 ret address exitcode calc .exe 0xffffffff
WinExec的第一个参数指向栈高位置的calc.exe,因为如果放在相对存放WinExec地址栈位置的低地址的话,WinExec里的流程可能就会把calc.exe 冲刷掉,那么我们本期望指向calc.exe的指针指向不可预知的内容,那么WinExec将会失败。
另外,calc.exe后跟的0xffffffff的意义是x00是坏字节,我们不能在shellcode中存在x00,而我们的calc.exe字符串必须后跟随一个x00作为结束符。那么紧跟在上述栈结构的位置后面的是修正参数的一些指令。
使用WinDbg中的python插件mona生成一些关于rop有用的信息,命令的使用方法是先设置生成后文件的存放位置,如!py mona config -set workingfolder ” 你希望的位置”,再使用命令!py mona rop -m kernel32.dll,ntdll.dll,msvcr120.dll即在这3个指定的加载模块中搜索对rop有用的指令。
由于修正shellcode的loader位于前面讨论的结构的下方,而且那个栈位置布置占9个栈空间,那我们需要9*4字节(32位漏洞程序每个栈位置占用的空间大小)=0x 24 字节的调整。
我们打开之前在mona工作目录下的stackpivot.txt。但是发现没有直接add esp,0×24 retn的,所以选择导致栈位置调整0×28的add esp,0×24 pop ebp retn。其在MSVCR120.dll中,位置为MSVCR120 base 0x476c6中。
这个stackpivot操作对应在WinExec-ROP的低位置。然后我们开始了解一下WinExec和ExitThread 的RVA信息。
WinExec位于kernel32.dll,将其拖入depends工具中。
通过depends工具查看kernel32.dll 0x3ff70的位置处。
ExitThread位于kernel32.dll中,但是该工具中发现其对应ntdll.dll中的RtlExitUserThread,也就是ntdll base 0x681b0。
现在再来回头看看之前描述结构对应的实体信息。
根据之前的分析,运行了第一个语句后,将会运行到在栈中定位到0xffffffff(add esp,0×24 pop ebp retn对应pop到ebp的值)后的一个地址进行retn。
在这里我们先完成修正calc.exe字符串后的x00的工作。
其中,对应的工作就是得到当前的栈地址(每次运行时的栈位置各不相同),再根据该栈位置向低地址调整到需要被修改为0的0xfffffff地址处。所以我们需要类似push esp,pop reg32类型的指令。
我们在生成的rop_suggestion.txt中我们看到如下几类指令(使用move esp ->搜索)。
我们先点击move esp->eax处查看一下对应指令的具体情况,我们截取了一小部分,可以发现并不适用,因为eax和esp交换了之后,eax的不确定性可能导致程序崩溃。
同样地,move esp->esi对应如下,以不确定的寄存器值作为寻址的决定因素是不可靠的,只能放弃。
经过一系列的排查后,发现少数能使用的语句如下:
如当使用第2个时,则不会存在访问违例的问题。
但是mov esp,ebp则需要保证ebp的有效性,而在前面选择stackpivot时,我们选择了add esp,0×24 pop ebp完成了栈位置的跳跃,pop ebp我们由于不知道在对应栈的位置给ebp设置成什么值来匹配而设置成了0xffffffff。
这也就违反了使用push esp,xor eax,eax……retn的mov esp,ebp问题。所以我们对前面的栈跳跃换选其他指令。我们尝试替换成如下。
但是,注意到move esp->edi的过程中,retn 后esp会进入ebp 4的位置也就是stackpivot的位置,而这里ebp我们同样无法正确设置(溢出后的ebp位置总是被我们的内容覆盖)。
所以我们还得不断替换现在的这个指令。继而选择move esp->edx类型的gadget。我们现在尝试把eax设置为为一个有效地址。
使用这个gadget时,注意需要在retn address前设置好pop ebp的位置。
我们这里在运行时输入!dh kernel32,以查看其pe头部信息,因为mov [eax],al代表了需要该ebp位置需要可写(存在一处ebp eax交换的指令,而ebp我们可控,那么我们就能控制eax内容指向地址的可写性质)。
通过输入!dh 模块名的命令,可以看到.data段使具有可写属性的。所以设置kernel32.dll的偏移0xb0000处。但是x00是一个坏字节,相应做一些修改即可(如0xb0101也在.data段范围内)。
现在经过这些处理,这里gadget使用的add byte ptr[eax],al便不会导致访问违例的异常了。
由于现在edx已经指向当前gadget的后4字节位置,所以还需要让edx向低地址移动。
可是我们搜索带有sub edx,imm 类型的指令并没有。只有sub eax,imm的类型。所以我们尝试将指向here的edx设置到eax中。
由于eax当前还和需要调整的calc.exe结尾的0xffffffff相差6*4的距离。所以使用SUB EAX,0×16 RETN和7次SUB EAX,2 POP EBP RETN组合调整到该位置。
现在我们目标将该dword大小的地址的低字节设置为0,我们在rop.txt里搜索mov dword ptr[eax]的gadget看看结果如何?
我们看到了一个mov dword ptr [eax],0xb5000000的指令感兴趣。只要低字节为0即可。
其位于ntdll 0x196cb处。至此,我们完成了修正rop链的第一步。
现在我们进入第二步,将WinExec的参数1指向calc.exe。别忘了,我们现在的eax位于calc.exe 0×8处哦。
由于前面看到只有eax可以进行sub调整,所以我们将当前eax存储到其他寄存器中,并将eax调整到calc.exe的起始位置。我们使用下图的gadget将eax存储到ecx中。
现在我们进入第二步,将WinExec的参数1指向calc.exe。别忘了,我们现在的eax位于calc.exe 0×8处哦。
由于前面看到只有eax可以进行sub调整,所以我们将当前eax存储到其他寄存器中,并将eax调整到calc.exe的起始位置。我们使用下图的gadget将eax存储到ecx中。
其位于ntdll.dll的0×21110处。之后,我们将eax向低地址推进8个字节的距离。我们依旧选取之前使用过的gadget(# SUB EAX,2 # POP EBP # RETN)。
ecx在被设置为eax原值后和WinExec所需的_stdcall参数1位置相差0xc。我们理想中的gadget为mov [ecx 0ch],eax retn类型的。
非常幸运,我们在ntdll中找到了这个gadget。
接着最后一步我们需要调整esp使其进入WinExec的流程。
我们输入正则表达式mov esp,(?!ebp)来匹配直接对esp值设置的指令(由于mov esp,ebp的指令会很多)。
简单地发现了,ntdll.dll中存在mov esp,ebx pop ebx retn。这也就需要ebx在WinExec-0×4位置处才行。
依据这些,我们寄希望ebx,能通过eax交换到ebx(因为能方便加减的寄存器为eax),所以我们尝试搜索,xchg eax,ebx mov ebx,eax等。我们看到了如下的结果。
比如针对前2条我们看到了mov esp,ebp的指令,之前由于一些gadget存在pop ebp,导致垃圾数据被填充到了ebp中。
又如倒数第2句,ebx必然不为-2所以导致肯定会跳转导致崩溃。
所以,我们在文本编辑器中继续输入正则表达式xchg (.*?),esp时发现了直接将eax,设置到esp的指令。位于ntdll偏移0x916c9。
这下好办了,我们在最下方部署gadget1:调整eax到rop链中的WinExec类似add esp,xx;gadget2:设置eax到esp如xchg eax,esp。便成功异常绕过dep并通过rop弹出了计算器。
攻击姿势2:
绕过DEP的方式各版本系统有VirtualProtect设置shellcode所在内存页面为可执行;使用VirtualAlloc存在执行权限的内存页;使用HeapCreate HeapAlloc分配有执行权限的堆。
而如一些其他的公布的绕过方式中,有些方式的限制较多。
如SetProcessDEPPolicy设置当前进程的限制仅能为32bit的程序且系统DEP选项不能为AlwaysOn以及调用过SetProcessDEPPolicy的程序。
如NtSetInformationProcess同样对AlwaysOn的无效以及对NtSetInformationProcess调用过的程序无效。
对于使用VirtualProtect的情况,我们只需要对shellcode范围内的地址设置为0×40(PAGE_EXECUTE_READWRITE)便可以使用之前的方式调用shellcode了。
在生成的rop_chain.txt可以看到python版的VirtualProtect的绕过实现。
现在,我们开始对这个自动生成的shellcode进行改造,因为它包含了一些空字节以及未使用base rva的表示方式导致了ASLR被允许的情况下的无法运行。
可以观察到最后第二句为pushad retn的指令,这里我们补充一下PUSHAD/POPAD将EAX ECX EDX EBX ESP(该指令执行之前的初始值) EBP ESI EDI分别出入堆栈。
所以大致上这个rop chain的做法是设置好对应寄存器在压栈时位置对应的参数和地址pushad retn以进入Virtual Protect将shellcode范围设置为可执行,之后直接跳入shellcode中。
edi nop retn的地址 esi jmp [eax]的地址 ebp pop retn的地址 esp LpAddress ebx dwSize edx NewProtect ecx lpOldProtect eax dll中存放VirtualProtect的iat jmp esp的地址
第一步使ebp指向pop retn的指令搜索# pop ebp # retn。
设置完ebp->pop retn后,我们开始设置ebx为0×400,即VirtualProtect的dwsize为0×400(1k)大小。
由于存在本程序的坏字节0×400,我们构造2个相加为0×400的数这里我们使用0x1111221f和0xeeeee1e1,分别通过如pop ebx,retn等设置到ebx和ecx后转入add ebx,ecx retn。
使用相同的办法我们使得edx(NewProtect)为0×00000040。只是需要注意我们相加使用edx和另外的一个寄存器,ebp,ebx都使用过了我们需要选择其他的。
这里我们输入正则表达式add edx,eSS #retn的指令 发现只有较少的指令与edx有相加的操作,并且普遍都是edi寄存器。
对于ecx需要设置为填充lpOldProtect即out 原先保护熟悉的地址。我们这里设置一个可写的地址即可。
即kernel32.dll 0xb0b38处。
通过windbg使用!dh kernel32命令看到其实这里就是一个.data段的地址,确实可写。
现在我们让esi指向jmp [eax],其位于ntdll.dll偏移0x00042cde,注意这里的eax会指向某个模块中virtualprotect(KERNELBASE.dll)的IAT。
最后我们需要布置lpaddress对应的esp由于我们需要设置的是栈中可能动态变化的shellcode的位置。
比较技巧的是,由于esp提供lpaddress,在pushad retn后对应virtualprotect也将会在附近。所以无需我们手工再次专门设置esp。
最后的成功溢出执行代码的效果便是连接本地的1514端口反向shell。
本次实验测试内容包括漏洞程序工程、2类漏洞编写程序工程等(系统版本不同可能需要自行修改漏洞利用程序中的部分编码才能成功运行)。
内容可以通过链接:http://pan.baidu.com/s/1pLs6m2v,密码:p5um进行下载。
* 本文原创作者:与非门salome,本文属FreeBuf原创奖励计划,未经许可禁止转载