[授权发表]缓冲区溢出与注入分析

本文详细探讨了缓冲区溢出和注入的概念,从进程内存映像、常用寄存器、系统调用、ELF文件格式等方面展开。通过实例分析了字符串复制导致的缓冲区溢出,揭示了其可能造成的后果,并介绍了保护措施。同时,文章还展示了如何通过缓冲区注入执行字符串化的代码,解析了注入的原理。
摘要由CSDN通过智能技术生成

by falcon wuzhangjin@gmail.com of TinyLab.org
2008-2-13

最初发表:泰晓科技 – 聚焦嵌入式 Linux,追本溯源,见微知著!
原文链接:缓冲区溢出与注入分析
评论说明:为更好地聚合大家的讨论,请到上面原文的评论区回复。


【注】这是开源书籍《C语言编程透视》第五章,如果您喜欢该书,请关注我们的新浪微博@泰晓科技

缓冲区溢出与注入分析

前言

虽然程序加载以及动态符号链接都已经很理解了,但是这伙却被进程的内存映像给”纠缠"住。看着看着就一发不可收拾——很有趣。

下面一起来探究“缓冲区溢出和注入”问题(主要是关心程序的内存映像)。

进程的内存映像

永远的Hello World,太熟悉了吧,

#include <stdio.h>
int main(void)
{
    printf("Hello Worldn");
    return 0;
}

如果要用内联汇编(inline assembly)来写呢?

 1  /* shellcode.c */
 2  void main()
 3  {
 4      __asm__ __volatile__("jmp forward;"
 5                   "backward:"
 6                           "popl   %esi;"
 7                           "movl   $4, %eax;"
 8                           "movl   $2, %ebx;"
 9                           "movl   %esi, %ecx;"
10                           "movl   $12, %edx;"
11                           "int    $0x80;"    /* system call 1 */
12                           "movl   $1, %eax;"
13                           "movl   $0, %ebx;"
14                           "int    $0x80;"    /* system call 2 */
15                   "forward:"
16                           "call   backward;"
17                           ".string "Hello World\n";");
18  }

看起来很复杂,实际上就做了一个事情,往终端上写了个Hello World。不过这个非常有意思。先简单分析一下流程:

  • 第4行指令的作用是跳转到第15行(即forward标记处),接着执行第16行。
  • 第16行调用backward,跳转到第5行,接着执行6到14行。
  • 第6行到第11行负责在终端打印出Hello World字符串(等一下详细介绍)。
  • 第12行到第14行退出程序(等一下详细介绍)。

为了更好的理解上面的代码和后续的分析,先来介绍几个比较重要的内容。

常用寄存器初识

X86处理器平台有三个常用寄存器:程序指令指针、程序堆栈指针与程序基指针:

<th align="left">
  名称
</th>

<th align="left">
  注释
</th>
寄存器
EIP
<td align="left">
  程序指令指针
</td>

<td align="left">
  通常指向下一条指令的位置
</td>
ESP
<td align="left">
  程序堆栈指针
</td>

<td align="left">
  通常指向当前堆栈的当前位置
</td>
EBP
<td align="left">
  程序基指针
</td>

<td align="left">
  通常指向函数使用的堆栈顶端
</td>

当然,上面都是扩展的寄存器,用于32位系统,对应的16系统为ip,sp,bp。

call,ret指令的作用分析

  • call指令

跳转到某个位置,并在之前把下一条指令的地址(EIP)入栈(为了方便”程序“返回以后能够接着执行)。这样的话就有:

call backward   ==>   push eip
                      jmp backward
  • ret 指令

通常call指令和ret是配合使用的,前者压入跳转前的下一条指令地址,后者弹出call指令压入的那条指令,从而可以在函数调用结束以后接着执行后面的指令。

ret                    ==>   pop eip

通常在函数调用后,还需要恢复esp和ebp,恢复esp即恢复当前栈指针,以便释放调用函数时为存储函数的局部变量而自动分配的空间;恢复ebp是从栈中弹出一个数据项(通常函数调用过后的第一条语句就是push ebp),从而恢复当前的函数指针为函数调用者本身。这两个动作可以通过一条leave指令完成。

这三个指令对我们后续的解释会很有帮助。更多关于Intel的指令集,请参考:Intel 386 Manual, x86 Assembly Language FAQ:part1, part2, part3.

什么是系统调用(以Linux 2.6.21版本和x86平台为例)

系统调用是用户和内核之间的接口,用户如果想写程序,很多时候直接调用了C库,并没有关心系统调用,而实际上C库也是基于系统调用的。这样应用程序和内核之间就可以通过系统调用联系起来。它们分别处于操作系统的用户空间和内核空间(主要是内存地址空间的隔离)。

用户空间         应用程序(Applications)
                        |      |
                        |     C库(如glibc)
                        |      |
                       系统调用(System Calls,如sys_read, sys_write, sys_exit)
                            |
内核空间         内核(Kernel)

系统调用实际上也是一些函数,它们被定义在arch/i386/kernel/sys_i386.c(老的在arch/i386/kernel/sys.c)文件中,并且通过一张系统调用表组织,该表在内核启动时就已经加载了,这个表的入口在内核源代码的arch/i386/kernel/syscall_table.S里头(老的在arch/i386/kernel/entry.S)。这样,如果想添加一个新的系统调用,修改上面两个内核中的文件,并重新编译内核就可以。当然,如果要在应用程序中使用它们,还得把它写到include/asm/unistd.h中。

如果要在C语言中使用某个系统调用,需要包含头文件/usr/include/asm/unistd.h,里头有各个系统调用的声明以及系统调用号(对应于调用表的入口,即在调用表中的索引,为方便查找调用表而设立的)。如果是自己定义的新系统调用,可能还要在开头用宏_syscall(type, name, type1, name1…)来声明好参数。

如果要在汇编语言中使用,需要用到int 0x80调用,这个是系统调用的中断入口。涉及到传送参数的寄存器有这么几个,eax是系统调用号(可以到/usr/include/asm-i386/unistd.h或者直接到arch/i386/kernel/syscall_table.S查到),其他寄存器如ebx,ecx,edx,esi,edi一次存放系统调用的参数。而系统调用的返回值存放在eax寄存器中。

下面我们就很容易解释前面的shellcode.c程序流程的2,3两部分了。因为都用了int 0x80中断,所以都用到了系统调用。

第3部分很简单,用到的系统调用号是1,通过查表(查/usr/include/asm-i386/unistd.h或arch/i386/kernel/syscall_table.S)可以发现这里是sys_exit调用,再从/usr/include/unistd.h文件看这个系统调用的声明,发现参数ebx是程序退出状态。

第2部分比较有趣,而且复杂一点。我们依次来看各个寄存器,首先根据eax为4确定(同样查表)系统调用为sys_write,而查看它的声明(从/usr/include/unistd.h),我们找到了参数依次为文件描述符、字符串指针和字符串长度。

  • 第一个参数是ebx,正好是2,即标准错误输出,默认为终端。
  • 第二个参数是ecx,而ecx的内容来自esi,esi来自刚弹出栈的值(见第6行popl %esi;),而之前刚好有call指令引起了最近一次压栈操作,入栈的内容刚好是call指令的下一条指令的地址,即.string所在行的地址,这样ecx刚好引用了"Hello Worldn"字符串的地址。
  • 第三个参数是edx,刚好是12,即"Hello Worldn"字符串的长度(包括一个空字符)。这样,shellcode.c的执行流程就很清楚了,第4,5,15,16行指令的巧妙之处也就容易理解了(把.string存放在call指令之后,并用popl指令把eip弹出当作字符串的入口)。

什么是ELF文件

这里的ELF不是“精灵”,而是Executable and Linking Format文件,是Linux下用来做目标文件、可执行文件和共享库的一种文件格式,它有专门的标准,例如:X86 ELF format and ABI中文版

下面简单描述ELF的格式。

ELF文件主要有三种,分别是:

  • 可重定位的目标文件,在编译时用gcc的-c参数时产生。
  • 可执行文件,这类文件就是我们后面要讨论的可以执行的文件。
  • 共享库,这里主要是动态共享库,而静态共享库则是可重定位的目标文件通过ar命令组织的。

ELF文件的大体结构:

ELF Header               #程序头,有该文件的Magic number(参考man magic),类型等
Program Header Table     #对可执行文件和共享库有效,它描述下面各个节(section)组成的段
Section1
Section2
Section3
.....
Program Section Table   #仅对可重定位目标文件和静态库有效,用于描述各个Section的重定位信息等。

对于可执行文件,文件最后的Program Section Table(节区表)和一些非重定位的Section,比如.comment,.note.XXX.debug等信息都可以删除掉,不过如果用strip,objcopy等工具删除掉以后,就不可恢复了。因为这些信息对程序的运行一般没有任何用处。

ELF文件的主要节区(section)有.data,.text,.bss,.interp等,而主要段(segment)有LOAD,INTERP等。它们之间(节区和段)的主要对应关系如下:

<th align="left">
  解释
</th>

<th align="left">
  实例
</th>
Section
.data
<td align="left">
  初始化的数据
</td>

<td align="left">
  比如int a=10
</td>
.bss
<td align="left">
  未初始化的数据
</td>

<td align="left">
  比如char sum[100];这个在程序执行之前,内核将初始化为0
</td>
.text
<td align="left">
  程序代码正文
</td>

<td align="left">
  即可执行指令集
</td>
.interp
<td align="left">
  描述程序需要的解释器(动态连接和装载程序) 存
</td>

<td align="left">
  有解释器的全路径,如/lib/ld-linux.so
</td>

而程序在执行以后,.data, .bss,.text等一些节区会被Program header table映射到LOAD段,.interp则被映射到了INTERP段。

对于ELF文件的分析,建议使用file, size, readelf,objdump,strip,objcopy,gdb,nm等工具。

这里简单地演示这几个工具:

$ gcc -g -o shellcode shellcode.c  #如果要用gdb调试,编译时加上-g是必须的
shellcode.c: In function ‘main’:
shellcode.c:3: warning: return type of ‘main’ is not ‘int’
f$ file shellcode  #file命令查看文件类型,想了解工作原理,可man magic,man file
shellcode: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), not stripped
$ readelf -l shellcode  #列出ELF文件前面的program head table,后面是它描
                       #述了各个段(segment)和节区(section)的关系,即各个段包含哪些节区。
Elf file type is EXEC (Executable file)
Entry point 0x8048280
There are 7 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0044c 0x0044c R E 0x1000
  LOAD           0x00044c 0x0804944c 0x0804944c 0x00100 0x00104 RW  0x1000
  DYNAMIC        0x000460 0x08049460 0x08049460 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r
          .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
   03     .ctors .dtors .jcr .dynam
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值