Pwn-Stack based attack
Author:堇姬
我用的debug環境
- Pwndbg+pwngdb
- r2
- IDA or Ghidra
- local debug
1
2
3
4
5tmux
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(r)
What is Pwn?
二進制程式檔滲透,
其實就是找尋程式中的漏洞,或是取得伺服器權限,使用伺服器 shell 偷取檔案、修改資料等等。
- 看原始碼、組合語言找漏洞
- 透過input、output來get shell等…
pwn target
很多東西都可以打
VM、IoT、linux、windows…
memory
1 | +----------------------+ <-- High Memory Address |
Text
- 程式碼
- r-x(不可寫)
Data
- 已給值全域變數
BSS
- 未給值全域變數
Heap
- 動態記憶體空間
- malloc()、free()
- 低位往高位
stack
- 暫存空間
1 區域變數
2 return address
3 參數
4 return value - 高位往低位
- rsp:stack pointer,指向stack頂端
- rbp:base pointer,指向stack底部
保護機制
1 | checksec ./檔案 |
- RELRO
- Stack canary
- NX
- PIE
- ASLR
RELRO(ReLocation Read-Only)
分為 Partial RELRO 與 Full RELRO
No RELRO -> Link Map、GOT 可寫
Partial RELRO -> Link Map 不可寫、GOT 可寫
Full RELRO -> Link Map、GOT 皆不可寫
1 | -z norelro / -z lazy / -z now |
stack canary
在retuen address前面加一層8 bytes的隨機值,return 前檢查是否相同,不同就會crash
特徵是程式disassemble底下會有像是stack_chk
之類的function
1 | gcc test.c -o test -fno-stack-protector #不開啟 |
1 | [Stack] |
NX
寫入及執行權限不同時存在,所以不太能寫shellcode
1 | execstack // 禁用NX |
PIE
.text、.data、.bss地址隨機
1 | -no-pie #關閉PIE |
ASLR
library、heap、stack 隨機化
seccomp
可以禁用掉一下syscall,如execve、system這些危險函數
可以用這東西看禁用了哪些
https://github.com/david942j/seccomp-tools
register
oob
一個陣列,若對於輸入的索引沒有限制,就可以讀到陣列外stack上的資料
1 | arr[i] (int) = *(arr + i *sizeof(int)) |
int overflow
類型 | byte | 範圍 |
---|---|---|
short int | 2byte(word) | 0 ~ 32767(0 ~ 0x7fff) 及 -32768 ~ -1(0x8000~0xffff) |
unsigned short int | 2byte(word) | 0 ~ 65535(0 ~ 0xffff) |
int | 4byte(dword) | 0 ~ 2147483647(0 ~ 0x7fffffff) 及 -2147483648 ~ -1 (0x80000000 ~ 0xffffffff) |
unsigned int | 4byte(dword) | 0 ~ 4294967295(0 ~ 0xffffffff) |
long int | 8byte(qword) | 0 ~ 0x7fffffffffffffff 及 0x8000000000000000 ~ 0xffffffffffffffff(負) |
unsigned long int | 8byte(qword) | 0 ~ 0xffffffffffffffff |
partial overwrite
printf會印到直到碰到\x00
,所以如果可以把canary、或stack 上libc的\x00
蓋掉,就可以leak canary或各種值
stack(堆疊)
old rbp
- 產生原因:一開始做了push rbp,所以rbp被丟到了stack上,通常為了stack平衡所以最後會leave,來pop rbp
- 如果一開始沒有push rbp,通常也就不會有leave
stack return address
- call function前會先將return address存到stack
- 這樣function結束後就可以從stack拿出return address
buffer ovweflow
輸入如果沒有上限,就可以一直寫到return address來控制程式流程
1 | gets、strcpy、strncpy、scanf、read(長度沒寫好)... |
1 |
|
return2code
透過BOF把return address 改到code任意處
1 |
|
這邊要注意一件事,跳過去時要注意該位址存不存在push rbp之類的,如果有要往後跳一點
ret2shellcode
- 須關閉NX
- return到自己寫的shellcode
- 不要蓋掉重要的程式
- 看有rwx,-wx(gdb)
Shellcode:
https://shell-storm.org/shellcode/index.html
https://www.exploit-db.com/exploits
可以自己寫shellcode
orw
1 | sc = asm('\n\n'.join([ |
mprotect
可以改一塊記憶體權限,並在上面做更多操作
sys_mprotect
1 | mprotect(const void *start, size_t len, int prot); |
register | value |
---|---|
rax | 10 |
rdi | 開始的address |
rsi | 長度 |
rdx | rwx |
ROP
gadget
- 結尾是
ret
或是jump <addr>
的gadget- 可以控制register
- 可對任意address寫入資料
- syscall
- ROPgadget尋找適合的gadget
1 | ROPgadget --binary ./<your binary> |
ROP chain
我們可以找可以控制register的gadget,串成一條鏈,設定好register後call syscall
stack -> call execve
1 | pop rax |
https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
題目
https://hackmd.io/@naup96321/SkGljHPZC
stack migration
https://hackmd.io/@naup96321/B1FUxutQC
Fixed Size Migration
https://hackmd.io/@naup96321/Hyown32m0
libc 相關
libc是啥
如果把所有函數都包進去程式,那整個檔案就會很肥,所以把函式備份在每個人的電腦裡,再透過一些機制去把它拿出來
static
把所有函式包進去ELF
demo
1 |
|
1 | gcc static.c -o static --static |
static沒有libc段
並且可以發現他把函式實現方式都直接包進去了
dynamic
libc函數在dynamic link時候都是透過GOT表來進行跳轉找到函式的
GOT(global offset table)
GOT存著很多個一樣大小的元素,形成一個table(基本上就是array)。每個元素都是一個指標(32bit->4 byte,64bit -> 8 byte),指向程式所需要的變數或者是函數
位置 | 內容 |
---|---|
.got | 全域變數的位置 |
.got.plt | 儲存的則是其他library中函式的位置 |
PLT(procedure linkage table)
PLT存著很多個一樣大小的元素,形成一個table
第一個元素是公共plt,負責呼叫動態鏈接器。從第二個開始每個元素分別對應到一個動態鏈接的函數
dynamic link
- 當程式碼中 Call Printf(),在對應的組語會看見 看到call printf@plt。
- printf@GOT會回去向.got.plt取值
- 由於是第一次使用函數,在GOT 表中並不會找到該函數地址,因此必須透過 PLT 將 GOT 重定位
- 將找到的函數位置所需的參數 推入 stack中
- 透過執行dl_runtime_resolve 找出函式位址
- 系統就會把 function 的位址寫進 .got.plt 當中
GOT hijack
如果我可以改寫GOT,那我就可以跳到我指定的位置
例如,我把puts GOT內容改成system plt,那他就會抓到system address,那只要call puts就會呼叫到system
demo
source code
1 |
|
分析
分析一下發現idx並沒有限制,所以可以透過輸入非預期的範圍來oob寫到其他位置,這邊先看一下array位置
0x555555558080
然後看GOT的位置
0x4030(atoi@GLIBC_2.2.5)+0x555555554000=0x555555558030
輸入-10就可以對該位置任意讀寫了
再來是要把他atoi改寫成libc system然後跳到上面去
script
1 | from pwn import * |
ret2libc
- 若能得知 libc base,則可以計算出 libc 中 function 的位置,便能調⽤ library 中的函式。
- 關鍵為 bypass ASLR,找出 libc 的隨機 base
- 透過 information leak 漏洞,洩漏 memory 上的內容,獲取屬於 libcsegment 的 address
- 此 address 會是隨機的 base address 加上⼀固定位移植 ofset (不同版本的 libc ofset 不同)
看libc
1 | readelf -a <your libc> | less |
- libc base只能在執行中leak出來
1.尋找執行中用到的libc位置
2.stack殘渣
算libc base
1 | puts_got_value = libc_base + puts_libc |
可以確認最後1.5 byte是否為000,來確認算出的base正不正確
demo
https://hackmd.io/@naup96321/rynMnRBmR
ret2csu
https://xz.aliyun.com/t/13313?time__1311=GqmxuD2DgQi%3DD%3DD%2FD0erGktKq0KlPgr4rD
1 | .text:00000000004005C0 ; void _libc_csu_init(void) |
首先我們先看這段,可以控 rbx, rbp, r12, r13, r14, r15
1 | .text:000000000040061A pop rbx |
這段可以控
r14 -> rdx
r13 -> rsi
r12 -> edi
最後call r15+rbx*8 跳到其他位置
1 | .text:0000000000401250 4C 89 F2 mov rdx, r14 |
srop
直接參考我這篇
https://hackmd.io/@naup96321/BJh1D4BHA
ret2 dl_runtime_resolve
有時候在沒有適合gadget的時候,可以利用lazy binding時,dl_runtime_resolve解析函數時,通過修改的解析字串符,來達到解析任意函數
複習一下GOT
GOT內有很多段
- .dynamic
- .got -> 紀錄用到的全域變數
- .got.plt -> 紀錄函式引用位置
- .data
got.plt又有三塊
Name | Description |
---|---|
address of .dynamic | 指向 GOT 的 .dynamic |
link_map | 一個link list,用來紀錄用到的 Library |
dl_resolve | 找出函式的絕對位置 |
1 | ------------ ----------------- |
追gdb
一樣開追gdb進去看他lazy binding做了啥
env
跑這支binary
1 |
|
首先他先call gets@plt
1 | 0x00000000004005b9 <+35>: call 0x400480 <gets@plt> |
1 | pwndbg> disassemble 0x400480 |
大概可以分成幾個部分
1 | 跳到gets@got.plt |
這邊先跳到gets@got.plt
裡面存的是0x400486
1 | pwndbg> x/10xg 0x601028 |
會跳回來gets@plt+0x6
並推入0x2這個offset (reloc_arg)
之後跳進去dl_runtime_resolve
1 | pwndbg> x/10i 0x400450 |
0x601000 -> .dynamic section (_GLOBAL_OFFSET_TABLE)
0x601008 -> linkmap (_GLOBAL_OFFSET_TABLE+8)
0x601010 -> _dl_runtime_resolve (_GLOBAL_OFFSET_TABLE + 0x10)
1 | pwndbg> x/10xg 0x601008 |
push了linkmap到stack
並跳到 _dl_runtime_resolve_xsave 他用來把 function 真正的 address 填入 GOT
具體實作如下
1 | => 0x7cdd0d2ebf10 <_dl_runtime_resolve_xsavec>: push rbx |
他將register的值存到stack上
並將rsi設為offset
1 | pwndbg> x/10xg 0x7ffc0454e910+0x10 |
rdi設為linkmap
1 | pwndbg> x/10xg 0x7ffc0454e910+0x8 |
並call _dl_fixup_dl_fixup(l,reloc_arg)
https://elixir.bootlin.com/glibc/glibc-2.31/source/elf/dl-runtime.c#L61
他會尋找函式庫具體的位置並填入到GOT中
詳細解釋在下面
最後callelf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value)
將GOT填入function在libc的位置
這樣就寫入成功了,跑完剩下的xsave就進入到libc gets function內部
1 | pwndbg> x/10xg 0x601028 |
fixup詳細展開
看完上方lazy binding過程,不難理解 _dl_runtime_resolve_xsave 之前跟最後填表,比較複雜的是fixup
先來看看linkmap是啥
https://elixir.bootlin.com/glibc/glibc-2.37/source/include/link.h#L95
關注到 l_info,他指向 ElfW(Dyn) 也就是 .dynamic
這邊回去關注到 .dynamic
https://elixir.bootlin.com/glibc/glibc-2.37/source/elf/elf.h#L849
1 | /* Dynamic section entry. */ |
還有一堆tag
1 | /* Legal values for d_tag (dynamic entry type). */ |
有許多常用的pointer或是value
上面的_dl_fixup調用了
pointer | 意義 |
---|---|
l_info[DT_SYMTAB] | symbol table 位址 (.dynsym) |
l_info[DT_STRTAB] | string table 位址 (.dynstr) |
l_info[DT_JMPREL] | .rel.plt |
l_info[VERSYMIDX (DT_VERSYM)] | .gnu.version |
等dynamic段的東西
那回來看看 _dl_fixup
直接看source code
https://elixir.bootlin.com/glibc/glibc-2.31/source/elf/dl-runtime.c#L61
1 | DL_FIXUP_VALUE_TYPE |
懶人包
- 取得函式的.rel.plt位址,存入reloc
1
2const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); - 取得symbol table ,存入 sym
1
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
- strtab + sym->st_name取得函式名稱,跟其他資料一起丟進去_dl_lookup_symbol_x
- _dl_lookup_symbol_x 搜尋出哪個函式的東西,跟第一個參數,也就是傳入的函式名稱字串是直接相關的。如果丟進去的是gets,那搜尋出來的就會是gets的東西,最後修復出來的就會是gets的位址。
dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL) - 繼續運算,修復出函式位址,寫入函式的GOT中,並回傳
jmprel 是這東西
1 | LOAD:0000000000400670 ; ELF JMPREL Relocation Table |
存的值是<404038h, 500000007h, 0>
0x404038是printf GOT
0x500000007是info
最後是addend
https://elixir.bootlin.com/glibc/glibc-2.37/source/elf/elf.h#L652
1 | /* Relocation table entry with addend (in section of type SHT_RELA). */ |
至於為何 strtab + sym->st_name
見下方
1 | LOAD:00000000004004D0 ; ELF String Table |
string table位於 0x4004D0
printf在string table的位置是 0x400526
去看 symbol table
offset aPrintf -> 0x400526
offset unk_4004D0 -> 0x4004D0
1 | LOAD:0000000000400338 ; ELF Symbol Table |
所以 sym = 0x56
0x4004D0 + 0x56 = 0x400526
也就是 st_name
總而言之,經過了蒐集各個table等等的資訊,修復GOT的位置
最後成功填入function libc address,就是fixup在做的事情
利用 64 bits NO RELRO
No RELRO - .dynamic 跟 GOT 都可以寫
Partial RELRO - .dynamic不可寫,GOT可以寫
Full RELRO - .dynamic 跟 GOT 都不可寫,函式的地址在程式開始執行之前就會都先填好
開始講,怎麼樣利用dl_runtime_resolve這個機制
首先我們說到他會call dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL)
而會修復成甚麼東西跟第一個參數 strtab + sym->st_name 有很大的關係
l_info[DT_STRTAB] -> .dynamic DT_STRTAB pointer
如果我偽造一張string table在其他地方,並且我們去修改
DT_STRTAB pointer指向位置成我們的假table,就可以在讓我們實際修出來的是system或execve之類的(NO RELRO .dynamic可寫,所以可以動)
1 | // gcc chal.c -o chal -fno-stack-protector -no-pie -z noexecstack -z norelro |
ret2vdso
virtual dynamic shared object
早期 32 bits的時候 OS 會透過 interrupt的方式在usermode去處發syscall
https://en.wikipedia.org/wiki/Call_gate_(Intel)
中斷號 0x80 就是syscall
也就是int 0x80
但是用 int 0x80 速度有點慢,所以出現了快速syscall
32 位元的處理器 (即 IA32): sysenter 和 sysexit
64 位元的處理器 (即 x86_64): syscall 和 sysret
然而在兼容性上出了問題,所以出現了vsyscall,具體怎麼syscall由kernel決定,vsyscall實現在vdso中實現
memory segment
1 | naup@naup-virtual-machine:~/Desktop/NHNC-CTF-challege/slime_revenge_revenge/solver$ cat /proc/self/maps | grep vdso |
vdso ASLR
先看x86的vdso ASLR,基本上沒有很多種可能
reference
https://xz.aliyun.com/t/5236?time__1311=n4%2BxnieWw4yDRDRxQqGNDQa4C3xBll7B1rAoD
https://blog.csdn.net/Rong_Toa/article/details/115299552
One gadget
下載
1
2sudo apt -y install ruby
sudo gem install one_gadget使用
1
one_gadget libc-2.23.so
libc中有些one gadget只要滿足條件跳過去就可以get shell(用one gadget看到的地址要加上libc base)
1 | $ one_gadget libc-2.23.so |
demo
https://hackmd.io/@naup96321/HkWFe5-SC
.init_array & .fini_array hijack
https://nocbtm.github.io/2020/02/20/%C2%96-fini-array%E6%AE%B5%E5%8A%AB%E6%8C%81/#%E5%89%8D%E8%A8%80
https://blog.csdn.net/SmalOSnail/article/details/106005946
TLS bypass canary
TLS(Thread Local Storage)
TLS 是個讓每個執行緒都可以有一份自己的資料的機制
每個線程可以獨立的修改自己的副本,而不會影響到其他的線程,造成race condition等問題
就好像一個圖書館有很多書(全局變量),學生(線程)可以去拿書,但可能會造成資源衝突,所以做法是複製圖書館的書,存到自己的櫥櫃(TLS),這樣就可以避免衝突了
TLS誤區
然而若global variable本意是要讓線程間共享,那TLS就不適用了,仍然需要mutex等做同步
TLS用途
執行緒內共享變量:在一個執行緒內的不同方法之間共享變量,而不需要通過參數傳遞。TLS允許每個執行緒持有自己的變量副本,這樣可以在方法之間直接訪問這些變量。
每個執行緒獨立實例:在某些情況下,每個執行緒可能需要一個獨立的實例來存儲特定的數據。例如,資料庫連接對象、用戶會話信息等。使用TLS可以確保每個執行緒擁有自己的實例,從而避免執行緒之間的衝突。
累加操作優化:對於一些全域變量的累加操作,傳統上會使用互斥鎖(mutex)來避免race condition。但使用TLS,可以在每個執行緒中局部累加,然後在某個時機將結果匯總到全域變量。這種做法可以減少鎖的使用,提高程式的並發性能。
執行緒安全的全域變量:當需要使用全域變量但又希望執行緒安全時,TLS可以幫助實現這一點。每個執行緒使用自己的本地副本來存儲數據,而不直接訪問全域變量,從而避免race condition。
TLS & canary
首先canary常被儲存在TLS中
多線程中的canary在每個線程都是被獨立存在TLS中,並由TCB管理,並在線程運行中拿出插到stack上(獨立的canary值)
然而TLS在stack高位,通過overflow可以覆蓋到TLS上的canary
條件是:
- 創建一個線程,並在線程中overflow
- 通常需求overflow的長度高達一個page
條件很嚴苛,但某些失誤導致使用者可以修改到stack_guard段的內容
struct
攻擊前須要先了解兩個struct
struct pthread
1 |
|
tcbhead_t
1 | typedef struct { |
stack_guard那塊存放的就是canary的值
TLScanary攻擊流程
- 定位到canary存放的位置
- 蓋掉canary
- bypass !
https://liupzmin.com/2019/09/30/concurrence/tls-summary/
題目
DASCTF X CBCTF 2023 binding
https://xz.aliyun.com/t/13074?time__1311=GqmhBKqIxGxBMx%2BoEYqi%3D5G%3D8itr4vYx
format string
有哪些format string
fmt | 意思 |
---|---|
%d | 讀取十進制數值(存的值當指標去讀) |
%x | 讀取十六進制數值(存的值當指標去讀) |
%s | 讀取字串符數值(存的值當指標去讀) |
%p | 直接讀取 |
讀取
printf參數依序為
rdi → rsi → rdx → rcx → r8 → r9 → rsp -> rsp+0x8 -> rsp+0x10
1 | printf("%d%c%s", a, b, c); |
使用 %x$y 直接指定第幾個參數,這邊的 x 是第幾個參數,y 則是要使用的方法
1 | %2$p // 印出 rdx (第 2 個參數) |
1 |
|
輸入%p %p %p %p %p %p %p
輸出分別是 rsi → rdx → rcx → r8 → r9 → rsp -> rsp+0x8 -> rsp+0x10
寫入
printf
可以使用%n來寫入指定位置(跟%s很像,只是變成寫入)
1 | rsp + 0x10 -> adress -> %n寫入的位置 |
format string | bytes |
---|---|
%lln | 8 bytes |
%n | 4 bytes |
%hn | 2 bytes |
%hhn | 1 byte |
%n
會將printf輸出過的字元數量寫入指定位置
寫入字元數
寫入字元數字元數 % 256*(bytes數)
ex:若printf 20個字元,會寫入20 % 256 = 20
若printf 258個字元,會寫入258 % 256 = 2
%c
可以透過%c 來指定要寫入的大小
1 | %<寫入幾個字元>c%<位置>$hhn |
ex: 寫入0x1234
第一次: %52c%<位置>$hhn (0x34)
第二次: %222c%?<位置>$hhn (0x12) -> 52+222=274、274-256=0x12
(256-上一次寫入的值+這次寫入的值)%256
舉例
1 | rsp -> %65c%8$h |
argv chain
argv chain 是利用 stack 上的 argv 來達成任意寫入的功能
舉例
1 | 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6f682f |
argv 0
1 | 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6f682f |
argv 1
1 | 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6f682f |
argv 2
1 | 0x7fffffffeb80 --> 0x72662f656d6f682f |
我想把最後的0x72662f656d6f682f寫成0xdeadbeef
並把東西寫道這上面
開始構造
首先我可以透過argv 1改到 0x7fffffffeb80這個位址
rsi → rdx → rcx → r8 → r9 → rsp -> rsp+0x8 -> rsp+0x10 …
$rsp + (n-6) \times 0x8$
rsp+0x128 -> 43
所以
%48879c%43$hn
會讓最後2 bytes變成beef
argv 0
1 | 0x7fffffffe878 --> 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6fbeef |
argv 1
1 | 0x7fffffffe948 --> 0x7fffffffeb80 --> 0x72662f656d6fbeef |
argv 2
1 | 0x7fffffffeb80 --> 0x72662f656d6fbeef |
SSP(Stack Smashing Protect) leak
https://elixir.bootlin.com/glibc/glibc-2.23.90/source/debug/stack_chk_fail.c
1 | void |
https://elixir.bootlin.com/glibc/glibc-2.23.90/source/debug/fortify_fail.c#L26
1 | void |
如果觸發了stack smashing detect的話,可以觀察到 ./demo來自於argv 0存的指標指向的值,所以如果我可以蓋掉argv0就可以任意讀了
暴力送了 b’A’*2000
發現變成segement default
1 | ► 0x728e0a65f82d <getenv+173> cmp r12w, word ptr [rbx] |
去看了一下rbx是啥
變成了RBX 0x4141414141414141 ('AAAAAAAA')
導致pointer指向錯誤
確實可以一次任意i