兴海北路

---男儿仗剑自横行
<2008年3月>
2425262728291
2345678
9101112131415
16171819202122
23242526272829
303112345

统计

  • 随笔 - 85
  • 文章 - 0
  • 评论 - 17
  • 引用 - 0

常用链接

留言簿(6)

随笔分类

随笔档案

收藏夹

全是知识啊

搜索

  •  

最新评论

阅读排行榜

评论排行榜

C语言程序缓冲区注入的分析(第一部分:进程的内存映像)
by falcon <zhangjinw@gmail.com>
2008-2-13

  • 闲言戏语

        最近在写《Shell编程范例之进程操作》,到现在也没完。本打算介绍进程的相关操作,后面竟写到Linux下的C语言开发过程,想把文件是怎么变成进程 的整个过程给全部捣弄一遍。虽然到程序加载以及动态符号连接都已经很理解了,但是这伙却被进程的内存映像给”纠缠“住。看着看着就一发不可收拾——很有 趣。
        下面一起来分享人家研究的”缓冲区溢出和注入“问题(主要是关心程序的内存映像)吧,更多内容见最后的参考资料(第二部分最后有列出)。

  • 转入正题

    1、Hello World

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



    Code:

    [Ctrl+A Select All]



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



    Code:

    [Ctrl+A Select All]



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

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

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

    1.1 三个比较重要的寄存器

    EIP: 程序指令指针,通常指向下一条指令的位置
    ESP:程序堆栈指针,通常指向当前堆栈的当前位置
    EBP:程序基指针,通常指向函数使用的堆栈顶端

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

    1.2 call,ret指令的作用分析

    call指令

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

    call backward   ==>   push eip
                                      jmp backward



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

    ret                    ==>   pop eip



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

    这三个指令对我们后续的解释会很有帮 助。更多关于Intel的指令集,请参考本节参考手册,关于AT&T的,看本文最后的参考资料[4]和本节参考资料[2],更多关于X86的汇 编,请参考本节资料[3],关于其他平台的,可以考虑看看"See Mips Run"和一些专门平台自己的手册。

    本节参考资料

    [1] Intel 386 Manual
    http://www.x86.org/intel.doc/386manuals.htm
    [2] Linux_Assembly_Language_Programming
    http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Books/
    http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Programming/Assembly/
    [3] x86 Assembly Language FAQ
    http://www.faqs.org/faqs/assembly-language/x86/general/part1/
    http://www.faqs.org/faqs/assembly-language/x86/general/part2/
    http://www.faqs.org/faqs/assembly-language/x86/general/part3/

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

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

    Quote:

    用户空间         应用程序(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...)来声明好参数。具体用法见参考资料[3]里的实例。

    如果要在汇编语言中使用,需要用到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 World\n"字符串的地址。第三个参数呢,是edx,刚好是12,即"Hello World\n"字符串的长度(包括一个空字符)。这样,shellcode.c的执行流程就很清楚了,第4,5,15,16行指令的巧妙之处也就容易理 解了(把.string存放在call指令之后,并用popl指令把eip弹出当作字符串的入口)。

    本节推荐资料;

    [1] 深入理解linux系统调用
    http://www.linuxdiyf.com/bbs/redirect.php?tid=2270&goto=lastpost&highlight=
    [2] 在Linux操作系统下如何截获系统调用(如果无法访问,可以看linux module programing how to)
    http://www.xker.com/html/czxt/linux/2006_03_10_10_925.html

    1.4 什么是ELF文件

    这里的ELF不是“精灵”,而是Executable and Linking Format文件,是Linux下用来做目标文件、可执行文件和共享库的一种文件格式,它有专门的标准(见本节参考资料)。这里简单描述ELF的格式。

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

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

    ELF文件的大体结构:

    Quote:

    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等。它们之间(节区和段)的主要对应关系如下:

    .data 初始化的数据,比如int a=10
    .bss 未初始化的数据,不如char sum[100];这个在程序执行之前,内核将初始化为0
    .text 程序正文,即可执行指令
    .interp 这里描述了程序需要的解释器(动态连接和装载程序),存有解释器的全路径,如/lib/ld-linux.so

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

    对于ELF文件的分析,建议使用file, size, readelf,objdump,strip,objcopy,gdb,nm等工具,如果有兴趣,可以考虑阅读一下本节参考资料[2]。

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

    Quote:

    $ 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 .dynamic .got .got.plt .data .bss
       04     .dynamic
       05     .note.ABI-tag
       06
    $ size shellcode   #可用size命令查看各个段(对应后面将分析的进程内存映像)的大小
       text    data     bss     dec     hex filename
        815     256       4    1075     433 shellcode
    $ strip -R .note.ABI-tag shellcode    #可用strip来给可执行文件“减肥”,删除无用信息
    $ size shellcode                               #“减肥”后效果“明显”,对于嵌入式系统应该有很大的作用
       text    data     bss     dec     hex filename
        783     256       4    1043     413 shellcode
    $ objdump -s -j .interp shellcode   #这个主要工作是反编译,不过用来查看各个节区也很厉害

    shellcode:     file format elf32-i386

    Contents of section .interp:
     8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
     8048124 2e3200                               .2.



    补充:如果要删除可执行文件的program section table,可以用参考资料[2]一文的作者写的elf kicker[3]工具链中的sstrip工具。

    本节参考资料:
    [1] ELF format and ABI
    http://www.x86.org/ftp/manuals/tools/elf.pdf
    http://162.105.203.48/web/gaikuang/submission/TN05.ELF.Format.Summary.pdf
    http://www.xfocus.net/articles/200105/174.html
    [2] A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
    http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
    [3] Kickers of ELF
    http://www.muppetlabs.com/~breadbox/software/elfkickers.html

    1.5 程序执行基本过程

    在命令行下,敲入程序的名字或者是全路径,然后按下回车就可以启动程序,这个具体是怎么工作的呢?

    首 先要再认识一下我们的命令行,命令行是内核和用户之间的接口,它本身也是一个程序。在Linux系统启动以后会为每个终端用户建立一个进程执行一个 Shell解释程序,这个程序解释并执行用户输入的命令,以实现用户和内核之间的接口。这类解释程序有哪些呢?目前Linux下比较常用的有 /bin/bash。那么该程序接收并执行命令的过程是怎么样的呢?

    先简单描述一下这个过程[4]:
    1)读取用户由键盘输入的命令行。
    2)分析命令,以命令名作为文件名,并将其它参数改为系统调用execve( )内部处理所要求的形式。
    3)终端进程调用fork( )建立一个子进程。
    4)终端进程本身用系统调用wait4( )来等待子进程完成(如果是后台命令,则不等待)。当子进程运行
    时调用execve( ),子进程根据文件名(即命令名)到目录中查找有关文件(这是命令解释程序构成的
    文件),将它调入内存,执行这个程序(解释这条命令)。
    5)如果命令末尾有&号(后台命令符号),则终端进程不用系统调用wait4( )等待,立即发提示符,让
    用户输入下一个命令,转1)。如果命令末尾没有&号,则终端进程要一直等待,当子进程(即运行命令
    的进程)完成处理后终止,向父进程(终端进程)报告,此时终端进程醒来,在做必要的判别等工作后,
    终端进程发提示符,让用户输入新的命令,重复上述处理过程。

    现在用strace来跟踪一下程序执行过程中用到的系统调用。
    Quote:

    $ strace -f -o strace.out test
    $ cat strace.out | grep \(.*\) | sed -e "s#[0-9]* \([a-zA-Z0-9_]*\)(.*).*#\1#g"
    execve
    brk
    access
    open
    fstat64
    mmap2
    close
    open
    read
    fstat64
    mmap2
    mmap2
    mmap2
    mmap2
    close
    mmap2
    set_thread_area
    mprotect
    munmap
    brk
    brk
    open
    fstat64
    mmap2
    close
    close
    close
    exit_group



    相关的系统调用基本体现了上面的执行过程,需要注意的是,里头还涉及到内存映射(mmap2)等。

    下面再罗嗦一些比较有意思的内容,参考〈深入理解Linux内核〉的"程序的执行", P681和本节参考资料。

    Linux 支持很多不同的可执行文件格式,这些不同的格式是如何解释的呢?平时我们在命令行下敲入一个命令就完了,也没有去管这些细节。实际上Linux下有一个 struct linux_binfmt结构来管理不同的可执行文件类型,这个结构中有对应的可执行文件的处理函数。大概的过程(详细的过程见本节参考资料)如下:

    1) 在用户态执行了execve后,引发int 0x80中断,进入内核态,执行内核态的相应函数do_sys_execve,该函数又调用do_execve函数。do_execve函数读入可执行文 件,检查权限,如果没问题,继续读入可执行文件需要的相关信息(struct linux_binprm描述的)。

    2)接着执行 search_binary_handler,根据可执行文件的类型(由上一步的最后确定),在linux_binfmt结构链表(formats,这个 链表可以通过register_binfmt和unregister_binfmt注册和删除某些可执行文件的信息,因此注册新的可执行文件成为可能,后 面再介绍)上查找,找到相应的结构,然后执行相应的load_binary()函数开始加载可执行文件。在该链表的最后一个元素总是对解释脚本 (interpreted script)的可执行文件格式进行描述的一个对象。这种格式只定义了load_binary方法,其相应的load_script()函数检查这种可执 行文件是否以两个#!字符开始,如果是,这个函数就以另一个可执行文件的路径名作为参数解释第一行的其余部分,并把脚本文件名作为参数传递以执行这个脚本 (实际上脚本程序把自身的内容当作一个参数传递给了解释程序(如/bin/bash),而这个解释程序通常在脚本文件的开头用#!标记,如果没有标记,那 么默认解释程序为当前SHELL)。

    3)对于ELF类型文件,其处理函数是load_elf_binary,它先读入ELF文件的头部, 根据头部信息读入各种数据,再次扫描程序段描述表(program header table),找到类型为PT_LOAD的段(即.text,.data,.bss等节区),将其映射(elf_map)到内存的固定地址上,如果没有动 态连接器的描述段,把返回的入口地址设置成应用程序入口。完成这个功能的是start_thread,它不启动一个线程,而只是用来修改了pt_regs 中保存的PC等寄存器的值,使其指向加载的应用程序的入口。当内核操作结束,返回用户态时接着就执行应用程序本身了。

    4)如果应用程序使 用了动态连接库,内核除了加载指定的可执行文件外,还要把控制权交给动态连接器(ld-linux.so)以便处理动态连接的程序。内核搜寻段表 (Program Header Table),找到标记为PT_INTERP段中所对应的动态连接器的名称,并使用load_elf_interp()加载其映像,并把返回的入口地址设 置成load_elf_interp()的返回值,即动态链接器的入口。当execve系统调用退出时,动态连接器接着运行,它检查应用程序对共享链接库 的依赖性,并在需要时对其加载,对程序的外部引用进行重定位(具体过程见〈shell编程范例之进程操作〉)。然后把控制权交给应用程序,从ELF文件头 部中定义的程序进入点(用readelf -h可以出看到,Entry point address即是)开始执行。(不过对于非LIB_BIND_NOW的共享库装载是在有外部引用请求时才执行的)。

    对于内核态的函数调 用过程,没有办法通过strace(它只能跟踪到系统调用层)来做的,因此要想跟踪内核中各个系统调用的执行细节,需要用其他工具。比如可以通过给内核打 一个KFT(Kernel Function Tracing)的补丁来跟踪内核具体调用了哪些函数(见本节参考资料[2])。当然,也可以通过ctags/cscope/LXR等工具分析内核的源代 码。

    Linux允许自己注册我们自己定义的可执行格式,主要接口是/procy/sys/fs/binfmt_misc/register,可以往里头写入特定格式的字符串来实现。该字符串格式如下:
    Quote:

    :name:type:offset:string:mask:interpreter:


    name 新格式的标示符
    type 识别类型(M表示魔数,E表示扩展)
    offset 魔数(magic number,请参考man magic和man file)在文件中的启始偏移量
    string 以魔数或者以扩展名匹配的字节序列
    mask 用来屏蔽掉string的一些位
    interpreter 程序解释器的完整路径名

    本节参考资料;

    [1] 应用程序在Linux上是如何执行的
    http://javadino.blog.sohu.com/74639896.html
    [2] KFT基本使用
    http://dslab.lzu.edu.cn/members/zhangwei/doc/KFT-HOWTO
    [3] Cscope基本使用
    http://dslab.lzu.edu.cn/members/zhangwei/doc/cscope-HOWTO
    [4] Shell基本工作原理
    http://www.gbunix.com/htmldata/2006_08/14/18/article_1381_1.html

    1.6 Linux下程序的内存映像

    Linux下是如何给进程分配内存(这里仅讨论虚拟内存的分配)的呢?可以从/proc/<pid>/maps文件中看到个大概。这里的pid是进程号。

    /proc下有一个文件比较特殊,是self,它链接到当前进程的进程号,例如:
    Quote:

    $ ls /proc/self -l
    lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11291/
    $ ls /proc/self -l
    lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11292/



    看到没?每次都不一样,这样我们通过cat /proc/self/maps就可以看到cat程序执行时的内存映像了。
    Quote:

    $ cat -n /proc/self/maps                                                        
         1  08048000-0804c000 r-xp 00000000 03:01 273716     /bin/cat
         2  0804c000-0804d000 rw-p 00003000 03:01 273716     /bin/cat
         3  0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
         4  b7b90000-b7d90000 r--p 00000000 03:01 87528      /usr/lib/locale/locale-archive
         5  b7d90000-b7d91000 rw-p b7d90000 00:00 0
         6  b7d91000-b7ecd000 r-xp 00000000 03:01 466875     /lib/libc-2.5.so
         7  b7ecd000-b7ece000 r--p 0013c000 03:01 466875     /lib/libc-2.5.so
         8  b7ece000-b7ed0000 rw-p 0013d000 03:01 466875     /lib/libc-2.5.so
         9  b7ed0000-b7ed4000 rw-p b7ed0000 00:00 0
        10  b7eeb000-b7f06000 r-xp 00000000 03:01 402817     /lib/ld-2.5.so
        11  b7f06000-b7f08000 rw-p 0001b000 03:01 402817     /lib/ld-2.5.so
        12  bfbe3000-bfbf8000 rw-p bfbe3000 00:00 0          [stack]
        13  ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]



    编号是原文件里头没有的,为了说明方便,用-n参数加上去的。我们从中可以得到如下信息:
    1)  第1,2行对应的内存区是我们的程序(包括指令,数据等)
    2)  第3到12行对应的内存区是堆栈段,里头也映像了程序引用的动态连接库
    3)  第13行是内核空间

    总结一下:

    1) 前两部分是用户空间,可以从0x00000000到0xbfffffff(在测试的2.6.21.5-smp上只到bfbf8000),而内核空间从0xC0000000到0xffffffff,分别是3G和1G,所以对于每一个进程来说,共占用4G的虚拟内存空间
    2) 从程序本身占用的内存,到堆栈段(动态获取内存或者是函数运行过程中用来存储局部变量、参数的空间,前者是heap,后者是stack),再到内核空间,地址是从低到高的
    3) 栈顶并非0xC0000000下的一个固定数值

    结合参考资料[2]以及本小节列出的资料,可以得到这么一个比较详细的进程内存映像表(以2.6.21.5-smp为例):
    Quote:

                                内核空间
    0xC0000000       
                                (program flie)程序名          #execve的第一个参数
                                (environment)环境变量      #execve的第三个参数,main的第三个参数
                                (arguments)参数               #execve的第二个参数,main的形参
                                (stack)栈                           #自动变量以及每次函数调用时所需保存的信息都
                                       |                                #存放在此,包括函数返回地址、调用者的
                                      \|/                               #环境信息等,函数的参数,局部变量都存放在此
                                       ...
                                       ^
                                        |
                                (heap)堆                            #主要在这里进行动态存储分配,
                                                                          #比如malloc,new等。
                                .bss(uninitilized data)       #没有初始化的数据(全局变量哦)
                                .data(initilized global data) #已经初始化的全局数据(全局变量)   
                                .text(Executable Instructions) #通常是可执行指令
    0x08048000
                                            
    0x00000000



    光看没有任何概念,我们用gdb来看看刚才那个简单的程序。

    Quote:

    $ gcc -g -o shellcode shellcode.c #要用gdb调试,在编译时需要加-g参数
    $ gdb ./shellcode
    (gdb) set args arg1 arg2 arg3 arg4     #为了测试,设置几个参数
    (gdb) l                                                   #浏览代码
    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;"
    (gdb) break 4                                       #在汇编入口设置一个断点,让程序运行后停到这里
    Breakpoint 1 at 0x8048332: file shellcode.c, line 4.
    (gdb) r                                                   #运行程序
    Starting program: /mnt/hda8/Temp/c/program/shellcode arg1 arg2 arg3 arg4

    Breakpoint 1, main () at shellcode.c:4
    4     __asm__ __volatile__("jmp forward;"
    (gdb) print $esp                                    #打印当前堆栈指针值,用于查找整个栈的栈顶
    $1 = (void *) 0xbffe1584
    (gdb) x/100s $esp+4000                     #改变后面的4000,不断往更大的空间找
    (gdb) x/1s 0xbffe1fd9            #在 0xbffe1fd9 找到了程序名,这里是该次运行时的栈顶
    0xbffe1fd9:      "/mnt/hda8/Temp/c/program/shellcode"
    (gdb) x/10s 0xbffe17b7         #其他环境变量信息
    0xbffe17b7:      "CPLUS_INCLUDE_PATH=/usr/lib/qt/include"
    0xbffe17de:      "MANPATH=/usr/local/man:/usr/man:/usr/X11R6/man:/usr/lib/java/man:/usr/share/texmf/man"
    0xbffe1834:      "HOSTNAME=falcon.lzu.edu.cn"
    0xbffe184f:      "TERM=xterm"
    0xbffe185a:      "SSH_CLIENT=219.246.50.235 3099 22"
    0xbffe187c:      "QTDIR=/usr/lib/qt"
    0xbffe188e:      "SSH_TTY=/dev/pts/0"
    0xbffe18a1:      "USER=falcon"
    ...
    (gdb) x/5s 0xbffe1780    #一些传递给main函数的参数,包括文件名和其他参数
    0xbffe1780:      "/mnt/hda8/Temp/c/program/shellcode"
    0xbffe17a3:      "arg1"
    0xbffe17a8:      "arg2"
    0xbffe17ad:      "arg3"
    0xbffe17b2:      "arg4"
    (gdb) print init    #打印init函数的地址,这个是/usr/lib/crti.o里头的函数,做一些初始化操作
    $2 = {<text variable, no debug info>} 0xb7e73d00 <init>
    (gdb) print fini     #也在/usr/lib/crti.o中定义,在程序结束时做一些处理工作
    $3 = {<text variable, no debug info>} 0xb7f4a380 <fini>
    (gdb) print _start  #在/usr/lib/crt1.o,这个才是程序的入口,必须的,ld会检查这个
    $4 = {<text variable, no debug info>} 0x8048280 <__libc_start_main@plt+20>
    (gdb) print main   #这里是我们的main函数
    $5 = {void ()} 0x8048324 <main>



    补 充:在进程的内存映像中你可能看到诸如init,fini,_start等函数(或者是入口),这些东西并不是我们自己写的啊?为什么会跑到我们的代码里 头呢?实际上这些东西是链接的时候gcc默认给连接进去的,主要用来做一些进程的初始化和终止的动作。更多相关的细节可以看看本节参考资料[1][2], 如果想了解链接(ld)的具体过程,可以看看本节参考资料[3]。

    上面的操作对堆栈的操作比较少,下面我们用一个例子来演示栈在内存中的情况。

    本节参考资料:

    [1] 如何获取当前进程之静态影像文件
    http://edu.stuccess.com/KnowCenter/Unix/13/hellguard_unix_faq/00000089.htm
    [2] "The Linux Kernel Primer", P234, Figure 4.11
    [3] 《Unix环境高级编程编程》第7章 "UnIx进程的环境", P127和P133
    [4] C/C++程序编译步骤详解
    http://www.xxlinux.com/linux/article/development/soft/20070424/8267.html
    [5]  ELF: From The Programmer's Perspective
    http://linux.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html
    [6] GNU-ld连接脚本 Linker Scripts
    http://womking.bokee.com/5967668.html

    1.7 栈在内存中的组织

    这 一节主要介绍一个函数被调用时,参数是如何传递的,局部变量是如何存储的,它们对应的栈的位置和变化情况,从而加深对栈的理解。在操作时发现和参考资料的 结果不太一样(参考资料中没有edi和esi相关信息,再第二部分的一个小程序里头也没有),可能是gcc版本的问题或者是它对不同源代码的处理不同。我 的版本是4.1.2(可以通过gcc --version查看)。

    先来一段简单的程序,这个程序除了做一个加法操作外,还复制了一些字符串。



    Code:

    [Ctrl+A Select All]



    上面这个代码没有什么问题,编译执行一下:

    Quote:

    $ make testshellcode
    cc     testshellcode.c   -o testshellcode
    $ ./testshellcode
    sum = 6



    下面调试一下,看看在调用func后的栈的内容。

    Quote:

    $ gcc -g -o testshellcode testshellcode.c          #为了调试,需要在编译时加-g选项
    $ gdb ./testshellcode              #启动gdb调试
    ...
    (gdb) set logging on               #如果要记录调试过程中的信息,可以把日志记录功能打开
    Copying output to gdb.txt.
    (gdb) l main                             #列出源代码
    20
    21              return sum;
    22      }
    23
    24      int main()
    25      {
    26              int sum;
    27
    28              sum = func(1, 2, 3);
    29
    (gdb) break 28       #在调用func函数之前让程序停一下,以便记录当时的ebp(基指针)
    Breakpoint 1 at 0x80483ac: file testshellcode.c, line 28.
    (gdb) break func     #设置断点在函数入口,以便逐步记录栈信息
    Breakpoint 2 at 0x804835c: file testshellcode.c, line 13.
    (gdb) disassemble main   #反编译main函数,以便记录调用func后的下一条指令地址
    Dump of assembler code for function main:
    0x0804839b <main+0>:    lea    0x4(%esp),%ecx
    0x0804839f <main+4>:    and    $0xfffffff0,%esp
    0x080483a2 <main+7>:    pushl  0xfffffffc(%ecx)
    0x080483a5 <main+10>:   push   %ebp
    0x080483a6 <main+11>:   mov    %esp,%ebp
    0x080483a8 <main+13>:   push   %ecx
    0x080483a9 <main+14>:   sub    $0x14,%esp
    0x080483ac <main+17>:   push   $0x3
    0x080483ae <main+19>:   push   $0x2
    0x080483b0 <main+21>:   push   $0x1
    0x080483b2 <main+23>:   call   0x8048354 <func>
    0x080483b7 <main+28>:   add    $0xc,%esp
    0x080483ba <main+31>:   mov    %eax,0xfffffff8(%ebp)
    0x080483bd <main+34>:   sub    $0x8,%esp
    0x080483c0 <main+37>:   pushl  0xfffffff8(%ebp)
    0x080483c3 <main+40>:   push   $0x80484c0
    0x080483c8 <main+45>:   call   0x80482a0 <printf@plt>
    0x080483cd <main+50>:   add    $0x10,%esp
    0x080483d0 <main+53>:   mov    $0x0,%eax
    0x080483d5 <main+58>:   mov    0xfffffffc(%ebp),%ecx
    0x080483d8 <main+61>:   leave
    0x080483d9 <main+62>:   lea    0xfffffffc(%ecx),%esp
    0x080483dc <main+65>:   ret
    End of assembler dump.
    (gdb) r        #运行程序
    Starting program: /mnt/hda8/Temp/c/program/testshellcode

    Breakpoint 1, main () at testshellcode.c:28
    28              sum = func(1, 2, 3);
    (gdb) print $ebp     #打印调用func函数之前的基地址,即Previous frame pointer。
    $1 = (void *) 0xbf84fdd8
    (gdb) n                   #执行call指令并跳转到func函数的入口

    Breakpoint 2, func (a=1, b=2, c=3) at testshellcode.c:13
    13              int sum = 0;
    (gdb) n      
    16              sum = a + b + c;
    (gdb) x/11x $esp  #打印当前栈的内容,可以看出,地址从低到高,注意标记有蓝色和红色的值
                                 #它们分别是前一个栈基地址(ebp)和call调用之后的下一条指令的指针(eip)
    0xbf84fd94:     0x00000000      0x00000000      0x080482e0      0x00000000
    0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
    0xbf84fdb4:     0x00000001      0x00000002      0x00000003
    (gdb) n       #执行sum = a + b + c,后,比较栈内容第一行,第4列,由0变为6
    18              memset(buffer, '\0', BUF_SIZE);
    (gdb) x/11x $esp
    0xbf84fd94:     0x00000000      0x00000000      0x080482e0      0x00000006
    0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
    0xbf84fdb4:     0x00000001      0x00000002      0x00000003
    (gdb) n      
    19              memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
    (gdb) x/11x $esp #缓冲区初始化以后变成了0
    0xbf84fd94:     0x00000000      0x00000000      0x00000000      0x00000006
    0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
    0xbf84fdb4:     0x00000001      0x00000002      0x00000003
    (gdb) n
    21              return sum;
    (gdb) x/11x $esp      #进行copy以后,这两列的值变了,大小刚好是7个字节,最后一个字节为'\0'
    0xbf84fd94:     0x00000000      0x41414141      0x00414141      0x00000006
    0xbf84fda4:     0xb7f2bce0      0x00000000      0xbf84fdd8      0x080483b7
    0xbf84fdb4:     0x00000001      0x00000002      0x00000003
    (gdb) c
    Continuing.
    sum = 6

    Program exited normally.
    (gdb) quit



    从上面的操作过程,我们可以得出大概的栈分布(func函数结束之前)如下:

    Quote:

    低地址(栈顶方向)
    地址               值(hex)             符号或者寄存器
    ---------------------------------------------------------------------------------------
    0xbf84fd98   0x41414141      buf[0]   #可以看出little endian(小端,重要的数据在前面)
    0xbf84fd9c   0x00414141      buf[1]
    0xbf84fda0   0x00000006      sum     #可见这上面都是func函数里头的局部变量
    ----------------------------------------------------------------------------------------
    0xbf84fda4   0xb7f2bce0       esi, 源索引指针,可以通过产生中间代码查看,貌似没什么作用
    0xbf84fda8   0x00000000      edi, 目的索引指针
    0xbf84fdac    0xbf84fdd8       ebp,调用func之前的栈的基地址,以便调用函数结束之后恢复
    0xbf84fdb0   0x080483b7      eip,调用func之前的指令指针,以便调用函数结束之后继续执行
    -----------------------------------------------------------------------------------------
    0xbf84fdb4   0x00000001       a,第一个参数
    0xbf84fdb8   0x00000002       b,第二个参数
    0xbf84fdbc   0x00000003        c,第三个参数,可见参数是从最后一个开始压栈的
    高地址(栈底方向)



    先说明一下edi和esi的由来(在上面的调试过程中我们并没有看到),是通过产生中间汇编代码分析得出的。
    Quote:

    $ gcc -S testshellcode.c



    在产生的testshellcode.s代码里头的func部分看到push ebp之后就push了edi和esi。但是搜索了一下代码,发现就这个函数里头引用了这两个寄存器,所以保存它们没什么用,删除以后编译产生目标代码后证明是没用的。

    Quote:

    $ cat testshellcode.s
    ...
    func:
            pushl   %ebp
            movl    %esp, %ebp
            pushl   %edi
            pushl   %esi
    ...
             popl    %esi
            popl    %edi
            popl    %ebp
    ...



    下面就不管这两部分(edi和esi)了,主要来分析和函数相关的这几部分在栈内的分布:

    1、函数局部变量,在靠近栈顶一端
    2、调用函数之前的栈的基地址(ebp, Previous Frame Pointer),在中间靠近栈顶方向
    3、调用函数指令的下一条指令地址 (eip),在中间靠近栈底的方向
    4、函数参数,在靠近栈底的一端,最后一个参数最先入栈

    到这里,函数调用时的相关内容在栈内的分布就比较清楚了,在具体分析缓冲区溢出问题之前,我们再来看一个和函数关系很大的问题,即函数返回值的存储问题:函数的返回值存放在寄存器eax中[2]。

    先来看这段代码:



    Code:

    [Ctrl+A Select All]



    编 译运行后,可以看到返回值为1,刚好是我们在func函数中mov到eax中的“立即数”1,因此很容易理解返回值存储在eax中的事实,如果还有疑虑, 可以再看看汇编代码。在函数返回之后,eax中的值当作了printf的参数压入了栈中,而在源代码中我们正是把func的结果作为printf的第二个 参数的。

    Quote:

    $ make test_return
    cc     test_return.c   -o test_return
    $ ./test_return
    the return of func: 1
    $ gcc -S test_return.c
    $ cat test_return.s
    ...
            call    func
            subl    $8, %esp
            pushl   %eax      #这个是printf的第二个参数,把func的返回值压入了栈底
            pushl   $.LC0     #这个是printf的第一个参数the return of func: %d\n
            call    printf
    ...



    对于系统调用,返回值也存储在eax寄存器中。

    (由于BLOG空间限制,其他内容见下一部分,主要包括缓冲区溢出和缓冲区注入实例)
  • posted on 2008-03-14 15:29 随意门 阅读(2006) 评论(0)  编辑 收藏 引用


    只有注册用户登录后才能发表评论。
    网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理