汇编网首页登录博客注册
240420643的学习博客
博客首页博客互动【做检测题】论坛求助

我的博客

个人首页 |  我的文章 |  我的相册 |  我的好友 |  最新访客 |  文章收藏 |  论坛提问 |  友情链接 |  给我留言  
图片载入中
学习动态
好友圈
文章收藏

[2010-01-12 15:10] 推荐博文 《网路对抗原理》第六章笔记--缓存溢出攻击

图片载入中
第六章 缓存溢出攻击
6.1 绪论
过去10年中计算机的最大安全隐患究竟是什么呢?
计算机科学和安全问题的分析家认为,计算机的最大隐患不是“千年虫”问题,二是被称为“缓存溢出”的安全漏洞。“千年虫”只会使计算机无法正确识别用两位数表示的年份,二缓存溢出则不同,它为心怀恶意的黑客打开了攻击他人计算机的大门,使他们可以利用这个漏洞打入想要攻击的计算机,从而达到部分甚至完全控制对方的计算机系统。
在存在缓存溢出安全漏洞的计算机中,攻击者可以用超出常规长度的字符数来填满一个域,通常是内存区地址。在某些情况下,这些过量的字符能够作为“可执行”代码来运行,从而使得攻击者可以不受安全措施的约束来控制被攻击的计算机。
美国俄勒冈州科学与技术研究生院(OGI)最近公布的一篇论文称:“过去10年中,缓存溢出一直是计算机的最大安全隐患。由于受到这种攻击,使得任何人都能够完全控制一台主机,因此这构成了对计算机安全的最大威胁。”
本章将分别介绍在windows系统和Linux系统下的缓存溢出原理和攻击方式。
6.2 windows下的缓存溢出
6.2.1 简介
首先让我们来看以个例子
 
 
 
这就是由于缓冲区溢出造成的。下面来分析造成这种结果的原因。
上面的代码中定义的子函数MYCOPY只有一个入口参数(char *str),同时定义了一个局部变量buffer[256]。该函数在调用strcpy时把主函数传递过来的511字节长的字符没有经过长度检查,直接拷贝到职分配了256字节的缓冲区中,也就是说,网缓冲区中拷贝了过多的数据,这就有可能覆盖指令指针(eip)即覆盖返回地址、ebp甚至子函数的局部变量。在这个例子中,eip和ebp均被设置为0x43434343(0x43是字符C的ASCII码)。简单地说,当一个大的物体放入一个小的容器时,就会出现“缓存溢出”现象。在代码中,我们可以认为给变量分配的内存空间就是一个容器。如果仅仅想使程序崩溃,只需往内存这个容器中放置过大的数据,使程序无法正常返回即可。更进一步,可以通过覆盖返回地址,改变程序的执行流向,使程序返回后能执行特定代码的特定功能。
6.2.2 基本原理
为了深入的分析,首先学习有关堆栈、ebp、esp和eip的有关知识。
1)        堆栈
堆栈一般是用来传递参数和保存子程序的局部变量,也成为后进先出(LIFO)。堆栈的生长顺序与内存的生长顺序相反,即内存的高端是堆栈的低端,内存的低端是堆栈的高端。压栈(PUSH)指往堆栈保存数据,一般保存的是数据值或指向数据的指针。如参数是短整型时,其数据值被压入栈;参数是字符串时,指向该字符串的指针被压栈。每一个函数调用都有其栈帧,包括数据、返回地址、ebp和先前的esp等等。
2)        寄存器
Intel处理器有四个通用寄存器(EAX、EBX、ECX和EDX)、一个标志寄存器、四个段寄存器、两个变址寄存器、一个堆栈指针寄存器(ESP)、一个基址寄存器(EBP)和一个指令指针(EIP)。EIP(32位指令指针)是下一条指令在代码段(CS:指令或代码存放的位置)里的偏移,一般代码是不能够直接访问EIP值的。在每条指令被执行之后EIP值就自动增加一个值以指向下一条要执行的指令地址。当你要调用子程序时,系统就需要知道下一条指令的地址以及如何返回原始调用处。调用指令通常规定了要往EIP所加的值,并把它压入堆栈。而调用函数中的返回指令会把堆栈值弹出给EIP以恢复调用后下一条指令的执行。同样,堆栈指针ESP指向堆栈的最高端,EBP可指向堆栈的任意位置。变址寄存器SI和DI类似。
换句话说,EBP是具有固定偏移的局部变量和参数堆栈信息的偏移参考寄存器;ESP地址经常发生变化,因为它一直指向堆栈的顶端。任何的压栈或出栈操作均会影响ESP的值。还以上面的例子为例来说明堆栈的情况。
编译后得到main()函数的汇编指令如下:
 
这里只有一个入口参数,当为多个参数时,入口参数的压栈顺序是逆序进栈,即按从右到左的顺序依次把参数压入堆栈。接下来看子函数MYCOPY()的汇编指令
 
根据分析可以看出,发生函数调用时,首先将参数压栈;然后保存指令寄存器EIP中的内容作为返回地址(RET),再把机制寄存器(EBP)压入堆栈;随后将当前的堆栈指针(ESP)拷贝到EBP作为新的基址;最后将ESP的值减一定的值从而为本地变量留出一定空间(减去的值一般大于给局部变量分配的空间)。因此函数要引用局部变量就可通过EBP来完成。例如,设函数中定义了整型局部变量I和K,则ESP要减去8字节(其中4字节给I,四字节给K)。
要访问变量I时可通过汇编指令“MOV EAX,[EBP-4]”来完成。需要注意的是,在定义char型数组变量如BUFFER[N]时,当N不是4的倍数如N=9时,子函数要访问BUFFER[0]时,EBP减去的值不是9,而是12。这是因为在32位机种,地址是32位的(按字计算)。因此,BUFFER[9]栈3个字(4+4+4)。由此得到栈帧结构如图6-2所示。
 
接下来看程序运行时发生的情况。当程序调用子函数MYCOPY,在执行STRCPY()之前,堆栈的情况如图6-3所示。
 
调用STRCPY()将长字符串(STR)中的字符拷贝到BUFFER中,由于STR中共有512个字节,而MYCOPY给BUFFER只分配了256个字节,因此,在STR中的字符(0XC)填满BUFFER后,又继续往高地址即堆栈的低端写数据,从而覆盖了调用函数MYCOPY时保存的EBP,RET和入口参数甚至其他。这就导致子程序返回时转向地址0X43434343(0X43是字符C的ASCII码)去执行,从而出现了图中提示的该地址不能为“READ”的错误信息。
这就是所谓的缓存溢出。
从上面的例子中不难看出,我们可以通过缓存溢出来改变堆栈中存放的函数返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方。这就为我们提供了可乘之机,最常见的方法是:在长字符串中嵌入一段代码,并将函数的返回地址覆盖为这段代码的地址,这样当函数返回时,程序就转而开始执行这段我们自编的代码了。至于这段代码具体执行些什么样的指令以及完成什么样的功能将根据实际的攻击来确定。通常我们称这段代码为shellcode。这部分代码的编写将在以后的章节中详细讨论。
6.2.3 获得EIP
从上面的介绍的原理知道,当函数中有数据拷贝操作,而数据源可由用户提供,且程序不对拷贝数据的长度进行检查,就可以通过提供过长的数据覆盖程序的返回地址RET来改变程序流程。如果仅仅想使程序崩溃,那么可以不必过分关心RET保存在堆栈中的具体位置和所提供的覆盖RET的新的EIP。但是,我们往往希望程序退出时区执行我们提供的代码如蠕虫病毒(这个代码在这里一般称为shellcode)。这时,需要找到RET在堆栈中的准确位置,同时要把shellcode存放的地址作为新的EIP覆盖RET。为此,提供的数据(称填充字节代码)一般可以采用如下形式:
Address=…256periods…1234xyz
由于缓存长度等于256字节,我们可以通过经验得出结论,慢慢地增加和缩短地址address=line length,知道我们准确无误地测试出使程序溢出的字符数目。上述字符串将用256个句点填充缓存,并且把EBP覆盖为0X34333231和设置EIP等于0X00AAYYXX,由于字符串以一个NULL terminator。这使得我们可以把程序转到堆栈的任何地址值。
在某些情况下,这应当是能够正常工作的。但是在某些情况下,例如缓存太小或者缓存中还夹杂着其他字符操作码,这时就得采取措施了。在大多数情况下,把代码放在返回地址的后面是以个比较好的办法,如下就是一个例子:
Address=…256periods…1234WXYZSHELLCODE
在这种情况下,往往有足够的空间用来编写和存放你的缺陷代码,但是不利的地方是,我们无法再利用最后的一个空字符来产生可以任意跳转到堆栈中任何地方的地址。在这里例子中,我们将把字节代码直接放在返回地址的后面。返回地址前面的内容在我们有机会对它操作之前是不可用的。在末尾是跳转到0XZZYYXXWW,其中WW、XX、YY或ZZ中的任何一个都不能是无效字符(如包含NULL(0x00)字符时,字符串就被截断)。所以我们该跳转到何处呢?
首先打开实时程序调试器并且构造能够引起程序崩溃的一个却小字符串,并选定一个环地址(例如把0XZZYYXXWW改成0X34333231,此内存地址处不存在任何代码)。现在运行程序并进入调试器,检查各个寄存器状态。在这里,我们发现ESP是惟一的寄存器,其所指向的地址离我们的缺陷代码不远。是事实上,它所指的位置等于被我们覆盖的保存EBP的那个位置再加上16个字节。
现在我们需要跳转进入堆栈。事实上,只要简单地跳转到ESP即可。最聪明的方法是把0XZZYYXXWW设置成指向内存中某字节代码,如“JMP ESP”或者“CALL ESP”或者其他类似的代码,同时这个地址中不应当有一个坏字节“baD BYTE”特别是0X00。一般在常驻内存空间DLL文件中有“JMP ESP”指令。接下来介绍如何找到DLL文件中指令“JMP ESP”在内存中的位置。
为了找到指令JMP ESP,首先要获得指令的操作码(即机器码)。这很容易,通过VC++编译器,在调试状态下选择“CODE BYTES”即可获得其机器码为FF F4(FF对应JMP,E4对应ESP).由此可通过以下程序来寻找“JMP ESP”指令:
#include <windows>
#include <stdio.h>
Unsigned long filesize(char *s)
{
FILE *fp;
Unsigned long I;
If((fp=fopen(s,”r”))==NULL)
{
  Printf(“cannot open %s.path needed.\n”,s);
  Exit(0);
}
Fseek(fp,0;,seek_set);
Fseek(fp,0l,seek_end);
I=ftell(fp);
Fclose(fp);
Return I;
}
Int main(int argc,char *argv[])
{
  Int nretcode=0;
  Int loaded=0;
  HINSTANCE h;
  Char *dllname;
  Byte *ptr;
  Int pos,y;
  Unsigned long file_size;
  Pos=0;
  Y=0;
  If(argc<=1)
{
Printf(“usage:%s<path>dllfile\n”,argv[0]);
Exit(0);
}
File_size=filesize(argv[1]);
H=getmodulhandle(argv[1]);
If(h==NULL)
{
  H=loadlibrary(argv[1]);
  If(h==NULL)
  {
     Printf(“can’t load %s”,argv[1]);
     Return 1;
}
Loaded=1;
}
Else
{
  Printf(“%s already loaded”,argv[1]);
}
Ptr=(BYTE *)h;
Printf(“\nsearching JMP ESP…\n\n”);
While(y<=file_size)
{
  If(ptr[y]==0xFF&&ptr[y+1]==0Xe4)
{
  POS=(int)ptr+y;
  Printf(“found at 0x%x\n”,pos);
}
Y++;
}
Printf(“\nsearching finished…\n);
If(loaded)
{
  Freelibrary(h);
  Printf(“%s freed\n”,argv[1]);
}
Return nretcode;
}
运行结果如下:
 
还可对其他链接库进行查找。需要注意的是,不同版本的操作系统,该指令地址可能有所不同。
找到内存中的JMP ESP指令的地址后,就可以以此地址作为EIP覆盖RET(注:不是任何一个JMP ESP指令的地址都可以用,只有不包括坏地址如0X00的地址才能利用)。
6.2.4 构造shellcode
在介绍构造shellcode之前需要指出的是:
1)        Shellcode中绝对不允许有任何的NULL字符,字符串是以NULL字符作为字符串结尾的。一旦发现NULL字符就会截断提供的填充字符,从而shellcode不能完整地拷贝到堆栈中。
2)        Shellcode应短小精悍。
3)        Shellcode是依赖于操作系统的,适用于WIN32系统的SHELLCODE不能再Linux上使用。
下面以一个现实消息框的SHELLCODE例子来说明如何构造SHELLCODE。
消息框函数messageBOX是user32.DLL的导出函数。为了获得其在user32.dll的入口地址,首先介绍相对虚拟地址(RVA,Relative virtual address)的有关知识。
EXE、DLL、VXD和OCX等类型的文件均为PE(portable executable)格式的文件。它们需要在内存中拥有固定的地址。PE文件可以装载在程序空间的任意地址,这个地址是文件映射开始的起始地址,也称映像基地址。每个DLL文件的导出函数均驻留在专门的地址内。但内存基地址是随机的,因此,导出函数的位置就很混乱。为此,我们采用了RVA技术。RVA是内存中相对PE文件装载位置的偏移量。设EXE文件装载在地址(映像基地址):0X10000000,代码段起始地址:0X10001000,数据段地址:0X100B7000.则代码段和数据段的RVA分别是其地址减基地址,即:
代码段RVA:0X10001000(代码段)-0X10000000(基地址)=0X00001000(RVA)
数据段RVA:0X100B7000(数据段)-0X10000000(基地址)=0X000B7000(RVA)
在PE文件中,除非做着更改,否则RVA是保持不变的。一般系统文件(如kernel32.dll、user32.dll)等会随着操作系统补丁的不同而不同。
下面就来寻找messageBOX在user32.dll中的入口地址。利用工具PEBROWSE可以获得基地址为0X77DF0000,messageBOX的RVA为0X00025B82。由此可得入口地址为
0X77DF0000+0X00025B82=0X77E15B82
接下来看其函数原型。
MessageBOX(HWND HWND,LPCTSTR LPTEXT,LPCTSTR LPCAPTION,UINT UTYPE)由此可以调用messageBOX的shellcode的汇编代码。
Push ebp                         ;ebp压栈
Mov ebp,esp                      ;拷贝ESP到EBP作为新的基址
Xor edi,edi
Push edi
Mov byte ptr[ebp-04h],48h        ;字符’H’
Mov byte ptr[ebp-03h],69h        ; 字符‘i’
Mov byte ptr[ebp-02h],21h        ;字符‘!’
Mov edx,0x77e15b82               ;messageBOX的入口地址
Push edx
Push edi
Lea edx,[ebp-04h]
Push edx
Push edx
Push edi
Call dword ptr[ebp-08h]            ;调用messagebox
测试这段代码:
/*——MSGBOX.C——*/
#include <stdio.h>
#include <windows.h>
Int main()
{
  HINSTANCE h=loadlibrary(“user32”);
  _asm{
  Push ebp
  Mov ebp,esp
  Xor edi,edi
  Push edi
  Mov byte ptr[ebp-04h],48h
  Mov byte ptr[ebp-03h],69h
Mov byte ptr[ebp-02h],21h
  Mov edx,0x77e15b82
  Push edx
  Push edi
  Lea edx,[ebp-04h]
  Push edx
  Push edx
  Push edi
Call dword ptr[ebp-08h]
}
Freelibrary(h);
}
编译后运行,出现“Hi!”消息框,如图6-4所示
 
这说明shellcode成功调用了messageBOX,接下来需要得到汇编指令的机器码。利用VC++编译器得到:
 
 
把机器码存放在字符串数组中:
 
为了检验该shellcode的机器码在buffer中是否可以执行,编写以下测试代码:
/*——check.c——*/
#include <windows.h>
#include <stdio.h>
Char shellcode[]=”\x55\x8B\xEC\X33\XFF\X57\XC6\X45\XFC\X48\XC6\X45\XFD\X69\XC6\X45\XFE\X21\XBA\X82\X5B\XE1\X77\X52\X57\X8D\X55\XFC\X52\X52\X57\XFF\X55\XF8”;
Int main()
{
  HINSTANCE h=loadlibrary(“user32”);
  Void (*s)()=(void *)shellcode;
  S();
Freelibrary(h);
}
定义的函数void (*s)()仅仅是指向一个缓冲区,调用函数S时就会执行机器码。编译执行后同样得到显示“Hi!”的消息框。
在上面的程序退出时,会出现引用内存错误的提示。为了解决这个问题,在shellcode最后还要调用exitprocess(0)来正常退出。Exitprocess的入口地址为0X77E60000+0X0001CF5C=0X77E7CF5C,因此机器码为
“\X55\X8B\XEC\X33\XFF\XBA\X5C\X7C\XE7\X77\X52\X57\XFF\X55\XFC”
最后得到的shellcode为
Char buffer[]=” \x55\x8B\xEC\X33\XFF\X57\XC6\X45\XFC\X48\XC6\X45\XFD\X69\XC6\X45\XFE\X21\XBA\X82\X5B\XE1\X77\X52\X57\X8D\X55\XFC\X52\X52\X57\XFF\X55\XF8\X55\X8B\XEC\X33\XFF\XBA\X5C\X7C\XE7\X77\X52\X57\XFF\X55\XFC”;
6.2.5 小结
在本节中,我们详细介绍了windows系统的缓存溢出原理以及相关的获得EIP的方法、构造shellcode的过程和方法。需要反复强调的是,不同版本的操作系统,DLL导出函数的入口地址、指令地址等均有所不同,不过其基本原理都是一致的。
6.3 linux缓存溢出
6.3.1 基本原理
由于操作系统在实现上的不同,我们首先来看Linux下栈帧的结构。
Void proc(int i)           //被调用函数
{
  Int local;                本地变量
  Local=i;           
}
Void main()                  //主函数
{
  Proc(1);                   //调用proc函数,带一个参数
}
这段代码在Linux系统下经过编译器后编译为(以PC为例)
Main:push 1                              //入口参数压栈
     Call proc                           //主函数调用子函数处
.
.
.
      Proc:push ebp                            //子函数入口处,将基址寄存器压栈
      Mov ebp,esp         
      Sub esp,4                                //ESP指针减4,为局部变量分配空间
      Mov eax,[ebp+08]                        //EBP+08即访问参数1
      Mov [ebp-4],eax                         //EBP-4即访问局部变量
      Add ebp                                 //收回局部变量空间
      Pop ebp                                 //回复EBP值
      Ret 4                                   //调用返回
      不难看出,这个程序在执行proc过程时,栈帧的结构如图6-5所示。
       
     由此可以看出,当程序中发生函数调用时,计算机完成操作的操作和windows下基本相同。区别在于Linux下最后为本地变量留出的内存空间大小有所差别。其栈帧的一半结构如图6-6所示。
 
这和windows系统下一样,只是windows系统下预留了更多的内存空间。但多出的空间位于堆栈的顶端,即内存的低端。根据栈帧的结构可以看出,在Linux下可以和windows一样,当函数发生数据拷贝,二待拷贝的数据可由用户提供,同时程序对数据长度不作检查时,可通过提供过长的数据来达到覆盖ret的目的,从而造成缓存溢出。这里不再详述。
6.3.2 有关shellcode.
前面提到,shellcode是依赖于操作系统的。现今在Linux下存在好几种缓存溢出的shellcode。早期的shellcode功能比较简单,往往是仅仅(通过执行/BIN/SH)获得一个Shell。但是现今的shellcode已具备更多样化的方法,如绕过过滤器限制、建立套接字和突破chroot等等。这里我们将介绍给予Intel X86的Linux下缓冲区溢出shellcode的编写及较为高级的使用技巧。
1.        创建Shell的shellcode
这里通过介绍创建Shell的shellcode来说明有关shellcode的编写。首先编写一个创建Shell的C程序shellcode.
#include <stdio.h>
Void main()
{
  Char *name[2];
  Name[0]=”/bin/sh”;
  Name[1]=NULL;
  Execve(name[0],name,NULL);
}
这段代码利用execve()调用来执行/bin/sh,即获得Shell命令权。编译(-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入)后启动GDB调试,查看反汇编后的代码。首先分析一下main()反汇编后的代码。
0X8000130:PUSH1 %EBP                    #EBP压栈
0X8000131:MOV1 %ESP,%EBP                #当前栈指针作为新的EBP,注意和windows下的区别windows下数据传递的目的和源于这里相反
0X8000133:SUB1 $0X8,%ESP                #最后为局部变量留出空间。在这里,局部变量为:char *name[2]:两个字符指针,每个字符指针占用4个字节,所以总共留出了8个字节的位置
0X8000136:MOV1 $0X80027B8,0XFFFFFFF8(%EBP) #将字符串“/BIN/SH”的地址放入name[0]的内存单元中
0X800013D:MOV1 $0X0,0XFFFFFFFC(%EBP)    #将NULL放入name[1]的内存单元中
接下来开始对execve()的调用。
0X8000144:PUSH1 $0X0                       #将参数以逆序压入堆栈,第一个是NULL
0X8000146:LEA1 0XFFFFFFF8(%EBP),%EAX        
0X8000149:PUSH1 %EAX                       #将name[]的起始地址压入堆栈
0X800014A:MOV1 0XFFFFFFF8(%EBP),%EAX
0X800014D:PUSH1 %EAX                       #将字符串”/BIN/SH”的地址压入堆栈
0X800014E:CALL 0X80002BC <_EXECVE>         #调用execve().CALL指令首先将EIP压入堆栈
再来看一下execve()的代码。不同的操作系统,不同的CPU产生系统调用
方法不尽相同。有些使用软中断,有些使用远程调用。从参数传递的角度来,
有些使用寄存器,有些使用堆栈。给予Intel X86的Linux系统,系统调用采
用软中断(INT 80H),参数是通过寄存器传递给系统的。
 
由此可以看出,为完成execve()系统调用,要依次完成以下操作:
1)        在内存中有以NULL结尾的字符串“BIN/SH”;
2)        在内存中有“BIN/SH”的地址,其后是一个长字符型的NULL值;
3)        将0XB拷贝到EAX中;
4)        将“BIN/SH”的地址拷贝到EBX中;
5)        将“BIN/SH”地址的地址拷贝到ECX中;
6)        将NULL串的地址拷贝到EDX中;
7)        执行终端INT $0X80。
如果execve()调用失败,程序可能会core dump。为了在execve()后加调用exit()进行保护,有关exit()调用的过程这里不作描述。最后,我们的shellcode要完成的工作(利用execve()打开Shell,exit()调用保证程序退出)可由以下伪汇编语言来描述。
 
 
这段代码中还存在着一个问题,就是在编写shellcode时并不知道这段程序执行时在内存中所处的位置,所以像MOV1 STRING_ADDR,%EBX这种需要将绝对地址编码进机器语言的指令没法使用。解决这个问题的一个办法就是使用一条额外的JMP和CALL指令。因此这两条指令编码使用的都是相对于IP的偏移地址而不是绝对地址,所以我们可以在shellcode的最开始加入一条JMP指令,在string(“/BIN/SH”)前加入一条CALL指令。只要我们计算好程序编码的字节长度,就可以使用JMP指令跳转到CALL指令处执行;而CALL指令则指向JMP的下一条指令。因为在执行CALL指令时,CPU会将下一个要执行的指令(也就是string的地址) 作为返回地址压入堆栈。所以这样我们就可以在运行时获得string的绝对地址。
到这里还有一个小问题。在介绍windows缓存溢出时提到,C语言中的字符串是以‘\0’结尾的,strcpy等函数遇到‘\0’就结束运行。因此,为了保证我们的shellcode能被完整地拷贝到buffer中,shellcode中一定不能含有‘\0’。为此包含‘\0’的指令进行替换以去掉其中的‘\0’,例如:
 
最后得到的程序清单为:
 
 
编译后用GDB得到这段汇编语言的机器代码即shellcode为
 
同样,可以编写一个小程序来验证这段shellcode.
 
运行后出现Shell提示符$.接着我们就可以利用这段代码编写溢出程序了。
2.        绕过过滤器限制
许多程序存在缓冲区溢出问题。但是为什么并非所有的缓冲区溢出程序都能被用于获得Shell呢?这是因为即使某个程序具备了缓冲区溢出的条件,也许仍然很难攻击成功。在许多情况下是由于程序过滤了一些字符或者把一些字符转变为另一些字符。如果以个程序过滤了所有的非打印字符,溢出漏洞就几乎不可利用了。但如果程序只过滤了部分的字符,那你可以通过编写巧妙的缓冲区溢出代码来染过这些机制。
举一个简单的例子,某个程序之完成将用户输入的小写的字母转换为大写的字符,么这时必须编写一个不包含任何小写字母的shellcode,否则达不到预期的效果。在Linux系统下的缓存溢出的shellcode中往往有“/bin/sh”且必须是小写。我们不能直接使用“/bin/sh”,因为会被过滤掉。这时可以用“\x2f\x12\x19\x1e\x2f\x23\x18”来代替“\x2f\x62\x69\x6e\x2f\x73\x68\”(“/bin/sh”)。但是在缓冲区溢出后,我们必须把“\x2f\x12\x19\x1e\x2f\x23\x18”变成“\x2f\x62\x69\x6e\x2f\x73\x68\”,这样才可以执行“/bin/sh”。这可以通过在shellcode执行时加\x50使\x62、\x69、\x6e、\x2f、\x73和\x68\成为执行代码。
利用这个技巧,我们可以绕过各种系统不同的过滤机制。当被攻击程序过滤了!、@、#、¥、%、^、&、*、()时,我们可以编制一个新的shellcode,使之不包括以上字符。然而,如果程序过滤了更多字符,编制shellcode也会变得更加困难。
3.        改变uid为0
Setuid成为root的程序时在运行时具有root权限的程序,一直被认为是安全的一个隐患,因为它在执行过程中调用了setudi(0)。许多程序员认为使用setuid(getuid())会更安全些,但事实并非如此,用户标识uid还是可以变为0.
4.        突破Chroot
如果setuid成为root的程序具有chroot的特性,那就只能访问被chroot保护的目录,而不能访问真正的目录。但是,通过在shellcode里吧root的目录变为“/“,你仍旧可以访问任意的目录。
5.        建立套接字(socket)连接
如果我们攻击一个存在缓冲区溢出漏洞的守护程序,会使它崩溃。因此在大多数情况下,我们必须先执行一个Shell,然后打开一个套接字端口,再利用标准I/O建立连接。否则,不可能获得所需的Shell。即使得到了Shell,由于该守护程序已经崩溃,我们也无法执行任何指令,所以,必须编制更为复杂的Shellcode,用于与我们的机器建立连接。
6.        缓冲区不足
有时待溢出的缓冲区太小,可能不足以容纳Shellcode。在这种情况下,可以使用环境变量。一般环境变量区足以容纳很大的Shellcode,这样用于覆盖问题函数返回地址的buffer填充为环境标量的返回地址即可,二不必容纳shellcode本身。
评论次数(0)  |  浏览次数(2032)  |  类型(网络对抗原理笔记) |  收藏此文  | 
 
 请输入验证码  (提示:点击验证码输入框,以获取验证码