Heap Exploitation-Off by one
Heap Exploitation - Off By One
原理
off-by-one指单字节缓冲区溢出,是一种特殊的栈溢出漏洞,指向缓冲区写入时,写入的字节数超过缓冲区本身申请的字节数并只越界一个字节。这种漏洞的产生往往与边界验证不严和字符串操作有关。off-by-one可以基于各种缓冲区,栈、堆。bss段等等都可以,CTF中常见的是堆中的off-by-one。
循环边界不严谨
int my_gets(char *ptr,int size) |
创建了两个16字节的堆,但是在my_gets函数执行时,循环多了一次,会溢出一个字节。
字符串长度判断有误
int main(void) |
将字符串放到chunk1中,忽略结束符\x00,strcpy会将结束符也存在堆块中,溢出一个字节。
利用思路
溢出字节为可控制任意字节,通过修改大小造成结构之间的重叠,从而泄露其他块的数据或者是覆盖其他块数据,从而达到读写的目的,也可以使用NULL字节溢出的方法
溢出字节为NULL字节,在size为0x100的时候溢出NULL字节就可以使得pre_in_use(就是chunk中的P位)被覆盖,这样前一个块会被认为是空闲块
- 可以使用unlink来进行处理(unlink挖个坑,以后补)
- 由于前一个块被改为free,那么prev_size就可以被伪造,使得块之间发生重叠。
方法的实现前提是unlink时没有按照prev_size找到的块的大小与prev_size一致来检查。
防护
在libc-2.29之后,针对利用prev_size的方法加入了检测机制。
/* consolidate backward */ |
可以绕过,不太好懂,可以通过例题Balsn_CTF_2019-PlainText,先挖个坑
示例
Asis CTF 2016 b00ks
题目来源:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/off_by_one/Asis_2016_b00ks
代码
首先分析代码
__int64 __fastcall main(int a1, char **a2, char **a3) |
在updateAuthor函数里,调用了sub_9F5函数
__int64 __fastcall sub_9F5(_BYTE *a1, int a2) |
这个函数没有对循环边界做限制,所以会判断变量i等于32,实际上执行了33次。off_202018存放的是作者名。
后面的createBook函数
*((_DWORD *)v3 + 6) = v1; |
创建了一个图书的结构体,将图书的指针放到off_202010里面。
struct book |
而之前溢出的那个函数将作者名存放到了off_202018,这俩挨着
那么循环多出来的一次会将\x00写到off_202010也就是结构体的第一个字节,会将创建的第一本图书的低字节覆盖
调试
定位作者名
代码跑起来,输入长度为32的字符串,然后ctrl+c进调试
定位刚才输入的字符串,直接search
或者,vmmap看代码段起始位置
加上偏移0x202018,0x555555554000 + 0x202018 = 0x555555602018
找到地址就能直接看到字符串了
圈住的这个就是溢出的
定位图书结构体
创建两本书,图书1的书名大小为128,内容大小尽量大一点(如140),图书2的书名和内容大小为135168
还是基址加偏移,0x555555400000 + 0x202010 = 0x555555602010
能看到两个指针,book1的结构体指针为0x555555603770,book2的结构体指针为0x5555556037a0,那么book2相对于book1的偏移为0x30,也能看到刚才输入的作者名。
而由于作者名和图书结构体相连,所以实际上结构体指针的低位将原来的\x00覆盖了,这时候如果输出作者名,那么图书结构体的低位也会被输出。这个低位是图书结构体指针的位置,那么图书结构体指针也会被输出。打印图书信息就会泄露book1的地址。
上面找到了偏移,通过偏移就能够找到book2。
覆盖结构体指针
接下来尝试覆盖结构体指针,查看book1内容
0x555555603770存放的book id,0x555555603778存放的是book name,0x555555603780存放的是book description。
上半部分是book1结构体,下半部分是book2结构体。
作者名读入的\x00可以覆盖结构体指针的低位,程序后还有修改作者名的功能,那么可以直接通过修改作者名将结构体指针的低位覆盖为0,那么0x555555603770就会被修改为0x555555603700。验证一下:
修改成功。
伪造结构体
运用change_author_name的功能,可以重新溢出一个字节‘\00’,然后global_struct_array中的第一个元素的地址改变,可以通过为book1_description申请大一点的空间,来使的被修改后 global_struct_array中的第一个元素的地址指向book1_description内的地址,然后我们可以在相应的地址重新伪造一个book1_struct。因为有print description以及edit description的功能,所以我们通过伪造book1_struct的description使其指向任意地址,通过打印或者edit来实现任意地址的读写。
接下来要在book1_description中伪造一个 book1_struct,让book1_struct指针指向book1_description中;然后在book1_description中伪造一个book1_struct,使得其中的book1_description_ptr指向book2_description_ptr;通过先后修改book1_description和 book2_description,从而实现任意地址写任意内容的功能。
所以在之前创建book_struct时,book1 的 description 的大小要尽量大一点 ,是为了保证当单字节溢出后 book1_struct 指针落在 book1 的 description 中,从而对其可控。book2 的 description 的大小为 0x21000(135168),这样会通过 mmap () 函数去分配堆空间,而该堆地址与 libc 的基址相关,这样通过泄露该堆地址可以计算出 libc 的基址。
伪造的结构体
{ |
这里直接修改数据,构造结构体
set {unsigned long long}0x555555603710 = 0x00005555556037b0
set {unsigned long long}0x555555603708 = 0x00005555556037a8
set *0x555555603700 = 0x1
构造好后继续运行看执行情况
可以看到将book2_name和book2_desc打印出来了
计算libc基地址、freehook、onegadget
vmmap寻找libc,可读可执行的就就是要找的
选择book2_name_addr或book2_des_addr其中一个计算偏移,得到基地址就可以利用pwntools查找函数了。
由于该程序启用了 FULL RELRO
保护措施,无法对 GOT
进行改写,但是可以改写__free_hook
或__malloc_hook
,来实现劫持程序流。
在调用malloc或者free的时候,如果 malloc_hook 和free_hook的值存在,则会调用malloc_hook或者free_hook指向的地址,假设在使用one_gadget的时候满足one_gadget的调用条件,当overwrite malloc_hook和free_hook的时候,便可以getshell,执行malloc的时候,其参数是size大小,所以overwrite malloc_hook的时候使用one_gadget的地址可以getshell。执行free的时候,可以将__free_hook的值overwrite为system的地址,通过释放(/bin/sh\x00)的chunk,可以达到system(/bin/sh)来getshell
找到gadgets后,可以先向伪造的结构体的desc中写入free_hook,然后向book2的desc中写入onegadget。而伪造的结构体指向book2的name,所以当调用删除函数时,会首先调用free释放book2指向的地址,原本的free(book2_name_ptr)会变为system(binsh_addr)。
通过
payload = p64(binsh_addr) + p64(free_hook) |
将改写fake_book1中的description的内容,因为description指向的是book2_name,也就是说 payload = p64(binsh_addr) + p64(free_hook)将覆盖book2_name以及book2_description。
payload = p64(system) |
这里是edit book2_description,因为book2_description被覆盖为free_hook()的地址信息,因此此操作时将free_hook指向system。
EXP
from pwn import * |
参考
CTF竞赛权威指南-pwn篇