从另一个进程获取 win32 线程的起始地址

本文关键字:地址 线程 win32 另一个 进程 获取 | 更新日期: 2023-09-27 18:19:02

背景:

我在 Win32 中编写了一个多线程应用程序,我使用System.Diagnostics命名空间中的类从 C# 代码开始Process该应用程序。

现在,在 C# 代码中,我想获取在 Win32 应用程序中创建的每个线程的开始地址的名称/符号,以便我可以将线程相关信息(如 CPU 使用率(记录到数据库中。基本上,C# 代码启动 Win32 应用程序的多个实例,监视它们,在需要时终止,然后将信息/错误/异常/原因/等记录到数据库中。

为此,我包装了两个 Win32 API 可视化。 SymInitializeSymFromAddr我自己编写的程序员友好的 API,如下所示:

extern "C"
{
    //wraps SymInitialize
    DllExport bool initialize_handler(HANDLE hModue);
    //wraps SymFromAddr
    DllExport bool get_function_symbol(HANDLE hModule, //in
                                       void *address,  //in
                                       char *name);    //out
}

然后使用 pinvoke 从 C# 代码调用这些 API。但它不起作用,GetLastError给出了126错误代码,这意味着:

找不到指定的模块

我将Process.Handle作为hModule传递给这两个函数; initialize_handler似乎有效,但get_function_symbol不起作用;它给出了上述错误。我不确定我是否传递了正确的句柄。我尝试传递以下句柄:

Process.MainWindowHandle
Process.MainModule.BaseAddress

两者都在第一步本身失败(即调用initialize_handler时(。我将Process.Threads[i].StartAddress作为第二个参数传递,这似乎是失败的原因,因为ProcessThread.StartAddress似乎是RtlUserThreadStart函数的地址,而不是特定于应用程序的启动函数的地址。MSDN对此说:

每个 Windows 线程实际上都在系统提供的函数中开始执行,而不是应用程序提供的函数。因此,对于系统中的每个 Windows 进程,主线程的起始地址相同(因为它表示系统提供的函数的地址(。但是,属性允许您获取特定于应用程序的起始函数地址。

但它没有说明如何使用ProcessThread.StartAddress获取特定于应用程序的startinbg函数地址。

问题:

我的问题归结为从另一个应用程序(用 C# 编写(获取 win32 线程的开始地址,因为一旦我得到它,我也会使用上面提到的 API 获得名称。 那么如何获取起始地址呢?

<小时 />

我从C++代码测试了我的符号查找 API。如果一开始就给出了正确的地址,则可以将地址解析为符号。

这是我的 p/invoke 声明:

[DllImport("UnmanagedSymbols.dll", SetLastError = true, CallingConvention= CallingConvention.Cdecl)]
static extern bool initialize_handler(IntPtr hModule);
[DllImport("UnmanagedSymbols.dll", SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
static extern bool get_function_symbol(IntPtr hModule, IntPtr address, StringBuilder name);

从另一个进程获取 win32 线程的起始地址

关键是调用NtQueryInformationThread函数。这不是一个完全"官方"的函数(过去可能没有记录?(,但文档建议没有替代方法来获取线程的开始地址。

我已经把它包成一个.NET 友好调用,采用线程 ID 并将起始地址作为 IntPtr 返回。此代码已在 x86 和 x64 模式下进行了测试,在后者中,它在 32 位和 64 位目标进程上进行了测试。

我没有测试的一件事是以低权限运行它;我希望这段代码要求调用者具有SeDebugPrivilege

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
class Program
{
    static void Main(string[] args)
    {
        PrintProcessThreads(Process.GetCurrentProcess().Id);
        PrintProcessThreads(4156); // some other random process on my system
        Console.WriteLine("Press Enter to exit.");
        Console.ReadLine();
    }
    static void PrintProcessThreads(int processId)
    {
        Console.WriteLine(string.Format("Process Id: {0:X4}", processId));
        var threads = Process.GetProcessById(processId).Threads.OfType<ProcessThread>();
        foreach (var pt in threads)
            Console.WriteLine("  Thread Id: {0:X4}, Start Address: {1:X16}",
                              pt.Id, (ulong) GetThreadStartAddress(pt.Id));
    }
    static IntPtr GetThreadStartAddress(int threadId)
    {
        var hThread = OpenThread(ThreadAccess.QueryInformation, false, threadId);
        if (hThread == IntPtr.Zero)
            throw new Win32Exception();
        var buf = Marshal.AllocHGlobal(IntPtr.Size);
        try
        {
            var result = NtQueryInformationThread(hThread,
                             ThreadInfoClass.ThreadQuerySetWin32StartAddress,
                             buf, IntPtr.Size, IntPtr.Zero);
            if (result != 0)
                throw new Win32Exception(string.Format("NtQueryInformationThread failed; NTSTATUS = {0:X8}", result));
            return Marshal.ReadIntPtr(buf);
        }
        finally
        {
            CloseHandle(hThread);
            Marshal.FreeHGlobal(buf);
        }
    }
    [DllImport("ntdll.dll", SetLastError = true)]
    static extern int NtQueryInformationThread(
        IntPtr threadHandle,
        ThreadInfoClass threadInformationClass,
        IntPtr threadInformation,
        int threadInformationLength,
        IntPtr returnLengthPtr);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, int dwThreadId);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool CloseHandle(IntPtr hObject);
    [Flags]
    public enum ThreadAccess : int
    {
        Terminate = 0x0001,
        SuspendResume = 0x0002,
        GetContext = 0x0008,
        SetContext = 0x0010,
        SetInformation = 0x0020,
        QueryInformation = 0x0040,
        SetThreadToken = 0x0080,
        Impersonate = 0x0100,
        DirectImpersonation = 0x0200
    }
    public enum ThreadInfoClass : int
    {
        ThreadQuerySetWin32StartAddress = 9
    }
}

我的系统上的输出:

Process Id: 2168    (this is a 64-bit process)
  Thread Id: 1C80, Start Address: 0000000001090000
  Thread Id: 210C, Start Address: 000007FEEE8806D4
  Thread Id: 24BC, Start Address: 000007FEEE80A74C
  Thread Id: 12F4, Start Address: 0000000076D2AEC0
Process Id: 103C    (this is a 32-bit process)
  Thread Id: 2510, Start Address: 0000000000FEA253
  Thread Id: 0A0C, Start Address: 0000000076F341F3
  Thread Id: 2438, Start Address: 0000000076F36679
  Thread Id: 2514, Start Address: 0000000000F96CFD
  Thread Id: 2694, Start Address: 00000000025CCCE6

除了括号中的内容,因为这需要额外的P/Invoke。


关于SymFromAddress"找不到模块"错误,我只想提一下,需要调用SymInitialize fInvadeProcess = true或手动加载模块,如 MSDN 中所述。

我知道你说在你的情况下情况并非如此,但为了任何通过这些关键字找到这个问题的人的利益,我会把它留在这里。

以下是我对这个问题的理解。

你有一个 C# 应用 APP1,用于创建一堆线程。

反过来,这些线程各自创建一个进程。 我假设这些线程保持活动状态,并负责监视它生成的进程。

因此,对于 APP1 中的每个线程,您希望它枚举有关在该线程的子进程中生成的线程的信息。

他们的方式是

,我在过去美好的日子里会这样做:

    将给定 Win32
  • 进程的所有 Win32 线程监视编码到 DLL 中
  • 将该 DLL 注入到我要监视的进程中
  • 使用命名管道或其他 RPC 机制从注入的 Win32 进程与主机 APP1 进行通信

因此,在 C# 的主 threadproc 中,您将创建并监视一个命名管道,以便进程在注入后进行通信。

在C++情况下,伪代码是创建一个挂起的进程,在该进程中分配一些内存,将 DLL 注入该进程,然后创建一个远程线程来执行注入的 dll:

char * dllName = "your cool dll with thread monitoring stuff.dll"
// Create a suspended process
CreateProces("your Win32 process.exe", ...CREATE_SUSPENDED..., pi)
// Allocate memory in the process to hold your DLL name to load
lpAlloc = VirtualAlloc(ph.hProcess, ... MEM_COMMIT, PAGE_READWRITE)
// Write the name of your dll to load in the process memory
WriteProcessMemeory(pi.hProcess, lpAlloc, dllName, ...)
// Get the address of LoadLibrary
fnLoadLibrary = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA")
// Create a remote thread in the process, giving it the threadproc for LoadLibrary
// and the argument of your DLL name
hTrhead = CreateRemoteThread(pi.hProcess, ..., fnLoadLibrary, lpAlloc, ...)
// Wait for your dll to load
WaitForSingleObject(hThread)
// Go ahead and start the Win32 process
ResumeThread(ph.hThread)

在 DLL 中,可以将代码放入将连接到您设置的命名管道的DLL_PROCESS_ATTACH中,并初始化所有内容。 然后触发一个函数以开始监视和报告命名管道。

C# threadproc 将监视命名管道的进程,并将其报告到 APP1。

更新:

我错过了您控制 Win32 进程代码的事实。 在这种情况下,我只会将一个参数传递给过程,该过程将控制您选择的通信 RPC 机制(共享内存、命名管道、队列服务、剪贴板 (ha( 等(。

这样,C# threadproc 将设置 RPC 通信通道和监视,然后向 Win32 进程提供"地址"信息,以便它可以"回拨你"。

我将把其他东西留在那里,以防它对想要监视他们不负责代码的 Win32 进程的任何其他人有用。

好吧,这绝对不是简单的方法,但也许它会以某种方式帮助您。您应该能够以此项目 (StackWalk64( 使用的方式获取另一个线程的堆栈跟踪,并最终看到所需函数的名称。它有自己的问题,特别是这种方法的性能可能不会太高,但据我了解,这是每个线程一次的操作。问题是,它通常是否能够正确遍历您的(可能优化的(应用程序的堆栈。

首先,你无法真正可靠地做到这一点:如果你碰巧在线程执行函数指针之前或函数返回之后访问Thread.StartAddress,你将无法知道起始函数实际上是什么。

其次,更可能的答案是,在管理线程启动函数时,没有直接映射到起始函数。