ROP 繞過 NX 技術--pwntools--shellcode 資料 入門基礎

最近做pwn頻繁地遇到開啓了 NX 保護的二進制程序,繞過 NX 保護最常用的方法就是 ROP。網絡上關於 ROP 的原理和 CTF 這類題目的文章較多,但是這些文章要不就是給出了一堆代碼,要不只是單純地講解 CTF 題目和 ROP 原理(寫的還不詳細),也缺乏系統性地講解這類 CTF 題目的解題步驟,這通常會阻礙初學者的學習步伐和熱情。

函數調用約定

函數的調用約定就是描述參數是怎麼傳遞和由誰平衡堆棧的,以及返回值的。常見的四種調用約定如下:

_stdcall (windowsAPI 默認調用方式)
_cdecl (c/c++默認調用方式)
_fastcall
_thiscall

每種約定的具體內容你可以自行去 Google 學習,但是在 CTF 中你只需要打開 IDA 閱讀二進制的彙編代碼就可以清晰地看懂程序的參數傳參順序 、棧傳參還是寄存器傳參 、函數內平衡堆棧還是函數外 。用 IDA直接閱讀程序的彙編代碼非常關鍵——第一,出題人很可能會在彙編中做些手腳;第二,國際大佬都是不用 F5 的。

清楚地掌握常用輸入輸出函數在處理字符串上的區別,尤其是在處理空白字符,\n,\0 上的異同

scanf("%s",str):匹配連續的一串非空白字符,遇到空格、tab 或回車即結束,並在字符串結尾添加 \0,這些空白字符會留在緩衝區;字符串前的空白字符沒有存入 str,只表示輸入還未開始。
gets(str):可接受回車鍵之前輸入的所有字符,並用'\0'替代 '\n'. 回車鍵不會留在輸入緩衝區中。
printf("%s",str)和puts(str)均是輸出到'\0'結束,遇到空格不停,但puts(str)會在結尾輸出'\n'
read 和 write 參數雖然較多但是相比前面的函數非常可控,嚴格按照指定的字節數操作,並且能處理任意字符

什麼是 NX 保護

最早的緩衝區溢出攻擊,直接在內存棧中寫入 shellcode 然後覆蓋 EIP 指向這段 shellcode 去執行,所以 NX 即 No-eXecute (不可執行) 的基本原理是將數據所在內存頁標識爲不可執行,當程序溢出成功轉入 shellcode 時,程序會嘗試在數據頁面上執行指令,此時 CPU 就會拋出異常,而不是去執行惡意指令。

ROP 繞過 NX 原理

這裏引用別人的圖片和說明。最基本的 ROP 攻擊緩衝區溢出漏洞的原理:(圖裏基於 x64 平臺,注意 x64 使用 rdi 寄存器傳遞第一個函數參數)

在這裏插入圖片描述

原理:

①當程序運行到 gadget_addr 時(rsp 指向 gadget_addr),接下來會跳轉到小片段裏執行命令,同時 rsp+8(rsp 指向 bin_sh_addr)
②然後執行 pop rdi, 將 bin_sh_addr 彈入 rdi 寄存器中,同時 rsp + 8(rsp 指向 system_addr)
③執行 return 指令,因爲這時 rsp 是指向 system_addr 的,這時就會調用 system 函數,而參數是通過 rdi 傳遞的,也就是會將 /bin/sh 傳入,從而實現調用 system(’/bin/sh’)

所以縱觀我們整個 ROP 利用鏈的環節,有三個很重要的問題需要解決:

怎麼去搜索這樣的 gadget_addr,當然不止一次 pop,還可以多個 pop 加 ret 組合等等,看你希望怎麼去利用
如何得到 '/bin/sh\0' 這樣的字符串,通常程序沒有這樣的字符串
如何得到 libc 中 system 實際運行的地址(libc 的基地址+system 在 libc 中的偏移地址)

其實還有一個問題很重要,就是確定你的返回地址 return_addr 前面緩衝區到底有多大,這樣才能準確的實現緩衝區溢出覆蓋。做法有二種:一是直接從 IDA 的 F5 源碼和彙編計算得到;二是使用 GDB 動態調試一下

ROP 繞過 NX 具體步驟

下面分別就上面三個問題給出具體的解決方案,最終完成整個 ROP 繞過 NX 保護的攻擊。

(1)如何搜索你需要的 gadget_addr?

gadget_addr 指向的是程序中可以利用的小片段彙編代碼,在上圖的示例中使用的是 pop rdi ; ret ;

對於這種搜索,我們可以使用一個工具:ROPgadget

項目地址:https://github.com/JonathanSalwan/ROPgadget.git

(2)bin_sh_addr 指向的是字符串參數:/bin/sh\0。

首先你需要搜索一下程序是否有這樣的字符串,但是通常情況下是沒有的。這時候就需要我們在程序某處寫入這樣的字符串供我們利用。我們需要用 IDA 打開程序,看左邊函數窗口程序加載了下面哪些函數:

read、scanf、gets

通常我們將這個字符寫入 .bss 段。.bss 段是用來保存全局變量的值的,地址固定,並且可以讀可寫。通過 readelf -S pwnme 這個命令就可以獲取到 bss 段的地址了(ida 的 segements 也可以查看)。

(3)system_addr 則指向 libc 中的 system 函數 。

可以先查看一下程序本身有木有可以利用的子函數,這樣可以大大減少 EXP 開發時間。因爲從 libc 中使用函數,需要知道 libc 的基地址。通常得到 libc 基地址思路就是:

泄露一個 libc 函數的真實地址 => 由於給了 libc.so 文件知道相對偏移地址 => libc 基地址 => 其他任何 libc 函數真實地址

泄露一個 libc 函數的地址需要使用一個能輸出的函數,同樣用 IDA 打開程序,看左邊的函數窗口程序加載了哪些函數可以利用:

write、printf、puts

特別注意:由於 libc 的延遲綁定機制,我們需要選擇已經執行過的函數來進行泄露。你需要找到函數的 plt 地址,找到 jmp 指向的那個地址纔是我們需要泄露的(參考後文 classic)。

ROP 繞過 NX 實現框架

總結上面的內容,一個基本的的 ROP 繞過 NX 利用的流程是 :

在同一次遠程訪問中,我們首先通過泄露一些函數真實地址結合相對偏移得到 system 函數的真實地址
執行完上述步驟後,我們需要控制程序再次執行到緩衝區溢出漏洞點
再利用緩衝區溢出漏洞,寫入 "/bin/sh\0" 字符串到 .bss,並觸發 system 執行

網上很多人會使用 pwntool 的 DynELF 作爲泄露的工具,但是這種方法經常會遇到各種問題,尤其是隻能利用 puts 函數泄露的時候。另外的做法是自己編寫二個 payload 完成上述的利用流程,第一個 payload 去完成泄露並再次到漏洞函數執行,第二個 payload 執行寫入 “/bin/sh\0” 字符串到 .bss 並讓程序調用 system ,這樣得到 shell。

我強烈推薦後面的雙 payload做法,也是我去解決後文 classic 題目的做法。因爲我最初明白 ROP 步驟中的泄露原理還是通過鑽研網上的 DynELF leak 案例搞懂的,下文會先幫助讀者分析明白 DynELF leak 函數編寫的原理,再討論我推薦的做法。(當然 pwntool 有 ROP 模塊可以直接用,但是這種不是萬能,處理一些不常規的題目時效果不好,也不利於學習 ROP 的原理)

剛接觸pwn沒多久,有些東西理解的還不夠,下面負載學習的查找的資料:

pwntools:https://blog.csdn.net/qq_29343201/article/details/51337025http://brieflyx.me/2015/python-module/pwntools-intro/

shellcode: http://blog.nsfocus.net/easy-implement-shellcode-xiangjie/

感覺對入門學習pwn有很好的理解,可以參考下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章