2015 0ctf freenote writeup
这道题的堆指针没有清空,导致释放堆内存后仍然指针任然指向堆,由于释放指针没有有效性检查,经过再次申请重新利用释放掉的内存,可以再将原来释放的堆指针再次释放。
# 分析 拿到这道题,先看软件开启了什么保护。
1
2
3
4
5Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
64位小端对齐程序,开启了canary
和NX
保护,运行程序发现程序有如下功能。
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
将程序拖入IDA中,很快定位漏洞位置。在操作4
中,free
堆内存后并没有将指针置NULL
。
1
2
3
4
5
6
7
8if ( v1 >= 0 && (signed __int64)v1 < *(_QWORD *)qword_6020A8 )
{
--*(_QWORD *)(qword_6020A8 + 8);
*(_QWORD *)(qword_6020A8 + 24LL * v1 + 16) = 0LL;
*(_QWORD *)(qword_6020A8 + 24LL * v1 + 24) = 0LL;
free(*(void **)(qword_6020A8 + 24LL * v1 + 32));
result = puts("Done.");
}
要理解程序,需要理解全局变量qword_6020A8
。它的初始化在sub_400A49
中。qword_6020A8
是个指针,指向了0x1810大小的内存。
第一个元素保存256,从循环看,这个应该是256个最大值的意思。
第二个元素保存存储note的数量。
之后是每个note的结构体信息,每个结构体24字节,第一个标记变量note[i]->flag,1表示有效,0表示无效;第二个保存note的长度note[i]->length;第三个保存note的指针note[i]->str,通过malloc
申请内存,最小128,最大4096长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20_QWORD *sub_400A49()
{
_QWORD *v0; // rax@1
_QWORD *result; // rax@1
signed int i; // [sp+Ch] [bp-4h]@1
v0 = malloc(0x1810uLL);
qword_6020A8 = (__int64)v0;
*v0 = 256LL;
result = (_QWORD *)qword_6020A8;
*(_QWORD *)(qword_6020A8 + 8) = 0LL;
for ( i = 0; i <= 255; ++i )
{
*(_QWORD *)(qword_6020A8 + 24LL * i + 16) = 0LL;
*(_QWORD *)(qword_6020A8 + 24LL * i + 24) = 0LL;
result = (_QWORD *)(qword_6020A8 + 24LL * i + 32);
*result = 0LL;
}
return result;
}
unlink
可以通过我博客里面的unsafe unlink
来达到任意地址的读写。这时候需要一个全局指针来作为victim
。前文分析到,note[i]->str指向了堆,而且note[i]还保存在堆上,所以有必要泄露堆地址来获取victim。
泄露堆地址
由于字符串读入时,没有补\0
,所以输出时可以一直把后面的内容打印出来。可以申请多个small chunk
的堆并释放其中几个,几个small
chunk保存在unsorted
bins内,让某个freed的chunk(比如A)的bk指向另一个freed
chunk(比如B),然后重新申请A的大小内存,将A块从unsorted
bins中释放出来,再次打印A块的内容即可泄露堆内存地址。
这里我学到了一个新的gdb命令,vmmap
来展示整个内存空间的映射。找到heap一栏,堆内存的起始地址可以查找。
double free思路
- 先连续申请4个0x80字节的堆内存,分别计为note0,note1,note2,note3。chunk大小为0x90。
- 先释放note0,再释放note2,分隔释放防止堆块合并。
- 重新申请0x80,内容少于8字节,不要覆盖bk指针,可以获取到note0。然后打印note0的内容可以leak堆地址,进而推算出note[i]->str地址。我这里取note[0]->str, 因为note[0]->str = note0。
- 将note0,note1,note3释放掉。
- 然后我们申请3个note,分别记为n_note0, n_note1, n_note2。因为我们要再次free note3。
- 利用unsafe unlink重新构造n_note0,n_note1,n_note2。具体如何构造,参见http://rk700.github.io/2015/04/21/0ctf-freenote/
- 再次释放note3,拿到note[0]->str,其指向了比它低3个地址长度的地址。
- 先利用victim指针指向free的got地址,泄露其在内存中加载的地址。
- 利用libc中free与system相对便宜地址,计算system在内存中加载的地址。
- 将system内存地址存入free的got表中,覆盖free内存地址。
- 将/bin/sh写入note中,free掉此note,相当于执行了system('bin/sh')。PWN!
总结
vmmap
常用,可以方便的查看包括堆内存分配情况。- pwntools工具中关于
recv
函数,有个参数keepends
表示接受行是否保留0a,有时候不需要换行符\n
,可以将其置为False
。 - unsafe unlink熟练运用,达到任意地址读和写的目的。
代码
1 | from pwn import * |
参考
[1] Command vmmap [2] 0CTF freenote [3] 0ctf 2015 Freenote Write Up