翻译链接:heap-tricks-never-get-old-insomnihack-teaser
文章分类:二进制漏洞分析
Synacktiv红队在上周末的Insomni'hack比赛,以280名队伍第九名完结。其中有一个挑战非常有趣,并且教会了我一些技巧和方法,所以我决定写一篇详细的博客。在这篇文章中,我会努力充分的解释解决这个问题的思考过程,绝不仅仅是一般的方法。希望你能享受这个阅读,最后的exp.py放在文末附录。
二进制文件和libc文件都是沿用pwn的方法进行使用。
从保护方法来看,ontestament.bin
文件有很好的保护措施,包括RELPO保护,NX位保护,PIE保护
。
$ checksec ontestament.bin
[*] '/home/bak/onetestament/ontestament.bin'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
提供的libc文件是与源文件剥离的,可以方便的使用debug符号重新得到等效项。辛运的是,pwninit工具就是这么做得,修补二进制文件,使用其提供的libc链接,而不是使用系统链接。
libc链接可以使用ldd
工具来验证:
$ ldd ontestament.bin_patched
linux-vdso.so.1 (0x00007ffc057eb000)
libc.so.6 => ./libc.so.6 (0x00007f84a2ec8000)
./ld-2.23.so => /lib64/ld-linux-x86-64.so.2 (0x00007f84a3499000)
同时,libc的版本号是多少呢?
$ ./libc6.so | head -n 1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.3) stable release version 2.23, by Roland McGrath et al.
如果你对常见heap堆技巧非常熟悉,那么你会知道知道libc的版本非常重要。
事实上,heap的内存管理随着时间的已经发展了很长一段时间了。保护措施的出现,内存结构的改变,他们的工作方式也在改变。这也是为什么基于堆的攻击常常影响的是libc的特定的版本。如果你想看更多这方面的资料,可以看看A repository for learning various heap exploitation techniques.。
在我的印象中,Glibc 2.23开始于2016年2月份,他已经是非常老的了。在开始挖漏洞之前,有一件非常有趣的事情需要做,那就是看看下一个版本新版本的libc中有什么安全修复措施和相关安全保护措施。
这个程序的运行结果是非常的直接。列出所有的堆技术,你可以创建对象,编辑对象,查看对象,删除对象。至少这也是我所想的,当我第一次看到这个程序运行的时候。
$ ./ontestament.bin
==========================
✝ OneTestament ✝
==========================
1. My new testament
2. Show my testament
3. Edit my testament
4. Delete my testament
5. Bye!
Please enter your choice:
在程序运行之前,一个alarm(20)
的调用产生了。这会在20秒之后触发SIGALRM信号,停止程序。为了避免这个令人烦恼的行为,可以修补二进制文件(nop对alarm的调用)或者简单的在gdb输入以下命令:
pwndbg> handle SIGALRM ignore
Signal Stop Print Pass to program Description
SIGALRM No Yes No Alarm clock
让我们迅速开始查看所有的重要函数。
函数的伪代码如下:
首先,我发现了程序只允许10次分配,全局变量(此处变量名是nb_testaments
)在每次新分配后都会递增。
可用的大小包含:0x18, 0x30, 0x60 and 0x7c 字节,这样可以方便我在快速bins和未排序bins中释放数据块chunks。提醒一下,每个大于0x58字节的数据块在释放后都会放入未排序的容器中。
数据块chunks由calloc()函数负责分配,关于calloc()
函数与malloc()
函数的主要区别是后者对分配的内存区域执行memset(mem,0sz)
。
之后,testament将用户指定的数据填入。与一般所想不同的是,fill_testament()
函数是安全的,这里不再详细给出缘由。
另一个有趣的地方是,testament指针和大小都存储在全局变量中,即.bss段。
细心地读者发现了我调过了read_input()
函数,他的伪代码如下:
__int64 read_input() { int v2; // [rsp+Ch] [rbp-4h] read(0, nptr, 5uLL); v2 = atoi(nptr); if ( v2 < 0 ) return 0; else return (unsigned int)v2; }
有5个字符是从用户数据读取的,并且存储在了全局变量的char nptr[4]
中。等等,有一个1个字节的溢出?是的,非常正确,让我们记住这一点,看看后面会有什么用处。
在每次学习heap堆技巧是,我有一个方法可以泄露地址,显示对象的内容:
int show_testament() { int index; // [rsp+Ch] [rbp-4h] index = init_rand(5, 0); return printf((&random_sentences)[index]); }
random_sentences
是一个数据,调用这个函数时会随机产生一个句子。
等一下,这是隐藏的hint吗?可能是的,我先来分析init_rand()
看看。
__int64 __fastcall init_rand(int a1, int a2) { unsigned int ptr; // [rsp+14h] [rbp-Ch] BYREF FILE *stream; // [rsp+18h] [rbp-8h] stream = fopen("/dev/urandom", "r"); fread(&ptr, 4uLL, 1uLL, stream); fclose(stream); srand(ptr); return (unsigned int)(rand() % a1 + a2); }
看来这只是一个恶搞