Grahame Grieve
Grahame Grieve

Reputation: 3586

Getting access to the own thread information (delphi)

For debugging purposes, I am iterating the threads of my own application, and trying to report the thread times (looking for a rogue thread). When I iterate the threads, I get access denied if the threadId = GetCurrentThreadId.

Here's an example of code to demonstrate the problem (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.

I get access denied for this thread when pressing ctrl-c - why can't the thread get a handle to itself, but it can for all the other threads in the process?

Upvotes: 1

Views: 2374

Answers (2)

Grahame Grieve
Grahame Grieve

Reputation: 3586

So actually it turns out that there's a very specific set of conditions that means that a console can't get a handle to the thread in the thread itself - and that's when the thread is created in the console host to pass a CTRL+C notification to the console (the test conditions I was testing under)

Upvotes: 0

RbMm
RbMm

Reputation: 33804

are some kernel object can be opened, based on 2 things:

  • object security descriptor
  • caller token (thread token if exist, otherwise process token)

usually thread can open self handle, but can be exceptions, one is - thread created by system, for handle console control signals.

minimum code for reproduce (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;
}

and from console application call

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);
}

can in test view that OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId()) failed with error - ERROR_ACCESS_DENIED

why this happen ? need look for thread security descriptor. the simply code for this can look like:

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);
}

and call this from console handler thread and from usual (first thread) for compare.

the SD of usual process thread can look like:

for not elevated process:

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)

or for elevated ( running as admin)

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

but when this called from handler thread (auto created by system) - we got another dacl:

for not elevated:

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)

for elevated:

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

different here in SYSTEM_MANDATORY_LABEL

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

the "ML" here is SDDL_MANDATORY_LABEL (SYSTEM_MANDATORY_LABEL_ACE_TYPE)

Mandatory label rights:

"NW" - SDDL_NO_WRITE_UP (SYSTEM_MANDATORY_LABEL_NO_WRITE_UP)

"NR" - SDDL_NO_READ_UP ( SYSTEM_MANDATORY_LABEL_NO_READ_UP )

and point main - label value (sid):

handler thread always have "SI" - SDDL_ML_SYSTEM - System integrity level.

while usual threads have "ME" - SDDL_MLMEDIUM - Medium integrity level or

"HI" - SDDL_ML_HIGH - High integrity level, when run as admin

so - because this thread have higher integrity level ( System ) than usual process integrity level in token ( High integrity level or bellow, if not system process) and with no read and write up rights - we can not open this thread with read or write access, only with execute access.


we can do next test in HandlerRoutine - try open thread with MAXIMUM_ALLOWED and look for granted access with NtQueryObject ( use 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);
    }

we got here: [00101800] which mean:

SYNCHRONIZE | THREAD_RESUME | THREAD_QUERY_LIMITED_INFORMATION

also we can query ObjectTypeInformation and get GENERIC_MAPPING for thread object.

        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);
            }
        }

and got

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

so we in general can open this thread with GenericExecute access, except 00020000 (READ_CONTROL) because this access in GenericRead and GenericWrite and policy - no read/write up.


however for almost all api, where handle (thread or generic) required, we can use GetCurrentThread() - pseudo handle for the calling thread. of course this can use only for current thread. so we can call for example

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

the CloseHandle(GetCurrentThread()); also valid call - Calling the CloseHandle function with this handle has no effect. (simply nothing will be). and this pseudo handle have GENERIC_ALL granted access.

so your OpenThread routine can check for thread id - if it is equal to GetCurrentThreadId() - simply return GetCurrentThread().

also we can call

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

this also will be work well for this thread. however usual use GetCurrentThread() is enough

Upvotes: 4

Related Questions