網絡安全編程:創建進程
微信公眾號:計算機與網絡安全
ID:Computer-network
當運行一個程序的時候,操作系統就會將這個程序從磁盤文件裝入內存,分配各種運行程序所需的資源,創建主線程等一系列的工作。進程是運行當中的程序,進程是向操作系統申請資源的基本單位。運行一個記事本程序時,操作系統就會創建一個記事本的進程。當關閉記事本時,記事本進程也隨即結束。對進程感性上的認識,這么多也就夠了。
如果要觀察系統中正在運行的進程,那么同時按下鍵盤上的Ctrl+Shift+Esc組合鍵就可以打開“任務管理器”,也就看到了系統中正常的進程列表,如圖1所示。對于任務管理器中的眾多列,主要關心的是“映像名稱”“PID”和“線程數”3項,這3項在編程中都會用到和涉及。
圖1 任務管理器
任何一個計算機文件都是一個二進制文件。對于可執行程序來說,它的二進制數據是可以被CPU執行的。程序是一個靜態的概念,本身只是存在于硬盤上的一個二進制文件。當用鼠標雙擊某個可執行程序以后,這個程序被加載入內存,這時就產生了一個進程。操作系統通過裝載器將程序裝入內存時,會為其分配各種進程所需的各種資源,并產生一個主線程,主線程會擁有CPU執行時間,占用進程申請的內存……在編程的時候也經常需要通過運行中的程序再去創建一個新的進程,本文介紹常見的用于創建進程的API函數。
1. 簡單下載者的演示
在Windows下創建進程的方法有多種,這里通過一個例子先介紹最簡單的一種方法。該方法用到的API函數為WinExec(),其定義如下:
- UINT WinExec(
- LPCSTR lpCmdLine, // command line
- UINT uCmdShow // window style
- );
參數說明如下。
lpCmdLine:指向一個要執行的可執行文件的字符串。
uCmdShow:程序運行后的窗口狀態。
第1個參數比較好理解,比如要執行“記事本”程序,那么這個參數就可以是“C:\Windows\ System32\Notepad.exe”。第2個參數是指明程序運行后窗口的狀態,常用的參數有兩個,一個是SW_SHOW,另一個是SW_HIDE。SW_SHOW表示程序運行后窗口狀態為顯示狀態,SW_HIDE表示程序運行后窗口狀態為隱藏狀態。可以試著創建一個隱藏顯示狀態的“記事本”程序,方法如下:
- WinExec("c:\\windows\\system32\\notepad.exe", SW_HIDE);
這樣創建的“記事本”進程在“任務管理器”中可以看到“notepad.exe”這個進程,但是無法看到其窗口界面。
WinExec()函數在很多“下載者”中使用,“下載者”的英文名字為“Downloader”,也就是下載器的意思。它是一種惡意程序,其功能較為單一(相對木馬、后門來說,功能單一)。下載者程序的功能是讓受害者計算機到黑客指定的URL地址去下載更多的病毒文件或木馬文件并運行。下載者的體積較小,容易傳播。當下載者下載到病毒或木馬后,通常都會使用WinExec()來運行下載到本地的惡意程序,調用它的原因是只有兩個參數且參數非常簡單。
下面簡單來做一個下載者進行演示,這僅僅只是一個演示。如果心懷歹意的話,不要企圖拿它來做任何壞事,因為演示代碼會很輕易地被殺毒軟件干掉。記住,目的是學習編程知識。
要完成一個模擬的下載者,就要讓程序可以從網絡上某個地址下載程序。文件下載的方式比較多,相對簡單而又比較常用的函數是URLDownloadToFile()。這個函數也是被下載者進程使用的函數,其定義如下:
- HRESULT URLDownloadToFile(
- LPUNKNOWN pCaller,
- LPCTSTR szURL,
- LPCTSTR szFileName,
- DWORD dwReserved,
- LPBINDSTATUSCALLBACK lpfnCB
- );
在這個函數中,只會用到兩個參數,分別是szURL和szFileName。這兩個參數的說明如下。
szURL:指向下載地址的 URL 的字符串。
szFileName:指向要保存到本地位置的字符串。
其余的參數賦值為0或NULL即可。
使用URLDownloadToFile()函數,需要包含Urlmon.h頭文件和Urlmon.lib導入庫文件,否則在編譯和連接時會無法通過。
已經了解了需要用到的API函數,那么完成代碼也就非常簡單了。具體代碼不過幾行而已,具體如下:
- #include <windows.h>
- #include <urlmon.h>
- #pragma comment (lib, "urlmon")
- int main()
- {
- char szUrl[MAX_PATH] = "c:\\windows\\system32\\notepad.exe";
- char szVirus[MAX_PATH] = "d:\\virus.exe";
- URLDownloadToFile(NULL, szUrl, szVirus, 0, NULL);
- // 為了模擬方便看到效果,這里使用參數 SW_SHOW
- // 一般可以傳遞 SW_HIDE 參數
- WinExec(szVirus, SW_SHOW);
- return 0;
- }
這里的模擬是把C盤系統目錄下的記事本程序下載到D盤并保存成名為virus.exe,然后運行它。如果是從網絡上某個地址處進行下載,那么只要修改szUrl變量保存的字符串即可。我們的代碼是一個簡單的模擬代碼,如果真正完成一個“下載者”的話,要比這個代碼復雜很多,如果要在源代碼上進行“免殺”,那么要考慮到問題也會很多。我們還是以學習編程知識為目的,不要進行破壞,否則隨時可能會被“查水表”。
2. CreateProcess()函數介紹與程序的啟動
通常情況下,創建一個進程會選擇使用CreateProcess()函數,該函數的參數非常多,功能強大,使用也更為靈活。對于WinExec()函數來說,其使用簡單,也只能完成簡單的進程創建工作。如果要對被創建的進程具有一定的控制能力,那么必須使用功能更為強大的CreateProcess()函數。
在介紹CreateProcess()函數之前,先來介紹一個內容。通常,在編寫C語言的程序時,如果是控制臺下的程序,那么編寫程序的入口函數是main()函數,也就是通常所說的主函數。如果編寫一個Windows下程序,那么入口函數是WinMain()。即使是使用MFC進行開發,其實也是有WinMain()函數的,只不過是被龐大的MFC框架封裝了。那么程序真的是從main()函數或者是WinMain()函數開始執行的嗎?在寫控制臺程序時,如果需要給程序提供參數,那么這個參數是從哪里來的,主函數為什么會有返回值,它會返回哪里去呢?
使用VC6來寫一個簡單的程序。通過調試這個簡單的程序,看看C語言程序是否真的由main()函數開始執行。寫一個簡單的輸出“Hello World”的程序來進行調試。程序代碼如下:
- #include <stdio.h>
- int main()
- {
- printf("Hello World!!! \r\n");
- return 0;
- }
這是非常簡單的一個程序,按下F7鍵進行編譯和連接,然后按下F10鍵開始進行單步調試狀態,打開VC6的CallStack窗口(調用棧窗口),觀察其內容,如圖2所示。
圖2 CallStack窗口內容
在調用棧中有3行記錄,雙擊第2行“mainCRT Startup() line 206 + 25 bytes”,查看代碼編輯窗口的內容,此時的代碼為調用主函數main()的C運行時啟動函數(簡稱啟動函數)。代碼編輯窗口內容如圖3所示。
圖3 啟動函數
可以看到,在代碼編輯窗口的左側有一個綠色的三角,表示這行代碼調用了主函數main()。并且通過該行代碼可以發現,main()函數的返回值賦值給了mainret變量。將代碼上移,找到定義mainret變量的代碼處。mainret的定義如下:
int mainret;
該變量的類型為int型。通常在定義main()函數時,main()函數的返回值是int型。從上面的調用過程可以看出,main()函數只是程序員編程時的入口函數,程序的啟動并不是從main()函數開始。在執行main()函數前,操作系統及C語言的啟動代碼已經為程序做了很多工作。
上面的內容只是一個簡單的小插曲。回歸正題,開始介紹CreateProcess()函數的使用。CreateProcess()函數的定義如下:
- BOOL CreateProcess(
- LPCTSTR lpApplicationName, // name of executable module
- LPTSTR lpCommandLine, // command line string
- LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
- LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
- BOOL bInheritHandles, // handle inheritance option
- DWORD dwCreationFlags, // creation flags
- LPVOID lpEnvironment, // new environment block
- LPCTSTR lpCurrentDirectory, // current directory name
- LPSTARTUPINFO lpStartupInfo, // startup information
- LPPROCESS_INFORMATION lpProcessInformation // process information
- );
參數說明如下。
lpApplicationName:指定可執行文件的文件名。
lpCommandLine:指定欲傳給新進程的命令行的參數。
lpProcessAttributes:進程安全屬性,該值通常為 NULL,表示為默認安全屬性。
lpThreadAttributes:線程安全屬性,該值通常為 NULL,表示為默認安全屬性。
bInheritHandlers:指定當前進程中的可繼承句柄是否被新進程繼承。
dwCreationFlags:指定新進程的優先級以及其他創建標志。
該參數一般情況下可以為0。
如果要創建一個被調試進程的話,需要把該參數設置為DEBUG_PROCESS。創建進程的進程稱為父進程,被創建的進程稱為子進程。也就是說,父進程要對子進程進行調試的話,需要在調用CreateProcess()函數時傳遞DEBUG_PROCESS參數。在傳遞DEBUG_PROCESS參數后,子進程創建的“孫”進程同樣也處在被調試狀態中。如果不希望子進程創建的“孫”進程也處在被調試狀態,那么在父進程創建子進程時傳遞DEBUG_ONLY_THIS_PROCESS和DEBUG_PROCESS。
在有些情況下,希望被創建子進程的主線程暫時不要運行,那么可以指定CREATE _SUSPENDED參數。事后希望該子進程的主線程運行的話,可以使用ResumeThread()函數使子進程的主線程恢復運行。
lpEnvironment:指定新進程的環境變量,通常這里指定為 NULL 值。
lpCurrentDirectory:指定新進程使用的當前目錄。
lpStartupInfo:指向 STARTUPINFO 結構體的指針,該結構體指定新進程的啟動信息。
該參數是一個結構體,該結構體決定進程啟動的狀態。該結構體的定義如下:
- typedef struct _STARTUPINFO {
- DWORD cb;
- LPTSTR lpReserved;
- LPTSTR lpDesktop;
- LPTSTR lpTitle;
- DWORD dwX;
- DWORD dwY;
- DWORD dwXSize;
- DWORD dwYSize;
- DWORD dwXCountChars;
- DWORD dwYCountChars;
- DWORD dwFillAttribute;
- DWORD dwFlags;
- WORD wShowWindow;
- WORD cbReserved2;
- LPBYTE lpReserved2;
- HANDLE hStdInput;
- HANDLE hStdOutput;
- HANDLE hStdError;
- } STARTUPINFO, *LPSTARTUPINFO;
該結構體在使用前,需要對cb成員變量進行賦值,該成員變量用于保存結構體的大小。一般創建一個進程,只需要初始化其中幾個參數即可,如果要對新進程的輸入輸出重定向的話,會用到該結構體的更多成員變量等。
lpProcessInformation:指向PROCESS_INFORMATION結構體的指針,該結構體用于返回新創建進程和主線程的相關信息。該結構體的定義如下:
- typedef struct _PROCESS_INFORMATION {
- HANDLE hProcess;
- HANDLE hThread;
- DWORD dwProcessId;
- DWORD dwThreadId;
- } PROCESS_INFORMATION;
該結構體用于返回新創建進程的句柄和進程ID,進程主線程的句柄和主線程ID。
下面通過一個實例來對CreateProcess()函數進行演示。
- #include <windows.h>
- #include <stdio.h>
- #define EXEC_FILE "c:\\windows\\system32\\notepad.exe"
- int main()
- {
- PROCESS_INFORMATION pi = { 0 };
- STARTUPINFO si = { 0 };
- si.cb = sizeof(STARTUPINFO);
- BOOL bRet = CreateProcess(EXEC_FILE,
- NULL, NULL, NULL, FALSE,
- NULL, NULL, NULL, &si, &pi);
- if ( bRet == FALSE )
- {
- printf("CreateProcess Error ! \r\n");
- return -1;
- }
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- return 0;
- }
進程創建后,PROCESS_INFORMATION結構體變量的兩個句柄需要使用CloseHandle()函數進行關閉。