0×00 序
好久沒有寫文章了钾菊,最近在學(xué)習(xí)pwn,這次就分享pwnable.tw上的一道pwn的解題思路嗦篱。這篇文章主要目的并不是以做這道題為目的填物,而是以這個題為主線纹腌,我主要想講的是通過這道題霎终,我們能聯(lián)想或者學(xué)會一些其他的東西滞磺,所以叫做從一道pwn題說起。如果有什么不對的地方莱褒,歡迎大家指出击困。
0×01 題目分析
題目本身難度并不大,正好適合剛接觸堆的選手練習(xí)广凸,快速掌握堆利用的知識和技巧阅茶,下面我們開始分析,首先運行程序谅海,如下圖所示脸哀,這是一道菜單題目,可以添加扭吁,刪除和打印節(jié)點撞蜂,這應(yīng)該是一個堆上的pwn題盲镶。
然后可以使用IDA分析程序流程,首先看一下主函數(shù)蝌诡,我對其中的一些函數(shù)做了重名命溉贿,這樣可以對后面的分析更簡單,更方便浦旱,讓你更清楚程序的流程宇色。
主函數(shù)分析
主函數(shù)的主要流程很清晰,讀入你輸入的選項颁湖,然后執(zhí)行對應(yīng)的函數(shù)宣蠕,我們需要主要分析一下各個功能選項(如add,delete等)的詳細流程爷狈。
Add函數(shù)分析
在分析Add函數(shù)時植影,這里我分析出來一個結(jié)構(gòu)體,創(chuàng)建一些結(jié)構(gòu)體對程序的數(shù)據(jù)結(jié)構(gòu)進行描述涎永,對你分析程序有很大的幫助思币。
00000000 st struc ; (sizeof=0x8, mappedto_5) 00000000 func dd ? 00000004 msg_ptr dd ? 00000008 st ends
Add函數(shù)首先會判斷num的值已經(jīng)申請的結(jié)構(gòu)體數(shù)量,如果小于等于5就遍歷存儲結(jié)構(gòu)體指針的數(shù)組羡微,找到一個為空的項谷饿,然后申請一個8字節(jié)大小的堆塊,然后將sub_804862B賦值給func也就是結(jié)構(gòu)體的第一個成員妈倔,然后在讀入一個size申請一個size大小的堆塊博投,將其賦值給msg_ptr指針,然后讀入一個字符串到msg_ptr (新申請的堆塊),最后將結(jié)構(gòu)體數(shù)量加1
Delete函數(shù)分析
相對于Add盯蝴,delete函數(shù)的流程就簡單的多了毅哗,讀入要刪除index,然后判斷index是否合法捧挺,然后判斷這個index對應(yīng)的結(jié)構(gòu)體指針數(shù)組的是否存在虑绵,如果存在就釋放對應(yīng)的結(jié)構(gòu)體的msg_ptr和結(jié)構(gòu)體指針
print函數(shù)分析
print函數(shù)也很簡單和delete的類似,不同的是最后調(diào)用了func指針指向的函數(shù)闽烙,根據(jù)Add函數(shù)中的賦值翅睛,這個函數(shù)應(yīng)該是sub_804862B,下面我們看一下這個函數(shù)
這個函數(shù)也很簡單黑竞,就是輸入msg_ptr指向堆塊的內(nèi)容捕发。
0×02 漏洞分析
整個程序的流程上面已經(jīng)分析清楚,主要的漏洞點很魂,就在delete函數(shù)中扎酷,在釋放指針之后沒有將其賦值為空,這樣會引起UAF和double free 漏洞遏匆》òぃ可能對新手來說骤铃,對堆上的漏洞很陌生,我這里簡單的介紹一下坷剧,如果想要詳細了解可以閱讀文章最后的參考資料惰爬,要搞懂這些首先需要對堆的結(jié)構(gòu)有一定了解,這些網(wǎng)上有很多文章惫企。
UAF(use after free)釋放重用漏洞撕瞧,漏洞原理,釋放后的指針沒有賦值為空狞尔,在其他地方再次申請到這塊內(nèi)存并改變其的內(nèi)容丛版,而再次使用到之前釋放后的指針,就會造成程序的結(jié)果變得不正確偏序。如果這個釋放的指針中有函數(shù)指針等重要數(shù)據(jù)页畦,同時在其他的地方修改成精心構(gòu)造的數(shù)據(jù),就可能泄露數(shù)據(jù)研儒,甚至劫持控制流豫缨。
double free 雙重釋放漏洞,漏洞原理端朵,對釋放的堆塊再次進行釋放好芭,當(dāng)然連續(xù)釋放一塊堆塊,libc中有檢查冲呢,這個是報double free的錯舍败,但是中間釋放一個其他堆塊,程序不會報錯崩潰敬拓,這樣就將double free轉(zhuǎn)化成uaf邻薯,因為第一塊和第三塊指向同一個地址公用一塊內(nèi)存,同樣可以構(gòu)造特殊的數(shù)據(jù)完成利用乘凸。
下面我們首先明確一下自己的目標厕诡,通過漏洞拿到shell,完成這個目標我們需要什么條件呢
system地址 劫持控制流/bin/sh等字符串 堆結(jié)構(gòu)
在講我們怎么獲取這些信息之前翰意,我先來將一些堆結(jié)構(gòu)基礎(chǔ)知識木人,如果您已經(jīng)掌握信柿,請?zhí)^此處冀偶。在linux 的內(nèi)存管理中,主要是通過bins數(shù)組和鏈表來管理各個堆塊的渔嚷,首先他們分為fast bin ,small bin ,large bin, unsorted bin进鸠,這里我主要講一下fast bin和small bin,因為篇幅有限形病,這里沒一個部分都可以拿出來單獨將一篇客年,其中的細節(jié)也很多霞幅,如果自己有能力或者有興趣,強烈建議大家去閱讀glibc的源代碼量瓜,這會對你堆利用或者發(fā)現(xiàn)新的利用姿勢有很大幫助司恳,下面先介紹一下堆塊的結(jié)構(gòu)(x86平臺)
其中fd和bk只有釋放的堆塊才有效,同時NMP是三個標志位绍傲,P代表這前一個堆塊是否被釋放扔傅,pre_size也是前一個堆塊的大小(這里的前一塊只得是連續(xù)的前一塊),堆塊頭的大小也就是8字節(jié)烫饼。
Fast bin
在glibc內(nèi)存管理中猎塞,fastbinsY 這是一個鏈表數(shù)組, 這數(shù)組的大小是10杠纵,數(shù)組的每一項都是一個單項鏈表(只使用其中的fd)荠耽,每次堆塊都是從尾部添加和摘除(后進先出),每一個鏈表里的堆塊大小一樣比藻,相鄰兩個鏈表相差8字節(jié)铝量,堆塊大小從16到80,用戶申請大小小于等于64字節(jié)银亲,這都會分配到fastbin款违,fastbin的堆塊不會發(fā)生堆塊合并,它的P位一直是1群凶。
Small bin
smallbin 屬于bins中的一部分插爹,一共有62個,和fastbin一樣是個鏈表數(shù)組请梢,數(shù)組中的每一條鏈表都是雙向鏈表赠尾,堆塊的大小小于512字節(jié),連續(xù)free的堆塊會發(fā)生堆塊合并毅弧。
本題堆分析
根據(jù)上面的介紹气嫁,我們對堆的結(jié)構(gòu)有個大概的了解,下面我們對這個題目的堆進行一個簡單的分析和調(diào)試够坐,根據(jù)上面靜態(tài)分析寸宵,我們了解到程序首先會申請一個8字節(jié)大小的堆塊來存儲函數(shù)指針和字符串指針,根據(jù)上面的知識我們知道這個堆塊是屬于fastbin的元咙,同時程序還會分配一個我們自定義大小的堆塊梯影。首先我們使用pwntools寫一個腳本,我們要add一個大小為0×50的note庶香,(pwntools是一個很好的工具甲棍,可以幫助我們快速寫出exp,堆上的操作很多都是重復(fù)的建議寫成函數(shù)赶掖,我們的gdb也可以裝peda感猛,pwndbg七扰,gef等插件來幫助我們來調(diào)試,我這里裝了pwndbg)陪白,運行腳本颈走,這里加了gdb.attach(p),我們可以很方便的程序咱士,我們附加程序后疫鹊,在malloc函數(shù)的下一個指令下斷點,這時eax寄存器里的值就是返回的分配堆地址司致,具體如下圖拆吆。
程序斷在0x0804869f,這是我們下的斷點上脂矫,我們使用x/100wx $eax-8 可以查看程序當(dāng)前分配后的堆情況包括堆塊頭的信息(8是堆塊頭的大小枣耀,系統(tǒng)分配給程序的地址是從堆塊頭之后的),具體如圖庭再。
然后我在0×08048731,下斷點捞奕,然后繼續(xù)調(diào)試,依舊使用x/100wx $eax-8 查看堆信息拄轻,這里除了大小不一樣和上面的差不多颅围,我就不具體分析,我們記錄一下當(dāng)前的堆的起始地址0x898f000恨搓,然后再add函數(shù)返回的地方0x080487D3下斷點院促,然后使用x/100wx 0x898f000 查看堆的狀態(tài),具體如下圖斧抱。
下面我們修改腳本申請和釋放幾個堆塊分析一個堆塊釋放過程常拓,熟悉了堆的結(jié)構(gòu)我們可以使用pwndbg插件的一些特殊調(diào)試命令加快我們的調(diào)試速度,比如pwndbg給我們提供heap命令可以方便的查看堆塊的分配情況辉浦,bins命令可以快速查看bins的狀態(tài)弄抬,當(dāng)然還有一些其他方便的功能。
腳本主要代碼 add(0x50,"A"*10) add(0x50,"B"*10) add(0x50,"C"*10) gdb.attach(p) delete(0) delete(1) gdb.attach(p)
運行腳本宪郊,使用heap命令可以看到當(dāng)前申請的堆塊掂恕,具體如下圖所示。
然后繼續(xù)運行弛槐,程序會斷在釋放前兩個note(note之程序中的結(jié)構(gòu))之后懊亡,這時候我們可以使用bins命令查看一個堆塊的釋放情況,具體如下圖丐黄。
結(jié)合著兩個圖斋配,我們可看到孔飒,首先我們釋放的是note0(0x9fd1000)灌闺,然后釋放的是note1(0x9fd1068),我們可以觀察到fastbins的情況艰争,這兩個都是申請8字節(jié)大小,整個堆塊的大小是16=0×10桂对,所以這兩塊會連在一起甩卓,根據(jù)釋放的順序,是從鏈表的尾部插入(這里的頭和尾是相反的)的蕉斜,而其他兩個堆塊是先放到unsortbin里暫存逾柿,以提高分配速度。這里思考一些如果我們再 add(0×8,”X”)宅此,這時程序會分配到哪里的堆塊机错。
0×03漏洞利用 system地址獲取
通過上面的分析和調(diào)試,我相信大家對堆也有了一定的了解父腕,下面我們回到之前的幾個問題弱匪,首先是泄露system地址,這個題目給了libc文件璧亮,所以我們知道leak出libc地址或者是leak一個一直函數(shù)的真實地址(比如free,malloc等等都可以)萧诫,這里我講兩種方式類獲取這個地址。
方法一:通過leak函數(shù)got表的地址獲取libc基址
首先說這個方法枝嘶,因為ELF的動態(tài)鏈接帘饶,在got.plt段會存儲真正的函數(shù)地址(在這個函數(shù)被調(diào)用之后,程序的加載過程同樣很復(fù)雜群扶,這里我們可以去閱讀《程序員的自我修養(yǎng):鏈接及刻、裝載與庫》,相信會對你有很大的幫助)竞阐,還記得上面說到的問題嗎?如果我們再 add(0×8,”X”)的時候提茁,這里叫note2吧,具體我們可以先看下面這張圖馁菜。
note2分配的地址就是(note1的頭結(jié)構(gòu)體地址)0x9fd1068茴扁,而他的字符串也是8大小,它分配的地址就是(note0的頭結(jié)構(gòu)體的地址)0x9fd1000,也就是我們可以通過輸入來控制汪疮,note0頭結(jié)構(gòu)體的值峭火,讓他完成我的leak功能,最上面的函數(shù)分析智嚷,我們知道print函數(shù)的功能就是打印字符串的值卖丸,現(xiàn)在我們可以控制字符串的值了,相當(dāng)于我們可以控制他打印的值了盏道,那么我們可以這樣如下操作稍浆。
add(0x8,p32(0x804862B)+p32(0x0804A018)) 0x0804A018這是free函數(shù)真實函數(shù)的地址,在IDA中g(shù)ot.plt段可以找到
根據(jù)堆的信息我們可以發(fā)現(xiàn)我們成功的修改了note0的字符串指針,把他修改成了之前free函數(shù)的真實地址的位置衅枫,我們在調(diào)用print(0)嫁艇,就可以將free的函數(shù)地址獲取,然后通過下面的公式就可以獲取到system函數(shù)的地址(兩個函數(shù)的相對偏移是固定的)弦撩。
libc_base = free_addr - libc.symbols['free'] system_addr = libc_base + libc.symbols['system'] 方法二:使用main_arena獲取libc基址
上面我們已經(jīng)介紹了一種leak地址的方式步咪,下面我們介紹另一種方式獲取地址的方式,那就是通過main_arena來獲取益楼,在fastbin為空時猾漫,unsortbin的fd和bk指向自身main_arena,而main_arena存儲在libc.so.6文件的.data段感凤,通過這個偏移我們就可以獲取libc的基址悯周,這里我講一下怎么找到main_arena的地址,首先使用IDA打開libc文件陪竿,然后搜索函數(shù)malloc_trim()队橙,具體如下圖所示。
為什么是這個呢萨惑,我們可以對照一下malloc.c的源代碼捐康,源代碼如下圖。
我們可以如下構(gòu)造腳本
add(0x50,"A"*10) //申請一個不是fastbin的內(nèi)存 add(0x50,"B"*10) //防止發(fā)生堆塊合并 delete(0) add(0x50,"") //note0的頭已經(jīng)被破壞了它的(func位置)也就是fd位置會為0庸蔼,所以我們要再申請同樣大小的解总,才能正確的調(diào)用print gdb.attach(p)
根據(jù)上面的調(diào)試的結(jié)果,我們可以計算libc_base和system_addr姐仅,具體如下
libc_base = leak_addr - (main_arena+48) system_addr = libc_base + libc.symbols['system'] 劫持控制流和/bin/sh字符串
至此我們獲取了system的地址了花枫,我們看一下怎么劫持控制流,這個還是比較明顯的掏膏,上面獲取地址的時候劳翰,我們已經(jīng)可以修改func指針了,我們可以把它寫成我們獲取的system地址馒疹。
((void (__cdecl *)(st *))ptr[v1]->func)(ptr[v1]);
我們再來仔細分析一下print這個函數(shù)佳簸,主要就是上面這一行代碼,調(diào)用這個函數(shù)同時颖变,將這個頭結(jié)構(gòu)體的地址當(dāng)作參數(shù)傳遞給函數(shù)生均,但是我們這個結(jié)構(gòu)體開頭的是這個函數(shù)地址,system執(zhí)行到這里會報錯腥刹,找不這個指令马胧,我們跳過這個4個字節(jié)的函數(shù)指針,我們加一個 “;” 就可以寫下一條指令了衔峰,但是這里會有一個問題佩脊,那就是我們空間有限蛙粘,除了前四個字節(jié)之外還有四個字節(jié),在去除”;”就剩三個字節(jié)威彰,想寫入”/bin/sh”這是不可能的出牧,這里有兩個技巧可以解決這個問題,具體如下:
system("$0"); system("sh");
這兩種方法都可以啟動shell抱冷,最后exp構(gòu)造如下:
add(0x8,p32(system_addr)+p32(";$0\x00")) 或 add(0x8,p32(system_addr)+p32(";sh\x00"))
0×04 總結(jié)
經(jīng)過這道題目崔列,我們對堆更加的了解梢褐,一道簡單的題目也可以讓我們學(xué)到很多旺遮,同時體會到調(diào)試的重要性,親自動手去調(diào)試和只想不動手學(xué)到的知識和深度是不同的盈咳,只有親自動手才能加深自己的印象和理解的也會更深耿眉,強烈建議大家跟著我的流程調(diào)試一下,只看這個文章鱼响,不動手是不夠的鸣剪。
來自freebuf