遠端執行緒注入

2022-09-02 21:01:27

第一節 前置知識

  • 提起遠端執行緒注入,大家有可能會理解為我在廣西,你在北京,我注入你的執行緒。其實並不是這個樣子。
  • 系統在每次執行一個exe 程式的時候系統會預設分配一個4G 的地址空間,給這個exe 程式。
  • 然而,我們的系統有16G、32G等等。那豈不是隻能執行幾個exe 程式了?
  • 其實我們在給exe 程式分配地址空間的時候,是一個虛擬地址空間。
  • 通過對映的方式,對映到我們的真實機上。
  • 每個exe 程式之間是不能互相存取的。比如QQ 和 微信不能互相存取。如果可以存取,就會非常不安全。

第二節 注入初始

DLL檔案,是動態連結庫,我們執行程式的時候最後呼叫的都是這個DLL檔案。

每一個程序中都會有多個執行緒。
遠端執行緒注入,簡單來說,就是指我們通過一個工具或者其他的方法,在一個指定的程序中,開闢一個執行緒的記憶體地址空間,然後用來執行載入我們的DLL檔案,或者是我們想做的事情。

第三節 注入原理

  • 我們的程式在執行的過程中,都會建立一個程序、執行緒,然後通過程序執行緒載入DLL檔案,從而執行我們想要的功能。
  • 在我們的系統程式中, 有兩個DLL程式就是Kernel32.dll 和user32.dll 。這兩個DLL 是在大部分程式上都會呼叫的DLL。
  • 其中Kernel32.dll 檔案中有一個LoadLibraryW函數。這個函數是應用程式呼叫動態連結庫時的函數。
  • 為什麼使用這兩個函數呢?
  • 因為同一個DLL,在不同的程序中,不一定被對映(載入)到同一個記憶體地址下
    有的載入的是同一個DLL檔案的A函數
    有的載入的是同一個DLL檔案的B函數
  • 但是Kernel32.dll 和user32.dll 是例外的,他們總是被對映到程序的記憶體首選地址。
  • 因此在所有使用這個DLL檔案的程序中,這兩個DLL的記憶體地址是相同的

第四節 注入思路

思路一:

  • 獲取一個目標執行緒的控制程式碼
  • 在我們的程序中得到LoadLibrary 函數的地址,因為載入時這個DLL檔案的記憶體地址相同,所以這個地址也是目標程序中的地址
  • 傳入我們想要注入進去的DLL的地址
  • 開啟一個執行緒(開闢一塊記憶體地址空間)
  • 讓這個執行緒,在我們想要注入的目標程序中工作,這個執行緒的作用就是使用LoadLibrary 這個函數載入我們想注入的DLL

思路二:

  • 提升程序的許可權:因為我們要將程式注入到別的程序中,所有我們的許可權一定要夠,比如說我們的系統有system使用者和administrator使用者等
  • 檢視我們獲得到的特權資訊是什麼
  • 調節程序許可權
  • 查詢視窗,就是獲得指定程式的程序,可以理解為就是獲取視窗控制程式碼
  • 根據視窗控制程式碼獲取程序的PID(Process ID)
  • 根據PID獲取程序控制程式碼。由於PID只是一個程序的序號,不夠強大,所以我們需要獲取一個更加強大的控制程序的東西,叫做程序控制程式碼。這個程序控制程式碼可以控制exe的關閉、暫停、執行等等行為。
  • 根據程序控制程式碼在指定的程序中申請一塊記憶體地址空間。拿到程序控制程式碼後,就可以對exe進行操作了。由於我們想要學習的程序注入,所以演示程序注入
  • DLL的路徑寫入到遠端程序中
  • 在遠端程序中開闢一個執行緒

第五節 專案實戰

好了,經過上面的學習,我們對遠端執行緒注入有了一個基本的瞭解。雖然有了瞭解,但是還是對遠端執行緒注入的過程、原理以及使用有一些模糊,不太理解。(個人學習過程中的感悟)
我們需要配合一些遠端執行緒注入的實戰來進一步瞭解

專案一:
要求:編寫一個程式,在程式中,指定注入的DLL的檔案的路徑以及被注入程序的資訊。當我們點選注入後,就可以將DLL程式注入到程序中,從而執行我們注入的DLL檔案

成果展示:

程式碼展示:

  1. 步驟一獲取我們想要獲取許可權的鎖

	//1. 提升程序的許可權
	//   因為我們要將我們寫的程式注入到別的程序之中,所以我們的許可權一定要夠
	//   比如作業系統有system\administrator使用者
	/*
		函數簡介:
			OpenProcessToken:我們想要提升許可權,首先要進入到提升許可權的空間中,相當於那一把鑰匙,開啟提升許可權的盒子。
			GetCurrentProcess:獲取我的程序。得到我的自己的程序的鎖。
			TOKEN_ALL_ACCESS:開啟所有的許可權。開啟讀寫等等所有許可權
	*/

	HANDLE hToken;//將獲取到的「鑰匙」儲存到這個變數。
	// || OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken);
	//如果返回的結果是FALSE,就是獲取失敗,提示獲取鑰匙失敗
	if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)){
		MessageBox(L"開啟程序,存取令牌失敗");
		return;
	}

	//==================執行到這裡就說明是拿到了鎖==================

  1. 檢視程序裡面的特權資訊
        //2. 檢視程序裡面的特權資訊
	/*
		LookupPrivilegeValue:檢視盒子,去看一下盒子裡面的資訊
		引數一:系統特權名字
			NULL:檢視本機系統
		引數二:主要看什麼特權
			SE_DEBUG_NAME:偵錯許可權
		引數三:將系統的許可權賦值給變數
			luid
	*/
	//將獲取到的許可權,賦值給一個變數
	LUID luid;
	// || LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&luid);
	//判斷是否獲取成功
	if (FALSE == LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)){
		MessageBox(L"檢視程序裡面的特權資訊失敗。");
		return;
	}

	//==================執行到這裡就說明是獲取到了許可權==================
  1. 調節程序的許可權
        //3. 調節程序許可權
	/*
		AdjustTokenPrivileges:
			引數一:拿著鑰匙hToken,第一步獲取到的
			引數二:是否禁用所有的特權。我們使用的是不禁用
	*/
	//定義一個新的特權,用來接收函數調節後的許可權。
	TOKEN_PRIVILEGES tkp;
	tkp.PrivilegeCount = 1;//特權陣列的個數為一個
	tkp.Privileges[0].Attributes + SE_PRIVILEGE_ENABLED;//因為只有一個元素,所有陣列就是0
	tkp.Privileges[0].Luid = luid; //將獲得到的許可權賦值給這個特權。
	if (FALSE == AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL)){
		MessageBox(L"調節程序許可權失敗");
		return;
	}


	//==================執行到這裡就說明是已經調節了許可權==================
  1. 查詢視窗(就是獲取指定應用程式的程序)
	//4. 查詢視窗(就是獲取指定應用程式的程序)
	/*
		FindWindow:
			引數一:Notepad,程序的類,一個應用程式的類是一樣的
			引數二:就是標題
	*/
	//如果找到了,這個變數就有值了
	HWND hNotepader = ::FindWindow(L"Notepad",L"新建文字檔案.txt - 記事本");
	//判斷這個視窗是否開啟
	if (hNotepader == NULL){
		MessageBox(L"沒有開啟記事本");
		return;
	}

	//==================執行到這裡就說明是已經獲取到了應用程式的視窗==================
  1. 獲取程序PID(Process ID程序ID)
	//5. 獲取程序PID(Process ID程序ID)
	/*
		GetWindowThreadProcessId:函數就是根據視窗控制程式碼,獲取PID
			引數一:傳入一個視窗的控制程式碼
			引數二:傳入接收穫取到的PID的變數
	*/
	DWORD dwPID = 0;
	GetWindowThreadProcessId(hNotepader,&dwPID);
	if (dwPID == 0){
		MessageBox(L"獲取程序PID失敗");
		return;
	}


	//==================執行到這裡就說明是已經獲取到了程序PID==================

  1. 根據PID(程序的序號)獲取程序控制程式碼
	//由於PID只是一個程序的序號,不夠強大,所以我們需要獲取一個更加強大的控制程序的東西,叫做程序控制程式碼
	//這個程序控制程式碼可以控制exe的關閉、暫停、執行等等行為

	//6. 根據PID(程序的序號)獲取程序控制程式碼
	/*
		OpenProcess()函數:開啟一個程序
			引數一:以所有(最大)的許可權開啟,就是我們可以執行任何許可權
			引數二:是否可以繼承父程序的環境變數,一些屬性,FALSE是不繼承
			引數三:根據指定的PID開啟一個程序
		RETURN:
			開啟成功會返回一個程序控制程式碼
	*/
	//記事本的程序控制程式碼
	HANDLE hNotepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
	//判斷記事本的程序控制程式碼是否成功
	if (hNotepad == NULL){
		MessageBox(L"開啟程序失敗");
	}

	//==================執行到這裡就說明是已經獲取到了程序控制程式碼==================
  1. 因為每個程序中,有4G的虛擬地址空間,在遠端程序中申請一小塊記憶體空間
	//拿到程序控制程式碼後,就可以對exe進行操作了。
	//由於我們想要學習的程序注入,所以演示程序注入

	//7. 因為每個程序中,有4G的虛擬地址空間,在遠端程序中申請一小塊記憶體空間
	/*
		VirtualAllocEx()函數:專門在遠端程序中進行記憶體申請的
			引數一:指定在哪個程序中申請(根據程序控制程式碼)
			引數二:指定申請的位置,NULL是指不指定那一塊,隨便給一塊地址就可以
			引數三:指定申請的大小,0x1000:4096個位元組
			引數四:申請一塊實體地址,物理記憶體用來儲存虛擬記憶體。
			引數五:讓這個空間可讀可寫可執行。就是我們可以進行操作
		RETURN:
			返回一個地址空間
	*/
	LPVOID lpAddr = VirtualAllocEx(hNotepad, NULL, 0x0100, MEM_COMMIT,PAGE_EXECUTE_READWRITE);
	//判斷申請的空間是否成功
	if (lpAddr == NULL){
		MessageBox(L"在遠端程序中申請記憶體是否成功");
	}



	//==================執行到這裡就說明是已經獲取到了遠端程序的記憶體空間==================
  1. 將DLL路徑寫入遠端程序中
	//8. DLL的路徑寫入到遠端程序中
	/*
		WriteProcessMemory()函數:
			引數一:寫入到指定的程序中
			引數二:寫入到指定的申請的地址空間中
			引數三:將指定的DLL檔案的路徑寫入進去
			引數四:指定的DLL檔案大小,多少位元組
			引數五:實際寫入到多少個位元組,NULL不關注,只關注當前就可以
		RETURN:
			失敗返回FALSE
			成功放回TRUE
	*/
	//指定我們需要注入的DLL的檔案路徑
	TCHAR szDLLPath[] = L"C:\\Users\\lenovo\\Desktop\\123.dll";

	if (FALSE == WriteProcessMemory(hNotepad, lpAddr, szDLLPath, sizeof(szDLLPath), NULL)){
		MessageBox(L"在遠端程序中寫入資料失敗");
		return;
	}
	/*
		GetModuleHandle()函數:能夠獲得我們Kernel32.dll的控制程式碼
			引數一:獲取指定名字的控制程式碼
		GetProcAddress()函數:返回一個指定函數的地址
			引數一:是一個獲取到的dll
			引數二:是一個指定的函數


		Kernel32.dll是一個核心的動態庫,所有的exe程序都載入了這個動態連結函數
		記事本也載入了Kernel32.dll
		但是所有的dll動態連結函數在動態連結庫中都是隻有一份
		不同的exe程式呼叫的dll檔案都是隻有一份
		也就是說所有的exe都載入了Kernel32.dll檔案
		既然是共用的,那麼dll裡面的函數也是共用的

	*/
	//GetModuleHandle(L"Kernel32.dll");
	//GetProcAddress(GetModuleHandle(L"Kernel32.dll"),"LoadLibraryA");//窄字元
	//可以理解為返回一個函數指標
	PTHREAD_START_ROUTINE pfnStartAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "LoadLibraryW");//寬字元
	//LPTHREAD_START_ROUTINE *pfnStartAddr = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"Kernel32.dll"), "LoadLibraryW");//寬字元
	
	if (pfnStartAddr == NULL){
		MessageBox(L"返回指標失敗");
		return;
	}

	//==================執行到這裡就說明是已經獲取到了指定DLL檔案的指定函數的指標==================
  1. 再遠端執行緒中開闢一個執行緒
	//9. 在遠端程序中開闢一個執行緒
	/*
		CreateRemoteThread()函數:我們開啟註冊器,讓它在記事本中自己開一個執行緒
			引數一:在指定的程序中開執行緒
			引數二:執行緒的安全屬性為NULL
			引數三:堆疊大小預設為0
			引數四:遠端執行緒執行哪個函數( 執行緒入口函數的起始地址)
			引數五:傳進來我們申請的地址空間
			引數六:什麼時候啟動,0為馬上啟動
			引數七:執行緒ID,NULL就可以
		RETURN:
			返回一個遠端執行緒控制程式碼
	*/
	HANDLE hRemote = CreateRemoteThread(hNotepad, NULL, 0, (LPTHREAD_START_ROUTINE)pfnStartAddr, lpAddr, 0, NULL);
	WaitForSingleObject(hRemote, INFINITE);
	//判斷建立遠端執行緒是否成功
	if (hRemote == NULL){
		MessageBox(L"建立遠端執行緒失敗");
		return;
	}

第六節 結語

本次部落格只是演示了一個專案用於讓沒有基礎的同學,理解遠端執行緒注入的基礎,更加高深的利用手法,需要自行琢磨
本次的專案靈感來自於頓開教育裡奇老師。