从plt到ret2libc
从plt到ret2libc
最近开始学pwn了,别问为什么,问就是有这个需要。
记录一下学习ret2libc的全过程。
动态链接、plt表和got表
动态链接是什么:众所周知,c语言中使用的很多函数是调用的一些库里的,比如stdio.h。那么在编译的时候就有两种选项了。一种是直接把库里对应函数的具体内容copy到怎们写的程序里来,这个叫静态链接。而动态链接则是在程序里面放一个类似于超链接的东西,等到程序执行到这里了,再去找这个函数的代码具体放在哪里了并进行执行。
而plt表和got表就是在这个过程中要用到的两个东西。
plt表和got表
首先,毋庸置疑的是,不管静态链接还是动态链接,在call的时候后面总要跟一个地址,静态链接的时候,call后面的地址直接就指向函数入口地址,动态链接则不然。他要通过一些编译器单独生成的几行代码再做跳转才能找到函数的入口地址。而用来存放这些单独生成的代码的表就叫做plt表。那么这些代码的具体内容是什么呢?可以简单认为是
1 | jmp *func@got |
就是跳转到got表对应地址指向的位置。即got表是存放各个函数真实地址的位置,plt表中存放的代码会指引程序流跳转到那个地址指向的位置。
那么,这两个表是怎么生成的呢?只要代码一开始运行这两个表就已经确定好吗?答案是:plt表确实在编译结束后就确定了,但got表可不是。由于linux的延迟绑定机制,在首次调用函数时,还要进行一个“找”地址的工作,就是把函数的真实地址找到,然后填进got表里。
在了解他是怎么找的之前,先了解一下他是怎么判断我现在找没找到的呢?刚才咱们说plt的代码是
1 | jmp *func@got |
这里面的门道就在于func@got的内容。在没有查找之前,这个地址的内容指向了一个公共的plt表,可以做到查找函数地址,而在查找之后,func@got的内容就会变成这个函数的实际地址了。
具体找的过程调用了一个_dl_runtime_resolve函数。函数的声明是这样的_dl_runtime_resolve(link_map_obj, reloc_index)
第一个参数提供运行时的必要信息,而第二个参数则负责指明到底要找哪个函数。在call _dl_runtime_resolve之前,tls表中的代码会把刚才这两个参数入栈。
ret2libc
有了这些知识,就可以开始构造ret2libc了。这是基本ROP中利用条件最少的一个。题目的可执行代码中有system(“/bin/sh”)可以直接ret2text,给了所有需要用到的gadgets可以ret2syscall,但是如果都没给就需要ret2libc了。当然,ret2libc的最终目的也是system(“/bin/sh”)。那么无非需要解决两个问题:1. system函数在哪?2. /bin/sh在哪(透着那么讲理)
一次解决这两个问题。怎么找到system函数?其实上边那一大堆都是在为了这里做铺垫。在通过_dl_runtime_resolve函数完成一个函数地址的查找后,就可以确定该程序的libc,进而确定system函数的地址。同时,libc上也有/bin/sh,因此两个问题一次解决了。但是由于找到system函数和/bin/sh已经需要一次栈溢出了,跳转到system还需要再溢出一次,因此咱们需要在第一次栈溢出之后ret回mian函数。
so how?
现在已知:如果知道了libc版本和一个函数的真实地址,那咱们就可以拿到system和/bin/sh的真实地址了。那现在的问题变成了:1.如何得到一个函数的真实地址?2.如何在一次执行中,对一个漏洞函数溢出两次。
这次得一个一个解决了。第一个问题比较容易。等到一个函数执行之后,就可以得到其got表的位置了。
第二个问题麻烦一些。咱们要从栈帧分配说起。众所周知:32位系统通过栈来传参。当咱们写了一行代码func(arg1)
的时候,汇编代码的逻辑是这样的,在执行 call func 之前,系统会先把参数入栈,然后把返回地址入栈。在call之后,在func函数内,系统会将old ebp入栈。
咱们在进行栈溢出覆盖返回地址的时候,相当于只执行了call这一条指令,那么前面的参数和返回地址入栈就都是咱们可控且需要自行完成的内容了。因此在覆盖了返回地址之后,要继续覆盖返回地址和参数。这就可以解释ctf wiki上的ret2libc3的exp中的这一段
1 | payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got]) |
为什么可以把main函数的真实地址泄漏,并且控制返回地址回到main函数。
好了,两个问题都解决了,咱们来看一下ret2libc3上完整的exp
1 | #!/usr/bin/env python |
下一个问题:为什么非得把main函数的真实地址puts出去再recv回来?答曰:libc_start_main_got的内容是got表的地址,而把这个值作为puts的参数输出之后,得到的才是main真正的地址。相当于*libc_start_main_got。
后面就是计算基址,然后找用基础加system和binsh的偏移计算实际地址了。
好了就这样了,64位的exp后面再说吧。