栈溢出之ret2libc
/ret2libc原理
ret2libc,即控制执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。
具体过程为:在内存中确定某个函数的地址,并用其覆盖掉返回地址,让其指向前面确定的函数。由于 libc 动态链接库中的函数被广泛使用,所以有很大概率可以在内存中找到该动态库。同时由于该库包含了一些系统级的函数(例如 system() 等),所以通常使用这些系统级函数来获得当前进程的控制权。鉴于要执行的函数可能需要参数,比如调用 system() 函数打开 shell 的完整形式为 system(“/bin/sh”) ,所以溢出数据也要包括必要的参数。
payload: padding1 + address of system() + padding2 + address of “/bin/sh”
padding1 处的数据可以随意填充(注意不要包含 “\x00” ,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of system() 是 system() 在内存中的地址,用来覆盖返回地址。padding2 处的数据长度为4(32位机),对应调用 system() 时的返回地址。因为我们在这里只需要打开 shell 就可以,并不关心从 shell 退出之后的行为,所以 padding2 的内容可以随意填充。address of “/bin/sh” 是字符串 “/bin/sh” 在内存中的地址,作为传给 system() 的参数。
第一个问题——system()地址如何确定?
要回答这个问题,就要看看程序是如何调用动态链接库中的函数的。当函数被动态链接至程序中,程序在运行时首先确定动态链接库在内存的起始地址,再加上函数在动态库中的相对偏移量,最终得到函数在内存的绝对地址。说到确定动态库的内存地址,就要回顾一下 shellcode 中提到的内存布局随机化(ASLR),这项技术也会将动态库加载的起始地址做随机化处理。所以,如果操作系统打开了 ASLR,程序每次运行时动态库的起始地址都会变化,也就无从确定库内函数的绝对地址。在 ASLR 被关闭的前提下,我们可以通过调试工具在运行程序过程中直接查看 system() 的地址,也可以查看动态库在内存的起始地址,再在动态库内查看函数的相对偏移位置,通过计算得到函数的绝对地址。
第二个问题——“/bin/sh”字符串地址如何确定?
可以在动态库里搜索这个字符串,如果存在,就可以按照动态库起始地址+相对偏移来确定其绝对地址。如果在动态库里找不到,可以将这个字符串加到环境变量里,再通过 getenv() 等函数来确定地址。
前提条件
由前面分析可知,ret2libc这项技术的前提是需要操作系统关闭内存布局随机化(ASLR)。
ret2libc1——存在system()、/bin/sh
运行程序,提示应用ret2libc,且用file查看是动态链接文件,和libc有关:
查看保护机制,只开启了NX:
IDA查看:
搜索“/bin/sh”字符串,可通过string窗口或ROPgadget工具查找:
或
可知“/bin/sh”字符串所在地址为0x08048720。
因为要从libc中寻找利用函数,则可以在ida直接查看plt中是否有system()函数,发现是存在有的且地址为0x08048460:
至于用户输入的变量v4距函数返回地址的偏移地址的计算如之前所示,结果是一样的为0x70。
编写payload:
1 | from pwn import * |
运行getshell:
ret2libc2——只有system()
运行程序,file查看文件为动态链接即和libc相关,查看保护机制只开启NX:
使用IDA打开查看:
在string窗口确实找不到“/bin/sh”:
在plt中仍可找到system()函数,地址为0x08048490:
可以发现与示例1相比,这次不直接提供“/bin/sh”,那就换种思维,多利用一个gadgets,可以在plt中看到有gets()函数,即可以将该gets()函数地址用来踩掉原本程序函数的返回地址,然后通过输入的方式将“/bin/sh”输入进去。换句话说,整个过程分成了两部分,第一部分是将“/bin/sh”读入到内存中;第二部分是执行system()获取shell:
其中可知get()函数地址为08048460。
查看gets()函数,其需要一个可读可写的指针参数,且会返回值:
寻找一块可读可写的buffer区,通常会寻找.bss段,使用IDA查看可看到存在buf2[100]数组:
明确该.bss段是否可读可写:
最后就是payload的构造了。因为在gets()函数完成后需要调用system()函数需要保持堆栈平衡,所以在调用完gets()函数后提升堆栈,这就需要add esp, 4这样的指令但是程序中并没有这样的指令。更换思路,通过使用pop xxx指令也可以完成同样的功能,在程序中找到了pop ebx,ret指令。通过ROPgadget工具查看,发现存在一条符合条件的指令,地址为0x0804841d:
编写payload:
1 | from pwn import * |
运行getshell:
ret2libc3——无system()和/bin/sh
在ret2libc2的基础上,再次将system()函数的地址去掉。此时,我们需要同时找到system())函数地址与”/bin/sh”字符串的地址。
题目分析
运行程序,file查看文件为动态链接即和libc相关,查看保护机制只开启NX:
IDA打开查看,同样是栈溢出漏洞:
在String窗口找不到“/bin/sh”字符串,在Functions窗口中也找不到system()函数:
但是在libc中是有system()函数和/bin/sh字符串的。因此,我们可以通过泄露libc中某个被调用过的函数的地址来获取libc版本,获取libc中各个偏移地址值,然后通过某个函数的真实地址计算出system()和/bin/bash的真实地址。
结合前面知道,溢出点到函数返回地址的偏移量和前面的一样,为112。
结合libc的延迟绑定机制,下面要做的是需要我们泄露某个已经执行过的函数的真实地址,实现泄露地址功能的函数可以通过puts()函数来输出打印出来实现,而参数填的是某个已经执行过的函数的GOT地址;同时为了程序再次执行能重新实现栈溢出功能,在puts()函数的返回地址填的是_start()函数或main()函数地址即可。
对于system()函数,其属于libc,在libc.so动态链接库中的函数之间相对偏移是固定的。我们由泄露的某个函数的GOT表地址可以计算出偏移地址(A真实地址-A的偏移地址 = B真实地址-B的偏移地址 = 基地址),从而可以得到system()函数的真实地址(当然也可以直接调用pwntools的libc.address得到libc的真实地址,然后再直接查找即可找到真实的system()函数地址)。
利用过程图
以泄露puts()的GOT地址为例,构造过程如下图,红色箭头为第一次溢出调用,通过gets()栈溢出至函数返回地址处将其覆盖为puts的plt地址,将puts的GOT表地址泄露输出出来,再返回到_start()函数重新执行程序;蓝色箭头为程序第二次执行时的溢出调用,重新通过gets()输入内容栈溢出至函数返回地址处,覆盖该地址为libc中找到的system()地址(libc地址由泄露的puts函数地址计算得出),从而getshell:
利用过程小结
- 程序通过gets()函数获取输入的内容,存在明显的栈溢出漏洞;
- 在ELF中未找到system()和”/bin/sh”;
- 计算出输入内容地址到函数返回地址的偏移量为112;
- 将puts()的plt地址覆盖到函数返回地址处,通过puts()泄露某个已执行过的函数的GOT地址,并且返回地址设置为_start()或main(),以便于重新执行一遍程序;
- 通过recv(4)接收puts()输出泄露的某个已执行过的函数的GOT地址,再以此来计算libc中地址与真实地址的偏移量,从而计算出libc中system()函数和”/bin/sh”字符串的真实地址;或者通过泄露的某个已执行过的函数的GOT地址,直接使用pwntools的libc.address=func_got-libc.symbols[‘func’]的形式直接获取libc的真实地址,从而直接通过system_addr=libc.symbols[‘system’]的方式直接获取该函数真实地址;
- 程序再次执行时填充padding,在函数返回地址处覆盖为libc中system()函数的真实地址,其中参数为libc中”/bin/sh”字符串的真实地址。
payload编写
在第一次栈溢出puts()的plt地址覆盖函数返回地址时,puts()的返回地址可以设置为_start()或main()函数地址。
_start()和main()的区别
简单地说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。
我们可以看下本题的_start()函数内容,其包含main()和__libc_start_main()函数的调用,也就是说,它才是程序真正的入口:
返回地址为_start()函数
这里的示例只展示了两个可利用的函数puts()和__libc_start_main()。
泄露puts()函数地址
1 | from pwn import * |
泄露__libc_start_main()函数地址
1 | from pwn import * |
返回地址为main()函数
先将_start()换成main(),payload2的B字符的偏移量不变,运行脚本会报错,添加GDB调试交互发现溢出多了8个B:
相应的,减少8个B字符即112-8=104就可以有效溢出从而getshell:
1 | from pwn import * |