個人PWN入坑常見方法總結
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"#獲取引數"/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如下。