0×01 概述
本文介紹個人學習pwn過程中的一些總結,包括常用方法,網上諸多教程雖然有提供完整的exp,但並未解釋exp為什么是這樣的,比如shellcode寫到哪里去了(這關系到跳轉地址),ROP鏈怎么選擇的。對於pwn,本人也是新手,其中有總結錯誤的,歡迎各位大佬指正。
文中用到的測試程序都在:https://github.com/silience/pwn
0×02 PWN常用的基本知識
首先拿到一個PWN程序,可以先使用file命令,判斷是32位還是64位。
可以使用objdump讀取plt和got表,plt和got網上都有詳細的介紹,再此不再贅述。
這邊要提一下數據在寄存器中的存放順序,這個在格式化字符串漏洞中要格外注意,特別是64位,32位的先后順序是eax->edx->ecx->ebx,64位的先后順序是rdi->rsi->rdx->rcx->r8->r9。
剛開始學習的時候,個人經常把pop和push經常搞反,因此在此把這兩個指令的介紹說一下:push [reg]/[num] 是將reg寄存器中的值或是數字num壓入堆棧中,而pop [reg]是將堆棧棧頂的值彈出到reg寄存器中,並將這個值從堆棧中刪去。
有時候要查看寄存器中的值,可以用到如下命令:
print $esp:打印esp的值
x/10x $esp:打印出10個從esp開始的值
x/10x $esp-4:打印出10個從偏移4開始的值
x/10gx $esp:以64位格式打印
下面先使用hello練練手,首先使用IDA的F5大法可以看到內部有個getshell函數,可以直接跳轉到該函數getshell。
使用工具pade可以很方便的計算出偏移量,pattern create 100。
pattern offset 0×41284141,計算出偏移量為22。
查看匯編代碼,獲取getshell的地址,也就是要跳轉的地址。
最后得到完整的exp如下。
0×03 shellcode
生成方式
1、在shellcode數據庫網站找一個shellcode,http://shell-storm.org/shellcode/
2、使用kali的msfvenon生成shellcode,如命令msfvenon -p linux/x86/exec CMD=/bin/sh -f python
3、使用pwntools自帶的函數如asm(shellcraft.sh())
但有時候不知道shellcode寫到哪里去了,在回答這個問題前,要提一下bss段、data段、text段、堆(heap)、棧(stack)的一些區別。
1、bss段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域,bss段屬於靜態內存分配。
2、data段:數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域,數據段屬於靜態內存分配。
3、text段:代碼段(code segment/text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域通常屬於只讀(某些架構也允許代碼段為可寫,即允許修改程序)。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
4、堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。
5、棧(stack):棧又稱堆棧,是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束后,函數的返回值也會被存放回棧中。由於棧的先進先出(FIFO)特點,所以棧特別方便用來保存/恢復調用現場。
下面以ret2shellcode,同樣使用IDA看下代碼,很明顯,shellcode寫入到bss段。
使用命令readelf -S ret2shellcode查看獲取bss段地址為0x0804a040。
還必須保證bss段有可執行權限,shellcode才能運行,可用gdb調試的vmmap命令查看,發現bss段可讀可寫可執行。范圍是0x0804a000到0x0804b000,bss段地址0x0804a040在這個區間,且必須保證shellcode長度不超過這個區間即可,但到目前為止,shellcode具體地址依然不知道。
這時可以去調用它的函數strncpy前查看匯編代碼,一般通過push或者move進行參數傳遞,參數傳遞順序是從右到左,可以定位到shellcode地址0x804a80。
最后exp如下。
shellcode地址的位置其實是一個坑。因為正常的思維是使用gdb調試目標程序,然后查看內存來確定shellcode的位置。但當你真的執行exp的時候你會發現shellcode壓根就不在這個地址上!這是為什么呢?原因是gdb的調試環境會影響buf在內存中的位置,雖然我們關閉了ASLR,但這只能保證buf的地址在gdb的調試環境中不變,但當我們直接執行的時候,buf的位置會固定在別的地址上。怎么解決這個問題呢?有兩種方法,一種是 開啟core dump這個功能,另外一種是使用GDB的attach功能。
可以使用level1練手,有時checksec顯示PIE關閉。
其實用ldd會發現,地址依然會隨機變化。
可使用命令echo 0 > /proc/sys/kernel/randomize_va_space關掉整個linux系統的ASLR保護,再進行調試;開啟core dump這個功能,開啟之后,當出現內存錯誤的時候,系統會生成一個core dump文件在tmp目錄下。然后我們再用gdb查看這個core文件就可以獲取到buf真正的地址了。
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
0×04 格式化字符串漏洞
這要講一下字節序。
大端就是:存儲最高有效字節在最小的地址(網絡傳輸文件存儲常用)。
小端就是:存儲最低有效字節在最小的地址(計算機內部存儲)。
幫助記憶的法子:小端就是存儲先存最小有效字節,大端就是先存最大有效字節。
printf函數的格式化字符串常見的有 %d,%f,%c,%s(用於讀取內存數據),%x(輸出16進制數,前面沒有0x),%p(輸出16進制數,前面帶有0x);%n是一個不經常用到的格式符,它的作用是把前面已經打印的長度寫入某個內存地址,用於修改內存,除了%n,還有%hn,%hhn,%lln,分別為寫入目標空間4字節,2字節,1字節,8字節。
去讀內存,假如當偏移量為5時:
./a.out "`printf "\0x78\x56\x34\x12"`.%08x.%08x.%08x.%08x.%08s"
或者直接使用讀取地址0×12345678的內容:
./a.out "`printf "\0x78\x56\x34\x12"`.%5\$s"
比如要將跳轉地址0x0804a048改data為0×12345678,可使用%hhn;因為使用的是小段序,高字節保存在高地址。
所以poc如下,偏移量要從6開始,應為\x4b\xa0\x04\x08保存在偏移地址6。
./a.out "`printf "%18c%6\$hhn"."%34c%7\$hhn"." %34c%8\$hhn "."%34c%9\$hhn "."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"
但是為什么依次是%18c、%34c、%34c、%34c;第一個是0×12,很簡單,變成十進制就是18;第二個是0×34,十進制52,第二次總寫入數包括第一次的,即18+34=52;后面兩次依此類推。
實際使用中,可以直接使用pwntools的函數fmtstr_payload,或者fmt_str(offset,size,addr,target)(其中offset表示要覆蓋的地址最初的偏移,size表示機器字長,addr表示將要覆蓋的地址,target表示我們要覆蓋為的目的變量值)直接覆蓋。
可以以湖湘杯2017的pwn200進行練手,使用IDA,發現很明顯的格式化字符串漏洞。
首先輸入AAAA.%X.%X.%X.%X.%X.%X.%X.%X,可以發現在第七個%X輸出41414141,A的ascii碼是41(有時是61616161,a的ascii碼61,因為程序把輸入轉換成小寫),可知偏移量是7,首先使用%s獲取puts函數的真實地址,然后計算出system的真實地址,后面再利用函數fmtstr_payload,將atoi的地址替換為system地址,當執行atoi時,就會這些system函數,從而獲取shell。
0×05 libc
libc中提供了大量的函數,gdb調試時可直接使用如下命令獲取地址,如果未提供,可以去網站http://libcdb.com/下載對應的文件。
可依次執行以下命令,快速getshell。
print system#獲取system函數地址。
print __libc_start_main
find 0xb7e393f0, +2200000, "/bin/sh"
以level2為例,exp如下,利用鏈:偏移數據+system地址+返回地址+參數地址,本例是通過system獲得shell,不需要做其他操作,所以返回地址可以隨便寫。
0×06 ROP
Rop鏈順序,首先是跳轉地址,比如要調用的內置函數write泄露出system地址,然后是返回地址(如果泄露的地址要重復使用,則返回地址是write地址或者它前面的地址),再就是傳遞的參數是從右往左入棧。
以ret2syscall為例,rop鏈構造如下:因為要調用execve(“/bin/sh”,NULL,NULL),該系統函數的調用號為0xb,因此首先要將0xb給eax寄存器,可使用ROPgadget –binary ret2syscall –only “pop|ret” | grep “eax”進行查找。
因為函數execve有三個參數,接着可以使用命令。
ROPgadget –binary ret2syscall –only “pop|pop|pop|ret” | grep “ebx”,不能選包含esi(esi是下條指令執行地址)或者ebp(棧基址寄存器)。
使ROPgadget –binary ret2syscall –string ‘/bin/sh’,可查找參數/bin/sh 的地址。
最后再跳轉到int 0×80的地址就可執行對應的系統調用,也就是execve函數,可通過ROPgadget –binary ret2syscall –only ‘int’,找int 0×80的地址。
最后完整的exp如下。
0×07 參考地址
https://www.cnblogs.com/yanghong-hnu/p/4705755.html
https://www.cnblogs.com/gremount/p/8830707.html
https://blog.csdn.net/m0_37809075/article/details/81269697
https://blog.csdn.net/qq_43394612/article/details/84900668
https://blog.csdn.net/qq_38204481/article/details/80329885
https://www.freebuf.com/vuls/161116.html
https://blog.csdn.net/weixin_40850881/article/details/80216764
*本文作者:freebuf01,轉載請注明來自FreeBuf.COM