3.4.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即可。如果需要具体了解该函数,请参考MSDN。

使用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窗口(调用栈窗口),观察其内容,如图3-11所示。

图3-11 CallStack窗口内容

在调用栈中有3行记录,双击第2行“mainCRT Startup() line 206 + 25 bytes”,查看代码编辑窗口的内容,此时的代码为调用主函数main()的C运行时启动函数(简称启动函数)。代码编辑窗口内容如图3-12所示。

图3-12 启动函数

可以看到,在代码编辑窗口的左侧有一个绿色的三角,表示这行代码调用了主函数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()函数进行关闭。