本文為看雪論壇優秀文章
看雪論壇作者ID:sudozhange
當學習主動安全認證專家(OSCE)(連結:https://www.offensive-security.com/ctp-osce/)認證時,我花費了大量的時間用於研究如何編寫自定義shellcode。本文是我計劃要發布的一系列博文裡的第一篇,這個系列詳細介紹我在準備認證過程中所學習到的技術。
本文的重點是描述如何找到EIP/RIP以及找到後做什麼。OSCE專注於32位系統,作為繼續學習的一部分,我會研究並記錄下在64位系統上的方法。這超出了OSCE的需求,但這是我繼續學習過程的一部分。
在本文中,我不會介紹到所有的可能找到EIP和RIP的方法。我會介紹一些我熟悉並已經研究過的方法。如果你想找到更多的方法,我鼓勵你開始你自己的研究並開始做自己的學習總結。在這裡你看到的是我的學習總結,我希望它會在某些方面對你有幫助。
我知道這些主題多年來已經被多次提到,也沒有新的東西,但重點不是提出新的東西,而是在學習總結中提升自己,並且還可以幫助到剛開始學習的人。
從使用x86彙編(32位)指令定位EIP的方法開始。有兩種方法可以用,根據你可能會遇到的情況和限制,兩種方法可能都有用。兩種方法都可以找到EIP。我要介紹的第一種方法比第二種方法小一個字節。兩種方法都可以達到完全相同的目的,存儲EIP的值到EAX寄存器中。兩種方法都沒有空(0x00)字節。之所以使用一種而不是另一種,是因為您可能會遇到大小或字符限制。該方法最先由Aaron Adams在漏洞開發(Vulnerability Development)郵件列表中提出.他使用的這種方法利用x87浮點單元(FPU)寄存器來獲取EIP值。以下的基礎彙編代碼將EIP的值存儲到EAX寄存器中。
[SECTION .text]
BITS 32
global _start
_start:
fldz
fnstenv [esp-0x0C]
pop eax
add al, 0x07
執行fldz指令來激活FPU寄存器。該指令將常數值+0.0壓入FPU寄存器棧(ST(0))。在這個過程中,FPU寄存器被初始化,當前的EIP值被存儲到FPU指令指針偏移(FIP)寄存器中。圖 1描述了FPU寄存器的結構。該表拷貝自《Intel(r) 64 和 IA-32 架構軟體開發者手冊,卷1:基礎架構PDF》.
圖1:內存中保護模式x87 FPU狀態圖片(32位格式)下一條指令存儲FPU寄存器在特殊的地址。因此使用fnstenv指令將EIP的值存儲在便宜0x0C(12)的FIP寄存器中。ESP-0x0C的目標是固定的,所以FIP的值會存儲在當前ESP地址處。接下來,該值會從棧中彈出,並存儲到EAX寄存器中。由於在將EIP存儲到FPU的FIP寄存器和彈到EAX的這段時間已經執行了幾個字節的指令,所以需要調整該值來表示當前的EIP的值。用AL寄存器加0x7(7)完成此操作。AL寄存器被用來避免使用空字節。
測試nasm method1.asm -o method1.asm
char code[] = "\xD9\xEE\xD9\x74\x24\xF4\x58\x04\x07";
int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}
i686-w64-mingw32-gcc-win32 method1.c -o method1.exe -fno-stack-protector -no-pie -m32
運行最終的PE文件在你喜歡的調試器中,並看它是如何運行的。你需要將.data 節標記為可執行。你可以使用調試器來實現或者使用如LordPE這樣的工具。如果你不標記 .data 節為可執行,你會在執行你的shellcode時發生訪問違例。我需要在PE文件中搜索call eax指令,並設置斷點,第一個調用到EAX的會是shellcode。如果一切順利,EAX會在執行add al,7指令時指向EIP,如圖 2。
圖 2:EAX指向EIP感謝來自@TheColonial的建議,對EAX寄存器使用減法而不是add al指令可以再一些情況下避免錯誤,我添加了該替代方法。他正確的指出的問題,是通過對AL進行加法運算,如果加法導致了進位,EAX就會指向錯誤的地址。例如,如果AL寄存器包含大於或等於0xF9的任何數值,加上0x07後,進位的1會被丟棄掉。例如,如果EAX是0x001234F9,我們把0x07加到AL寄存器(0xF9)後,結果會是0x00123400而不是我們需要的0x00123500 。為了避免這個問題,並且避免空字節,可以讓EAX減去一個負值。基本的數學運算,減去一個負值等於加上它(的絕對值)。簡單卻有效。正確的代碼如下,還避開了空字節,並且只比之前大了1位元組:
[SECTION .text]
BITS 32
global _start
_start:
fldz
fnstenv [esp-0x0C]
pop eax
sub eax, -0x07
在圖 9中我們可以看出建議的代碼正常運行並且會比原來的代碼更加可靠。
圖 9 EAX指向EIP第二種方法僅比第一種方法長一個字節,也完成了將EIP的值存儲到EAX中的目標。我從 Phrack issue 62,Phile 7標題為History and Advances in Windows Shellcode中學到的這種方法。這篇文章由SK Chong所寫。為了獲得EIP這種方法使用了一系列的跳轉和調用,最終EIP的值被存儲在EAX中。SK Chong文章的原始文件已經找不到了,但是,我找到了其中的一些。在他的例子中,它使用了db項來硬編碼了那些跳轉和調用。我要提供的版本讀起來更容易並且以及更容易使用nasm彙編。
[SECTION .text]
BITS 32
global _start
_start:
jmp label2
label1:
jmp getEIP
label2:
call label1
getEIP:
pop eax
上面的代碼執行了跳轉到label2,一個call指令調用了label1。當call被執行時,返回地址,即下一條指令的地址,被壓入棧中。執行,隨後跳轉到getEIP,會彈出返回地址到EAX。不要小題大作(No muss, no fuss.)。
測試你可以使用測試方法 1的測試方法來測試方法2.當執行完,結果看起來如圖 3。
圖 3:EAX指向EIP查看是否32位架構中運行的方法在64位世界中是否運行是一個好的開始。如果只需做些微修改,為什麼還要重新造輪子?
嘗試1
使用FPU指令可以運行,因為指令在64位下仍然生效。首先,找出未對32位模式運行下的指令進行修改或微小修改發生的情況。要做到這點,我使用x64dbg並使其執行notepad.exe,運行直到命中EntryPoing。使用Assemble指令,我手動替換了開始的一些指令位32-bit的指令。這樣做的結果如圖 4。這裡有一些需要注意。首先,67:出現在fnstenv指令前。根據Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z 章節2.1.1,67h前綴是地址大小覆寫。這一定會出現的,因為我之前使用的是ESP寄存器而不是RSP。第二,我不能使用POP EAX指令,我需要使用POP RAX。
圖 4:使用FPU尋找RIP,嘗試1運行該序列會部分成功(見圖5)。當加法執行時,RAX指向RIP之前的指令。這是必須使用67h地址大小前綴的原因。使用0x08替代0x07就能很容易的使之平衡。繼續,自己嘗試下,看看它是否有效。
圖 5:使用FPU指令尋找RIP,嘗試1的結果
編寫彙編
既然已經證明了使用FPU寄存器來獲取RIP的值是可行的。我的下一步是看看是否能夠寫一些彙編代碼,可以在調試器中手工輸入命令可以使之平衡。經過一些調整後,這是我想出的代碼:
SECTION .text]
BITS 64
global _start
_start:
fldz
fnstenv [rsp-0x0C]
pop rax
add rax, 0x07
當使用Nasm彙編時,上面指令中的代碼結果如圖 6。為了測試那些指令,我對用x64dbg打開的64位進程的第一條指令做二進位粘貼。如你所看到的,在add rax,0x07指令前有了新的48h前綴。根據Intel® 64 and IA-32 Architectures Software Developer’s Manual, Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D and 4 章節3.7.2.1,這是一個REX前綴,允許指令尋址8位寄存器。彙編器可能夠以這種方式丟到空字節。而且,由於當使用RSP而不是ESP時,對於fnstenv指令,地址大小前綴是非必須的,所以,RAX加上0x07位元組的結果是RAX包含了RIP的地址。
圖 6:使用FPU'指令尋找RIP,終結在開始測試該方法前,我想做些微小的修改。我想有必要將EAX替換成RAX。這種跳轉和調用和手工幹擾到調試器中有一些不同,所以我選擇先開始編寫一些彙編代碼。下面的代碼就是我選擇用來最開始嘗試的:
[SECTION .text]
BITS 64
global _start
_start:
jmp label2
label1:
jmp getEIP
label2:
call label1
getEIP:
pop rax
執行了使用彙編的代碼覆蓋的notepad的入口點的結果並可以在圖 7中看到。這種方法依舊沒有空字節,長度上僅僅10個字節。在64位架構上獲取了兩種方法後,我想看看是否有更好的方式。當搜索時,我想到了一篇由 Booze | Allen | Hamilton發表的文章,詳細的描述了由Rapid7 在Metasploit中實現的方法,該方法使用LEA指令來獲取RIP。經過一番嘗試,在使用Nasm命令行時,我想出了下面的代碼,可以復現Metasploit使用的方法:
[SECTION .text]
BITS 64
default rel
global _start
_start:
lea rax, [_start]
當使用Nasm並使用一下標記彙編時,結果代碼沒有空字節,並僅僅7位元組長。-O0標記告訴Nasm不要執行優化。如果你沒有使用該標記,操作碼會包含空值。自己嘗試,並看看是如何功能做的:
nasm -O0 method3.asm -o method3.bin
圖 8:使用加載有效地址(LEA)指令尋找RIP,終結我已經詳細描述了幾種根據目標架構尋找EIP或RAX的方法。如果希望執行其他與shellcode交互的操作,動態獲取這些值是非常重要的。由於ASLR無法動態查找EIP/RIP,所以解碼,複製或者其他修改你的代碼的操作是不可能的。在之後的文章,我會繼續以此為基礎。本系列的最後一篇文章將會編寫一個完全自定義的shellcode,使用Intel x64彙編中的scatch編寫。感謝您花費時間閱讀我的博客!我希望你能夠從中學習並可能啟發你自己學習更多。https://blog.xenoscr.net/Finding-EIP/
看雪ID:sudozhange
https://bbs.pediy.com/user-703263.htm
*本文由看雪翻譯小組 sudozhange 編譯,lipss校對。
好書推薦