C++ 黑客编程揭秘与防范(第3版)
上QQ阅读APP看书,第一时间看更新

3.4.3 进程的枚举

进程的枚举就是把所有的进程都列举一边,列举的同时可以查找,可以显示等。当然,一些用特殊手段刻意隐藏的进程是无法通过常规的枚举方式枚举得到的。这里只是介绍应用层的进程枚举方法。在应用层,枚举进程有很多方法,这里介绍相对常见的进程枚举方法。在学习进程等相关知识的过程中,会逐步完成一个自己的进程管理器,包括“创建进程”“结束进程”“停止进程”“恢复进程”“枚举进程”和“查看指定进程中的DLL”信息。笔者自己写的进程管理器的界面如图3-13所示。在学习了线程的知识以后,可以通过进程去枚举线程、暂停线程、结束线程、恢复线程等。

图3-13 进程管理器

1.进程及DLL枚举的API函数介绍

无论是枚举进程还是枚举进程中的DLL文件,方法都是相同的,都是通过创建指定的相关快照,再通过循环逐条获取快照的内容。类似的枚举线程、枚举堆都是相同的方法,差别只是在创建快照时参数不同,在逐条获得快照内容时的API函数不同而已。

枚举进程需要的API函数是CreateToolhelp32Snapshot()、Process32First()和Process32 Next()。枚举线程时的API函数是CreateToolhelp32Snapshot()、Thread32First()和Thread 32Next()。枚举进程中的DLL文件时的API函数是CreateToolhelp32Snapshot()、Module32First()和Module32Next()。使用这些函数时,需要在代码中包含Tlhelp32.h头文件,否则在编译时会提示使用了未定义的函数。从上面这些API可以看出,无论是枚举进程、线程或者DLL,都需要调用CreateToolhelp32Snapshot()这个函数,那么记住这个函数就比较重要了。当我们平时忘记具体的枚举函数时,只要记得CreateToolhelp32Snapshot()函数,就可以通过这个函数在MSDN中找到其他枚举需要用的函数了。针对以上函数,下面分别进行介绍。

CreateToolhelp32Snapshot()函数的定义如下:

        HANDLE WINAPI CreateToolhelp32Snapshot(
          DWORD dwFlags,
          DWORD th32ProcessID
        );

参数说明如下。

dwFlags:指明要建立系统快照的类型。对于要枚举的内容,该参数可以指定如下值。

TH32CS_SNAPMODULE:在枚举进程中的DLL时指定。

TH32CS_SNAPPROCESS:在枚举系统中的进程时指定。

TH32CS_SNAPTHREAD:在枚举系统中的线程时指定。

th32ProcessID:该参数根据dwFlags参数的不同而不同。如果枚举的是系统中的进程或系统中的线程,该参数为NULL;如果枚举的是进程中加载的DLL的话,那么该参数是进程ID。

该函数返回一个快照的句柄,并提供给枚举函数使用。

Process32First()函数的定义如下:

        BOOL WINAPI Process32First(
          HANDLE hSnapshot,
          LPPROCESSENTRY32 lppe
        );

参数说明如下。

hSnapshot:该参数为CreateToolhelp32Snapshot()函数返回的句柄。

lppe:该参数为指向PROCESSENTRY32结构体的指针,该结构体的定义如下。

        typedef struct tagPROCESSENTRY32 {
          DWORD dwSize;
          DWORD cntUsage;
          DWORD th32ProcessID;              // 进程ID
          ULONG_PTR th32DefaultHeapID;
          DWORD th32ModuleID;
          DWORD cntThreads;
          DWORD th32ParentProcessID;       // 父进程ID
          LONG  pcPriClassBase;
          DWORD dwFlags;
          TCHAR szExeFile[MAX_PATH];       // 可执行文件的文件名
        } PROCESSENTRY32;
        typedef PROCESSENTRY32 *PPROCESSENTRY32;

在使用该结构体时,需要对该结构体中的程序变量dwSize进行赋值。该变量保存PROCESSENTRY32结构体的大小。

Process32Next()函数的定义如下:

        BOOL WINAPI Process32Next(
          HANDLE hSnapshot,
          LPPROCESSENTRY32 lppe
        );

该函数的使用方法与Process32First()相同。

枚举进程中加载的DLL文件和枚举系统中的线程都与以上两个函数类似,所不同的是使用的XXX32First()和XXX32Next()的第2个参数指向的结构体不同。

对于枚举DLL文件来说,指向的结构体定义如下:

        typedef struct tagMODULEENTRY32 {
          DWORD    dwSize;
          DWORD    th32ModuleID;
          DWORD    th32ProcessID;
          DWORD    GlblcntUsage;
          DWORD    ProccntUsage;
          BYTE  * modBaseAddr;
          DWORD    modBaseSize;
          HMODULE hModule;
          TCHAR    szModule[MAX_MODULE_NAME32 + 1];
          TCHAR    szExePath[MAX_PATH];
        } MODULEENTRY32;
        typedef MODULEENTRY32 *PMODULEENTRY32;

对于枚举系统中的线程来说,指向的结构体定义如下:

        typedef struct tagTHREADENTRY32{
          DWORD  dwSize;
          DWORD  cntUsage;
          DWORD  th32ThreadID;
          DWORD  th32OwnerProcessID;
          LONG    tpBasePri;
          LONG    tpDeltaPri;
          DWORD  dwFlags;
        } THREADENTRY32;
        typedef THREADENTRY32 *PTHREADENTRY32;

关于以上两个结构体,这里不再进行过多的描述,请读者参考MSDN自行了解。

2.枚举进程和枚举DLL的代码

对于枚举进程的API函数,读者都已经掌握了,那么就用循环来枚举进程和枚举进程中加载的DLL。

枚举进程的代码如下:

        VOID CManageProcessDlg::ShowProcess()
        {
        // 清空列表框内容
        m_ListProcess.DeleteAllItems();

        // 创建进程快照
        HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

        if ( hSnap == INVALID_HANDLE_VALUE )
        {
            AfxMessageBox("CreateToolhelp32Snapshot Error");
            return ;
        }

        PROCESSENTRY32 Pe32 = { 0 };
        Pe32.dwSize = sizeof(PROCESSENTRY32);

        BOOL bRet = Process32First(hSnap, &Pe32);
        int i = 0;
        CString str;

        // 循环获取进程快照中的每一项
        while ( bRet )
        {
            m_ListProcess.InsertItem(i, Pe32.szExeFile);
            str.Format("%d", Pe32.th32ProcessID);
            m_ListProcess.SetItemText(i, 1, str);

            i ++;
            bRet = Process32Next(hSnap, &Pe32);
        }

        CloseHandle(hSnap);
    }

枚举指定进程中加载的DLL的代码如下:

    VOID CManageProcessDlg::ShowModule()
    {
        // 清空列表框内容
        m_ListModule.DeleteAllItems();

        // 获取选中的进程号
        int nPid = GetSelectPid();

        // 进程ID为0,则返回
        if ( nPid == 0 )
        {
            return ;
        }

        MODULEENTRY32 Me32 = { 0 };
        Me32.dwSize = sizeof(MODULEENTRY32);

        // 创建模块快照
        HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, nPid);
        if ( hSnap == INVALID_HANDLE_VALUE )
        {
            AfxMessageBox("CreateToolhelp32Snapshot Error");
            return ;
        }

        BOOL bRet = Module32First(hSnap, &Me32);
        int i = 0;
        CString str;

        // 循环获取模块快照中的每一项
        while ( bRet )
        {
                    m_ListModule.InsertItem(i, Me32.szModule);
                    m_ListModule.SetItemText(i, 1, Me32.szExePath);
                    i ++;
                    bRet = Module32Next(hSnap, &Me32);
                }

                CloseHandle(hSnap);
            }

在枚举DLL的代码中有两点需要说明。首先说明GetSelectPid()函数,该函数用来获得选中的进程ID;其次,如果进程ID为0,则直接退出,不去获取其对应的DLL模块。这里给出GetSelectPid()函数的实现,代码如下:

          int CManageProcessDlg::GetSelectPid()
          {
              int nPid = -1;

              POSITION Pos = m_ListProcess.GetFirstSelectedItemPosition();
              int nSelect = -1;
              while ( Pos )
              {
                  nSelect = m_ListProcess.GetNextSelectedItem(Pos);
              }

              if ( -1 == nSelect )
              {
                  AfxMessageBox("请选中要显示DLL的进程");
                  return -1;
              }

              char  szPid[10] = { 0 };
              m_ListProcess.GetItemText(nSelect, 1, szPid, 10);
              nPid = atoi(szPid);

              return nPid;
          }

该函数主要是MFC的控件的操作,这里给出其程序方便读者阅读。

3.调整当前进程权限

编写的任务管理器已经完成了一部分,枚举系统进程和指定进程中的DLL的功能已经实现了。在VC6下编译连接并按Ctrl+F5键运行程序,可以看到任务管理器枚举出了系统中的进程。测试一下枚举进程中DLL的功能,选中“svchost.exe”进程,单击“查看DLL”按钮,“svchost.exe”进程中加载的DLL文件也都被枚举出来了。

程序看似是没有问题的,那么换一种方式让其运行。找到VC6生成好的任务管理器的可执行文件并双击运行它,再次选中“svchost.exe”进程,单击“查看DLL”按钮,是不是没有查看到“svchost.exe”进程加载的DLL文件?换一个其他的进程试试,比如选择自己编写的任务管理器测试是可以枚举到已经加载的DLL文件的。通过单击若干个进程可以发现,系统进程加载的DLL文件是无法枚举到的,但是在VC6下,通过Ctrl+F5组合键运行任务管理器是不存在该问题的。

无法枚举到系统进程加载的DLL列表的原因是CreateToolhelp32Snapshot()函数调用失败,失败的原因是进程的权限不够。进程的权限不够,除了导致CreateToolhelp32Snapshot()函数调用的失败,对于OpenProcess()函数打开smss.exe、winlogon.exe等系统进程时同样也是会调用失败的。解决这个问题的方式是将进程的权限提升至“SeDebugPrivilege”。

调整进程权限的步骤如下:

①使用OpenProcessToken()函数打开当前进程的访问令牌。

②使用LookupPrivilegeValue()函数取得描述权限的LUID。

③使用AdjustTokenPrivileges()函数调整访问令牌的权限。

调整权限使当前进程拥有“SeDebugPrivilege”权限。拥有该权限后,当前进程可以访问一些受限的系统资源。在后面讲到远程线程注入的时候,同样需要调整当前进程的访问权限,否则是无法对系统进程进行远程线程注入的。因为在进行远程线程注入的时候,同样要用到OpenProcess()函数。

调整当前进程权限的代码如下:

        VOID CManageProcessDlg::DebugPrivilege()
        {
            HANDLE hToken = NULL;

            BOOL bRet = OpenProcessToken(GetCurrentProcess(),
                              TOKEN_ALL_ACCESS, &hToken);

            if ( bRet == TRUE )
            {
                TOKEN_PRIVILEGES tp;
                tp.PrivilegeCount = 1;
                LookupPrivilegeValue(NULL,
                        SE_DEBUG_NAME,
                        &tp.Privileges[0].Luid);
                tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
                AdjustTokenPrivileges(hToken,
                        FALSE, &tp, sizeof(tp),
                        NULL, NULL);

                CloseHandle(hToken);
            }
        }