前置知识了解
随着我们做题的深入,我们会发现有些题目并不会给予我们后门函数,并且也没有ret2shellcode可以供我们存放shellcode的bss段变量
那么我们还有办法自己构建一个后门函数吗
不知道还记不记得在最开始的栈溢出那一题,我们提到了plt表和got表
在当时,为了照顾新手入坑pwn的感受,我们只是粗略的得出plt调用函数,got存真实地址的服务于做题的结论
现在,让我们解释一下这个结论的原因
我们先前已经讲过,got表的作用是因为动态链接的存在,为了使应用程序方便的获取libc中的真实地址
并且只有当程序运行和函数调用过后,got表中保存的才会是该函数的libc的绝对地址
而plt表虽然引用的也是got表中的真实地址,但是注意这里并不是说明got表能够调用这个函数
plt表之所以能够调用函数,而got不行的关键原因是因为plt表还起到了把控制(程序执行流)转移到对应的函数
当然上述的解释并不详细,许多原理性的问题没有讲到,如果将来想要死磕pwn的同学,建议花时间去专研透底层逻辑的问题(当然现在没有必要)
所以我们是不是可以得出一条逻辑链,当程序没有给予我们现成的后门函数的时候,我们可以通过system的plt表来调用system函数
但是说的容易做起来难,我们如何获得system函数的plt表地址呢?
这里我们只需要记住一个公式 真实地址 = 基址 + 偏移
即我们通过puts等函数泄露出来的函数地址是真实地址,我们可以通过计算偏移来求出libc基址
然后依据libc基址和偏移量得出其他函数的真实地址,从而随意调用
但是如果我们不了解libc版本,即题目附件并为给出呢
这里还需要了解一下libc中函数地址偏移的概念
如果开启了pie保护机制,函数的地址将在每次运行时发生变化
但是其后三位由于虚拟地址页的映射机制,将不会发生变化(前提是在同一个libc版本中)
因此,如果题目没有给予我们libc文件的话,我们可以通过函数的后三位来推演出libc版本,从而求得libc基址
wp演示
先看一下保护机制,但是看不出什么苗头
拖到ida里面看看
int __cdecl main(int argc, const char **argv, const char **envp) |
有一个fgets输入任意字节的数据可以用来栈溢出,但是看了下函数列表,好像没有后门函数可以供我们返回
并且程序也没有提供给我们可以用来泄露函数地址的puts等
没办法了,我们只能连同puts函数泄露其真实地址一起构造
看到这里是不是仍然不太明白,看看exp的构造就知道了
1 from pwn import* |
第四行这里,我们之所以要装载题目附件所给我们的libc-2.31.so文件
是因为我们需要获取该libc版本的各函数相较于基址的偏移
同理,这里还有两种办法可以获取(实际上还有三种,但是最后一种我还不会用[截止到文章发布,如果后续学会了将会补上])
获取libc版本偏移-第一种办法
在该网站 我们可以通过输入对应函数的后3位数值来检索对应的libc版本(比如图中检索到了3个版本,通常是都得试试的)
libc database search (blukat.me)
获取libc版本偏移-第二种办法
libcsearch这个工具也能获取偏移
由于网上对于这个工具的安装和使用不计其数
这里我只负责介绍这个工具,安装过程如果出现问题可以看看这个博客(3条消息) LibcSearcher的安装使用_Catch_1t_AlunX的博客-CSDN博客
说回到exp,我们继续往下看,截止到12行的第一个payload都是一些前置的要点获取
cyclic生成40个字节的垃圾数据这个没有任何难度理解
rdi寄存器传参这条之前也解释过了,puts_got显然就是将puts函数的真实地址传给rdi
接下来的puts_plt便是调用puts函数,输出puts函数的真实地址
接着为什么要返回到main函数?因为我们还需要接收puts的真实地址,并且我们只能输入96个字节的数据,如果一次性构造payload过长则无法成功
第三个疑点来到了15行,有很多我们没有见过的语法?
u64,[-6:],ljust?这些都是什么东西,一个一个讲
u64/u32
不知道你还记不记得我们之前讲过的bite型,他起到了数据的传输和存储的作用
你是不是一直有个疑问,为什么我们要用到p64和p32
实际上p是将括号内的数据打包成二进制字节数据流(可以理解为bite型)
而如果我们要想接收,并且转化为我们能看懂的数据类型,就需要用到u
为了方便理解,我们看一下如果没有u64,我们得到的数据会是什么样子
其作用就是决定recv从倒数第n个字节开始读取
但是为什么这里是6呢?我们试试4,5,7这些数字会导致什么结果
这里不知道你发现没有,一个字节对应着两个字符(之前提过了,这里小复习一下)
并且由于小端序,所以我们从倒数第几个字节开始接收,影响着我们得到的真实地址的后三位
拜托,这可是致命的错误,后三位错了我们还怎么获得偏移
通常,函数的真实地址虽然是8字节(64位),但是由于其头两个字节的数据恒为00 00
所以我们只需要从倒数第六字节开始读取(反而言之,就是你要从倒8处读也行)
欸 你说 我偏不要呢 我就不要[-6:]你来帮我限制读入的字节数量
反正我就8字节的地址呗
如果你尝试了以后就会报错,为什么?
因为我们不单单只读入了函数的真实地址,数据传输以及内存地址分配是一个复杂的过程
而我们将其改为100试试,仍然可以正常读入数据
但是你会发现在地址结尾处多出来了个0a,实际上他是换行符,这个换行符是哪里来的?
仔细观察14行 我们在接收的时候,并未一起接收换行符
这一点说明了什么?修改为100后都能读取倒上一个字符串的数据了,那我们刚刚不还说在函数地址上面,还有很多其他数据呢?
这里就可以介绍介绍ljust了
ljust
他的作用就是限制我们读入的字节总数,如果不够的话则以我们设定好的字符填充
所以我们哪怕[-n:]中的n取到了100也仍然不会报错
说回exp 在第一个payload输送完以后,我们成功获得函数的真实地址
接着就是计算偏移然后求得其他函数以及binsh字符串的地址
还是老办法构造payload,并且这里还需要一个ret来栈对齐
补充:
一点小补充吧 相信会有人和我有一样的疑问,在刚接触到ret2libc的时候
既然我们都将got表中的puts函数真实地址作为参数存储在了rdi寄存器中输送再接收
而且获取真实地址的方法只需要一个elf.got就行了
为什么我们不能直接拿这个地址来进行计算基址呢?
很简单,我们debug看一下,如果我们直接使用got表中的真实地址,他是一个什么东西
我们再看一下 通过我们上文的办法得到的真实地址长什么样子
可以看到明显不一样
这是因为got表中保存的值是需要运行过后才会为真实地址,所以我们需要将其打印出来再接收(这里我也有点不太理解,埋个坑,日后填)