# 2019q3 Homework1 (review) contributed by < `justapig9020` > ## 預期目標 - 隨堂測驗回顧 - 誠實面對自己 - 問題討論 ## 作業要求 * 從 [課程簡介和注意須知](https://docs.google.com/presentation/d/1Wo22s5aYuuyry97-z2MMmUmdxhjD0wKTdhxIW5oqjvo/edit?usp=sharing), [第 1 週測驗題](https://hackmd.io/@sysprog/HksKVpUIr) 選出你感興趣的 3 個題組進行作答,並且回答附帶的「延伸問題」 * 應當包含 [課程簡介和注意須知](https://docs.google.com/presentation/d/1Wo22s5aYuuyry97-z2MMmUmdxhjD0wKTdhxIW5oqjvo/edit?usp=sharing) 的 Page 9 到 Page 16 裡頭的 C 語言程式設計議題 * 比照 [課前測驗參考解答: Q1](https://hackmd.io/s/ByzoiggIb) 和 [對 Linked List 進行氣泡排序](https://hackmd.io/s/BklGm1MhZ) 的模式來撰寫共筆,需要詳細分析自己的想法、參閱的材料 (包含 C 語言規格書的章節),以及==進行相關實驗== * 閱讀 [第一週課程進度](http://wiki.csie.ncku.edu.tw/sysprog/schedule) 所有材料,記錄所見所聞並提問,若有不能理解的部分,請標註出來。善用 HackMD 的語法 `:::info` 和 `:::` 標註你的提問 :::info 像是這樣標註提問 ::: ## 參考資料閱讀 [參考資料閱讀筆記](https://hackmd.io/@justapig9020/2019q3_week1_info) ## 第ㄧ週 測驗``一`` ### Code ```c #include <stdio.h> #define align4(x) (((x) + K) & (-4)) int main(void) { int p = 0x1997; printf("align4(p) is %08x\n", align4(p)); return 0; } ``` ### Question && Answer 預期程式輸出 ``align4(p) is 00001998``, 則``K =`` ? - Ans: ``3`` --- ### 想法與思考 題目希望能透過 ``align4()`` 這個巨集來將位址以 ``4`` 個單位對齊,由 ``bit mask`` 為 ``-4 (0xfffffffc)`` 便可了解,設待處理之位址為 ``n`` ,為了讓所有 ``n%4 != 0`` 之位址都能向前平移至下一個對齊點,因此需要加上對齊基準 ``-1`` 之數值,在此為 ``4-1 = 3`` 來滿足條件。 在透過 google 翻譯了解到題目是在討論關於位址對齊 (英文苦手 Orz ),首先我所想到的是剛入門資安領域時的一個問題: ``` asm push ebp mov ebp,esp and esp,0xfffffff0 sub exp,0x10 ``` :::danger 文字訊息==不要==用圖片去表達,不僅排版不一致,日後要搜尋檢索也困難 :notes: jserv ::: 這是個執行於 ``x86`` 環境下的[程式](https://pwnable.tw/challenge/#3),透過 ``gdb`` 分析其 main function ,第 0, 1, 3 行十分容易理解,無非就是在對 stack 進行平移以利接下來函數的變數儲存,但是第二行的 ```and esp,0xfffffff0``` 卻百思不得其解,經過搜尋後得知是在對記憶體進行對齊。 ::: success 由於 stack 是由記憶體高位址長向記憶體低位址,而本題目預設似乎是往高處長,所以在對 stack 進行 alingment 時不需要像本題目一樣在添加一個 offset 後再進行 and 運算。 ::: ### 延伸題目 ::: success 寫至這裡突然發現似乎跟測驗``二``有點類似,姑且算是測驗``一`` ``二``的混合(? ::: ```cpp #define __ALIGN_KERNEL(x, a) __ALIGN_KERNEL_MASK(x, (typeof(x))(a) - 1) #define __ALIGN_KERNEL_MASK(x, mask) (((x) + (mask)) & ~(mask)) #define ALIGN(x, a) __ALIGN_KERNEL((x), (a)) ``` 查看引用本巨集的檔案,並選擇其中之一 [drivers/net/ethernet/stmicro/stmmac/stmmac_main.c](https://github.com/torvalds/linux/blob/master/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c) 研究。 並延伸如下之巨集 ```cpp #define STMMAC_ALIGN(x) __ALIGN_KERNEL(x, SMP_CACHE_BYTES) ``` 所延伸出之巨集發現被使用於此 function: ```cpp static int stmmac_open(struct net_device *dev) { ... priv->dma_buf_sz = STMMAC_ALIGN(buf_sz); priv->rx_copybreak = STMMAC_RX_COPYBREAK; ... } ``` 此程式是被用於控制網路介面,於 ifconfig xxx up 時執行,而 ``priv->dma_buf_sz = STMMAC_ALIGN(buf_sz);`` 的作用為根據 buf_sz 初始化 DMA 的 Buffer size ,其中 ``STMMAC_ALIGN`` 是先將 buf_sz 做對齊,而根據 ``#define STMMAC_ALIGN(x) __ALIGN_KERNEL(x, SMP_CACHE_BYTES)`` 可以得知對齊之標準為 ==SMP_CACHE_BYTES== ,是根據不同硬體所改變的 ``cache`` 大小的數值,推測是為了盡可能使 buffer 可以一次被全部讀入 cache 中,換言之是為了增加記憶體存取效率所使用。 :::warning ifconfig is deprecated in favor of iproute2 (the ip command) on GNU/Linux. :notes: jserv ::: 此外根據[文章](https://blog.csdn.net/heliangbin87/article/details/75997189)所示,這邊應該同時要初始化 `tx`, `rx` 的 buffer 不知道為何新版本的程式似乎將其移除了。 ### 記憶體對齊 #### 電腦的記憶單位 1. ``bit`` 電腦最小的記憶單位,可以保存 0/1 。 3. ``byte`` 一般來說 1 ``byte`` = 8 ``bits`` ,但也並非絕對。 - [DEFCON 25 cLEMENCy](https://github.com/legitbs/cLEMENCy) 為了 2017 年 DEFCON 專門開發的 1 ``byte`` = ==9== ``bits`` 架構。 5. ``word`` 早期記憶體的最小存儲單位,根據系統架構所定義,目前主流架構多為 4 ``bytes`` 或 8 ``btyes``。 - 以 [IBM 160](https://en.wikipedia.org/wiki/IBM_System/360#Technical_description) 為分界,之前多使用 ``word`` 或 ``bit`` 定址,之後則多為 ``byte``。 - ``cache`` 是以 ``word`` 的整數倍從記憶體存取資料,若 ``word`` 能被定義成 ``byte`` 的 2 的次方倍,在記憶體定址上會較有效率。 #### 電腦的儲存結構 由於 cpu 與週邊設備時脈速度差異過大,因此往往需要透過中間設備逐漸加速以確保寶貴得 cpu 資源不會被浪費在等待資料傳輸上,因此記憶體與 cache 在此背景下誕生。 ![](https://i.imgur.com/ht6mNst.png) > [reference](https://sites.google.com/site/nutncsie10412/ge-ren-jian-jie/ji-yi-ti) 由此圖可以看出越靠近 ``cpu`` 的設備速度越快,但儲存成本上升也意味著空間越少,以目前的電腦運作模式是將需要長久儲存的資料放置於 ``Mass Memory`` ,正在執行的 ``Process`` 則會被載入 ``Main Memory`` ,當 ``cpu`` 要存取 ``Main Memory`` 中的資料時則會被依序載入各級 ``cache`` 中,最後再將資料放入 ``Register`` 中做運算。 #### cache 的運作方式 當 ``cpu`` 需要使用某個記憶體位址中的資料時,會先去尋找該位址的資料是否有被儲存於 ``cache`` 中,如果存在便能直接存取,若無法找到則會觸發 ``cache miss`` , ``cache`` 必須要前往記憶體存取資料。 #### 記憶體對齊的優點 由於 ``cache`` 與記憶體之間的存取通常是以 ``word`` 為單位,因此若能將資料以 ``word`` 為單位對齊,就能確保資料在讀取時不會橫跨兩個 ``cache`` 之間,以免需要做複數次讀取才能將資料在 ``cpu`` ``cache`` ``memory`` 之間傳送。 :::danger 「確保資料在讀取時不會橫跨兩個 ``cache`` 之間」這樣的陳述有誤,請思考並修正。注意 cacheline 的行為,以及 cache 本質上是 sequential access,而 main memory 則是 random access,這兩者的落差又會對效能有何衝擊? (hint: locality) :notes: jserv ::: 以上面所提出的例子就是在規劃 ``buffer`` 時刻意以 ``cache`` 的 size 作為對齊依據,以確保資料盡可能的被存入同一 ``cache`` 中。 ### 類似應用 藉由相同概念所實作的 branchless 四捨五入 ```cpp #include <stdio.h> int r(int a) { return (a + 5) / 10 * 10; } ``` --- ## 第一週 測驗 ``三`` ### Code 考慮以下 C 程式碼: ``` c #include <stdbool.h> bool func(unsigned int x) { return x && ((x & (~x + 1)) == x); ``` 可改寫為以下等價的程式碼: ``` c bool func(unsigned int x) { unsigned int unknown = 0; while (x && unknown <= 1) { if ((x & Z) == 1) unknown++; x >>= 1; } return (unknown == 1); } ``` ### Question && Answer 請補完程式。 ``Z`` = ? - Ans: ``1`` --- ### 想法與思考 這個程式是在判別 ``x`` 是否為 2 的次方倍,若 ``x`` 為 2 的次方倍,則意味著 ``x`` 除最高一位元為 **1** 外,其他位元皆為 **0** (0b10...0) ,第二段城市正是利用這個特性去歷遍並檢測整個數中是否只有一個 **1** ,而第一段程式則是利用 ``bitwise`` 操作將整個檢測變成 ``branchless`` ,且運作時間也從 O(n) 縮減為 O(1) ,其原理是將 ``x`` 透過 ``not`` 運算並加一成為 ``x'`` ,如果 ``x`` 是二的倍數則 ``x'`` 會為 ``0b1...10...0`` 其中最低位的 1 正好會是 ``x`` 唯一的一的所在位置,之後再透過 ``and`` 運算(0b1...10...0 & 0b0...010...0),結果便會跟 ``x`` 相等。 若 ``x`` 不為二的次方倍,則最後結果必定不相等。 前面與 ``x`` 的邏輯 ``and`` 運算是為了排除 ``x==0`` 的狀況。 ### 延伸題目 ``` c int max (int a,int b) { return a^((a^b) & (-(b>a))); } ``` 由於 ``a`` **xor** ``a`` 會等於 ``0``,藉由這個原理,便可以透過用 cmp 的結果創造之 mask 控制 ``a`` **xor** ``0`` 或是 ``a^b`` 來得到要回傳的值。 #### setcc Sets the destination register operand to ``0`` or ``1`` depending on the settings of status flags. ``setcc`` 是一系列指令,作用是向目標暫存器存入當前某特定 ``status flag`` 的數值,以此獲得比較結果 透過 ``setcc`` 指令,能使 ``cmp`` 並回傳結果的流程變成 ``O(1)``,透過 ``objdump`` 觀察發現這邊是使用 ``setl`` 。 ``` asm emp ecx, dword ptr [rbp-8] setl dl ``` #### Constant time progeam 當程式存在分支時,表示其執行時期的特定某時間的狀態是難以預測的,目前能想到兩大缺點 1. 存在被 side channel attack 的風險 2. 在 Real time 環境下難以預測工作完成時間 :::warning 思考現代中央處理器的 branch predictor 原理 :notes: jserv ::: ##### 資料存取 某些資料存取,例如陣列,看似是常數時間,但是如果此資料大到足以導致 cache miss 或是 page fault 的話,依然是存在無法再嘗試時間內完成的風險。 #### Side channel attack 所謂 side channel atttack 是指不直接利用城市的邏輯漏洞,反而是利用程式邏輯,並透過觀測其他執行時期的行為來進行攻擊。 :::danger 注意偏好的程式碼縮排,4 spaces,` { ` 前後有空白 :notes: jserv ::: ```cpp #include <stdbool.h> bool my_strcmp(const char *c1, const char *c2) { while (*c1 | *c2) { if (*c1 != *c2) { return false; } c1++; c2++; } return true; } ```` 透過此 function 進行字串比較時,此程式並不能保證在常數時間內執行,若此 function 被用於密碼比較時,會產生被 Time-based side channel attack 的風險,由於指令運算需要時間,因此進行不同次比較的時間是不同的,例如 "hello" 分別與 "hell" "he" 比較時所花費的時間是不同的,因此透過觀測執行時間,可以推估出當前當前正確的密碼位數。 以一只允許英文小寫長度為 ``n`` 的密碼系統來說,原本的複雜度為 $24^n$ 但若有此 side-channel attack 漏洞,猜測密碼的複雜度會大幅下降至 24*n 。 **實驗** ::: success 為了方便觀測與實作,在比較時刻意加入 sleep() 來擴大執行時間差異 ::: ``` c #include<stdbool.h> #include<stdlib.h> #include<stdio.h> #include<time.h> #include<unistd.h> bool my_strcmp(char *c1) { char pw[] = "hi"; char *c2 = pw; while(*c1|*c2){ sleep(1); if(*c1!=*c2){ return false; } c1++; c2++; } return true; } char* side_channel() { puts("== side =="); char *c1; int i,t,o; bool ans; c1 = malloc(10); i = -1; // current char t = -1; // last function execut time o = 0; // running times c1[0] = '\0'; do { o++; printf ("%d: %s\n", o, c1); int ts, te; ts = time(0); ans = my_strcmp(c1); te = time(0); if (te-ts+1 > t){ // if find next pw c1[++i] = 'a'; c1[i+1] = '\0'; t = te-ts+1; }else{ // try next alphabet c1[i]++; } }while(!ans); c1[i]--; printf("Times:%d\n", o); return c1; } void is_z(char *c) { if(*c=='z'){ *c = 'a'; is_z(c+1); }else{ (*c)++; } } char* non_side_channel() { puts("== non-side =="); char *c1; int i=0, o=0, mod=26; c1 = malloc(10); c1[0] = 'a'; c1[1] = '\0'; while(!my_strcmp(c1)){ printf("%d: %s\n",++o , c1); is_z(c1); if(o%mod==0){ c1[++i] = 'a'; c1[i+1] = '\0'; mod *= 26; } } printf("Times:%d\n", o); return c1; } int main(void) { char *pw = side_channel (); printf ("%s\n\n", pw); pw = non_side_channel (); printf ("%s\n", pw); return 0; } ``` ``` == side == 1: 2: a ... 18: hi Times:18 hi == non_side == 1: a ... 241: gi Times:241 hi ``` 可以發現利用 side channel attack 可以大幅度縮減暴力破解時間。 --- ## 課程說明 第 ``十一`` 頁 ### Code ``` c #include <stdio.h> int main() { return (*******puts)("Hello"); } ``` ### Question && Answer 能否編譯並執行呢?若可,為什麼呢? ### 想法與思考 Function 的名字在 c 語言中被稱之為 ``function designator`` ,根據劍橋字典, [designate](https://dictionary.cambridge.org/zht/%E8%A9%9E%E5%85%B8/%E8%8B%B1%E8%AA%9E/designate) 為 > to choose someone officially to do a particular job 以此推測 ``designator`` 意味著叫某人去做某事的某人,也就是說我們透過 ``function designator`` (某人) 叫程序 (某人) 去執行 function 的內容 (某事),而 function designator 的型態即為該 function 的 return type。 摘錄 C99 規格書: > C99 [ 6.3.2.1 ] A ``function designator`` is an expression that has ``function type``. ==Except== when it is ``the operand of the sizeof operator`` or ``the unary & operator``, a ``function designator`` with type ‘‘``function returning type``’’ is converted to an expression that has type ‘‘``pointer to function returning type``’’. 根據此敘述所示,首先 ``funciton designator`` 表示其擁有 ``funtion type`` 且 ``function designator`` 的型態為 ``function returning type``。 當一 ``function designator`` 有 ``function returning type`` 時被視為 ``pointer to function returning type``。 ``` gdb-peda$ print func $1 = {int ()} 0x4004d6 <func> ``` 但有兩個例外,分別是被帶入 sizeof() 時,以及與一元運算子 `&` 進行運算。 首先關於一元運算子 `&` : > C99 [6.5.3.2] The unary `&` operator yields the address of its operand. If the operand has type ‘‘type’’, the result has type ‘‘pointer to type’’. If the operand is the result of a unary `*` operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. > C99 [6.5.3.2] The unary * operator denotes indirection. If the operand points to a function, the result is a function designator 首先我們能得知若對型態為 `type` 運算元進行一元運算子 `&` 運算,則會得到型態為 pointer to `type` 的結果, 若運算元進行過一元運算子 `*` 運算,則 `&` `*` 會相互抵銷。 若對 ``pointer to a function`` 進行一元運算子 `*` 運算,則會返回 ``function designator`` ``` gdb-peda$ print *func $2 = {int ()} 0x4004d6 <func> ``` > C99 [6.5.3.2] The operand of the unary & operator shall be either a function designator, the result of a [] or unary * operator, or an lvalue that designates an object that is not a bit-field and is not declared with the register storage-class specifier. 由上述可知由於 ``function designator`` 經過一元運算子 `&` 運算後結果為 ``pointer to function type``。 對 ``pointer to a function`` 進行一元運算子 `*` 運算,則會返回 ``function designator`` 由於 ``function designator`` 為 ``function returnng type`` 因此經過一次一元運算子 `&` 運算後的 ``pointer to function returning type`` 並不被視作 ``function designator`` 又因為此狀態下的 ``pointer to function returning type`` 是只保存在暫存器中,因此不符合 ``is not declared with the register storage-class specifier`` ,最終導致 ``Syntax error``。 ``` gdb-peda$ print &func $3 = (int (*)()) 0x4004d6 <func> gdb-peda$ print &*func $4 = (int (*)()) 0x4004d6 <func> gdb-peda$ print *&*func $5 = {int ()} 0x4004d6 <func> gdb-peda$ print &&*func A syntax error in expression, near `&&*func'. ``` 最後來探討一下 function call 這件事 > C99 [6.5.2.2] The expression that denotes the called function(80) shall have type pointer to function returning void or returning an object type other than an array type. > 80) Most often, this is the result of converting an identifier that is a function designator. 滿足 function call 的必要條件是,你所 call 的 function 必須是一個 ``pointer to function returning type`` ,也就是說一個 function call 可以被視為 ``identifier with pointer to function returning type`` ==(== ``argument list`` ==)== 這邊分成三種情況討論。 1. func() - 由於 func 代表 func(){} (我猜是因為能在 symbol table 中找到) 因此 func 具有 function type ,因此 func 是 ``function designator`` ,因此滿足 function call 的首要條件,再加上 ``()`` 就能進行 function call。 3. *func() - 由於 func 代表 func(){} (我猜是因為能在 symbol table 中找到) 因此 func 具有 function type ,因此 func 是 ``function designator`` ,又當 ``function designator`` 是 ``function returning type`` 時被視作 ``pointer to function return type`` ,當一元運算子 ``*`` 與 ``pointer to function`` 進行運算時會得到結果為 ``function designator`` ,因此滿足 function call 的首要條件,再加上 ``()`` 就能進行 function call。 5. &func() - 由於 func 代表 func(){} (我猜是因為能在 symbol table 中找到) 因此 func 具有 function type ,因此 func 是 ``function designator`` ,由於 ``function designator`` 經過一元運算子 `&` 運算後結果為 ``pointer to function type`` ,因此滿足 function call 的首要條件,再加上 ``()`` 就能進行 function call。 透過以下實驗觀察 ::: success 此程式必須在編譯時加入 -z execstack 來關閉 NX 保護才能正確執行。 ::: ```cpp char code1[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(void) { char code2[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; ((void(*)())code1)(); ((void(*)())code2)(); ((void())code1)(); // fun.c:7:6: error: cast specifies function type // ((void())code1)(); code2(); // fun.c:5:10: note: declared here // char code2[] = .... return 0; } ``` 我們透過字元陣列的方式寫入可執行的資料,再透過 ``function call`` 來執行他們,例如[這個程式](http://shell-storm.org/shellcode/files/shellcode-806.php)就是在測試 shell code ,前兩組 function call 透過強制轉型成 ``pointer to function returning type`` 的方式成功執行,而第三個因為無法被強制轉型成 ``function returning type`` 而失敗,最後一組因為在進行 function call 時, identifier type 並不為 ``pointer to funtion returning type`` 而失敗。 ::: success func: - type: function returning type - value: address of function &func: - type: pointer to function returning type. e.g. `int (*)()`` - value: address of function *func: - type: function returning type. e.g. ``int ()`` - value: address of function ::: ::: success argument / parameter 之前一直搞不懂兩者之間的差異,參照 c 語言規格書後發現, argument 指的是 function call 時提供的參數,而 parameter 則是 function 所預期傳入的參數 ::: ::: success sizeof 是一個 operator ::: :::danger 摘錄 C 語言規格書對應描述並論述 :notes: jserv ::: ~~根據 C 語言規格書, *、& 兩個都是右結合,且跟 ``指向 function 的東西``進行運算結果會是 ``function designator``,由於原本的 function designator 已經指向 function 的 address ,因此在運算後依然是 function designator,這就有點像對 $e^x$ 微分依然是 $e^x$ 一樣,不論經過幾次運算依舊回到原點。~~ ::: success 或許能在 compiler 的實作中找到驗證 ::: ###### tags: `sysprog2019`