contributed by < ambersun1234
>
quicksort
函式取 list 中第一個元素作為 pivot value,在迴圈內部從第二個元素開始依序比較;若元素值大於 pivot 則將其加入 right
的 list 中,反之則加入至 left
。
list_add_node_t
採用 insert head 的方式,即將原本的 list 串接在後面
實際上的串接方式就如下圖所示,足以證明為何 left
, right
初始值為 NULL。
divide 完之後就要把 list 合併,list_concat
就是單純將 right 串接在 left 後面而已;總共需要合併 3 次,left, pivot, right 因為 pivot 並未連接在任何 list 後面。
為了可以進行實驗,需要補齊上述程式碼; list_make_node_t
以及 list_free
的實做程式碼如下
list_free
從頭走訪到 list 尾端,依序釋放記憶體
list_make_node_t
新增一個 node,其 value 為 random() % 1024
所產生。將新的 node 放置於 list head,next 則接上原本的 list。
c=0
的 LCG程式執行結果為
結果驚訝的發現輸出亂數的部份竟是如此的相似,於是反過頭檢查 time(NULL)
輸出結果只相差 1。
根據 time(2) man page
time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
If t is non-NULL, the return value is also stored in the memory pointed to by t.
因為 time() 精度只有到 seconds 所以會導致輸出結果太接近
既然使用時間是不可靠的,於是我就嘗試去取一些常常變動的數值,受到 @linD026 所啟發,可以用地址去當作 srand 的參數。malloc
出來的地址經過實驗時常變動,所以很適合拿來當作亂數種子
要注意 ASLR 僅是一種針對攻擊的 mitigation,並不能避免資安問題
HexRabbit
ASLR 0: 關閉
ASLR 1: Conservative Randomization
ASLR 2: Full Randomization
考慮以下實驗結果
可以看到 heap 的位置,兩者是一樣的
00405000-00426000
c.f 64 位元下,no PIE 起點為
0x00400000
c.f 32 位元下,no PIE 起點為0x08048000
gcc 預設 PIE 是開啟的,需使用 -no-pie
關閉,參見 gcc/defaults.h
參考 Performance and Entropy of Various ASLR Implementations,分析 ASLR 的 entropy,考慮 stack pointer 的位置
考慮以下程式碼
得到以下執行結果
對照 /proc/PID/maps
不難發現 sp 的位置有一點誤差,我想這也是可接受的範圍內(0x7ffeb6b5d000
, 0x7ffeb6b5bd10
)
值得注意的是,上述 16 進位輸出共有 48
位元,而不是 64 位元
可參考 why virtual address are 48 bits not 64 bits? [duplicate]
多次實驗觀察 stack pointer 可得以下結果
可以發現到這些 virtual address 的 MSB 部分都是相同,而最後 4 bits 都是 0。這也就是為甚麼論文中實驗並不採用全部位元的原因
For Debian, we observed 30 bits of entropy in the stack. This was bits
4 to 34 (least significant to most significant)
有興趣的話也可以觀察看看 libc 或相關 library 的地址在開啟 ASLR 下有多少 bits 的 entropy
HexRabbit
考慮以下測試程式碼
為了驗證 ASLR 的 entropy,我參照論文設計了一個簡單的實驗,內容是取得 stack pointer 的位置去分析(搭配 execve 以及 fork)
根據 man execve
execve() executes the program referred to by pathname. This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments.
execve 會重新 new stack, heap 以及 data 區塊,藉由分析 stack pointer 我們可以知道 ASLR 的 entropy。這次實驗總共測試 1000000(一百萬) 次,分別在 32 位元(raspberry pi zero wh(rapbian)
),以及 64 位元(ubuntu 20.04 LTS
) 下測試
完整程式碼可參考 linux2021q1_quiz1/ASLR
總共統計結果
在1百萬重複次數中,僅有 917(457*2+1*3) 個地址重複到
可以看到在 32 位元架構下,重複機率很高,意味著攻擊者相較於 64 位元架構中,更容易猜到地址。
更改過的程式碼如下:
首先 malloc 一個空間出來,將其地址轉為儲存起來,使用 intptr_t
強制轉型(將 pointer 轉型為 integer),並且使用其作為亂數種子,使用完之後當然要把他 free 掉避免 memory leak。
TODO: 回顧 「Linux 核心設計」 課程說明 (2021年春季) 第 19 頁,嘗試學習早期 mimalloc 運用 ASLR 的手法。
考慮 ASLR 以及 PIE 可以將程式改寫如下,參考 mimalloc/random.c
執行結果如下
文章中所實作的程式碼如下
beg[], end[]
陣列的使用,因為遞迴可以使用 private array 簡化。文中提到 private array 相比 real array 來說慢得多,Recursive functions look neat and simple in part because they don’t have to have big arrays like my beg[] and end[]. But all they’re really doing is using the stack as their own private array. This is much slower than using a real array, and could cause stack overflow on some systems, crashing the app that called the quicksort.
TODO:
MAX_LEVELS
的範圍進行數學分析circular linked list
list_add
的實作我們可以發現,對於 head->next = new
這行的做法不一樣,差別在於 linux 使用了 WRITE_ONCE
macro
Prevent the compiler from merging or refetching reads or writes. The
compiler is also forbidden from reordering successive instances of
READ_ONCE and WRITE_ONCE, but only when the compiler is aware of some
particular ordering. One way to make the compiler aware of ordering is to
put the two invocations of READ_ONCE or WRITE_ONCE in different C
statements.These two macros will also work on aggregate data types like structs or
unions. If the size of the accessed data type exceeds the word size of
the machine (e.g., 32 bits or 64 bits) READ_ONCE() and WRITE_ONCE() will
fall back to memcpy and print a compile-time warning.
Their two major use cases are: (1) Mediating communication between
process-level code and irq/NMI handlers, all running on the same CPU,
and (2) Ensuring that the compiler does not fold, spindle, or otherwise
mutilate accesses that either do not require ordering or that interact
with an explicit memory barrier or atomic instruction that provides the
required ordering.
就是為了避免 compiler 做優化這件事,比如說,在不影響程式正確性下互調程式碼,讓 寫入資料
與 讀取資料
互換。在多執行緒的架構下,同一時間可能有多個 process 對同一資料進行存取,可能導致 data race。可參考 LINUX KERNEL MEMORY BARRIERS
volatile 關鍵字可以避免存取變數被過度優化(使用暫存器資料),因為變數有可能在另一個執行緒中被改變,導致資料不一致的行為發生。
上述關於 volatile 關鍵字的考量不充分,請閱讀規格書和思考 embedded 的場景。
那為甚麼又只有 WRITE_ONCE(prev->next, new);
需要 WRITE_ONCE 呢? 根據 include/linux/list.h 7f5f873 commit message 中的線索
Unfortunately, the compiler is within its rights to (for example) use
byte-at-a-time writes to update the pointer, which would fatally confuse
concurrent readers
由上可知,compiler 有可能會把 64 位元的資料分成兩個 32 位元進行讀取,導致資料未更新完全的情況下被存取,為了達成 atomic 所以需要 WRITE_ONCE 的協助,可參考 Load/Store tearing in this day and age?
總結來說,sysprog21 的 list 版本簡化了多執行緒下的必要保護措施。
WRITE_ONCE in linux kernel lists
Linux内核中的READ_ONCE和WRITE_ONCE宏
linux2021