7.6 實現程式掛起與恢復

lyshark發表於2023-09-24

掛起與恢復程式是指暫停或恢復程式的工作狀態,以達到一定的控制和管理效果。在 Windows 作業系統中,可以使用系統提供的函式實現程式的掛起和恢復,以達到對程式的控制和排程。需要注意,過度使用程式掛起/恢復操作可能會造成系統效能的降低,導致死鎖等問題,因此在使用時應該謹慎而慎重。同時,透過和其他程式之間協同工作,也可以透過更加靈活的方式,實現程式的協調、互動等相應的功能,從而實現更加高效和可靠的程式管理。

要實現掛起程式,首先我們需要實現掛起執行緒,因為掛起程式的實現原理是透過呼叫SuspendThread函式迴圈將程式內的所有執行緒全部掛起後實現的,而要實現掛起執行緒則我們需要先確定指定程式內的執行緒資訊,要實現列舉程式內的執行緒資訊則可以透過以下幾個步驟實現。

首先透過CreateToolhelp32Snapshot得到當前系統下所有的程式快照,並透過遍歷程式的方式尋找是否符合我們所需要列舉的程式名,如果是則呼叫CreateToolhelp32Snapshot並透過傳入TH32CS_SNAPTHREAD代表列舉執行緒,透過迴圈的方式遍歷程式內的執行緒,每次透過呼叫OpenThread開啟執行緒,並呼叫ZwQueryInformationThread查詢該執行緒的入口資訊以及執行緒所在的模組資訊,最後以此輸出即可得到當前程式內的所有執行緒資訊。

#include <iostream>
#include <Windows.h>  
#include <TlHelp32.h>
#include <Psapi.h>

using namespace std;

typedef enum _THREADINFOCLASS
{
  ThreadBasicInformation,
  ThreadTimes,
  ThreadPriority,
  ThreadBasePriority,
  ThreadAffinityMask,
  ThreadImpersonationToken,
  ThreadDescriptorTableEntry,
  ThreadEnableAlignmentFaultFixup,
  ThreadEventPair_Reusable,
  ThreadQuerySetWin32StartAddress,
  ThreadZeroTlsCell,
  ThreadPerformanceCount,
  ThreadAmILastThread,
  ThreadIdealProcessor,
  ThreadPriorityBoost,
  ThreadSetTlsArrayAddress,
  ThreadIsIoPending,
  ThreadHideFromDebugger,
  ThreadBreakOnTermination,
  MaxThreadInfoClass
}THREADINFOCLASS;

typedef struct _CLIENT_ID
{
  HANDLE UniqueProcess;
  HANDLE UniqueThread;
}CLIENT_ID;

typedef struct _THREAD_BASIC_INFORMATION
{
  LONG ExitStatus;
  PVOID TebBaseAddress;
  CLIENT_ID ClientId;
  LONG AffinityMask;
  LONG Priority;
  LONG BasePriority;
}THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;

extern "C" LONG(__stdcall * ZwQueryInformationThread)
(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
) = NULL;

// 列舉程式內的執行緒
BOOL EnumThread(char *ProcessName)
{
  // 程式快照控制程式碼
  HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  PROCESSENTRY32 process = { sizeof(PROCESSENTRY32) };

  // 遍歷程式
  while (Process32Next(hProcessSnap, &process))
  {
    // char* 轉 string
    string s_szExeFile = process.szExeFile;
    if (s_szExeFile == ProcessName)
    {
      HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
      THREADENTRY32 te32;

      // 建立執行緒快照
      hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
      if (hThreadSnap == INVALID_HANDLE_VALUE)
      {
        return FALSE;
      }

      // 為快照分配記憶體空間
      te32.dwSize = sizeof(THREADENTRY32);

      // 獲取第一個執行緒的資訊
      if (!Thread32First(hThreadSnap, &te32))
      {
        return FALSE;
      }

      // 遍歷執行緒
      while (Thread32Next(hThreadSnap, &te32))
      {
        // 判斷執行緒是否屬於本程式
        if (te32.th32OwnerProcessID == process.th32ProcessID)
        {
          // 開啟執行緒
          HANDLE hThread = OpenThread(
            THREAD_ALL_ACCESS,        // 訪問許可權
            FALSE,                    // 由此執行緒建立的程式不繼承執行緒的控制程式碼
            te32.th32ThreadID         // 執行緒 ID
            );
          if (hThread == NULL)
          {
            return FALSE;
          }

          // 將區域設定設定為從作業系統獲取的ANSI內碼表
          setlocale(LC_ALL, ".ACP");

          // 獲取 ntdll.dll 的模組控制程式碼
          HINSTANCE hNTDLL = ::GetModuleHandle("ntdll");

          // 從 ntdll.dll 中取出 ZwQueryInformationThread
          (FARPROC&)ZwQueryInformationThread = GetProcAddress(hNTDLL, "ZwQueryInformationThread");

          // 獲取執行緒入口地址
          PVOID startaddr;                          // 用來接收執行緒入口地址
          ZwQueryInformationThread(
            hThread,                              // 執行緒控制程式碼
            ThreadQuerySetWin32StartAddress,      // 執行緒資訊型別 ThreadQuerySetWin32StartAddress 執行緒入口地址
            &startaddr,                           // 指向緩衝區的指標
            sizeof(startaddr),                    // 緩衝區的大小
            NULL
            );

          // 獲取執行緒所在模組
          THREAD_BASIC_INFORMATION tbi;            // _THREAD_BASIC_INFORMATION 結構體物件
          TCHAR modname[MAX_PATH];                 // 用來接收模組全路徑
          ZwQueryInformationThread(
            hThread,                             // 執行緒控制程式碼
            ThreadBasicInformation,              // 執行緒資訊型別,ThreadBasicInformation :執行緒基本資訊
            &tbi,                                // 指向緩衝區的指標
            sizeof(tbi),                         // 緩衝區的大小
            NULL
            );

          // 檢查入口地址是否位於某模組中
          GetMappedFileName(
            OpenProcess(                                        // 程式控制程式碼
            PROCESS_ALL_ACCESS,                                 // 訪問許可權
            FALSE,                                              // 由此執行緒建立的程式不繼承執行緒的控制程式碼
            (DWORD)tbi.ClientId.UniqueProcess                   // 唯一程式 ID
            ),
            startaddr,                            // 要檢查的地址
            modname,                              // 用來接收模組名的指標
            MAX_PATH                              // 緩衝區大小
            );
          std::cout << "執行緒ID: " << te32.th32ThreadID << " 執行緒入口: " << startaddr << " 所在模組: " << modname << std::endl;
        }
      }
    }
  }
}

int main(int argc, char* argv[])
{
  EnumThread("lyshark.exe");

  system("pause");
  return 0;
}

讀者可自行執行上述程式碼片段,即可列舉出當前執行程式lyshark.exe中所有的後動執行緒資訊,如下圖所示;

當我們能夠得到當前程式內的執行緒資訊後,接下來就是實現如何掛起或恢復程式內的特定執行緒,掛起執行緒可以使用SuspendThread 其函式宣告如下:

DWORD SuspendThread(
  HANDLE hThread
);

其中,hThread 是一個指向執行緒控制程式碼的指標,指向要掛起的執行緒的控制程式碼,該函式返回掛起前執行緒的執行緒計數器值,表示被掛起執行緒在掛起前還未執行的指令數目。

可以多次呼叫 SuspendThread 函式將同一個執行緒進行多次掛起,每次返回被掛起前執行緒的執行緒計數器值,每呼叫一次則會阻塞該執行緒,其狀態會變為掛起狀態。當該執行緒被 ResumeThread 恢復時,它將繼續從上次掛起時的位置開始執行。

ResumeThread 函式宣告如下:

DWORD ResumeThread(
  HANDLE hThread
);

其中,hThread 是執行緒控制程式碼,指向要恢復的執行緒的控制程式碼。

呼叫 ResumeThread 函式可以讓一個被掛起的執行緒從上次掛起的位置開始繼續執行,函式返回值是被恢復的執行緒的先前掛起次數。當被恢復的執行緒的掛起計數器歸零時,其狀態將自動變為非掛起狀態,並開始繼續執行。

當有了上述兩個函式的支援那麼掛起執行緒將變得很容易實現了,首先後去所有程式快照,接著就是直接開啟OpenThread()符合要求的執行緒,此時只需要呼叫SuspendThread(hThread)即可掛起一個執行緒,呼叫ResumeThread(hThread)則可以恢復一個執行緒,具體實現程式碼如下所示;

#include <windows.h>
#include <stdio.h>
#include <TlHelp32.h>

int Start_Stop_Thread(DWORD Pid, DWORD ThreadID, BOOL flag)
{
    THREADENTRY32 te32 = { 0 };
    te32.dwSize = sizeof(THREADENTRY32);

    // 獲取全部執行緒快照
    HANDLE hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (INVALID_HANDLE_VALUE != hThreadSnap)
    {
        // 獲取快照中第一條資訊
        BOOL bRet = Thread32First(hThreadSnap, &te32);
        while (bRet)
        {
            // 只過濾出 pid 裡面的執行緒
            if (Pid == te32.th32OwnerProcessID)
            {
                // 判斷是否為ThreadID,暫停指定的TID
                if (ThreadID == te32.th32ThreadID)
                {
                    // 開啟執行緒
                    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);

                    if (flag == TRUE)
                    {
                        ResumeThread(hThread);     // 恢復執行緒
                    }
                    else
                    {
                        SuspendThread(hThread);     // 暫停執行緒
                    }
                    CloseHandle(hThreadSnap);
                }
            }
            // 獲取快照中下一條資訊
            bRet = Thread32Next(hThreadSnap, &te32);
        }
        return 0;
    }
    return -1;
}

int main(int argc, char* argv[])
{
    // 暫停或恢復程式ID = 4204 裡面的執行緒ID = 10056
    int ret = Start_Stop_Thread(4204, 10056, TRUE);    // TRUE = 恢復執行緒 FALSE = 掛起執行緒
    printf("狀態: %d \n", ret);

    system("pause");
    return 0;
}

當有了上述功能的支援以後,那麼實現掛起程式將變得很容易,讀者只需要在特定一個程式內列舉出所有的活動執行緒,並透過迴圈的方式逐個掛起即可實現掛起整個程式的效果,這段完整程式碼如下所示;

#include <windows.h>
#include <iostream>
#include <tlhelp32.h>
#include <Psapi.h>

#pragma comment(lib,"psapi.lib")

#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)
#define SystemProcessesAndThreadsInformation    5       // 功能號
typedef DWORD(WINAPI* PQUERYSYSTEM)(UINT, PVOID, DWORD, PDWORD);

// 執行緒狀態的列舉常量
typedef enum _THREAD_STATE
{
  StateInitialized,    // 初始化狀態
  StateReady,          // 準備狀態
  StateRunning,        // 執行狀態
  StateStandby,
  StateTerminated,     // 關閉
  StateWait,           // 等待
  StateTransition,     // 切換
  StateUnknown
}THREAD_STATE;

// 執行緒處於等待的原因的列舉常量
typedef enum _KWAIT_REASON
{
  Executive,
  FreePage,
  PageIn,
  PoolAllocation,
  DelayExecution,
  Suspended,
  UserRequest,
  WrExecutive,
  WrFreePage,
  WrPageIn,
  WrPoolAllocation,
  WrDelayExecution,
  WrSuspended,
  WrUserRequest,
  WrEventPair,
  WrQueue,
  WrLpcReceive,
  WrLpcReply,
  WrVirtualMemory,
  WrPageOut,
  WrRendezvous,
  Spare2,
  Spare3,
  Spare4,
  Spare5,
  Spare6,
  WrKernel,
  MaximumWaitReason
}KWAIT_REASON;

typedef LONG   NTSTATUS;
typedef LONG   KPRIORITY;

typedef struct _CLIENT_ID
{
  DWORD        UniqueProcess;
  DWORD        UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

typedef struct _VM_COUNTERS
{
  SIZE_T        PeakVirtualSize;
  SIZE_T        VirtualSize;
  ULONG         PageFaultCount;
  SIZE_T        PeakWorkingSetSize;
  SIZE_T        WorkingSetSize;
  SIZE_T        QuotaPeakPagedPoolUsage;
  SIZE_T        QuotaPagedPoolUsage;
  SIZE_T        QuotaPeakNonPagedPoolUsage;
  SIZE_T        QuotaNonPagedPoolUsage;
  SIZE_T        PagefileUsage;
  SIZE_T        PeakPagefileUsage;
} VM_COUNTERS;

// 執行緒資訊結構體
typedef struct _SYSTEM_THREAD_INFORMATION
{
  LARGE_INTEGER   KernelTime;
  LARGE_INTEGER   UserTime;
  LARGE_INTEGER   CreateTime;
  ULONG           WaitTime;
  PVOID           StartAddress;
  CLIENT_ID       ClientId;
  KPRIORITY       Priority;
  KPRIORITY       BasePriority;
  ULONG           ContextSwitchCount;
  LONG            State;// 狀態,是THREAD_STATE列舉型別中的一個值
  LONG            WaitReason;//等待原因, KWAIT_REASON中的一個值
} SYSTEM_THREAD_INFORMATION, * PSYSTEM_THREAD_INFORMATION;


typedef struct _UNICODE_STRING
{
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// 程式資訊結構體
typedef struct _SYSTEM_PROCESS_INFORMATION
{
  ULONG            NextEntryDelta;    // 指向下一個結構體的指標
  ULONG            ThreadCount;       // 本程式的匯流排程數
  ULONG            Reserved1[6];      // 保留
  LARGE_INTEGER    CreateTime;        // 程式的建立時間
  LARGE_INTEGER    UserTime;          // 在使用者層的使用時間
  LARGE_INTEGER    KernelTime;        // 在核心層的使用時間
  UNICODE_STRING   ProcessName;       // 程式名
  KPRIORITY        BasePriority;
  ULONG            ProcessId;         // 程式ID
  ULONG            InheritedFromProcessId;
  ULONG            HandleCount;       // 程式的控制程式碼總數
  ULONG            Reserved2[2];      // 保留
  VM_COUNTERS      VmCounters;
  IO_COUNTERS      IoCounters;
  SYSTEM_THREAD_INFORMATION Threads[5];    // 子執行緒資訊陣列
}SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;

// 獲取執行緒是被是否被掛起 1=表示執行緒被掛起  0=表示執行緒正常 -1=未知狀態
int IsThreadSuspend(DWORD dwProcessID, DWORD dwThreadID)
{
  int nRet = 0;
  NTSTATUS Status = 0;

  PQUERYSYSTEM NtQuerySystemInformation = NULL;
  PSYSTEM_PROCESS_INFORMATION pInfo = { 0 };

  // 獲取函式地址
  NtQuerySystemInformation = (PQUERYSYSTEM) GetProcAddress(LoadLibrary("ntdll.dll"), "NtQuerySystemInformation");

  DWORD   dwSize = 0;

  // 獲取資訊所需的緩衝區大小
  Status = NtQuerySystemInformation(SystemProcessesAndThreadsInformation,// 要獲取的資訊的型別
    NULL, // 用於接收資訊的緩衝區
    0,  // 緩衝區大小
    &dwSize
  );

  // 申請緩衝區
  char* pBuff = new char[dwSize];
  pInfo = (PSYSTEM_PROCESS_INFORMATION)pBuff;
  if (pInfo == NULL)
    return -1;

  // 再次呼叫函式, 獲取資訊
  Status = NtQuerySystemInformation(SystemProcessesAndThreadsInformation, // 要獲取的資訊的型別
    pInfo, // 用於接收資訊的緩衝區
    dwSize,  // 緩衝區大小
    &dwSize
  );
  if (!NT_SUCCESS(Status))
  {
    /*如果函式執行失敗*/
    delete[] pInfo;
    return -1;
  }

  // 遍歷結構體,找到對應的程式
  while (1)
  {
    // 判斷是否還有下一個程式
    if (pInfo->NextEntryDelta == 0)
      break;

    // 判斷是否找到了ID
    if (pInfo->ProcessId == dwProcessID)
    {

      // 找到該程式下的對應的執行緒,也就是遍歷所有執行緒
      for (DWORD i = 0; i < pInfo->ThreadCount; i++)
      {
        if (pInfo->Threads[i].ClientId.UniqueThread == dwThreadID)
        {
          // 找到執行緒 
          // 如果執行緒被掛起
          if (pInfo->Threads[i].State == StateWait&& pInfo->Threads[i].WaitReason == Suspended)
          {
            nRet = 1;
            break;
          }
        }
      }
      break;
    }
    // 迭代到下一個節點
    pInfo = (PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo) + pInfo->NextEntryDelta);
  }

  delete[] pBuff;
  return nRet;
}

// 設定程式狀態 掛起/非掛起
int SuspendProcess(DWORD dwProcessID, BOOL fSuspend)
{
  HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);

  if (hSnapshot != INVALID_HANDLE_VALUE)
  {
    THREADENTRY32 te = { sizeof(te) };
    BOOL fOk = Thread32First(hSnapshot, &te);
    for (; fOk; fOk = Thread32Next(hSnapshot, &te))
    {
      if (te.th32OwnerProcessID == dwProcessID)
      {
        HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,FALSE, te.th32ThreadID);
        if (hThread != NULL)
        {
          if (fSuspend)
          {
            if (IsThreadSuspend(dwProcessID, te.th32ThreadID) == 0)
            {
              SuspendThread(hThread);
            }
            else
            {
              return 0;
            }
          }
          else
          {
            if (IsThreadSuspend(dwProcessID, te.th32ThreadID) == 1)
            {
              ResumeThread(hThread);
            }
            else
            {
              return 0;
            }
          }
        }
        CloseHandle(hThread);
      }
    }

  }
  CloseHandle(hSnapshot);
  return 1;
}

int main(int argc, char *argv[])
{
  // 掛起程式
  SuspendProcess(20308, TRUE);

  // 恢復程式
  SuspendProcess(20308, FALSE);

  return 0;
}

讀者可自行編譯並執行上述程式碼,透過呼叫SuspendProcess函式並以此傳入需要掛起的程式PID以及一個狀態,當該狀態為TRUE時則代表掛起程式,而當狀態值為FALSE時則代表為恢復一個程式,當一個程式被掛起後其會出現卡死的現象,當恢復後一切都會變得正常。

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/5fbc3082.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

相關文章