PWN Intro
What is PWN?
控制程式流程進而觸發攻擊
正常程式流程 Start -> Input <-> Output -> End
Input:使用者輸入、操作
Output:反應 Input 所產生的動作(運算、輸出等)
不正常的程式流程 Start -> Input <-> Output -> Segmentation Fault
HOW TO PWN
尋找漏洞,進一步利用
方法:
- Fuzz 模糊測試:自動產生隨機輸入來尋找漏洞
- 看原始碼、組合語言找漏洞
目標:Start -> Input <-> Output -> Shell
Program Structure
Text - 程式碼 Binary
r-x 可讀 不可寫 可執行
Data - 已初始化的全域變數
Ex. 字串、數字
BSS - 未初始化的全域變數
Heap - 動態記憶體空間
從低位往高位長
c 常用的 malloc()
/free()
拿出來的空間來源為 Heap
Stack - 存放暫存資料
Stack:FILO 先進後出
暫存資料 Ex.區域變數、return address、參數、回傳值
從高位往低位長
stack top 存在 rop
Stack 跟 Heap 不會碰到
區域變數 Local Variable
先宣告的先存
int main(){
int a = 0x123;
int b = 0x456;
...
}
rsp 表示最低位
一個 block 8 個 byte
Return address
Q:Call function 時,怎麼知道執行完要回到哪裡?
call function前,將 return address 存進 stack
return 時,回到 stack 中所存的位址
void process(){
...
return;
}
int main(){
int a = 8;
process();
a = a+1;
...
return 0;
}
process()
執行完後...
會還原 ,跳過去return address
指向的位置,接著 pop
掉return address
Security Options
checksec ./exe
查看保護機制
RELRO
RELocation Read Only
Static linking 把所有的函式實作都包在一起
好處:執行方便
壞處:檔案太肥
Dynamic linking 的程式在執行過程中,有些 library 的函式到結束都不會執行到。
ELF 採取 Lazy binding 的機制,在第一次 call library 函式時,才會去尋找函式真正的位置進行 binding
實現 Lazy Binding
GOT & PLT
GOT(Global Offset Table):函式指標陣列,存了其他 library 中 function address。
因為 Lazy binding 的機制,一開始只會填上一段 .plt
的 code
第一次執行時,plt
會呼叫_dl_fixup()
,才會去真正的 function 並填入 GOT。
第二次執行時,直接透過 GOT 找到 function address
- call
read()
- Read 的 GOT 先去 GOT 表查找有沒有已經存放 read address
- 沒有 -> 進入
resolve()
來找read()
真正的位址,然後記錄到 GOT 表,並 return 回 call read (執行) - 有 -> 直接 return 回 call read (執行)
- No RELRO - link map 和 GOT 都可寫
- Partial RELRO - link map 不可寫,GOT 可寫(可利用來PWN)
- Full ReLRO - link map 和 GOT 都不可寫
Stack Canary
stack 上的柵欄
在 rbp 之前塞一個 random 值,ret 之前檢查是否相同,不同的話就會abort
沒有 canary 的話,發生 buffer overflow 時就可以一直往下寫(return address等)
有 canary 的話,它發現後就會直接關掉程式
繞過
- 想辦法事先取得 Canary 的值
- 只蓋掉 canary 前的值 / 直接蓋掉 canary 後的值
NX(No eXecute)
又稱 DEP(Data Execution Prevention)
可寫的不可執行,可執行的不可寫
可寫可執行很危險(把程式碼寫到這個空間,跳過去直接執行)
PIE(Position Independent Executable)
開啟時, data 段以及 code 段位址隨機化
關閉時, data 段以及 code 段位址固定
攻擊時有時須跳到程式中的某個區段,PIE打開的話不能跳
ASLR(Address Space Layout Randomization)
記憶體位址隨機化
每次執行時,stack、heap、library 位置都不一樣
ASLR為系統設定,不是程式設定
Tools
nc/ncat
pwn 題目常用的遠端連線工具
使用 ncat 將程式在遠端架起來,接著使用 nc 連線
% ncat -vc $binary -kl $ip $port
% nc $ip $port
gdb
% j *0x4896aa
跳到某個位址(jmp)
% x/10gx 0x400686
秀出某位址的值
% set $rax=0x5
設定某個位址/暫存器的值
checksec 查看程式的安全性保護
%checksec $library
gdb - vmmap
查看目前程式的記憶體分布、rwn 權限設定% vmmap
b main (breakpoint)
r
vmmap
pwntools
用來和遠端程式互動的 python 套件
% pip install pwntools
from pwn import *
# connect to server(二擇一)
r = process('./add') # localhost binary 本地端程式
r = remote('140.113.0.3' , 8080) # remote binary(IP,port) 遠端程式
s = r.recvuntil(':') # receive from binary until ':'
print '1: ' + s
s = r.recvuntil('?')
print('$' + s + '$') #辨識 s 取到那些
r.sendline('3 5') # send to server (相當於在鍵盤上打 3 5 )
r.interactive() # switch to interation mode
readelf
分析 elf / binary 的工具% readelf -a $libc | less
% readelf -a $libc | grep 'printf'
找出所有有printf
的function
ROPgadget
列出 binary 中可以使用的 ROP gadget% ROPgadget --binary $ binary
radare2
動態、靜態分析都可以⽤的⼯具
講師的使用教學
% git clone https://github.com/radare/radare2.git
% sudo ./radare2/sys/install.sh
% r2 $binary
q # 退出
? # 顯示指令
p? # 顯示 p 開頭的指令
aa #分析
afl # 列出所有 function
afn $new_name $old_name #修改 function 名
afvn $new_name $old_name #修改區域變數名
s $function_name #移動到其他位置
#Ex. s main
s $address #移動到其他位置
#Ex. s 0x0000610
pd $n_line #印出從現在位置開始 n 行的 asm
pdf # 印出現在位置的整個 function 內容
Mode
- CLI Mode
- Hex Mode
- Visual Mode
Buffer Overflow
- 輸入時沒有控制輸入長度,導致記憶體空間被輸入覆蓋掉
- 通常發⽣在 char 陣列 (字串) 的輸入
示例
#include <stdio.h>
int main(){
char buffer[8];
gets(buffer); // Input
puts(buffer); // Output
return 0;
}
compile and execute
% gcc test.c -fno-stack-protector -o test
% ./test
hello
hello
Q:若輸入很大字串?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
輸入:gets / read
- gets 沒有限制輸入長度
#include <stdio.h> int main(){ char buffer[8]; gets(buffer); puts(buffer); return 0; }
- read 有限制最大輸入長度,可 overflow ⼤⼩為最⼤輸入長度與 buffer 長度之間
#include <stdio.h> int main(){ char buffer[8]; read(0, buffer, 16); //只能 overflow 8 bytes(最大長度16-buffer長度8) puts(buffer); return 0; }
應用
stack 上
- local variable
- saved rbp -> stack migration
- return address -> ret2 series
local variable
#include <stdio.h>
#include <stdlib.h>
int main(){
int a = 8;
char buffer[8];
gets(buffer);
if(a == 3){
system(“/bin/sh”);
}
return 0;
}
計算 offset (從開始寫的位置到目標位置間的長度)
- 先隨意輸入來確定 buffer 位置
- 計算 buffer 位置和⽬標位置距離多遠
0x7fffffffe7a8:buffer 位置
0x7fffffffe798:目標位置offset = 0x7fffffffe7a8 - 0x7fffffffe798 = 0x10
ret2code
透過 Buffer Overflow 改變 return address
將 return address 改到 code 中任意處
須關閉 PIE
#include <stdio.h>
#include <stdlib.h>
void shell(){
system(“/bin/sh”);
}
int main(){
char buffer[8];
gets(buffer);
return 0;
}
把 return address 覆蓋成 shell 的 address
找 shell 的 address
- shell:
% objdump -M intel -d test | less
- gdb:
gdb-peda$ p shell
- r2:
[0x000005d0]> afl~shell
(~
->grep)
ret2sc
- 透過 Buffer Overflow 改變 return address
- 將 return address 改到 ⾃⼰寫的 shell code 處並執⾏
須關閉 NX
寫 shell code
- 選有 rwx 處
- 選中間部分 (前後可能有其他用處)
Problem
- 漏洞沒利用好
- 寫的地方蓋到重要東西
- shell code 寫壞
ret2libc
GOT Hikcaking
透過改寫 GOT 使得呼叫該函式時,跳到指定位置
不能是 Full RELRO (GOT 不能動)
把 puts libc
的地方覆蓋成我們要的
ret2libc
通常 libc 中的函式並不會全部⽤到,但其中包含許多好⽤的函式(Ex.system()
,execve()
)
因為 ASLR,libc 的位址會有 libc base(隨機值),必須有 libc base 才可以使⽤ libc 函式
libc base 在哪?
因為是隨機值,每次都不一樣,只能在執行過程中 leak 出來
尋找執⾏中有⽤到 libc 的位址
- GOT 的內容
- stack 上的殘渣
libc base 計算
puts_got_value = libc_base + puts_libc
libc_base = puts_got_value - puts_libc
system_got_value = libc_base + system_libc
取得puts_libc
% readelf -a /libc.so.6 | grep puts
one_gadget
在 libc 中可以一次拿到 shell address
必須符合規定限制 && 有 libc base
usage: % one_gadget /libc.so.6
ROP(Return Oriented Programming)
串接 Gadget 來控制流程
Gadget - 結尾是 ret
的程式碼片段
找適合的 gadget% ROPgadget --binary ./test | less
% ROPgadget --binary ./test | grep '*ret'
好用的 gadget
leave ; ret
pop * ; ret
ROP chain
把 gadget 串起來
Step
- ret address of gadget 1
- pop address of gadget 1
- rsp 移動到 0xabcd
- pop rax -> rax = 0xabcd
- ret 到 address of gadget 2
- pop address of gadget 2
- pop rbx -> rbx = 0x1234
- ret …….依此循環
利用 ROP 攻擊 - 串接 syscall
x86_64_syscall_table
利用 ROP chain,依照 syscall table 把 rax , rdi , rsi , rdx 等設定好後 call syscall
Syscall - execve(“/bin/sh”)
設定
rax = 0x3b
rdi = address of ‘/bin/sh’
(rdi -> buffer -> ‘/bin/sh’)
rsi = 0
rdx = 0
設定完後 call syscall 就可以了
Format String
printf 使用上可以操作的漏洞,可以做到任意讀寫
#include <stdio.h>
int main(){
char buffer[8];
scanf(“%s”, buffer); // Input
printf(buffer); // Output
return 0;
}
其中 scanf 有指定 format(%s
),但 printf 沒有指定 format
若是輸入 format,input 會作為 format 被輸出
% ./test
%p,%p,%p
0x1,0x7f10e7dc3790,0xa
printf 參數 %n$p
指定第 n 個參數
讀取
%p
:印出 register / stack 上存的值%s
: 將 register / stack 上的值作為位址,印出該位址所
存的值
%p 印出存的值
讀取順序:rsi -> rdx -> rcx -> r8 -> r9 -> *rsp -> *(rsp+0x8)
前幾個在 register 上,第 6 個開始在 stack 上(通常要的是 stack上的)
Ex. %11$p
會印出 *(rsp+0x28)
的值
%s 將存的值作為指標來讀取
若 payload 在 stack 上,可以把特定 address 寫在 stack 上來讀取
寫入
使用 %n
來將 printf 輸出過的字元數寫到指定的位置%hn
2 bytes%hhn
1 bytes (字元數 % 256)
%c
指定 %n
寫入大小
Ex. input:0x1234
%52c%?$hhn
0x34 = 52 ? 放 address%222c%?hhn
((256-0x34)+0x12)%256
若 payload 在 stack 上,可以把特定 address 寫在 stack 上來寫入
Stack Migration
當可輸入的 ROP chain 長度不足時,擴展輸入的方法
概念:將 ROP chain 寫在不同位置,透過移動 stack 來執⾏
假設能輸入的 gadget 長度只有一個
- 直接放 one gadget
- 跳到其他 function
- ⽤ stack migration 創造新空間
Stack Migration
- 把 ROP 先寫在 buffer 上
- ret 前,把 stack (rsp) 移到 buffer 上面
尋找 buffer:開 gdb vmmap
leave
leave
ret
leave 做的動作
mov rsp, rbp # 把 rsp 跳到 rbp 上面 (rbp 可控)
pop rbp
Simple Migration - 可控制輸入長度時
在 ROP chain 中 call input func,把新的 ROP chain 寫到 buffer 裡面
第一次輸入(stack)
call input func,把 input 儲存點(rbp)設為 buffer 1 的 address
第二次輸入(buffer1)
from ROP chain I 的 input func
[註]如果不需要再 migration,buffer2 可以隨便填(因為不會用到 leave
去移動 stack 到 buffer2 上),也不用重新 call input func
Fixed Size Migration - 不能控制輸入長度時
直接用原本的 read 來輸入
一次可以輸入的 ROP 長度為 payload 長度