我们按照学习原理,然后找一道例题学习,最后复盘总结的方式来学习这个知识点。
off-by-one原理
off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。
Asis CTF 2016 b00ks
配置搭建环境
环境:pwndocker配置:
# 配置libc的版本 cp /glibc/2.23/64/lib/ld-2.23.so /tmp/ld-2.23.so patchelf --set-interpreter /tmp/ld-2.23.so ./b00ks LD_PRELOAD=/glibc/2.23/64/lib/libc.so.6 # 转发输入输出流到 10001 端口 socat tcp-listen:10001,reuseaddr,fork EXEC:./b00ks,pty,raw,echo=0
题目分析
按照惯例,我们先大概浏览一下题目,题目是一个图书管理系统:
1. Create a book 2. Delete a book 3. Edit a book 4. Print book detail 5. Change current author name 6. Exit
简单分析下Create
book 结构中存在 name 和 description , name 和 description 在堆上分配。首先分配 name buffer ,使用 malloc ,大小自定但小于 32 。
printf("\nEnter book name size: ", *(_QWORD *)&size); __isoc99_scanf("%d", &size); printf("Enter book name (Max 32 chars): ", &size); ptr = malloc(size);
之后分配 description ,同样大小自定但无限制。
printf("\nEnter book description size: ", *(_QWORD *)&size); __isoc99_scanf("%d", &size); v5 = malloc(size);
之后分配 book 结构的内存
book = malloc(0x20uLL); if ( book ) { *((_DWORD *)book + 6) = size; *((_QWORD *)off_202010 + v2) = book; *((_QWORD *)book + 2) = description; *((_QWORD *)book + 1) = name; *(_DWORD *)book = ++unk_202024; return 0LL; }
综合调试,我们得出book的结构体如下:
struct book{ int id; char *name; char *description; int size; }
漏洞点
程序编写的 read 函数存在 null byte off-by-one 漏洞,仔细观察这个 read 函数可以发现对于边界的考虑是不当的。
signed __int64 __fastcall MyRead(char *a_string, int str_size) { int i; // [rsp+14h] [rbp-Ch] char *buf; // [rsp+18h] [rbp-8h] if ( str_size <= 0 ) return 0LL; buf = a_string; for ( i = 0; ; ++i ) { if ( (unsigned int)read(0, buf, 1uLL) != 1 ) return 1LL; if ( *buf == 10 ) break; ++buf; if ( i == str_size ) break; } *buf = 0; return 0LL; }
利用思路
攻击过程
1.填充满 author
2.创建堆块1,覆盖author结尾的\x00,这样我们输出的时候就可以泄露堆块1的地址
3.创建堆块2,为后续做准备,堆块2要申请得比较大,因为mmap申请出来的堆块地址与libc有固定的偏移
4.泄露堆块1地址,记为first_heap
5.在调试中我们知道,创建的book结构体的地址是存储在全局变量中的,该全局变量正好与author相临,更改 author,利用多写的一个\x00字节,可以覆盖到堆块1的地址的最后一位,如果我们提前将堆块1的description内容编辑并伪造一个book的结构体,然后多次调试构造好位置,让覆盖过后的地址刚好是book1的description部分的话,我们相当于获得了一个任意地址读写
6.任意读:获得libc地址
7.任意写:将__free_hook函数的地址改写成one_gadget地址
__free_hook若没有则不调用,若有将先于free函数调用
调试
OK现在我们用IDA远程调试的方式按照上边的利用思路来调试一下:
填充 author
我们创建一个32字节大小的名字A
createname(b"A"*32)
然后我们看到填充的内存,除了32个A之外,还额外增加了一个\x00。
创建堆块1、创建堆块2
创建堆块1后,我们看到0x55C38D8C7530覆盖了author的\x00字节。创建的堆块2的地址也紧接着堆块1。
我们看看堆块1和堆块2的结构体如下:
泄露堆块1地址
我们通过上一步覆盖了末尾的\x00后,就可以打印出堆块1的地址,如下图:
编辑堆块1的description字段,伪造堆块1
Payload代码如下,我们注意到我们构造这个假堆块1,它的name指针0x55C38D8C7568指向的是堆块2的name,它的description指针0x55C38D8C7570指向的是堆块2的description。
payload=b'A'*0x50+p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x40)+p64(0xffff) editbook(book_id_1,payload)
让伪造的堆块1恰巧在地址0x55C38D8C7500处,有人或许会问为什么你构造的堆块1是恰巧在这个位置的,因为这是经过多次尝试和调试出来的,适当的将堆块1的description大小分配的大一些,然后增加Payload的偏移。
接下来,最关键的时候到了,如下图,我们再次修改author,使得author填充完32个A之后,还是将\x00覆盖了原始堆块1的地址,导致现在的地址变为了0x55C38D8C7500,恰好指向了我们构造的假的堆块1的地址,这样以来,我们就可以实现任意地址的读写。
获取libc地址
接下来,我们利用打印功能,打印出堆块1的相关值,因为mmap申请出来的堆块地址与libc有固定的偏移,所以我们利用前边多次调试得到的偏移,计算出libc。
将__free_hook函数的地址改写成one_gadget地址
首先使用editbook(1,p64(free_hook))将free_hook的指针写到堆块2的description变量中去,然后使用editbook(2,p64(one_gadget)),将one_gadget地址写入到free_hook中去。
最后我们再调用free获得shell
小结
在刚开始调试的时候以为是使用pwntools+GDB的调试方式效率比较高一些,但最终发现,图形界面的IDA使得调试时候的视野更加广泛一些,而且还有强大的反编译功能,特别适合初学,也增加了学习的效率。此外针对off-by-one,我们更多的是与其他的技巧结合的方式,做题之前得多次调试理解题目的逻辑和内存的构造,这样做题目才能够得心应手。
来源:freebuf.com 2021-02-15 10:46:38 by: ATL安全团队
请登录后发表评论
注册