微信公众号:计算机与网络安全
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
- #include
- #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
- 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
- #include
- #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()函数进行关闭。