【问题标题】:Getting access to the own thread information (delphi)访问自己的线程信息(delphi)
【发布时间】:2018-04-30 01:57:49
【问题描述】:

出于调试目的,我正在迭代我自己的应用程序的线程,并尝试报告线程时间(寻找流氓线程)。当我迭代线程时,如果threadId = GetCurrentThreadId,我会被拒绝访问。

这是一个演示问题的代码示例(delphi):

  program Project9;

  {$APPTYPE CONSOLE}

  {$R *.res}

  uses
    Windows, System.SysUtils, TlHelp32;

  type
    TOpenThreadFunc = function(DesiredAccess: DWORD; InheritHandle: BOOL; ThreadID: DWORD): THandle; stdcall;
  var
    OpenThreadFunc: TOpenThreadFunc;

  function OpenThread(id : DWORD) : THandle;
  const
    THREAD_GET_CONTEXT       = $0008;
    THREAD_QUERY_INFORMATION = $0040;
  var
    Kernel32Lib, ThreadHandle: THandle;
  begin
    Result := 0;
    if @OpenThreadFunc = nil then
    begin
      Kernel32Lib := GetModuleHandle(kernel32);
      OpenThreadFunc := GetProcAddress(Kernel32Lib, 'OpenThread');
    end;
    result := OpenThreadFunc(THREAD_QUERY_INFORMATION, False, id);
  end;

  procedure dumpThreads;
  var
    SnapProcHandle: THandle;
    NextProc      : Boolean;
    TThreadEntry  : TThreadEntry32;
    Proceed       : Boolean;
    pid, tid : Cardinal;
    h : THandle;
  begin
    pid := GetCurrentProcessId;
    tid := GetCurrentThreadId;
    SnapProcHandle := CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); //Takes a snapshot of the all threads
    Proceed := (SnapProcHandle <> INVALID_HANDLE_VALUE);
    if Proceed then
      try
        TThreadEntry.dwSize := SizeOf(TThreadEntry);
        NextProc := Thread32First(SnapProcHandle, TThreadEntry);//get the first Thread
        while NextProc do
        begin
          if TThreadEntry.th32OwnerProcessID = PID then //Check the owner Pid against the PID requested
          begin
            write('Thread '+inttostr(TThreadEntry.th32ThreadID));
            if (tid = TThreadEntry.th32ThreadID) then

            write(' (this thread)');
            h := OpenThread(TThreadEntry.th32ThreadID);
            if h <> 0 then
              try
                writeln(': open ok');
              finally
                CloseHandle(h);
              end
            else
              writeln(': '+SysErrorMessage(GetLastError));
          end;
          NextProc := Thread32Next(SnapProcHandle, TThreadEntry);//get the Next Thread
        end;
      finally
        CloseHandle(SnapProcHandle);//Close the Handle
      end;
  end;


  function DebugCtrlC(dwCtrlType : DWORD) :BOOL;
  begin
    writeln('ctrl-c');
    dumpThreads;
  end;

  var
    s : String;
  begin
    SetConsoleCtrlHandler(@DebugCtrlC, true);
    try
      writeln('enter anything to see threads, ''x'' to exit. or press ctrl-c to see threads');
      repeat
        readln(s);
        if s <> '' then
          dumpThreads;
      until s = 'x';
    except
      on E: Exception do
        Writeln(E.ClassName, ': ', E.Message);
    end;
  end.

当我按下 ctrl-c 时,我被拒绝访问该线程 - 为什么线程不能获得自身的句柄,但它可以用于进程中的所有其他线程?

【问题讨论】:

  • 通常一个线程使用 DuplicateHandle 获得一个自己的句柄 - 如果需要一个真正的句柄。如果没有,GetCurrentThread。顺便说一句,“windows.OpenThread”有什么问题?
  • 这里有很多代码,比必要的要多得多。需要minimal reproducible example。因为你声称是真实的东西不容易被复制。 idownvotedbecau.se/nomcve
  • @RbMm 如果您使用 cmets 中的信息和答案,您可以复制。但不在问题中。您必须了解 SO 不是针对提问者的临时问题解决服务,而是问答主题的资源。一旦提供了minimal reproducible example,这是一个很好的问题,但就目前而言,它对未来的访问者没有用处。
  • @DavidHeffernan - 是的,基于从ctrl+c 处理程序调用的代码中的答案信息。当然需要重写问题。不需要任何CreateToolhelp32Snapshot 等。只需从HandlerRoutine 调用SetConsoleCtrlHandler(HandlerRoutine, TRUE);OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId());。但真正有趣的是这个新创建的线程 (CtrlRoutine) 具有系统完整性级别,这会阻止该线程打开
  • @SertacAkyuz - 很有趣,但你从一开始就更正了 - DuplicateHandle 对于当前线程总是可以的,尽管当OpenThread 可能失败时,尽管线程 SD、令牌等。即使在致电 ZwImpersonateAnonymousToken(GetCurrentThread()) 之后 - DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &amp;hThread, 0, 0, DUPLICATE_SAME_ACCESS); 也可以

标签: multithreading delphi winapi


【解决方案1】:

是否可以打开一些内核对象,基于 2 件事:

  • 对象安全描述符
  • 调用者令牌(线程令牌如果存在,否则处理令牌)

通常线程可以打开自己的句柄,但也可以是例外,一种是-系统创建的线程,用于处理控制台控制信号。

最小重现代码(c++):

HANDLE g_hEvent;

BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
    if (CTRL_C_EVENT == dwCtrlType)
    {
        if (HANDLE hThread = OpenThread(THREAD_QUERY_INFORMATION, 
            FALSE, GetCurrentThreadId()))
        {
            CloseHandle(hThread);
        }
        else GetLastError();

        SetEvent(g_hEvent);
    }

    return TRUE;
}

并从控制台应用程序调用

if (g_hEvent = CreateEvent(0, TRUE, FALSE, 0))
{
    if (SetConsoleCtrlHandler(HandlerRoutine, TRUE))
    {
      // send ctrl+c, for not manually do this
        if (GenerateConsoleCtrlEvent (CTRL_C_EVENT, 0))
        {
            WaitForSingleObject(g_hEvent, INFINITE);
        }
        SetConsoleCtrlHandler(HandlerRoutine, FALSE);
    }
    CloseHandle(g_hEvent);
}

可以在测试视图中 OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId()) 失败并出现错误 - ERROR_ACCESS_DENIED

为什么会这样?需要寻找线程安全描述符。简单的代码如下所示:

void DumpObjectSD(HANDLE hObject = GetCurrentThread())
{
    ULONG cb = 0, rcb = 0x40;

    static volatile UCHAR guz;
    PVOID stack = alloca(guz);

    PSECURITY_DESCRIPTOR psd = 0;

    do 
    {
        if (cb < rcb)
        {
            cb = RtlPointerToOffset(psd = alloca(rcb - cb), stack);
        }

        if (GetKernelObjectSecurity(hObject, 
            OWNER_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION|LABEL_SECURITY_INFORMATION,
            psd, cb, &rcb))
        {
            PWSTR sz;
            if (ConvertSecurityDescriptorToStringSecurityDescriptor(psd, SDDL_REVISION_1, 
                OWNER_SECURITY_INFORMATION|DACL_SECURITY_INFORMATION|LABEL_SECURITY_INFORMATION, &sz, &rcb))
            {
                DbgPrint("%S\n", sz);
                LocalFree(sz);
            }

            break;
        }

    } while (GetLastError() == ERROR_INSUFFICIENT_BUFFER);
}

并从控制台处理程序线程和通常(第一个线程)调用它以进行比较。

普通进程线程的SD可以是这样的:

对于未提升的进程:

O:S-1-5-21-*
D:(A;;0x1fffff;;;S-1-5-21-*)(A;;0x1fffff;;;SY)(A;;0x121848;;;S-1-5-5-0-LogonSessionId)
S:AI(ML;;NWNR;;;ME)

或用于提升(以管理员身份运行)

O:BA
D:(A;;0x1fffff;;;BA)(A;;0x1fffff;;;SY)(A;;0x121848;;;S-1-5-5-0-LogonSessionId)
S:AI(ML;;NWNR;;;HI)

但是当它从处理线程调用时(由系统自动创建) - 我们得到了另一个 dacl:

对于未提升的:

O:BA
D:(A;;0x1fffff;;;S-1-5-21-*)(A;;0x1fffff;;;SY)(A;;0x121848;;;S-1-5-5-0-LogonSessionId)
S:AI(ML;;NWNR;;;SI)

对于提升:

O:BA
D:(A;;0x1fffff;;;BA)(A;;0x1fffff;;;SY)(A;;0x121848;;;S-1-5-5-0-LogonSessionId)
S:AI(ML;;NWNR;;;SI)

这里在SYSTEM_MANDATORY_LABEL不同

S:AI(ML;;NWNR;;;SI)

"ML"这里是SDDL_MANDATORY_LABEL (SYSTEM_MANDATORY_LABEL_ACE_TYPE)

强制性标签权利:

"NW" - SDDL_NO_WRITE_UP (SYSTEM_MANDATORY_LABEL_NO_WRITE_UP)

"NR" - SDDL_NO_READ_UP (SYSTEM_MANDATORY_LABEL_NO_READ_UP)

并指向主标签value(sid):

处理程序线程始终具有 "SI" - SDDL_ML_SYSTEM - 系统完整性级别。

虽然普通线程具有 "ME" - SDDL_MLMEDIUM - 中等完整性级别或

"HI" - SDDL_ML_HIGH - 高完整性级别,以管理员身份运行时

所以 - 因为这个线程在令牌中具有比通常进程完整性级别更高的完整性级别(系统)(高完整性级别或以下,如果不是系统进程)并且没有读写权限 - 我们不能打开这个线程读取或写入权限,仅具有执行权限。


我们可以在HandlerRoutine 中进行下一个测试 - 尝试使用MAXIMUM_ALLOWED 打开线程并使用NtQueryObject 查找授予的访问权限(使用ObjectBasicInformation

    if (HANDLE hThread = OpenThread(MAXIMUM_ALLOWED, FALSE, GetCurrentThreadId()))
    {
        OBJECT_BASIC_INFORMATION obi;
        if (0 <= ZwQueryObject(hThread, ObjectBasicInformation, &obi, sizeof(obi), 0))
        {
            DbgPrint("[%08x]\n", obi.GrantedAccess);
        }
        CloseHandle(hThread);
    }

我们到了:[00101800] 这意味着:

SYNCHRONIZE | THREAD_RESUME | THREAD_QUERY_LIMITED_INFORMATION

我们也可以查询ObjectTypeInformation 并获取GENERIC_MAPPING 的线程对象。

        OBJECT_BASIC_INFORMATION obi;
        if (0 <= ZwQueryObject(hThread, ObjectBasicInformation, &obi, sizeof(obi), 0))
        {
            ULONG rcb, cb = (obi.TypeInfoSize + __alignof(OBJECT_TYPE_INFORMATION) - 1) & ~(__alignof(OBJECT_TYPE_INFORMATION) - 1);
            POBJECT_TYPE_INFORMATION poti = (POBJECT_TYPE_INFORMATION)alloca(cb);
            if (0 <= ZwQueryObject(hThread, ObjectTypeInformation, poti, cb, &rcb))
            {
                DbgPrint("a=%08x\nr=%08x\nw=%08x\ne=%08x\n", 
                    poti->GenericMapping.GenericAll,
                    poti->GenericMapping.GenericRead,
                    poti->GenericMapping.GenericWrite,
                    poti->GenericMapping.GenericExecute);
            }
        }

得到了

a=001fffff
r=00020048
w=00020437
e=00121800

所以我们通常可以使用GenericExecute 访问权限打开此线程,00020000 (READ_CONTROL) 除外,因为此访问权限在 GenericRead 和 GenericWrite 以及策略中 - 没有读/写。


然而,对于几乎所有需要句柄(线程或通用)的 API,我们可以使用 GetCurrentThread() - 调用线程的伪句柄。当然这只能用于当前线程。所以我们可以调用例如

FILETIME CreationTime, ExitTime, KernelTime, UserTime;
GetThreadTimes(GetCurrentThread(), &CreationTime, &ExitTime, &KernelTime, &UserTime);

CloseHandle(GetCurrentThread()); 也是有效调用 - 使用此句柄调用 CloseHandle 函数无效。(根本不会有任何效果)。这个伪句柄有GENERIC_ALL 被授予访问权限。

所以您的 OpenThread 例程可以检查线程 ID - 如果它等于 GetCurrentThreadId() - 只需返回 GetCurrentThread()

我们也可以调用

DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hThread, 0, 0, DUPLICATE_SAME_ACCESS);

这也适用于该线程。但是通常使用GetCurrentThread()就足够了

【讨论】:

    【解决方案2】:

    所以实际上事实证明,有一组非常具体的条件意味着控制台无法在线程本身中获取线程的句柄 - 这就是在控制台主机中创建线程以传递 CTRL+C 通知控制台(我正在测试的测试条件)

    【讨论】:

    • 嗯,线程可以在所有其他情况下打开自我,除非是线程处理 ctrl-c
    • 可以或不能是某些线程(和任何对象)仅基于两件事打开 - 线程(对象)安全描述符和线程(尝试打开)令牌。或进程令牌,如果正在运行的线程没有令牌。
    • 当然可以专门设置一些进程令牌(例如设置低完整性)。等,这可以防止在自进程中打开线程,但这是非常特殊的情况
    • 这并不能真正回答问题。我强烈建议您删除答案,并编辑问题以包含minimal reproducible example,而不是根据问题中不存在的细节发布推测性答案。那么就有了价值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-05
    相关资源
    最近更新 更多