免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
12下一页
最近访问板块 发新帖
查看: 5422 | 回复: 10
打印 上一主题 下一主题

手把手调试简单的缓冲溢出程序 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2008-02-29 23:26 |只看该作者 |倒序浏览
今天在群上看见了下面这样一个程序,说 x 能打印出 5这个值来,刚开始以为是简单的溢出,后面仔细一看和我以前的溢出猜想不一样,虽然这个技术hacker早就熟得都烂了,但是我一直都只是知道原理,没有亲手去调试过,今天提前完成了上班的任务,所以调试一下,使用的工具有gcc,objdump,gdb

returnadress.c

#include<stdio.h>

void function(void)
{
    char buf[9];

    int* ret;

    //ret =  buf + 49;
    ret =  buf + 28;

    //*ret = 5;
   
    printf(" *ret = %d\n", *ret);
    (*ret) += 10;
}

int main(int argc, char* argv[])
{
    int x;

    x = 5;
    function();

    x = 2;
    printf(" printf x = %d\n", x);

    return 0;
}


我使用的gcc版本是gcc 版本 4.1.2 20070925 (Red Hat 4.1.2-33)
gcc -v这个命令可以查看,在不同的编译器上,打印出来的值应该不是5.

首先编译它,然后gdb 调试:
gcc -g -o retaddr returnadress.c

在我的编译器上执行./retaddr,结果是 5
为什么呢,常规的初学者思维应该说是在函数function中没有对x=2修改,即使修改了,在printf前面也将x赋值为2了阿。以前我思维的缓冲溢出程序,应该像我注释掉的那几行互换一样,并把x=2注释掉,向下面的代码一样,应该是5,这个相对好理解一点。

如果把注释的代码换换输出,程序如下:
#include<stdio.h>

void function(void)
{
    char buf[9];

    int* ret;

    ret =  buf + 49;
    //ret =  buf + 17;
   
    *ret = 5;
    //printf(" *ret = %d\n", *ret);
    (*ret) += 7;
}

int main(int argc, char* argv[])
{
    int x;

    x = 5;
    function();

    //x = 2;
    printf(" printf x = %d\n", x);

    return 0;
}

这里很好理解,利用数组缓冲溢出,确定我的编译器上buf的起始地址+49就是main函数中的x的地址。原理很简单关键在于调试的过程。
要是程序是buf + 28 然后再+7的那个,那么在我的编译器上输出的结果
printf x = 5

这是因为在调用函数前,先要保存函数的返回地址(将函数返回地址压栈),然后再去调用函数,buf + 28,就是函数的返回地址(关于为什么 buf + 28 是函数的返回地址,请参考我blog里的另外一篇文章《 毕业设计:linux入侵检测安全增强实现》),取到函数的返回地址以后,用这个语句(*ret) += 7跳过main函数中的 x = 2的赋值,直接去执行printf(" printf x = %d\n", x),所以打印出来的值就是main函数第一次对x赋的值5,第二次赋值被function里面的(*ret) += 7语句跳过了。所以打印出来的是5.

这里是上面的原理,原理比较简单,一看就明白了,关键是function函数里面的buf 应该加多少和 *ret应该加多少才能得到我们想要的结果?如何去确定这些数值呢?其实也不难,只是我以前不会,今天问了问,学会了操作,记路下来,以后可以复习,也希望能帮到看这篇文章的其他人:)

首先有几个基础的gdb命令:
第一个是设置断点:break 行号
例如: break 18
第二个是continue,让程序接着断点往下走
例如:continue
第三个打印值:print 变量
例如:print &buf
第四个显示行号左后的程序源码:L 行号
例如:L
     l 13
第五个是开始运行程序
例如:run


我现在编译程序,若想gdb能反汇编,需要加上-g选项给gcc
gcc -g -o retaddr retaddress.c
得出elf文件retaddr

执行程序,结果是:
[hongmy525@lhc laboratory]$ ./ret
*ret = -204642304
printf x = 2

这个结果不是我们想要的,因为他们没有给我们带来预想的惊喜。
我把它反汇编看看:

[hongmy525@lhc laboratory]$ gdb ret
GNU gdb Red Hat Linux (6.6-40.fc8rh)
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome 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-gnu"...
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) L 1
1       #include<stdio.h>
2
3       void function(void)
4       {
5               char buf[9];
6
7               int* ret;
8
9               //ret =  buf + 49;
10              ret =  buf + 28;
(gdb) break 9
Breakpoint 1 at 0x80483ca: file retaddr.c, line 9.
(gdb) run
Starting program: /home/hongmy525/laboratory/ret

warning: Missing the separate debug info file: /usr/lib/debug/.build-id/ac/2eeb206486bb7315d6ac4cd64de0cb50838ff6.debug

warning: Missing the separate debug info file: /usr/lib/debug/.build-id/ba/4ea1118691c826426e9410cafb798f25cefad5.debug

Breakpoint 1, function () at retaddr.c:10
10              ret =  buf + 28;
(gdb) print &buf
$1 = (char (*)[9]) 0xbff0b4bb
(gdb) L 19
14      }
15
16      int main(int argc, char* argv[])
17      {
18              int x;
19
20              x = 5;
21              function();
22
23              x = 2;
(gdb) break 22
Breakpoint 2 at 0x804840d: file retaddr.c, line 22.
(gdb) continue
Continuing.
*ret = -1074744105

Breakpoint 2, main () at retaddr.c:23
23              x = 2;
(gdb) print &x
$2 = (int *) 0xbff0b4f0


现在我们知道了function函数中的buf数组的地址$1 = (char (*)[9]) 0xbff0b4bb 和main函数中的变量x的地址$2 = (int *) 0xbff0b4f0。
   
         0xbff0 b4 f0
    ─    0xbff0 b4 bb
────────────────
                                                       35
因为是16进制:3 × 16 + 5 = 53

buf的地址往上偏移 53 就能找到变量 x。于是我们可以在没有函数传参数的情况下在function函数中改变main函数的变量x的值。把程序修改如下:
#include<stdio.h>

void function(void)
{
    char buf[9];

    int* ret;

    //ret =  buf + 49;
    ret =  buf + 53;
   
    printf(" *ret = %d\n", *ret);
    (*ret) += 10;
}

int main(int argc, char* argv[])
{
    int x;

    x = 5;
    function();

    x = 2;
    printf(" printf x = %d\n", x);

    return 0;
}
程序的输出结果就是:
*ret = 5
printf x = 2


这里,我们已经得到了一个想要的结果,还差printf x ,要是printf x 也能如意的打印出5,那么前面的原理就能实现了。当然,这里我不是指同时打印出5.

如果说打印出*ret = 5是一道应用题,那么打印printf x = 5应该算一道小综合。现在我们分析一下该如何去求解我们的答案。

首先,我们需要整理一下思路。
一、找到返回地址,因为调用函数以前会将函数的返回地址压栈,我们首先需要找到main函数调用函数function之前的返回地址。

二、以&buf为基点,找到x=2的赋值语句地址(即是找到function函数的返回地址,这个地址压栈在main函数调用function之前) ret =  buf + ??;

三、跳过它·[ (*ret += ??) ]

这样,我们就能让printf x = 5了。

ok,let‘ go on.

[hongmy525@lhc laboratory]$ objdump -d ret

ret:     file format elf32-i386

Disassembly of section .init:

080483f7 <main>:
80483f7:       8d 4c 24 04             lea    0x4(%esp),%ecx
80483fb:       83 e4 f0                and    $0xfffffff0,%esp
80483fe:       ff 71 fc                pushl  -0x4(%ecx)
8048401:       55                      push   %ebp
8048402:       89 e5                   mov    %esp,%ebp
8048404:       51                      push   %ecx
8048405:       83 ec 24                sub    $0x24,%esp
8048408:       c7 45 f8 05 00 00 00    movl   $0x5,-0x8(%ebp)
804840f:       e8 b0 ff ff ff          call   80483c4 <function>
8048414:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%ebp)
804841b:       8b 45 f8                mov    -0x8(%ebp),%eax
804841e:       89 44 24 04             mov    %eax,0x4(%esp)
8048422:       c7 04 24 1c 85 04 08    movl   $0x804851c,(%esp)
8048429:       e8 ae fe ff ff          call   80482dc <printf@plt>
804842e:       b8 00 00 00 00          mov    $0x0,%eax
8048433:       83 c4 24                add    $0x24,%esp
8048436:       59                      pop    %ecx
8048437:       5d                      pop    %ebp
8048438:       8d 61 fc                lea    -0x4(%ecx),%esp
804843b:       c3                      ret   

8048414:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%ebp)
这里,就是我们要找的关键,下面我们继续。

[hongmy525@lhc laboratory]$ gdb ret
(gdb) break 12
Breakpoint 1 at 0x80483d3: file retaddr.c, line 12.
(gdb) break 22
Breakpoint 2 at 0x8048414: file retaddr.c, line 22.
(gdb) run
Starting program: /home/hongmy525/laboratory/ret

warning: Missing the separate debug info file: /usr/lib/debug/.build-id/ac/2eeb206486bb7315d6ac4cd64de0cb50838ff6.debug

warning: Missing the separate debug info file: /usr/lib/debug/.build-id/ba/4ea1118691c826426e9410cafb798f25cefad5.debug

Breakpoint 1, function () at retaddr.c:12
12              printf(" *ret = %d\n", *ret);
(gdb) print &buf
$1 = (char (*)[9]) 0xbfd37adb
(gdb) x/50x 0xbfd37adb
0xbfd37adb:     0x04832000      0x00000008      0xd37b1000      0xd37b18bf
0xbfd37aeb:     0x048414bf      0xae3ff408      0xae21f800      0xd37b2800
0xbfd37afb:     0x048469bf      0x9bb6c508      0xd37bbc00      0xd37b28bf
0xbfd37b0b:     0xae3ff4bf      0x00000500      0xd37b3000      0xd37b88bf
0xbfd37b1b:     0x9a5390bf      0x98bca000      0x04845000      0xd37b8808
0xbfd37b2b:     0x9a5390bf      0x00000100      0xd37bb400      0xd37bbcbf
0xbfd37b3b:     0x98c810bf      0x00000000      0x00000100      0x00000100
0xbfd37b4b:     0x00000000      0xae3ff400      0x98bca000      0x00000000
0xbfd37b5b:     0xd37b8800      0xef8985bf      0xbf42fb0c      0x0000009e
0xbfd37b6b:     0x00000000      0x00000000      0x9838c000      0x9a52bd00
0xbfd37b7b:     0x98bfc000      0x00000100      0x0482f000      0x00000008
0xbfd37b8b:     0x04831100      0x0483f708      0x00000108      0xd37bb400
0xbfd37b9b:     0x048450bf      0x04844008

没有结果,因为我们没有4字节对齐吧?呵呵,OK,4字节对齐的再来一次(这个gdb命令是查看这个地址往后50×4=200个字节的内存中的存放内容)

(gdb) x/50x 0xbfd37adc
0xbfd37adc:     0x08048320      0x00000000      0xbfd37b10      0xbfd37b18
0xbfd37aec:     0x08048414      0x00ae3ff4      0x00ae21f8      0xbfd37b28
0xbfd37afc:     0x08048469      0x009bb6c5      0xbfd37bbc      0xbfd37b28
0xbfd37b0c:     0x00ae3ff4      0x00000005      0xbfd37b30      0xbfd37b88
0xbfd37b1c:     0x009a5390      0x0098bca0      0x08048450      0xbfd37b88
0xbfd37b2c:     0x009a5390      0x00000001      0xbfd37bb4      0xbfd37bbc
0xbfd37b3c:     0x0098c810      0x00000000      0x00000001      0x00000001
0xbfd37b4c:     0x00000000      0x00ae3ff4      0x0098bca0      0x00000000
0xbfd37b5c:     0xbfd37b88      0x0cef8985      0x9ebf42fb      0x00000000
0xbfd37b6c:     0x00000000      0x00000000      0x009838c0      0x009a52bd
0xbfd37b7c:     0x0098bfc0      0x00000001      0x080482f0      0x00000000
0xbfd37b8c:     0x08048311      0x080483f7      0x00000001      0xbfd37bb4
0xbfd37b9c:     0x08048450      0x08048440

很明显,我们找到了我们想要的东西
0xbfd37aec:     0x08048414      0x00ae3ff4      0x00ae21f8      0xbfd37b28
看见了吗?0x08048414!!哈哈,兴奋啦~~,有时这样的输出是以字节为单位的,因为x86是little-endian。这时找着就比较费眼睛了。

0x08048414 在0xbfd37aec 这个地址中,又该做减法了

                0xbfd37a ec
        ─        0xbfd37a db
──────────────
                                         11

1 × 16 + 1 = 17

也就是说函数的返回地址距离&buf有17个字节,只有17个字节,哇~~~,太开心了,我给他加上就好了。

ret = buf +17;
这样,现在的ret指向的就是function函数的返回地址了。
我们看源码,程序返回以后要做的是赋值,那我们不想让他赋值,
804840f:       e8 b0 ff ff ff          call   80483c4 <function>
8048414:       c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%ebp)
804841b:       8b 45 f8                mov    -0x8(%ebp),%eax

于是我们跳过这条指令,
让ret指向804841b,

                0x080484 1b
        ─        0x080484 14
──────────────
                                          7
(*ret)+= 7

这样,我们函数的返回地址+7,正好跳过赋值语句,执行printf(" printf x = %d\n", x);
显眼,这样打印出来的就是5,程序如下:

#include<stdio.h>

void function(void)
{
    char buf[9];

    int* ret;

    //ret =  buf + 49;
    ret =  buf + 17;

    printf(" *ret = %d\n", *ret);
    (*ret) += 7;
}

int main(int argc, char* argv[])
{
    int x;

    x = 5;
    function();

    x = 2;
    printf(" printf x = %d\n", x);

    return 0;
}

程序输出如下:
[hongmy525@lhc laboratory]$ ./ret
*ret = 134513684
printf x = 5
完工。由群鬼舞者发起讨论,罗琰指导,谢谢他们。

[ 本帖最后由 hongmy525 于 2008-3-2 18:33 编辑 ]

评分

参与人数 1可用积分 +12 收起 理由
MMMIX + 12 鼓励一把 :-)

查看全部评分

论坛徽章:
0
2 [报告]
发表于 2008-02-29 23:27 |只看该作者
我也是入门菜鸟:)
不知道该不该放这,写了一晚上,从我的blog直接贴过来的,里面可能有不太清楚的地方。困了,阿`~~~

[ 本帖最后由 hongmy525 于 2008-2-29 23:43 编辑 ]

论坛徽章:
0
3 [报告]
发表于 2008-03-02 11:57 |只看该作者
无人响应啊~~,真伤心

论坛徽章:
0
4 [报告]
发表于 2008-03-02 12:02 |只看该作者
我来响应一下。

论坛徽章:
0
5 [报告]
发表于 2008-03-02 12:06 |只看该作者
不错不错, 学习了.

论坛徽章:
0
6 [报告]
发表于 2008-03-02 14:04 |只看该作者
多谢两位老大哥~~

论坛徽章:
0
7 [报告]
发表于 2008-03-02 14:37 |只看该作者
收藏了,等会实际操作一下,谢谢分享

论坛徽章:
0
8 [报告]
发表于 2008-03-02 14:54 |只看该作者
写的非常精彩, 而且这样的代码初一看确实不可思议,找到了原因是跳过了"x=2"这一句代码后感到豁然开朗,非常巧妙!!!

“关键是function函数里面的buf 应该加多少和 *ret应该加多少才能得到我们想要的结果?”关键是这两个数值确定,
后者要跳过x=2一句即movl   $0x2,-0x8(%ebp),如果熟悉汇编就可以知道这占用代码段7个字节,movel指令占用字节数可以查表得到。否则
只能反汇编来计算了。

前者实际上调用函数过程,如果对函数调用过程非常熟悉,可以推断出17个字节:

在执行到 function,堆栈结构如下(右侧表示前面名称在堆栈中占用的最低字节地址)
Return_Address         <--- 4(%ebp)     
Old %ebp                  <--- (%ebp)         这儿占用4个字节
局部变量ret                 <--- -4(%ebp)      这儿占用4个字节
局部变量buf[9]            <--- -13(%ebp)  这实际是buf[0]的地址, buf[]一共占用9个字节

所以buf距离return_address之间的距离一共有(4+4+9=)17个字节

论坛徽章:
0
9 [报告]
发表于 2008-03-02 18:24 |只看该作者
原帖由 Kissfox 于 2008-3-2 14:54 发表
在执行到 function,堆栈结构如下(右侧表示前面名称在堆栈中占用的最低字节地址)
Return_Address         <--- 4(%ebp)     
Old %ebp                  <--- (%ebp)         这儿占用4个字节
局部变量ret                 <--- -4(%ebp)      这儿占用4个字节
局部变量buf[9]            <--- -13(%ebp)  这实际是buf[0]的地址, buf[]一共占用9个字节


哈哈,分析更详细些了,真好,谢谢:)
我修正了一些笔误,原本人家给的原程序不是这个,我稍稍改了点点,不过无碍于学习里面的东西。

若是程序如下:
#include<stdio.h>

void function(void)
{
    char buf[9];

    int* ret;

    ret =  buf + 49;
    //ret =  buf + 17;
   
    *ret = 5;
    //printf(" *ret = %d\n", *ret);
    (*ret) += 7;
}

int main(int argc, char* argv[])
{
    int x;

    x = 5;
    function();

    //x = 2;
    printf(" printf x = %d\n", x);

    return 0;
}

那中间的49里除了数组9,ret变量4,还有函数返回地址4,其32他还填充了什么东西?

[ 本帖最后由 hongmy525 于 2008-3-2 18:38 编辑 ]

论坛徽章:
0
10 [报告]
发表于 2008-03-02 18:57 |只看该作者
原帖由 hongmy525 于 2008-3-2 18:24 发表


哈哈,分析更详细些了,真好,谢谢:)
我修正了一些笔误,原本人家给的原程序不是这个,我稍稍改了点点,不过无碍于学习里面的东西。

若是程序如下:
#include

void function(void)
{
    char  ...



我一个下午在看这个,发现在gcc4.1.0中函数堆栈确实是这么存储的:
Return_Address         <--- 4(%ebp)     
Old %ebp                  <--- (%ebp)         这儿占用4个字节
局部变量ret                 <--- -4(%ebp)      这儿占用4个字节
局部变量buf[9]            <--- -13(%ebp)  这实际是buf[0]的地址, buf[]一共占用9个字节

这样的堆栈布局很好理解,但可惜的是在gcc4.1.2中就变化很大了,我很难理解。同样的function函数,
void function(void)
{
   char buf[9];
   int* ret;
   ret =  buf + 17;
  (*ret) =(*ret)+7;
}
gcc4.1.2汇编成
function:
        pushl        %ebp
        movl        %esp, %ebp
        subl        $40, %esp

        movl        %gs:20, %eax              《=这儿是干什么的???
        movl        %eax, -4(%ebp)               
        xorl        %eax, %eax

        leal        -13(%ebp), %eax
        addl        $17, %eax
        movl        %eax, -20(%ebp)

        movl        -20(%ebp), %eax
        movl        (%eax), %eax
        leal        7(%eax), %edx
        movl        -20(%ebp), %eax
        movl        %edx, (%eax)

        movl        -4(%ebp), %eax
        xorl        %gs:20, %eax
        je        .L3
        call        __stack_chk_fail
.L3:
        leave
        ret

堆栈如下(高地址往低地址):
return_adderss     (%ebp)+4     4个字节
old %ebp           (%ebp)            4个字节
(%gs:20的值)       -4(%ebp)          4个字节   (不知道这个干什么)
buf[]              -5~-13(%ebp)      9个字节与buf[]数组大小相等
????               -14~-19(%ebp)     6个字节    (这是为了对齐吗?)
变量ret              -20(%ebp)         4个字节

[ 本帖最后由 Kissfox 于 2008-3-2 18:58 编辑 ]
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP