6.1.7 pwn 0CTF2015 freenote
题目复现
$ file freenote
freenote: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=dd259bb085b3a4aeb393ec5ef4f09e312555a64d, stripped
$ checksec -f freenote
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 2 freenote
$ strings libc-2.19.so | grep "GNU C"
GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.6) stable release version 2.19, by Roland McGrath et al.
Compiled by GNU CC version 4.8.2.因为没有 PIE,即使本机开启 ASLR 也没有关系。
玩一下,它有 List、New、Edit、Delete 四个功能:
$ ./freenote
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 2
Length of new note: 5
Enter your note: AAAA
Done.
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 1
0. AAAA
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 3
Note number: 0
Length of note: 10
Enter your note: BBBBBBBBB
Done.
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 1
0. BBBBBBBBB
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 4
Note number: 0
Done.
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 1
You need to create some new notes first.
== 0ops Free Note ==
1. List Note
2. New Note
3. Edit Note
4. Delete Note
5. Exit
====================
Your choice: 5
Bye然后漏洞似乎也很明显,如果我们两次 Delete 同一个笔记,则触发 double free:
在 Ubuntu 14.04 上把程序跑起来:
题目解析
我们先来看一下 main 函数:
main 函数首先调用 sub.malloc_a49() 分配一块内存并进行初始化,然后读取用户输入,根据偏移跳转到相应的函数上去(典型的switch实现):
所以四个功能对应的函数如下:
List:
sub.You_need_to_create_some_new_notes_first._b14New:
sub.Length_of_new_note:_bc2Edit:
call sub.Note_number:_d87Delete:
call sub.No_notes_yet._f7d
函数 sub.malloc_a49 如下:
该函数调用 malloc 在堆上分配 0x1810 字节的内存用来存放数据。我们可以猜测这里还存在两个结构体,Note 和 Notes:
接下来依次分析程序四个功能的实现,先从 List 开始:
该函数会打印出所有 isValid 成员为 1 的笔记。
New 的实现如下:
该函数首先对你输入的大小进行判断,如果小于 128 字节,则默认分配 128 字节的空间,如果大于 128 字节且小于 4096 字节时,则分配比输入稍大的 128 字节的整数倍的空间,如果大于 4096 字节,则默认分配 4096 字节。
Edit 的实现如下:
该函数在输入了笔记序号和大小之后,会先判断新的大小与现在的大小是否相同,如果相同,则不重新分配空间,直接编辑其内容,否则调用 realloc() 重新分配一块空间(地址可能与原地址相同,也可能不相同)。
Delete 的实现如下:
该函数在读入要删除的笔记序号后,首先将 Notes 结构体成员 length -1,然后将对应的 Note 结构体的 isValid 和 length 修改为 0,然后 free 掉笔记的内容(*content)。
漏洞利用
在上面逆向的过程中我们发现,程序存在 double free 漏洞。在 Delete 的时候,只是设置了 isValid =0 作为标记,而没有将该笔记从 Notes 中移除,也没有将 content 设置为 NULL,然后就调用了 free 函数。整个过程没有对 isValid 是否已经为 0 做任何检查。于是我们可以对同一个笔记 Delete 两次,造成 double free,修改 GOT 表,改变程序的执行流。
泄漏地址
第一步先泄漏堆地址。为方便调试,就先关掉 ASLR 吧:
为了泄漏堆地址,我们需要释放 2 个不相邻且不会被合并进 top chunk 里的 chunk,所以我们创建 4 个笔记,可以看到由初始化阶段创建的 Notes 和 Note 结构体:
下面是创建的 4 个笔记:
现在我们释放掉 chunk 0 和 chunk 2:
chunk 0 和 chunk 2 被放进了 unsorted bin,且它们的 fd 和 bk 指针有我们需要的地址。
为了泄漏堆地址,我们分配一个内容长度为 8 的笔记,malloc 将从 unsorted bin 中把原来 chunk 0 的空间取出来:
为什么是 8 呢?我们同样可以看到程序的读入是有问题的,没有在字符串末尾加上 \0,导致了信息泄漏的发生,接下来只要调用 List 就可以把 chunk 2 的地址 0x604940 打印出来。然后根据 chunk 2 的偏移,即可计算出堆起始地址:
其实我们还可以得到 libc 的地址,方法如下:
我们看到 __malloc_hook 在这个地址 0x00007ffff7dd37b8-0x78 的地方。其实 0x7ffff7dd3760 地方开始就是 main_arena,但在这个 libc 里符号被 stripped 扔掉了。看一下 __malloc_hook 在 libc 中的偏移:
因为偏移是不变的,我们总是可以计算出 libc 的地址:
过程和泄漏堆地址是一样的,这里就不展示了,代码如下:
这一步的最后只要把创建的所有笔记都删掉就好了:
所有的 chunk 都被合并到了 top chunk 里,需要重点关注的是 chunk 3 的 prev_size 字段:
所以其实它指向的是 chunk 0,如果再次释放 chunk 3,它会根据 prev_size 找到 chunk 0,并执行 unlink,然而如果直接这样做的话,马上就被 libc 检查出来了,double free 异常被触发,程序崩溃。所以我们接下来我们通过修改 prev_size 指向一个 fake chunk,来成功 unlink。
unlink
有了堆地址,根据 unlink 攻击的一般思想,我们总共创建 3 块 chunk,在 chunk 0 中构造 fake chunk,在 chunk 1 中放置 /bin/sh 字符串,用来作为 system() 函数的参数,chunk 2 里再放置两个 fake chunk:
为什么这样构造呢?回顾一下 unlink 的操作如下:
需要绕过的检查:
最终效果是:
为了绕过它,我们需要一个指向 chunk 头的指针,通过前面的分析我们知道 Note.content 正好指向 chunk 头,而且没有被置空,那么就可以通过泄漏出来的堆地址计算出这个指针的地址。
全部堆块的情况如下:
首先是 chunk 0,在它里面包含了一个 fake chunk,且设置了 bk、fd 指针用于绕过检查。
chunk 2 分配了很大的空间,把 chunk 3 指针也包含了进去,这样就可以对 chunk 3 的 prev_size 进行设置,我们将其修改为 0x1a0,于是 0x6049d0 - 0x1a0 = 0x604830,即 fake chunk 的位置。另外,在释放 chunk 3 时,libc 会检查后一个堆块的 PREV_INUSE 标志位,同时也为了防止 free 后的 chunk 被合并进 top chunk,所以需要在 chunk 3 后布置一个 fake chunk,同样的 fake chunk 的后一个堆块也必须是 PREV_INUSE 的,以防止 chunk 3 与 fake chunk 合并。
接下来就是释放 chunk 3,触发 unlink:
我们看到,note 0 的 content 指针被修改了,原本指向 fake chunk,现在却指向了自身地址减 0x18 的位置,这意味着我们可以将其改为任意地址。
overwrite note
这一步我们利用 Edit 功能先将 notes[0].content 该为 free@got:
另外这里将 length 设置为 8 也是有意义的,因为我们下一步修改 free 的地址为 system 地址,正好是 8 个字符长度,程序直接编辑其内容而不会调用 realloc 重新分配空间:
于是最后一步调用 free 时,实际上是调用了 system('/bin/sh'):
pwn
开启 ASLR。Bingo!!!
exploit
完整的 exp 如下:
参考资料
Last updated
Was this helpful?