2021 Advanced CyberSecurity Day Note - PWN


PWN Intro

What is PWN?

控制程式流程進而觸發攻擊

正常程式流程 Start -> Input <-> Output -> End

Input:使用者輸入、操作
Output:反應 Input 所產生的動作(運算、輸出等)

不正常的程式流程 Start -> Input <-> Output -> Segmentation Fault

HOW TO PWN

尋找漏洞,進一步利用

方法:

  1. Fuzz 模糊測試:自動產生隨機輸入來尋找漏洞
  2. 看原始碼、組合語言找漏洞

目標: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指向的位置,接著 popreturn address

Security Options

checksec ./exe 查看保護機制

RELRO

RELocation Read Only

  • 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
    switch

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

  1. gets 沒有限制輸入長度
    #include <stdio.h>
    int main(){
     char buffer[8];
     gets(buffer);
     puts(buffer);
     return 0;
    }
    
  2. 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
[題目]Luck write-up

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

  1. ret address of gadget 1
  2. pop address of gadget 1
  3. rsp 移動到 0xabcd
  4. pop rax -> rax = 0xabcd
  5. ret 到 address of gadget 2
  6. pop address of gadget 2
  7. pop rbx -> rbx = 0x1234
  8. 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

  1. %52c%?$hhn 0x34 = 52 ? 放 address
  2. %222c%?hhn ((256-0x34)+0x12)%256

若 payload 在 stack 上,可以把特定 address 寫在 stack 上來寫入

Stack Migration

當可輸入的 ROP chain 長度不足時,擴展輸入的方法
概念:將 ROP chain 寫在不同位置,透過移動 stack 來執⾏

假設能輸入的 gadget 長度只有一個

  1. 直接放 one gadget
  2. 跳到其他 function
  3. ⽤ stack migration 創造新空間

Stack Migration

  1. 把 ROP 先寫在 buffer 上
  2. 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 長度


Author: Gunjyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Gunjyo !
  TOC