# 用 GDB 從組合語言角度觀察 Buffer Overflow > - CMU 15-213「Machine-Level Programming V: Advanced Topics」的課程筆記 > - 使用課程提供的 buffer overflow 程式碼觀察實際執行情形,並: > - 用 gdb 觀察 buffer overflow > - 透過 buffer overflow 更改 return address > - 觀察有/無使用 stack canary 的組合語言變化 ## 事前準備 由於我的 MacBook 是 M3 的,是 ARM64 架構,所以 VirtualBox 的 Ubuntu 虛擬機也是裝 ARM64 版本的。 但我想觀察 x86-64 組合語言,所以若要能在 ARM64 的 Ubuntu 虛擬機編譯和執行 x86-64 的程式,要先安裝 x86-64 的 cross-compiler、QEMU 模擬器、以及 `gdb-multiarch`: ```shell $ sudo apt -y install gcc-x86-64-linux-gnu qemu qemu-user gdb gdb-multiarch ``` ## 課程提供的程式碼 以下是 CMU 15-213 課程提供觀察 buffer overflow 的程式碼: ```cpp /* bufdemo-nsp.c */ #include <stdio.h> void echo() { char buf[4]; gets(buf); puts(buf); } void call_echo() { echo(); } int main() { call_echo(); return 0; } ``` 以上程式碼,會要求使用者輸入 string,並將使用者輸入的 string 放到 buffer `buf`。 但有一個問題:這個程式用的是 `gets` 這個==不安全==的 C library function! 為什麼說 `gets` 是不安全的呢? 因為這個 function 不會做 bound checking,他不會檢查 buffer 是否已經被裝滿。他會從 terminal input 去 scan string,只要沒遇到 `EOF` 或 `\n`,他就會一直把 char 丟到 buffer 內。若已經超過 buffer 的容量,那多餘的 char 就會覆蓋到其他記憶體的資料,所以駭客就可以用這種方式把想要的東西丟進去。 以此例而言,若 `gets` 丟了超過 4 個 char 到 buffer,那其他記憶體位址所存放的資料就會被覆蓋掉。 其他同樣類似較不安全的 standard C library functions 包括 `strcpy`、`strcat` 等等,因為他們沒有做 bound checking。 為了安全起見,可以用比較安全的版本,像是 `fgets`、`strncpy`、`strncat` 等等。 ### 編譯成 x86-64 執行檔 ```shell $ x86_64-linux-gnu-gcc -m64 -g -fno-stack-protector -z execstack -no-pie -static bufdemo-nsp.c -o bufdemo-x86 ``` - `-m64`:指定產生 64-bit x86 code - `-g`:可以用 gdb debug - `-fno-stack-protector`:關掉 stack canary 保護機制 - `-z execstack`:允許執行位在 stack 的程式碼 - `-no-pie`:不要使用 position-independent executable,這樣才能讓記憶體位址固定,方便分析 > 將前述幾個保護措施關掉,才可以好好觀察 buffer overflow ### 用 QEMU 在 ARM64 架構的 Ubuntu 虛擬機執行 x86-64 執行檔 ```shell $ qemu-x86_64 ./bufdemo-x86 ``` ### 執行結果 我在我的虛擬機執行的結果,輸入 11 個 characters 沒事,但輸入 12 個 characters 就 segmentation fault,如下截圖: ![image](https://hackmd.io/_uploads/Byme-PzBlg.png) ## 反組譯後的 x86-64 組合語言 ```shell $ x86_64-linux-gnu-objdump -d bufdemo-x86 -M intel > disasm_x86-64_NASM.txt ``` 使用 `objdump` 反組譯產生 x86-64 NASM 組合語言程式碼 - `-d`:disassemble 反組譯 - `-M intel`:使用 Intel/NASM 語法(預設是用 AT&T 語法,但因為個人比較習慣 Intel 語法,所以改成 Intel 語法) ![image](https://hackmd.io/_uploads/SyXSlvMHge.png) 以下是反組譯後得到的組合語言程式碼,後面會再做詳細的說明~ ```nasm 0000000000401905 <echo>: 401905: f3 0f 1e fa endbr64 401909: 55 push rbp ; 將 rsp - 8,再將前一個 stack frame 的 base 的記憶體位址放進去這個位址 40190a: 48 89 e5 mov rbp,rsp ; 將目前 rsp 的值傳給 rbp 40190d: 48 83 ec 10 sub rsp,0x10 ; 分配 16 bytes 給 local variable 401911: 48 8d 45 fc lea rax,[rbp-0x4] ; 計算 rbp-0x4,也就是 buf 的起始位址,並將這個得到的記憶體位址存到 rax 401915: 48 89 c7 mov rdi,rax ; 將 rax 的值傳到 rdi,依據 calling convention,rdi 存放 function 的第一個參數,所以等下會被當作 gets 的參數使用 401918: b8 00 00 00 00 mov eax,0x0 40191d: e8 8e 34 00 00 call 404db0 <_IO_gets> 401922: 48 8d 45 fc lea rax,[rbp-0x4] 401926: 48 89 c7 mov rdi,rax 401929: e8 82 36 00 00 call 404fb0 <_IO_puts> 40192e: 90 nop 40192f: c9 leave 401930: c3 ret 0000000000401931 <call_echo>: 401931: f3 0f 1e fa endbr64 401935: 55 push rbp 401936: 48 89 e5 mov rbp,rsp 401939: b8 00 00 00 00 mov eax,0x0 40193e: e8 c2 ff ff ff call 401905 <echo> 401943: 90 nop 401944: 5d pop rbp 401945: c3 ret 0000000000401946 <main>: 401946: f3 0f 1e fa endbr64 40194a: 55 push rbp 40194b: 48 89 e5 mov rbp,rsp 40194e: b8 00 00 00 00 mov eax,0x0 401953: e8 d9 ff ff ff call 401931 <call_echo> 401958: b8 00 00 00 00 mov eax,0x0 40195d: 5d pop rbp 40195e: c3 ret 40195f: 90 nop ``` ## 用 GDB 觀察 Buffer Overflow ### 前置作業 讓 `qemu-x86_64` 模式開啟 GDB 伺服器: ```shell $ (echo "123456789012"; sleep 1) | qemu-x86_64 -g 1234 ./bufdemo-x86 ``` 開啟另一個 terminal,執行以下指令: ```shell $ gdb-multiarch ./bufdemo-x86 (gdb) target remote localhost:1234 ``` ![image](https://hackmd.io/_uploads/By0ogD1Lxx.png) 設置自訂的 gdb tui layout,可同時顯示 source code、組合語言、以及 registers 的值: ```gdb (gdb) tui new-layout mylayout {-horizontal src 2 asm 3 regs 3} 3 status 1 cmd 1 ``` 將 gdb 的組合語言語法設置為 Intel 語法(預設會顯示成 AT&T 語法): ```gdb (gdb) set disassembly-flavor intel ``` 開啟前述自定義的 gdb tui layout: ```gdb (gdb) layout mylayout ``` ![image](https://hackmd.io/_uploads/B1taP0kUlx.png) - 下半部是輸入 gdb command 的地方 - 上半部左側到時候會顯示目前在 source code 的位址、中間則會顯示組合語言、右側則是顯示當前 registers 的值 ### 開始用 gdb 觀察 執行以下指令設置 break point: ```gdb (gdb) break main /// 在 main 設置 break point (gdb) break echo /// 在 echo 設置 break point (gdb) continue /// 若是在原生 x86-64 環境,用 run 指令即可,但由於我的情況是跑在 ARM64 虛擬機,所以用 continue 指令 ``` ![image](https://hackmd.io/_uploads/S1mNxalUlg.png) - 輸入 `focus src`、`focus asm`、`focus regs` 就會分別跳到程式碼、組合語言、registers 的畫面,這時可用上跟下來移動要看到的內容。 - 輸入 `focus cmd` 則會回到下半部輸入 command 的部分。 --- <!-- ![image](https://hackmd.io/_uploads/ry6h1_1Igl.png) --> 查看 `rbp` 和 `rsp` registers 的值,以得知目前 `main` 的 stack frame: ```gdb (gdb) info registers rbp rsp rip rbp 0x2aaaab2aae30 0x2aaaab2aae30 rsp 0x2aaaab2aae30 0x2aaaab2aae30 rip 0x40194e 0x40194e <main+8> ``` - `rbp`:指向目前 stack frame 的 base - `rsp`:指向目前 stack frame 的 top - `rip`:目前 CPU 準備執行的指令的位址 可以看到目前 `main` 的 stack frame 就只有 `0x2aaaab2aae30`,因為沒有 local variable,所以就沒有分配額外的 stack 空間給 `main`。 --- 接下來輸入 `s` 就會進入 `call_echo()`: ```gdb (gdb) s ``` ![Screenshot 2025-07-13 at 1.13.46 PM (1)](https://hackmd.io/_uploads/rk_axaxUxg.png) 此時的 `rbp` 和 `rsp` 都是 `0x2aaaab2aae20`,也就是 `call_echo` 的 stack frame 是 `0x2aaaab2aae20`。 --- 接下來由於會涉及到組合語言層面的東西,所以用 `si` 指令,`si` 可以讓我們一個一個執行組合語言的指令。 輸入 `si` 之後,可以看到接下來++即將++要執行的指令是位在記憶體位址 `0x40193e` 的 `call 0x401905 <echo>` 指令。 ![image](https://hackmd.io/_uploads/H1afW6lIxe.png) :::info x86-64 的 `call` 指令會做以下的事情: 1. 將 `rsp` -= 8 2. 將 `call` 指令的下一個指令的記憶體位址(依此例就是 `0x401943`,也就是 return address)存放到前述 `rsp` - 8 的記憶體位址內 3. 將 `call` 要跳過去的記憶體位址放到 `rip`,依此例即 `0x401905` ::: 輸入 `si`,就會執行 `0x40193e <call_echo+13> call 0x401905 <echo>` 這一個指令。 ```gdb (gdb) info registers rbp rsp rip rbp 0x2aaaab2aae20 0x2aaaab2aae20 rsp 0x2aaaab2aae18 0x2aaaab2aae18 rip 0x401905 0x401905 <echo> ``` - 可看到這時 `rsp` 的值從原本的 `0x2aaaab2aae20` 變成了 `0x2aaaab2aae18`,被扣掉 8 了。 用以下指令查看記憶體位址 `0x2aaaab2aae18` 存了什麼 data: ```gdb (gdb) x/1xg 0x2aaaab2aae18 0x2aaaab2aae18: 0x0000000000401943 ``` - 可以看到存的是 `0x0000000000401943`,也就是 `call_echo` 的 `call` 指令的下一個指令的記憶體位址 - 也就是當要從 `echo` return 到 `call_echo` 時要執行的指令,也就是 return address - 這時 `call_echo` 的 stack frame 範圍就變成 `0x2aaaab2aae20`(stack base)~ `0x2aaaab2aae18`(stack top) --- ![image](https://hackmd.io/_uploads/HysnGagLgg.png) 接下來即將被執行的組合語言指令是 `0x401905 <echo> endbr64`,輸入 `si` 到下一個指令。 --- ![image](https://hackmd.io/_uploads/ByUlX6xUee.png) 接下來要被執行的指令是 `push rbp`,而 `push rbp` 和他的下一條指令 `mov rbp,rsp` 這兩條指令是所謂的 function prologue。 :::info 當進入一個新的 function,一定要執行 function prologue,也就是以下兩個指令: ```asm push rbp mov rbp,rsp ``` - `push rbp`: 1. 先將 `rsp` -= 8 2. 再將 `rbp` 目前的值存放到前述 `rsp` - 8 的記憶體位址內,也就是將前一個 stack frame 的 base,也就是 `call_echo` 的 stack frame 的 base 存放進去 - `mov rbp,rsp`: - 將目前 `rsp` 的值 copy 到 `rbp`,也就是更新目前 stack frame 的 base 到 `rbp` ::: --- 輸入 `si`,就執行 `0x401909 <echo+4> push rbp`。 ![image](https://hackmd.io/_uploads/H13IEkl8gx.png) - 此時使用 `info registers rbp rsp` 查看 `rsp` 的值,可看到 `rsp` 的值從 `0x2aaaab2aae18` 變成 `0x2aaaab2aae10`。 - 使用 `x/1xg 0x2aaaab2aae10` 查看 `0x2aaaab2aae10` 這個記憶體位址裡面存了什麼東西,可以看到存的是 `0x2aaaab2aae20`,也就是前一個 stack frame 的 base。 --- ![image](https://hackmd.io/_uploads/H1VAmag8xl.png) 接下來即將被執行的指令是 `0x40190a <echo+5> mov rbp,rsp`。輸入 `si` 執行這個指令。 ![image](https://hackmd.io/_uploads/SkHvHyx8ex.png) 可以看到 `rbp` 的值變成 `0x2aaaab2aae10`,也就是目前 `echo` 這個 function 的 stack frame 的 base。 --- ![image](https://hackmd.io/_uploads/rJ9bETlLxx.png) 下一個要被執行的指令是 `0x40190d <echo+8> sub rsp,0x10`,也就是將 `rsp` - 16,也就是分配給這個 function 的 stack 的空間。輸入 `si` 執行此指令。 ![image](https://hackmd.io/_uploads/S1eY48ye8xe.png) 可看到 `rsp` 從 `0x2aaaab2aae10` 變成 `0x2aaaab2aae00`。 --- ![image](https://hackmd.io/_uploads/BJ5_Vpx8lx.png) 接下來要執行的幾個指令是: 1. `0x401911 <echo+12> lea rax,[rbp-0x4]` - 將 `rbp` - 4 後的值存放到 `rax` 裡面,而這個 `rbp` - 4 就是 `char buf[4]` 的起始位址 - 用 `print &buf` 指令可看到 `buf` 的起始位址是 `0x2aaaab2aae0c`: ![image](https://hackmd.io/_uploads/Sy0odyxIgx.png) - PS. `lea` 只會計算記憶體位址,不會去存取那個記憶體位址裡面存放的值 2. `0x401915 <echo+16> mov rdi,rax` - 將 `0x2aaaab2aae0c` copy 到 `rdi` - x86-64 Linux 用的 calling convention 是 System V AMD64 ABI(https://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI ),`rdi` 會用來存放 function call 的第一個參數 - 等下會呼叫 `gets`,而 `gets` 的唯一參數是 `char *buf`,所以就是將 `buf` 的記憶體起始位址放到 `rdi` 裡面 3. `0x401918 <echo+19> mov eax,0x0` - 將 `rax` register 清空成 0 4. `0x40191d <echo+24> call 0x404db0 <gets>` - 呼叫 `gets` --- ![image](https://hackmd.io/_uploads/H19-B6e8xx.png) 當正準備執行 `0x40191d <echo+24> call 0x404db0 <gets>` 時,也就是在上述截圖畫面時,先使用以下指令查看幾個記憶體位址內存的值: ![image](https://hackmd.io/_uploads/HytipeeUge.png) | 記憶體位址 | 存放的資料 | | | -------- | -------- | -------- | | `0x2aaaab2aae18` | `0x401943` | return 到 `call_echo` 要執行的下一個指令的位址 | | `0x2aaaab2aae10` | `0x2aaaab2aae20` | `call_echo` 的 stack frame 的 base | | `0x2aaaab2aae08` | `0x4aa108` | garbage(舊 stack 遺留的資料)| | `0x2aaaab2aae00` | `0x49b220` | garbage(舊 stack 遺留的資料)| --- 接下來輸入 `ni`,讓他執行完 `gets`,`gets` 就會將前面輸入的 `123456789012` 從 `buf` 的起始位址開始將這 12 個 character 和一個 null terminator 放到 stack 裡面(所以總共放了 13 個 byte)。 ![image](https://hackmd.io/_uploads/SJnsbZlLel.png) 用跟前面一樣的指令觀察被放了什麼東西到 stack 裡面,可看到: | 記憶體位址 | 執行 `gets` 之前的資料 | 執行 `gets` 後 | | | -------- | -------- | -------- | --- | | `0x2aaaab2aae18` | `0x401943` | `0x401900` | 原本應該是 return 到 `call_echo` 要執行的下一個指令的位址,結果 `43` 被 null terminator 覆蓋了 | | `0x2aaaab2aae10` | `0x2aaaab2aae20` | `0x3231303938373635` | 原本應該要是 `call_echo` 的 stack frame 的 base,結果被更改了 | | `0x2aaaab2aae08` | `0x4aa108` | `0x34333231004aa108` | 從 `buf` 的起始位址開始放入輸入的 characters | | `0x2aaaab2aae00` | `0x49b220` | `0x49b220` | 不受影響| ![image](https://hackmd.io/_uploads/H1UASZl8gg.png) 從 `buf` 的起始位址 `0x2aaaab2aae0c` 查看: - `x/c`:查看指定的記憶體位址存放的 character 是什麼,可看到是 `'1'` - `x/x`:查看指定的記憶體位址一個 byte 的資料,並以 16 進位顯示,可看到是 `0x31`,也就是 character `'1'` 的 ASCII 編號 - `x/s`:從指定的記憶體位址查看 string,直到 null terminator 為止,可看到是 `123456789012` ![image](https://hackmd.io/_uploads/Hy1DXbgLgl.png) 這個例子總共輸入了 12 個 characters 加一個 null terminator,所以總共存了 13 個 characters,就從 `buf` 的起始位址查看連續 13 個 bytes 的資料,即可看到以上截圖的結果。 --- ![image](https://hackmd.io/_uploads/ByOKr6lIll.png) 下一個要執行的指令只是在設定 `puts` 的參數,也就是將 `buf` 的起始位址存放到 `rdi`,然後呼叫 `puts`: 1. `0x401922 <echo+29> lea rax,[rbp-0x4]` 2. `0x401926 <echo+33> mov rdi,rax` 3. `0x401929 <echo+36> call 0x404fb0 <puts>` 這裡連續輸入 4 個 `ni`,直到準備執行 `0x40192f <echo+42> leave`。 --- ![image](https://hackmd.io/_uploads/SkG6Hal8ex.png) 先記錄一下此時的 `rbp`、`rsp`、和 `rip`: ![image](https://hackmd.io/_uploads/HkZdO-eIll.png) :::info `leave` 指令做的事情相當於以下兩個指令在做的事,也就是所謂的 function epilogue: ```asm= mov rsp,rbp ; 將 rbp 的值複製給 rsp pop rbp ; 從 rsp 指向的位置取 8 bytes 放到 base pointer,然後 rsp += 8 ``` 1. `rbp` 目前的值是 `0x2aaaab2aae10`,也就是當前 stack frame 的 base,這個位址存放的資料是上一個 stack frame 的 base 位址 所以當執行了 `mov rsp,rbp`,就會讓 `rsp` 指向 `0x2aaaab2aae10` 2. 執行完 `pop rbp` 會將 `rsp` 指向的記憶體位址所存放的 data 放到 `rbp`,然後 `rsp` += 8 由於記憶體位址 `0x2aaaab2aae10` 存放的 data 在剛剛已經被覆蓋成 `0x3231303938373635`,所以執行完這個指令後 `rbp` 會變成 `0x3231303938373635`,而 `rsp` 會變成 `0x2aaaab2aae18` ::: 輸入 `si`,執行 `0x40192f <echo+42> leave`。 ![image](https://hackmd.io/_uploads/BkmFsWeUle.png) 可看到 `rbp` 跟 `rsp` 變得跟預期一樣。 --- ![image](https://hackmd.io/_uploads/BJoOL6eIxe.png) 接下來要執行的是 `ret` 指令,`ret` 會做的事: 1. 從 `rsp` 指向的位址取出 8 bytes 的資料,放入 `rip` 此時 `rsp` 是 `0x2aaaab2aae18`,而這位址原本存放的是 `0x401943`,也就是 return 到 `call_echo` 後要執行的下一個指令的位址,但因為被輸入的 string 的 null terminator 覆蓋,而變成 `0x401900` 2. `rsp` += 8,所以會變成 `0x2aaaab2aae20` 輸入 `si` 執行 `ret` 後,查看 registers 的值: ![image](https://hackmd.io/_uploads/HkNzT-e8ex.png) 可以看到: - `rsp` 如預期的變成 `0x2aaaab2aae20` - `rip` 也如預期的變成 `0x401900`,所以等下就會去執行位在 `0x401900` 這個記憶體位址的指令(接下來就會開始跳到不該過去的地方,最終造成 Segmentation fault) --- ![image](https://hackmd.io/_uploads/SJEyDTlUxl.png) 位在 `0x401900` 記憶體位址的指令,他會 `jmp` 到記憶體位址 `0x401850` ![image](https://hackmd.io/_uploads/B1GNvpeIxl.png) 跳到 `0x401850` 後,他會一路執行指令直到位在 `0x401880` 的 `ret` 指令 ![image](https://hackmd.io/_uploads/Bk6FAZlUxe.png) 那 `ret` 做的事情是會將 `rsp` 指向的記憶體位址儲存的 data 放到 `rip`,也就是下一個要執行的指令的記憶體位址。 ![image](https://hackmd.io/_uploads/r1HAJheIlg.png) 但此時 `rsp` 是 `0x2aaaab2aae20`,那裡面存的是 `0x2aaaab2aae30`,所以接下來就會去執行位在 `0x2aaaab2aae30` 的指令。 ![image](https://hackmd.io/_uploads/r1ZpDaxLxl.png) 從以上截圖可看到位在 `0x2aaaab2aae30` 的指令是 `0x2aaaab2aae30 shr BYTE PTR [rsi+0x2aaaab2a],1`,一旦執行這個指令後就 Segmentation fault,如下截圖。 ![image](https://hackmd.io/_uploads/r1fFbGgIle.png) 為什麼 `shr BYTE PTR [rsi+0x2aaaab2a],1` 在這裡會造成 Segmentation fault: - 這是 right shift 指令 - 他會將 `rsi` 的值加上 `0x2aaaab2a`,然後去存取這個記憶體位址所存放的資料,再做 right shift 的動作 - 但此時 `rsi` 的值是 `0x0`,所以加上 `0x2aaaab2a` 還是 `0x2aaaab2a`,所以他實際是去存取 `0x2aaaab2a` 這個記憶體位址的資料 - 但我們無法存取 `0x2aaaab2a` 這個記憶體位址,所以就造成 Segmentation fault! - 以下截圖就是用 gdb 檢視 `0x2aaaab2a` 這個記憶體位址的資料,可以看到他說 `Cannot access memory at address 0x2aaaab2a` ![image](https://hackmd.io/_uploads/S1lIdzxUgl.png) - 此外,`0x2aaaab2aae30` 這個記憶體位址存的資料其實也只是==記憶體位址 `0x2aaaab2aaed0`==(由下截圖可知),但因為這樣亂跳的結果導致 CPU 把它當成組合語言指令 `shr BYTE PTR [rsi+0x2aaaab2a],1` ![image](https://hackmd.io/_uploads/S1TlNGeUxe.png) ### 圖文版本說明 以下是前述流程的圖文說明版本~ ![001](https://hackmd.io/_uploads/rJ6vnvWLlx.png) ![002](https://hackmd.io/_uploads/H1mu3w-Ulg.png) ![003](https://hackmd.io/_uploads/H1kY3Pb8eg.png) ![004](https://hackmd.io/_uploads/rk4thDZLel.png) ![005](https://hackmd.io/_uploads/BJtY2P-Ilx.png) ![006](https://hackmd.io/_uploads/BJptnD-8xl.png) ![007](https://hackmd.io/_uploads/HkZ5hP-8le.png) ![008](https://hackmd.io/_uploads/B1HchD-Igl.png) ![009](https://hackmd.io/_uploads/HJtc3DZLgg.png) ![010](https://hackmd.io/_uploads/Hkpc2wZIgg.png) ![011](https://hackmd.io/_uploads/rJWo3DbUge.png) ![012](https://hackmd.io/_uploads/ByIsnwWLeg.png) ![013-1](https://hackmd.io/_uploads/HylgTw-8xx.png) ![014](https://hackmd.io/_uploads/S1u3nDbLxg.png) ![015](https://hackmd.io/_uploads/SJ223wW8gx.png) ## 透過 Buffer Overflow 更改 Return Address 接下來做個小測試,想透過 buffer overflow 漏洞來更改 return address。 ![image](https://hackmd.io/_uploads/Syq1Yg-Igg.png) 當要從 `echo` return 時,想要讓他回到 `main` 的 `0x40194e` 這個位址,所以要將 `0x40194e` 透過 buffer overflow 機制寫到存放 return address 的 `0x2aaaab2aae18` 這個記憶體位址。 那從前面的例子可以知道輸入 12 個 characters 後,是==第 13 個 character==,也就是 null terminator,==會覆蓋到 return address==。 所以要==先輸入 12 個隨機的 characters(ex. `AAAAAAAAAAAA`),再接著將 `0x40194e` 這個記憶體位址送進去==,那由於是 ==Little Endian==,所以就是輸入 `\x4e\x19\x40\x00\x00\x00\x00\x00`。 所以將一開始的指令改成輸入以下指令: ```shell $ ( printf 'AAAAAAAAAAAA\x4e\x19\x40\x00\x00\x00\x00\x00'; sleep 1 ) | qemu-x86_64 -g 1234 ./bufdemo-x86 ``` ![image](https://hackmd.io/_uploads/ryi2ilZLxe.png) 當執行到以上截圖的步驟時,也就是已經將輸入的 data 都放到 `buf` 裡面了,這時來查看幾個記憶體位址裡面被放了什麼東西: ![image](https://hackmd.io/_uploads/rJVcjxWLle.png) - 原本存放 return address 的位址 `0x2aaaab2aae18`,可看到他變成存放 `0x40194e` - 從 `buf` 的起始位址 `0x2aaaab2aae0c` 可看到存入了 12 個 `0x41`,也就是 `'A'` 的 ASCII 編號 - 下圖就是目前 stack 內的狀況: ![return_addr](https://hackmd.io/_uploads/rkQRLd-Igx.png) ![image](https://hackmd.io/_uploads/SkRr6eZIlg.png) - 這時用 `info symbol $pc` 可知道目前在 `echo + 43` - 輸入 `si` 之後再用 `info symbol $pc`,就變成是在 `main + 8 in section .text` - ![image](https://hackmd.io/_uploads/BJmTTeb8ll.png) 也可看到 `rip` 變成 `0x40194e`,也就是透過 buffer overflow 插入的 return address - 繼續執行下去的話,他又會再進入 `call_echo`,然後又進入 `echo` 裡面 ## 從組合語言角度觀察有/無使用 Stack Canary 的差異 ### Stack Canary Stack canary(金絲雀)是一個用來偵測是否有 buffer overflow 事件的一種機制。 會在 buffer 的上方放一個值,也就是 canary。當把東西放到 buffer 後,會檢查 canary 的值有沒有被更改過。若有被更改,就代表有 buffer overflow 發生。 ### 編譯出有 Stack Canary 保護機制的執行檔 現在的 GNU 編譯器預設都會加入 stack canary 這個保護機制。若要關閉這個機制,則編譯的指令要加入 `-fno-stack-protector` 這個 option。 前面的例子在編譯時,就有關閉 stack canary 這個保護機制。接下來要編譯出有使用 stack canary 的可執行檔: ```shell $ 86_64-linux-gnu-gcc -m64 -g -z execstack -no-pie -static bufdemo-nsp.c -o bufdemo-x86_w_stack_canary ``` 反組譯產生組合語言: ```shell $ x86_64-linux-gnu-objdump -d bufdemo-x86_w_stack_canary -M intel > disasm_x86-64_NASM_w_stack_canary.txt ``` ### 有/無 Stack Canary 的組合語言差異 觀察兩種版本組合語言到差異: ![asm_stack_canary](https://hackmd.io/_uploads/B1ApZy-8xg.png) - 上圖左側是沒有使用 stack canary 機制的 `echo` 的組合語言程式碼 - 右側則是有使用 stack canary 的 `echo` 的組合語言程式碼,主要的差異用藍字標示起來 ![canary_asm](https://hackmd.io/_uploads/HyxmBkW8lx.png) ![image](https://hackmd.io/_uploads/H15GG7f8ge.png) - 當執行完了 `0x40191a` 這個指令,也就是將 canary 放到 `rbp-0x8` 的位址後,用以上指令查看,可以看到這次的 canary 的值是 `0xfa2ca19b4a74ec00` - 也可以用 `x/gx $fs_base + 0x28` 指令查看從系統中讀取到的 canary 值是多少~ - PS. 每次執行程式的 canary 值都是不一樣的喔! ![canary_mem](https://hackmd.io/_uploads/HJEmb_-8ex.png) 上圖是此例有/無使用 stack canary 的記憶體空間示意圖。 ![image](https://hackmd.io/_uploads/BJ3EVXGLxl.png) 若偵測到 canary 有被修改,程式就會結束,並且印出 `stack smashing detected` 的訊息。 :::info Why Stack **CANARY**? 為什麼要叫做 Stack Canary(金絲雀)呢 🤔?為什麼不能是 Wombat(袋熊)?Quokka(短尾矮袋鼠)?或其他動物呢?難道其他動物沒有比金絲雀更可愛更值得這個殊榮/頭銜嗎 😡?! 這個名稱其實是有由來、有典故的~! 因為在 19~20 世紀的煤礦坑裡,最可怕的意外之一就是一氧化碳中毒。一氧化碳無色無味,人類察覺不到。而金絲雀對這些有毒氣體極度敏感,他們會比人類更早出現症狀,所以當時的礦工就會帶金絲雀一起到礦坑裡面,當金絲雀出現異常反應,礦工就能知道有有毒氣體洩漏、而能及早撤離。 這種使用金絲雀做 sentinel animal(哨兵動物)的用法,之後也衍生出更多不同的制度。像是使用哨兵雞偵測是否有禽流感,或是在實驗動物飼養環境內使用哨兵鼠偵測環境中是否存在傳染性病原。 所以衍伸到 buffer overflow 這個例子,也是利用寫入的 canary 值來偵測是否有 buffer overflow 事件,所以就叫做 stack canary~ https://en.wikipedia.org/wiki/Sentinel_species :::