pesp_off-by-null
1.还是之前的2018网鼎杯的pesp题目。这里假设没有堆溢出,有PIE保护,无法劫持got表。只使用0字节溢出漏洞来获取libc地址,再根据得到的libc地址来更改malloc_hook和realloc_hook里面保存的地址为one_gadget,一步getshell。(realloc_hook为onegadget,malloc_hook为__libc_realloc函数中调整栈帧的地方)
2.先梳理一下0字节溢出漏洞,一般的chunk改内容都是:
1 | #注释头 |
这样就只能输入size这么大的内容,但是这道题中,在add函数中和change函数中:
可以看到给chunk添加内容的语句都是:
1 | #注释头 |
这样当我们将chunk内容顶满之后,程序会将chunk指针最后部分再溢出一个字节赋值为0,这就是off-by-null。
由于scanf函数会在末尾自动补\x00,这其实也是一种off-by-null,
1 | #注释头 |
▲free源码:
1 | #注释头 |
也就是如果当前chunk的IN_USE位为0,那么就根据当前chunk的pre_size位,找到前一个chunk_pre,将chunk_pre的size位改成size+pre_size,通过Unlink取出chunk_pre,准备合并。(之后的检查代码会再循环,也就是再看chunk_pre的IN_USE位是不是0来判断要不要再向上合并,这里不重要)
所以如果我们通过off-by-null,溢出0到chunk_next的size位中的IN_USE位,那么当前chunk就会被标记为Free,这样在Free(chunk_next)时,就会将chunk和chunk_next合并。如果我们又更改了chunk_next的pre_size位,使它变得更大,那么就可以向上合并更大的chunk块。这就是off-by-null的利用方法,这题中如下:
(需要注意的是,由于溢出的是一个字节而不是一个Bit,所以size一般都设置为0xf0,从而使size位变成0x101,溢出之后进而变成0x100。但如果size最开始设置为0x20,size位就是0x30,溢出0字节就会变成0x00,那样程序照样崩溃。)
(1)先申请四个chunk,分别为chunk0,chunk1,chunk2,chunk3。(chunk3防止合并用)
(2)然后free掉chunk0,(这里chunk0需要足够大,一般得大于0x80,也就是MAX_fastbins),使其进入unsortedbins中。(这里必须free掉chunk0,不然之后修改掉chunk2的pre_size位时,然后free掉chunk2时,程序依据pre_size来判断是否合并时,发现chunk0仍旧处于使用当中,但pre_size包含了chunk0,就会造成程序崩溃)
(3)修改chunk1的最后八个字节,也就是chunk2的pre_size位,使得chunk2的pre_size位的大小为chunk0_size+chunk1_size。
(4)free掉chunk2,使得chunk0,chunk1,chunk2三个chunk作为一个整体被合并之后放入到unsortedbins中,调试过程可以发现,chunk0的size位被修改成了sizeof(chunk0+chunk1+chunk2)。但是这里实际情况中,chunk1并没有被释放,只是它的内存处在被释放的内存中间,依然可以通过chunk1的指针来操作。也就是在这个程序中依旧可以进行edit来修改chunk1,或者show来打印chunk1。
(5)再次申请chunk0大小,这样就会割裂unsortedbins中的remainder为chunk0+new_remainder,把chunk0申请回来,剩下的new_remainder依旧放在unsortedbins中。在unsortedbins中的remainder有个特点,就是该chunk的fd和bk一定指向unsortedbins链表头部,(如果有多个remainder,那么顺序类似于smallbins,依旧可以使用第一个chunk的bk来获取unsortedbins链表头部)
(6)那么现在unsortedbins中有chunk1和chunk2,而chunk1的fd和bk都指向unsortedbins链表头部,并且在程序中chunk1仍旧处于使用状态,fd和bk就是chunk1的data部分。所以show函数就可以打印出chunk1的data部分,从而打印出fd和bk指向的unsortedbins头部链表地址。又由于unsortedbins处在main_arena+0x58位置,而main_arena相对于libc基地址的偏移是固定的,为0x3c4b20(不同glibc版本可能不同,这是libc2.23的),所以这也就间接泄露出了libc基地址。
▲查询main_arena方法:
①工具查询:https://github.com/coldwave96/libcoffset:
②IDA查询:main_arena存储在libc.so.6文件的.data段,使用IDA打开libc文件,然后搜索函数malloc_trim()
3.现在有了libc基地址,那么接下来就考虑修改mallo_hook和realloc_hook的值。同样使用0字节溢出漏洞,伪造fakechunk为mallo_hook地址,修改fakechunk从而修改malloc_hook和realloc_hook。这里先忽略上面的,程序中的索引会不太一样。攻击思路如下:
(1)先申请一个较大的chunk,防止和上面的合并
(2)之后的操作方法类似,先申请四个chunk,分别为chunk0,chunk1,chunk2,chunk3。(chunk3防止合并用)
(3)free掉chunk0,使其进入unsortedbins中。修改chunk1的最后八个字节,也就是chunk2的pre_size位,使得chunk2的pre_size位的大小为chunk0_size+chunk1_size。
(4)free掉chunk2,使得chunk0,chunk1,chunk2三个chunk作为一个整体被合并之后放入到unsortedbins中。再free掉chunk1,使其进入fastbins中。这样chunk1即在fastbins中,也处在unsortedbins中。
(5)申请一个特殊大小的chunk块,最小为chunk0_size+0x20,使其的data部分足够大,能够修改掉chunk1的fd位,将chunk1的fd位指向fakechunk,由于需要从size位覆盖到fd,所以需要伪造合法的size,为chunk1_size。
(6)申请两个chunk1大小的chunk_a,chunk_b,这样第一次申请的chunk_a就是从fastbins中回来的chunk1。而chunk_b就是chunk1的fd指向的fakechunk。这样如果将fakechunk放在realloc_hook之前,那么就可以修改掉realloc_hook和malloc_hook,使得realloc_hook指向one_gadget,而malloc_hook指向__libc_libc函数中的某个可以控制栈帧的地址,从而满足gadget条件来getshell。
4.开始编写exp,增删改查函数就不说了:
(1)获取libc基地址:
1 | #注释头 |
(2)伪造fakechunk:
1 | #注释头 |
(3)申请获得fakechunk,同时修改该fakechunk,劫持malloc_hook和realloc_hook。
1 | #注释头 |
(4)随便申请一个chunk即可getshell,但是这里不要使用之前我们定义的add函数,因为程序一旦call malloc即可getshell,即运行到输入长度即可,但是我们的add函数中一直运行到输入内容才结束,会导致程序卡住,所以应该是:
1 | #注释头 |
5.这里需要很多调试的步骤:
(1)从最开始打算伪造fakechunk时就应该知道我们的chunk大小应该设置为0x68,因为程序跑起来是0x7f开头,拿来伪造size再合适不过,这样的chunk大小是0x70。而我们又只能用0字节溢出,所以需要将chunk设置到最大,也就是0x70-0x08。
(2)而0xf0可以改变,只要比fastbins_MAX大就行,那么对应的0x170也需要改变,同时也要改变0x120。
(3)申请fakechunk步骤中的’\0’*11也是需要通过调试算出来,方法就是gdb查看realloc_hook-0xxx附近的内存,选择合适的可以伪造size的地址,之后通过填充padding来覆盖realloc_hook和malloc_hook。
(4)onegadget相关的+16也得通过调试才能知道,这里选取的onegadget条件是[rsp+0x30]==Null。所以调试时将断点下在__libc_realloc函数上,进入__libc_realloc函数,从而一直进入到onegadget的执行代码中,如下:
先下断点让程序运行到这
再输入ni进入:
这时候就可以看rsp的值了:
这里的rsp是0x7ffca3331eb8,rsp+0x30=0x7ffca3331ee8,对应图中的值就得是0才满足条件(这里已经计算过了,+16)
▲如果从最开始进入__libc_realloc函数,进入到onegadget中之后,发现条件不满足,那么需要观察前后的值,从而决定从哪里进入__libc_realloc函数才能使得rsp满足。
因为__libc_realloc(就是realloc)函数有一堆的push和sub操作,少一个push,那么rsp就可以下挪0x08,相当于rsp+0x08,中间还有sub rsp,xxh,相当于上挪rsp。所以决定从哪里进入realloc函数决定了onegadget的成功与否。
而通过汇编代码可以看到,实际的onegaget是通过0x84724 mov rax,cs:__realloc_hook_ptr传进来,之后由于
1 | #注释头 |
rax不为0,必定跳转loc_84958:
这里才是我们选择调用进入onegadget的入口。所以之前在__libc_realloc的计算都是为了调整栈帧,不然其实如果栈帧不用调整就可以满足,那么我们可以直接将malloc_hook改成onegadget也可以直接getshell。因为__libc_malloc函数中的汇编代码也是类似的:
而且这还是一个无条件跳转。
可以看到有两个push,一个sub rsp,8;两个Pop。相当于只要在call malloc之前,我们的rsp+0x28==NULL即可满足onegadget的rsp+0x30==NULL的条件。当然,以上只是在Libc2.23下的,如果是其它版本的libc就可能不太一样。