编写自己的缓冲区溢出利用程序

  如何编写自己的缓冲区溢出利用程序?by 黑猫 (virtualcat@hotmail.com)内容: 本文主要讲解有关Buffer Overflow的原理, 以及结合实战范例介绍Linux和Solaris下的漏洞利用.本文并不介绍如何编写shell code.要求: 读者要有一点C和汇编语言基础.目标: 希望本文能够尽量做到通熟易懂,使得稍有计算机基础知识的朋友看后能够亲自动手写自己的Exploit如果你觉得自己对这些都懂了, 就请不要再往下看了.第一部份 概述篇1. Buffer overflow是如何产生的? 所谓Buffer overflow, 中文译为缓冲区溢出. 顾名思意, 就是说所用的缓冲区太小了, 以至装不下 那么多的东西, 多出来的东西跑出来了. 就好象是水缸装不了那么多的水, 硬倒太多会溢出来一样;) 那么, 在编程过程中为什么要用到buffer(缓冲区)呢? 简单的回答就是做为数据处理的中转站.2. UNIX下C语言函数调用的机制及缓冲区溢出的利用. 1) 进程在内存中的影像.我们假设现在有一个程序, 它的函数调用顺序如下.main(…) -> func_1(…) -> func_2(…) -> func_3(…)即: 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.(内存高址)+————————————–+|……| … 省略了一些我们不需要关心的区+————————————–+| env strings (环境变量字串)| \+————————————–+ \| argv strings (命令行字串)| \+————————————–+ \| env pointers (环境变量指针)| SHELL的环境变量和命令行参数保存区+————————————–+ /| argv pointers (命令行参数指针)| /+————————————–+ /| argc (命令行参数个数)| /+————————————–+|main 函数的栈帧| \+————————————–+ \|func_1 函数的栈帧| \+————————————–+ \|func_2 函数的栈帧|\+————————————–+\|func_3 函数的栈帧|Stack (栈)+………………………………..+/||/……/|| /+………………………………..+ /|Heap (堆)| /+————————————–+|Uninitialised (BSS) data| 非初始化数据(BSS)区+————————————–+|Initialised data| 初始化数据区+————————————–+|Text| 文本区+————————————–+(内存低址)这里需要说明的是:i) 随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的.随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地被遗弃而向内存的高址方向回缩.各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定.ii) 进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分配给进程的内存数量的增加, Heap(堆)有可能向高址或低址延伸, 依赖于不同CPU的实现. 但一般来说是向内存的高地址方向增长的.iii) 在BSS数据或者Stack(栈)的增长耗尽了系统分配给进程的自由内存的情况下,进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行.(虽然和exploit没有关系, 但是知道一下还是有好处的)iv) 函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈帧还是被调用函数栈帧, 则依赖于不同系统的实现),它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的数据, 其中包含了调用函数的下一条执行指令的地址.v) 非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的.初始化数据区用于存放可执行文件里的初始化数据.这两个区统称为数据区.vi) Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错. 文本区是被多个运行该可执行文件的进程所共享的. 文本区存放了程序的代码. 2) 函数的栈帧.函数调用时所建立的栈帧包含了下面的信息:i) 函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里,取决于不同系统的实现.ii) 调用函数的栈帧信息, 即栈顶和栈底.iii) 为函数的局部变量分配的空间iv) 为被调用函数的参数分配的空间–取决于不同系统的实现. 3) 缓冲区溢出的利用.从函数的栈帧结构可以看出:由于函数的局部变量的内存分配是发生在栈帧里的, 所以如果我们在某一个函数里定义了缓冲区变量, 则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈帧里.由于对缓冲区的潜在操作(比如字串的复制)都是从内存低址到高址的, 而内存中所保存的函数调用返回地址往往就在该缓冲区的上方(高地址)–这是由于栈的特性决定的, 这就为复盖函数的返回地址提供了条件. 当我们有机会用大于目标缓冲区大小的内容来向缓冲区进行填充时, 就有可以改写函数保存在函数栈帧中的返回地址, 从而使程序的执行流程随着我们的意图而转移. 换句话来说, 进程接受了我们的控制. 我们可以让进程改变原来的执行流程, 去执行我们准备好的代码.这是冯.诺曼计算机体系结构的缺陷.下面是缓冲区溢出利用的示意图:i) 函数对字串缓冲区的操作, 方向一般都是从内存低址向高址的.如: strcpy(s, “AAA…..”);s s+1 s+2 s+3 …+—+—+—+——–+—+…+(内存低址) | A | A | A | …… | A |…|(内存高址)+—+—+—+——–+—+…+ii) 函数返回地址的复盖/ |……| (内存高址)/ +——————–+调用函数栈帧 | 0x41414141|\ +——————–+\ | 0x41414141| 调用函数的返回地址\+——————–+/|……|/ +——————–+ s+8/ | 0x41414141|/ +——————–+ s+4被调用函数栈帧 | 0x41414141|\ +——————–+ s\ | 0x41414141|\ +——————–+\|……|+………………..+|……| (内存低址)注: 字符A的十六进制ASCII码值为0x41.iii) 从上图可以看出: 如果我们用的是进程可以访问的某个地址而不是0x41414141来改写调用函数的返回地址, 而这个地址正好是我们准备好的代码的入口, 那么进程将会执行我们的代码. 否则, 如果用的是进程无法访问的段的地址, 将会导致进程崩馈–Segment Fault Core dumped (段出错内核转储); 如果该地址处有无效的机器指令数据, 将会导致非法指令(Illigal Instruction)错误, 等等. 4) 缓冲区在Heap(堆)区或BBS区的情况i) 如果缓冲区的内存空间是在函数里通过动态申请得到的(如: 用malloc()函数申请), 那么在函数的栈帧中只是分配了存放指向Heap(堆)中相应申请到的内存空间的指针. 这种情况下, 溢出是发生在(Heap)堆中的, 想要复盖相应的函数返回地址, 看来几乎是不可能的. 这种情况的利用可能性要看具体情形, 但不是不可能的.ii) 如果缓冲区在函数中定义为静态(static), 则缓冲区内存空间的位置在非初始化(BBS)区,和在Heap(堆)中的情况差不多, 利用是可能的. 但还有一种特姝情况, 就是可以利用它来复盖函数指针, 让进程后来调用相应的函数变成调用我们所指定的代码.3. 从缓冲区溢出的利用可以得到什么? 从上文我们看到, 缓冲区溢出的利用可以使我们能够改写相关内存的内容及函数的返回地址, 从而 改变代码的执行流程, 让进程去执行我们准备好的代码. 但是, 进程是以我们当前登录的用户身份来运行的. 能够执行我们准备好的代码又怎样呢? 我们还 是无法突破系统对当前用户的权限设置, 无法干超越权限的事. 换句话来说, 要想利用缓冲区溢出得到更高的权限, 我们还得利用系统的一些特性. 对于UNIX来讲, 有两个特性可以利用. i) SUID及SGID程序UNIX是允许其他用户可以以某个可执行文件的文件拥有者的用户ID或用户组ID的身份来执行该文件的,这是通过设置该可执行文件的文件属性为SUID或SGID来实现的.也就是说如果某个可执行文件被设了SUID或SGID, 那么当系统中其他用户执行该文件时就相当于以该文件属主的用户或用户组身份来执行该文件.如果某个可执行文件的属主是root, 而这个文件被设了SUID, 那么如果该可执行文件存在可利用的缓冲区溢出漏洞, 我们就可以利用它来以root的身份执行我们准备好的代码. 没有比让它为我们产生一个具有超级用户root身份的SHELL更吸引人了, 是不是? ii) 各种端口守护(服务)进程UNIX中有不少守护(服务)进程是以root的身份运行的, 如果这些程序存在可利用的缓冲区溢出,那么我们就可以让它们以当前运行的用户身份–root去执行我们准备被好的代码.由于守护进程已经以root的身份在运行, 我们并不需要相对应的可执行文件为SUID或SGID属性.又由于此类利用通常是从远程机器上向目标机器上的端口发送有恶意的数据造成的, 所以叫做”远程溢出”利用.4. 一个有问题的程序 以下例程纯属虚构, 如有雷同, 纯属巧合./** 文件名 : p.c* 编译 : gcc -o p p.c*/#include void vulFunc(char* s){char buf[10];strcpy(buf, s);printf(“String=%s\n”, buf);}main(int argc, char* argv[]){if(argc == 2){vulFunc(argv[1]);}else{printf(“Usage: %s \n”, argv[0]);}} 这个例程接受用户在命令行的字串输入, 然后在标准输出(屏幕)上打印出来. 我们可以看出在 vulFunc()这个函数里, 定义了一个最多可以装十个字符的缓冲区buf. 如果我们在命令行输入 小于等于十个字符的字串, 则一切都很正常. 但是, 如果我们输入的字串长度大于十呢? 情况 会怎样? 缓冲区太小装不下了, 所以溢出了? 答案有待于具体分析一下才知道. 对于这个程序在不同操作系统下的分析和模拟攻击. 请看第二部份基楚篇第二部份 基楚篇5. Linux x86 平台 本文使用了如下Linux平台: Red Hat Linux release 6.2 (Zoot) Kernel 2.2.14-12 on an i586 所使用的编译器及版本: bash$ gcc -v Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) 注意: 不同版本的编译器编译相同代码所生成的机器指令可能不同. 1) 例程p.c在Linux x86平台下的剖析.i) 首先我们编译p.c并用gdb对相关函数进行反汇编结果见如下清单:bash$ gcc -o p p.cbash$ gdb pGNU gdb 19991004Copyright 1998 Free Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you arewelcome to change it and/or distribute copies of it under certain conditions.Type “show copying” to see the conditions.There is absolutely no warranty for GDB. Type “show warranty” for details.This GDB was configured as “i386-redhat-linux”…(gdb) disas mainDump of assembler code for function main:0x804842c :push %ebp0x804842d :mov %esp,%ebp0x804842f :cmpl $0x2,0x8(%ebp)0x8048433 :jne 0x8048448 0x8048435 :mov 0xc(%ebp),%eax0x8048438 : add $0x4,%eax0x804843b : mov (%eax),%edx0x804843d : push %edx0x804843e : call 0x8048400 0x8048443 : add $0x4,%esp0x8048446 : jmp 0x804845b 0x8048448 : mov 0xc(%ebp),%eax0x804844b : mov (%eax),%edx0x804844d : push %edx0x804844e : push $0x80484bb0x8048453 : call 0x8048330 0x8048458 : add $0x8,%esp0x804845b : leave0x804845c : ret0x804845d : nop0x804845e : nop0x804845f : nopEnd of assembler dump.(gdb) disas vulFuncDump of assembler code for function vulFunc:0x8048400 : push %ebp0x8048401 : mov %esp,%ebp0x8048403 : sub $0xc,%esp0x8048406 : mov 0x8(%ebp),%eax0x8048409 : push %eax0x804840a : lea 0xfffffff4(%ebp),%eax0x804840d : push %eax0x804840e : call 0x8048340 0x8048413 : add $0x8,%esp0x8048416 : lea 0xfffffff4(%ebp),%eax0x8048419 : push %eax0x804841a : push $0x80484b00x804841f : call 0x8048330 0x8048424 : add $0x8,%esp0x8048427 : leave0x8048428 : ret0x8048429 : lea 0x0(%esi),%esiEnd of assembler dump.这里我们只对所关心的main和vulFunc两个函数进行反汇编分析.ii) 进程的运行及其在内存中的情况分析我们用gdb来跟踪看看进程是如何在内存中运行的.首先把程序调入.bash$ gdb pGNU gdb 19991004Copyright 1998 Free Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you arewelcome to change it and/or distribute copies of it under certain conditions.Type “show copying” to see the conditions.There is absolutely no warranty for GDB. Type “show warranty” for details.This GDB was configured as “i386-redhat-linux”…(gdb)把断点设到main的第一条可执行汇编指令上(gdb) b *0x804842cBreakpoint 1 at 0x804842c运行程序(gdb) r AAAAAAAAStarting program: /home/vcat/p AAAAAAAABreakpoint 1, 0x804842c in main ()在断点处停下来了.看一下这时各寄存器的值(gdb) i regeax0x4010b3f81074836472ecx0x804842c134513708edx0x4010d0981074843800ebx0x4010c1ec1074840044esp0xbffff6bc-1073744196ebp0xbffff6d8-1073744168esi0x4000ae601073786464edi0xbffff704-1073744124eip0x804842c134513708eflags0x246 582cs0x2335ss0x2b43ds0x2b43es0x2b43fs0x00gs0x00cwd0xffff037f-64641swd0xffff0000-65536twd0xffffffff-1fip0x40034d701073958256fcs0x35d002356426531fopo0xbfffe400-1073748992fos0xffff002b-65493我们这里关心的是栈底(ebp), 栈顶(esp)及指令寄存器(eip).此时, ebp的值为0xbffff6d8, esp的值为0xbffff6bc, 相差28个字节.eip的值为0x804842c, 正好是我们所设的断点.(注: 这里的值可能会随着程序运行在不同的系统环境而不同)我们再看看当前栈帧里有什么内容?(gdb) x/8x $esp0xbffff6bc:0x400349cb0x000000020xbffff7040xbffff7100xbffff6cc:0x400138680x000000020x080483500x00000000也就是说, main函数刚被调用时进程在内存中的相关部份的影像是这样的:(内存高址)| …… |+——–+|00000000|0xbffff6d8 +——–+ ,生命不是一场赛跑,而是一次旅行。比赛在乎终点,而旅行在乎沿途风景。

编写自己的缓冲区溢出利用程序

相关文章:

你感兴趣的文章:

标签云: