遠程執行緒注入

第一節 前置知識

  • 提起遠程執行緒注入,大家有可能會理解為我在廣西,你在北京,我注入你的執行緒。其實並不是這個樣子。
  • 系統在每次運行一個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;
	}

第六節 結語

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