[資訊安全] 從毫無基礎開始 Pwn – 概念

Photo by ipet photo on Unsplash

首先 Pwn 一直都是我畏懼接觸的主題,僅次於 Crypto,還記得第一次接觸 Pwn 時,是在 AIS3-Pre-exam-2015,當時毫無基礎概念,甚至不知道什麼是 Pwn,而第一次解出 Pwn 卻是三年後的 BreakAll-CTF,不過當時僅解出透過 Pwntootls 送資料就解得出的題目,而該篇文章也將有淺入深探討 Pwn 需要那些基礎知識,並且在下一篇文章中帶入實作。

What’s Pwn

要學習 Pwn 之前,就必須先了解 Pwn 的來龍去脈,聽說次從 Own 這個字延伸而來的,而 P 所代表的意義為 Penetration(滲透)。

假設一般程式,具有輸入功能,並且會依照輸入經過演算法等處理後輸出,如下圖。

但若程式未設想使用者可能輸入,並且尚未做限制,當使用者輸入特定的字元、特殊符號時,可能造成程式崩壞,危害可能造成:

  1. 改變程式的執行流程
  2. 改變程式的執行結果

所以僅要有可以輸入,遞送參數的程式、服務,就可能造成 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)

列舉需要知道的知識清單:

  1. Program Structure
  2. Stack Data Structure
  3. Assembly Code
  4. Pointer
  5. Register
  6. Calling Convention
  7. Stack Frame
  8. Security Options

Program Structure

這邊簡單介紹 ELF 的結構。

Text

存放 Binary Code。

  1. 可讀
  2. 不可寫
  3. 可執行

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 相較於 PUSH0x10 次,原因在於 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,其對應的影響概括 LinkGOT

  • 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 結束後跳至由使用者控制的目的地。

參考資料

  1. linux程序的常用保护机制
  2. 緩衝區溢位攻擊之一(Buffer Overflow)
  3. 汇编语言入门教程

MksYi

透過網路分享知識的學習者。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

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