首先 Pwn 一直都是我畏懼接觸的主題,僅次於 Crypto,還記得第一次接觸 Pwn 時,是在 AIS3-Pre-exam-2015,當時毫無基礎概念,甚至不知道什麼是 Pwn,而第一次解出 Pwn 卻是三年後的 BreakAll-CTF,不過當時僅解出透過 Pwntootls 送資料就解得出的題目,而該篇文章也將有淺入深探討 Pwn 需要那些基礎知識,並且在下一篇文章中帶入實作。
What’s Pwn
要學習 Pwn 之前,就必須先了解 Pwn 的來龍去脈,聽說次從 Own 這個字延伸而來的,而 P 所代表的意義為 Penetration(滲透)。
假設一般程式,具有輸入功能,並且會依照輸入經過演算法等處理後輸出,如下圖。
但若程式未設想使用者可能輸入,並且尚未做限制,當使用者輸入特定的字元、特殊符號時,可能造成程式崩壞,危害可能造成:
- 改變程式的執行流程
- 改變程式的執行結果
所以僅要有可以輸入,遞送參數的程式、服務,就可能造成 Pwn 的危害,一般而言,駭客們都會用 Pwn 來取得系統權限,並建立後門取得 Shell。
Pwn 基礎知識
上訴說,只要是程式提供輸入就可能造成 Pwn 的風險,也就說不論何種語言撰寫的程式語言皆可能造成該危害,以 CTF 為例子,常見的 Pwn 題通常為 Linux 下的執行檔 ELF(Executable and Linking Format),並且撰寫語言為 C/C++。
所以還是需要一些程式基礎扶持才行,可以不會寫,但至看到 Code 要略懂略懂。
ELF 與 EXE 雷同,都是可執行檔,只是隸屬的作業系統不一樣。
- EXE 為 Windows 下的可移植性可執行文件 (PE, Portable Executable)
- ELF 為 Linux 下的可執行可連結格式(ELF, Executable and Linking Format)
列舉需要知道的知識清單:
- Program Structure
- Stack Data Structure
- Assembly Code
- Pointer
- Register
- Calling Convention
- Stack Frame
- Security Options
Program Structure
這邊簡單介紹 ELF 的結構。
Text
存放 Binary Code。
- 可讀
- 不可寫
- 可執行
Data
初始化過後的變數。
BSS
尚未初始化的變數。
Heap
動態記憶體配置相關區塊,記憶體位置由低位往高位長。
malloc() / free
Stack
存放區域變數、暫存、參數、回傳值、等資料…,記憶體位置由高位往低位長。
Stack Data structure
Stack 的資料結構採先進後出(FILO, First-in-Last-out),主要動作就是放(Push)、拿(Pop),可以想像成是品客洋芋片,想拿到最底部的,就必須先把上面的都拿出來。
Assembly Code
首先關於組合語言的部分,並沒有那麼直觀,而且又臭又長的,這邊會簡單舉幾個常見的 Assembly 的 OP Code(Operation Code)。
MOV
將 0x1 放至 RAX,相較於程式語言的指定 rax = 0x1。
範例:MOV RAX, 0x1;
PUSH
Stack 操作:將 0x1 推到 Stack。
範例: PUSH 0x1;
POP
Stack 操作:將 0x1 從 Stack 移除。
範例: POP 0x1;
LEA
取位址,將 MEM+0x8 的記憶體位置存入 RAX。
範例: LEA RAX, [MEM+0x8];
ADD/SUB
將 RAX 加/減 0x1 之後,存入 RAX。
範例: ADD/SUB RAX, 0x1;
AND/OR/XOR
且/或/互斥或 0x1 後,存入 RAX。
範例: AND/OR/XOR RAX, 0x1;
JMP
無條件轉跳至目標記憶體位置。
範例: JMP Address;
CALL
執行 Function => func。
範例: CALL func;
RET
轉跳回進入點的下一行指令,相較於使用 JMP 指令。
範例: RET;
Pointer
這邊指的 Pointer 簡單的說就是指標,在 Assembly 中的指標並沒有向 C/C++ 上的直觀,好比說 DWORD PTR [ebp+eax*4-0x34], edx
就是變數的起始位置為 0x34
,而陣列的 index 由 eax 控制。
Register
因應各個不同位元的系統,可以使用的 Register 也不相同,參照下方資料。
General Purpose Registers (GPRs)
64 bit: rax, rbx, rcx, rdx, rdi, rsi, r8 - r15
32 bit: eax, ebx, ecx, edx, edi, esi, r8d - r15d
16 bit: ax, bx, cx, dx, di, si, r8w - r15w
8 bit: ah, bh, ch, dh, al, bl, cl, dl, dil, sil, r8b - r15b
其次的是部分 Register 是有意義的,如下。
- RAX – accumulator
- RBX – base
- RCX – count
- RDX – data
- RSI – source index
- RDI – destination index
- RBP – base pointer
- RSP – stack pointer
- RFLAGS – Condition
Calling Convention
要把值當成參數送入 Function 中,需要一定的模式,其遞值方式是從「最後一個參數」開始,舉個簡單的例子。
void func(arg1, arg2)
其參數會先處理 arg2
,接著才處理 arg1
,此外 x86、x64 的參數處理方法不一,下方是 x86、x64 的差異。
x86 => Stack
x64 => rdi, rsi ,rdx ,rcx, r8, r9, Stack
x86 的遞值方式皆是透過 Stack,而 x64 可以使用六個暫存器,一旦參數超過六個,後方的參數會先 Push 到 Stack 上。
示意圖:
Stack Frame
在上頭 Register 的部分有提到,RBP 是指向 Stack 的底端,而 RSP 指向的是 Stack 的頂端,在 RBP 與 RSP 間的範圍,可以稱作是一個 Function 的區域,至於運作的,可以分成 Prologue、Epilogue 兩個部分做解釋。
Function Prologue
為 Function 的起頭,一個 Function 被呼叫時的起手式。
0x4004d7 PUSH RBP
0x4004d8 MOV RBP RSP
0x4004de SUB RSP 0x10
第一行:PUSH RBP
會將上一個 RBP POP 到 STACK 上,又稱作 Seave RBP。
第二行:接著將上一個 RBP 移動到目前 RSP 的位置(此時 RBP、RSP 指向同一個位置)。
第三行:SUB RSP 0x10
相較於 PUSH
了 0x10
次,原因在於 Stack 是由高往低位長,RSP 又是指向最頂端(最低位),此時在往下減就如同 Push,通常是用來存放區域變數空間。
Function Epilogue
為 Function 的結尾,一個 Function 結束生命周期之後的行為,簡單的說就是 Prologue 的逆向,怎麼開始,就怎麼結束。
0x400508 LEAVE
0x400509 RET
首先 LEAVE 會做兩件事情,所以將其去簡化如下。
MOV RSP, RBP
POP RBP
RET
第一行:是將 RSP 指向 RBP 的位置(Function 的底端)。
第二行:會將該 Function 的資料都拿(POP)掉。
第三行:返回到進入點的下一行指令處。
Security Options
像這樣的問題老早就存在了,至於有沒有什麼除了開發者原先就須具備資安意識以外,還是有機制上防範方法,如下。
- RELRO
- Stack Canary
- NX
- ASLR(on process)
- PIE(on executable)
RELRO
RELRO 全名為 RELocation Read Only,共有三種保護模式 No
/ Partial
/ Full
,其對應的影響概括 Link
、GOT
。
- No RELRO => Link map 與 GOT 可寫。
- Partial RELRO => Link map 不可寫; GOT 可寫。
- Full RELRO => Link map 與 GOT 都不可寫。
主要適用於防範 Lazy Binding 所造成的延伸問題。
關閉方法:
gcc $Source -o $Binary // 默认情况下,是Partial RELRO
gcc -z norelro $Source -o $Binary //No RELRO
gcc -z lazy $Source -o $Binary //Partial RELRO
gcc -z now $Source -o $Binary //Full RELRO
Stack Canary
Stack Canary 的機制就是在 RBP 之前加上一個 Radom 值,若該值被修改,則會產生 Stack Smashing Detected 事件,並直接結束程式執行。
主要防範 Buffer Over Flow 造成的延伸安全問題。
關閉方法:
gcc $Source -fno-stack-protector -o $Binary
NX
全名為 No eXecute,又稱 DEP(Data Execution Prevention),兩句話就可以解釋一切了。
- 可以寫的,不能執行。
- 可以執行的,不可以寫。
主要防範 Shell Code 造成的安全問題。
關閉方法:
gcc $Source -zexecstack -o $Binary
ASLR
位址空間組態隨機化(ASLR, Address space layout randomization),主要是作業系統上對於 Process 的防護機制,會使每次執行 Stack、Heap、library 位置會不一樣,進而達到一些保護效果。
關閉方法:
# echo 0 > /proc/sys/kernel/randomize_va_space
PIE
PIE 全名為 Position-Independent Executable,主要影響的範圍為 Program Structure 的 Data、Code 段,讓該地方的記憶體位置隨機化,來增加被利用的難度。
關閉方法:
gcc $Source -no-pie -o $Binary
基礎的 Pwn 題
如常見的 Buffer Over Flow,在 CTF 中是很常見的題型,範例程式碼如下。
#include <stdio.h>
void func(arg1, arg2){
char buffer[16];
gets(buffer);
printf("Hello, %s", buffer);
}
如果夠敏銳一定可以察覺到 buffer
僅有 16 個 Bytes 的長度,而 gets
並沒有限制使用者輸入的長度,當使用者輸入大於 16 個字元時,可能就會造成程式錯誤。
這樣說似乎有些抽象,假設 Stack 狀態如下。
當使用者輸入超過 16 個字元時的狀況,例如大量的 A
,並且觀察 Stack 的狀態,可以發現所有資料都被取代成 A
了。
其中比較關鍵的地方是,原先為 ret address
的藍色區塊,ret address
紀錄的是一個進入該 Function 前的下一行指令的記憶體位置。
所以只要可以很精確地把 ret address
覆蓋成其他 Function 的記憶體位置,即可在該 Func Function 結束後跳至由使用者控制的目的地。