用户
搜索
  • TA的每日心情

    2017-12-28 15:34
  • 签到天数: 5 天

    连续签到: 1 天

    [LV.2]偶尔看看

    版主

    Rank: 7Rank: 7Rank: 7

    74

    主题

    258

    帖子

    1609

    魔法币
    收听
    0
    粉丝
    63
    注册时间
    2016-6-21

    i春秋认证春秋巡逻i春秋签约作者春秋游侠春秋文阁

    发表于 2018-6-28 12:50:38 19021893

    作者:Tangerine@SAINTSEC

    0x00 函数的进入与返回

    要想理解栈溢出,首先必须理解在汇编层面上的函数进入与返回。首先我们用一个简单执行一次回显输入的程序hello开始。用IDA加载hello,定位到main函数后我们发现这个程序的逻辑十分简单,调用函数hello获取输入,然后输出“hello,”加上输入的名字后退出。使用F5看反汇编后的C代码可以非常方便的看懂逻辑。

    我们选中IDA-View窗口或者按Tab键切回到汇编窗口,在main函数的call hello一行下断点,开启32位的docker环境,启动调试服务器后直接按F9进行调试。

    如图,这是当前IDA的界面。在这张图中我们需要重点注意到的东西有栈窗口,EIP寄存器,EBP寄存器和ESP寄存器。
    首先我们可以看到EIP寄存器始终指向下一条将要执行的指令,也就是说如果我们可以通过某种方式修改EIP寄存器的值,我们就可以控制整个程序的执行,从而”pwn”掉程序(要验证这一点,我们可以在EIP后面的数字上点击右键选择Modify value.......把数值改成080484DE然后F9继续执行,从而跳过call hello一行)。
    剩下的东西都和栈相关。顾名思义,栈就是一个数据结构中的栈结构,遵循先入后出的规则。这个栈的最小单位是函数栈帧。一个函数栈帧的结构如图所示:

    局部变量1
    .......
    ......
    .......
    局部变量m
    局部变量n
    EBP
    EIP
    参数1
    .......
    参数n

    栈的生长方式是向低地址生长,也就是说这张图的方向和IDA中栈窗口的方向是一样的,越往上地址值越小。同样的,新入栈的栈帧在IDA的窗口中会把原来的栈帧“压”在下面。ESP和EBP两个寄存器负责标定当前栈帧的范围。图中标黑的部分即为实际上ESP和EBP中间的最大区域(为了方便讲解,我们把EIP和参数也列入一个函数的函数栈帧)。图中的局部变量和参数很好理解,但EBP和EIP又是什么意思呢?我们回到IDA调试窗口。按照程序的逻辑,接下来应该是执行call hello这行指令调用hello这个函数,函数执行完后回到下一行的mov eax, 0,其地址为080484DE.然后我们再把当前ESP和EBP的值记下来(受地址空间随机化ASLR的影响,每台电脑每次运行到此处的ESP和EBP值不一定相同),然后按F7进入hello函数。

    如图,执行完call hello这一行指令后发生了如下改变。由此我们可以得知call指令是可以改变EIP“始终指向下一条指令地址”的行为的,且call指令会把call下一条指令地址压栈。我们可以理解为call hello等价于push eip; mov eip, [hello]。所以我们的第一个问题“栈帧中的EIP是什么意思”的回答就是:栈帧中的EIP是call指令的下一条指令的地址。我们继续F8单步执行。



    如图,通过依次执行三条指令,程序为hello函数开辟了新的栈帧,同时把原来的栈帧,即执行了call hello函数的main函数的栈帧的栈底EBP保存到栈中。继续往下执行到read函数,然后随便输入一些比较有标志性的内容,比如12345678,我们就会发现存储输入的局部变量buf就在这片新开辟的栈帧中。

    我们已经接触到了栈帧的开辟与被使用情况,接下来我们再通过调试继续学习栈帧的销毁。继续F8到leave一行,此时我们会发现栈帧再次回到了刚执行完sub esp, 18h的状态。
    执行完leave一行指令后栈帧被销毁,整体状态回到了call hello执行前的状态。即leave指令相当于add esp, xxh; mov esp, ebp; pop ebp


    再次F8,发现EIP指向了call hello的下一行指令,同时栈中保存的EIP值被弹出,栈顶地址+4. 即retn等同于pop eip

    此时hello函数代码执行完毕,控制流程返回到了调用hello函数的main函数中。

    0x01 栈溢出实战

    通过上一节的调试,我们大概理解了函数栈的初始化和销毁过程。我们发现随着我们的输入变多,输入的内容离栈上保存的EIP地址越来越近,那么我们可不可以通过输入修改掉栈上的EIP地址,从而在retn指令执行完后“pwn”掉程序呢?我们按Ctrl+F2结束掉当前的调试,再试一次。为了节约时间,这回我们直接把断点下在hello函数里的call _read一行。
    启动调试,程序中断后界面如下

    通过观察read函数的参数和栈中的保存的EIP地址,我们计算出两者的偏移是0x16个字节,也就是说输入0x16=22个字节的数据,我们的输入就会和栈中的EIP“接上”,输入22+4=26个字节,我们的输入就会覆盖掉EIP。那么我们构造payload为‘A’*22+‘B’*4,即AAAAAAAAAAAAAAAAAAAAAABBBB,根据我们的推测,在EIP寄存器指向retn指令所在地址时,栈顶应该是‘BBBB’。即retn执行完之后,EIP里的值将不再是图中框起来的080484DE,而是42424242(BBBB的ASCII值),按F8使IDA挂起,在docker环境中输入payload

    栈中的EIP果然按照我们的推测被修改成42424242了。显然,这是一个非法的内存地址,它所在的内存页此时对我们来说并没有访问权限,所以我们运行完retn后程序将会报错。

    选择OK,继续F8并且选择将错误传递给系统,这个进程接收到信号后将会结束,调试结束。我们通过一个程序本身的bug构造了一个特殊输入结束掉了它。

    0x02 结合pwntools打造一个远程代码执行漏洞exp

    通过上一节的内容,我们已经可以做到远程使一个程序崩溃。不要小看这个成果。如果我们能挖掘到安全软件或者系统的漏洞从而使其崩溃,我们就可以让某些保护失效,从而使后面的入侵更加轻松。当然,我们也不应该满足于这个成果,如果可以继续扩大这个漏洞的利用面,制造一个著名的RCE(远程代码执行),为所欲为,岂不是更好?当然,CTF中的绝大部分pwn题也同样需要通过暴露给玩家的一个IP地址和端口号的组合,通过对端口上运行的程序进行挖掘,使用挖掘到的漏洞使程序执行不该执行的代码,从而获取到flag,这也是我们学习的目标。
    为了降低难度,我在编写hello这个小程序的时候已经预先埋了一个后门——位于0804846B的名为getShell的函数。

    如图,这个函数唯一的作用就是调用system("/bin/sh")打开一个bash shell,从而可以执行shell命令与系统本身进行交互

    正常的程序流程并不会调用这个函数,所以我们将会利用上一节中发现的漏洞劫持程序执行流程,从而执行getShell函数。
    首先我们把hello的IO转发到10001端口上

    然后我们从docker环境中获取其ip地址(我的是172.17.0.2,不同环境下可能不同)

    然后在kali中启动python,导入pwntools库并且打开一个与docker环境10001端口(即hello程序)的连接

    此时我们可以像上一篇文章一样打开IDA进行附加调试,在这里我就不再次演示了。从上一节的分析我们知道payload的组成应该是22个任意字符+地址。但是我们要怎么把16进制数表示的地址转换成4个字节的字符串呢?我们可以选用structs库,当然pwntools提供了一个更方便的函数p32()(即pack32位地址,同样的还有unpack32位地址的u32()以及不同位数的p16(),p64()等等),所以我们的payload就是22*'A'+p32(0x0804846B)

    由于读取输入的函数是read,我们在输入时不需要以回车作为结束符(printf,getc,gets等则需要),我们使用代码io.send(payload)向程序发送payload

    由于我在这里没有设置IDA附加调试,显然程序也不会被断点中断,那么这个时候hello回显我们的输入之后应该成功地被payload劫持,跳转到getShell函数上了。为了与被pwn掉的hello进行交互,我们使用io.interactive()

    可以看到我们已经成功地pwn掉了这个程序,取得了其所在环境的控制权。为了增加一点气氛,我们在/home下面放了一个flag文件。让我们来看一下flag是啥

    如图,我们成功地做出了第一个pwn题。为了加深对栈溢出的理解,我选了几个真实的CTF赛题作为作业,注意不要将思维固定在获取shell上哦。

    附件(课后例题和练习题,非常重要,请务必学习后下载练习)


    游客,如果您要查看本帖隐藏内容请回复



    本帖被以下淘专辑推荐:

    Debug The World
    有一道作业题不明白  就是doubly那道  跟着exp的思路是覆盖主函数的返回值 写代码后只显示nope!  复制exp也一样
    然后就是这道 v5的值等于0  后面会跟11.28125比较 然后我想着覆盖v5的值变为11.28125可不可行 已经找到v5跟输入字符串地址的偏移  但是不懂如何把11.28125存进去
    使用道具 举报 回复
    有一道作业题不明白  就是doubly那道  跟着exp的思路是覆盖主函数的返回值 写代码后只显示nope!  复制exp也一样
    然后就是这道 v5的值等于0  后面会跟11.28125比较 然后我想着覆盖v5的值变为11.28125可不可行 已经找到v5跟输入字符串地址的偏移  但是不懂如何把11.28125存进去
    使用道具 举报 回复
    icqbfd265d2 发表于 2018-8-17 00:27
    有一道作业题不明白  就是doubly那道  跟着exp的思路是覆盖主函数的返回值 写代码后只显示nope!  复制exp也 ...

    内存中已经保存了11.28125,你找到的那个偏移内存中保存的就是11.28125
    使用道具 举报 回复
    发表于 2018-9-2 10:58:12
    用户1024 发表于 2018-9-2 10:41
    我在做sCTF 2016 q1-pwn1时,直接执行完整的脚本偶尔会出错,docker中提示 Broken pipe
    。在python里一行一 ...

    你可以考虑在发送数据完之后用sleep()休眠0.5秒,这个问题好像是跟缓冲区刷新有关系233
    使用道具 举报 回复
    我在做sCTF 2016 q1-pwn1时,直接执行完整的脚本偶尔会出错,docker中提示 Broken pipe
    。在python里一行一行地交互执行就没问题了,这个问题还与时间有关?大家有这个问题吗
    使用道具 举报 回复
    发表于 2018-10-26 18:32:20
    ErosJohn 发表于 2018-10-26 11:49
    问一下问题,为什么每次遇到call使用单步步入就可以调试,但是用单步步过就会卡死在那儿。 ...

    呃哪题,具体位置?
    使用道具 举报 回复
    我想咨询下,这个easy ctf 这道题,是怎么得出 ESP-EIP = 70的?我找了半天看里面调用的so,ESP和EIP的堆栈地址差的好大啊。。
    使用道具 举报 回复
    问一下问题,为什么每次遇到call使用单步步入就可以调试,但是用单步步过就会卡死在那儿。
    使用道具 举报 回复
    发表于 2018-6-28 19:21:57
    Debug The World
    使用道具 举报 回复
    感谢楼主分享
    使用道具 举报 回复
    Debug The World
    使用道具 举报 回复
    发表于 2018-6-29 09:49:42
    SD As AD as A
    使用道具 举报 回复
    发表于 2018-6-29 10:05:23
    太好了,谢谢
    使用道具 举报 回复
    发表于 2018-6-29 21:08:46
    感谢大佬的无私分享
    使用道具 举报 回复
    发表于 2018-6-30 22:01:33
    感谢分享,
    使用道具 举报 回复
    发表于 2018-6-30 22:11:23
    给团队大佬顶贴
    使用道具 举报 回复
    发表于 2018-7-1 11:09:11
    好东西啊,谢谢楼主分享
    使用道具 举报 回复
    感谢大佬
    使用道具 举报 回复
    感谢大佬
    使用道具 举报 回复
    发表于 2018-7-2 23:49:05
    干货,想要在暑假入个门
    使用道具 举报 回复
    大佬66666666666666666666666
    使用道具 举报 回复
    发表于 2018-7-4 18:43:13
    66666666666666666666666666
    使用道具 举报 回复
    您需要登录后才可以回帖 登录 | 立即注册