Skip to content

Threads

Inside a Process

A process does nothing by itself, because its threads run the code. Each thread has a start address, the place in memory where it began. A healthy thread starts inside the program or a DLL it loaded, in memory a file on disk backs. A thread that starts in memory no file backs is a lead, since it points to code that arrived some other way or to a just-in-time compiler. Read where each thread starts, and which file is mapped there.

What It Is

A thread is a worker inside a process that runs its code, one step after another. A process does nothing until one of its threads runs.

Why Windows Needs It
A program often has several jobs at once, such as answering the keyboard while it saves a file. So Windows lets a process have many threads, and shares the processor's time between them in small slices.
An Everyday Example
Notepad's first thread draws its window and answers alice's typing, while Windows' own worker threads, which start in ntdll.dll, do tasks in the background. They all share Notepad's memory, handles and token, and each keeps its own stack and thread ID (TID).
notepad.exe's threads share its memory, and each keeps its own stack (example values)

notepad.exe

Shared by Every Thread
  • Its code, notepad.exe and its DLLs
  • Its heap
  • Its handles
  • Its token
  • TID 4316

    Draws the window and answers alice's typing

    Starts Innotepad.exe

    Its own stack and ID

  • TID 4320

    A Windows worker for tasks in the background

    Starts Inntdll.dll

    Its own stack and ID

  • TID 4324

    Another Windows worker

    Starts Inntdll.dll

    Its own stack and ID

Kept by the Kernel
The kernel keeps every thread, where it started and any token it carries. Where a thread started is not what it runs now.

Where Threads Start

Tools show two start addresses for a thread. Use the Win32 start address, the code the thread was asked to run, which the Threads tab, Sysmon event 8 and windows.threads' Win32StartAddress column show.

The other, StartAddress in the kernel's thread record, is where Windows itself set the thread going, nearly always inside ntdll.dll, so it tells you little. Never set one against the other.

An address such as spoolsv.exe+0x4B10 means 0x4B10 bytes into spoolsv.exe's code. A thread can also work under another account's token, and Token and Privileges explains impersonating threads.

Threads in the spooler on ws-042, set against its memory (story)
  • Image

    • Image region: spoolsv.exeTID 3276Win32 Start Addressspoolsv.exe+0x4B10
    • Image region: ntdll.dllTID 3304Win32 Start Addressntdll.dll+0x2A3B0
  • Private

    • Private region at 0x21C4F3A0000, no fileTID 9012Win32 Start Address0x21C4F3A1A40

What Normal Looks Like

On a healthy machine this part looks like this.

  • Threads start in the program itself or in a DLL it loaded, from System32 or its own folder.
  • Many threads start in ntdll.dll, where Windows runs its pools of worker threads.
  • Programs create some threads suspended and resume them shortly after.

Why It Matters

Code placed in a process does nothing until a thread runs it. A thread that starts outside every file the process loaded is one of the clearest traces injected code leaves.

Normal and Suspicious

A clean machine sits beside a suspicious one, and the pointer marks where to look.

Normal

A clean Windows 11 install

Two of spoolsv.exe's threads on a clean install. Example values.
TIDStart AddressFile at the Start
TID 2460spoolsv.exe+0x1240C:\Windows\System32\spoolsv.exe
TID 2464ntdll.dll+0x9A20C:\Windows\System32\ntdll.dll
See It in the Window

Suspicious

ws-042,

The spooler on ws-042 at 10:08 on 12 Oct 2026, in a memory image.
TIDStart AddressFile at the Start
TID 3276spoolsv.exe+0x4B10C:\Windows\System32\spoolsv.exe
TID 3304ntdll.dll+0x2A3B0C:\Windows\System32\ntdll.dll
TID 90120x21C4F3A1A40None (no file)
File at the StartStarts in memory no file backs: a spooler thread starts in spoolsv.exe or a DLL it loaded. Remote Thread Injection
See It in the Window

What a Malicious Example Shows

Each card is what malicious use of this part looks like, and where it shows.

  • A thread whose start address lies in memory no file backs.Where it shows: windows.malware.suspicious_threads, or Sysmon event 8 with an empty StartModule, where Sysmon runs.
  • A thread one process created in another, unrelated process.Where it shows: Sysmon event 8 from an uncommon source, or CreateRemoteThreadApiCall in DeviceEvents.
  • A thread steered from another process, which changed where it was about to run or gave it extra work to run next.Where it shows: SetThreadContextRemoteApiCall or QueueUserApcRemoteApiCall in DeviceEvents.
  • A thread starting inside a second executable that is not the process's own.Where it shows: windows.malware.suspicious_threads notes it, and windows.suspended_threads lists threads left suspended.

Live and in Memory

Each question has the tools that answer it on a running machine, and in a memory image, a copy of its memory saved to a file.

In a memory image, windows.threads lists each thread with both start addresses and the file mapped at each, while windows.malware.suspicious_threads flags the running threads that start outside every file. Live, Windows.System.Threads and Process Explorer show the Win32 start address.

  • Where does each thread start, and is a file mapped there?

    On a Running Machine

    In a Memory Image

  • Did another process start or steer a thread here?

    On a Running Machine

    In a Memory Image

  • Has a thread ended?

    On a Running Machine

    In a Memory Image

To take it offline, read Dumping a Process and Memory

What It Misses

Every way of looking has gaps, so cover these from another source.

  • A start address records where a thread began, not what it runs now. A thread whose registers were set from another process, or that was handed queued work, keeps a normal start. Defender's DeviceEvents table records those as SetThreadContextRemoteApiCall and QueueUserApcRemoteApiCall.
  • windows.malware.suspicious_threads ignores threads that have exited, but windows.thrdscan can still find them.
  • windows.threads resolves no path for System's (PID 4) threads, so an empty path there is not a finding.

False Positives

These look odd but are normal, so rule them out before you act.

  • .NET, Java, browsers and other just-in-time compilers run threads in private memory, so compare with a clean machine of the same build.
  • Debuggers, security products and some installers create threads in other processes.

Red Flags It Explains

Each red flag below is confirmed by reading this part.

Checks That Use It

These checks read this part as one of their steps.

Hunt It

The same hunt is written for four tools, so pick the one you use. Defender XDR and Sentinel queries are in KQL, which you paste into Advanced hunting in the Defender portal or into Sentinel's Logs page. Splunk queries are in SPL, for its Search page, and Velociraptor queries are in VQL, which you run as a hunt.

Needs Defender for Endpoint. Confirm the action types with DeviceEvents | distinct ActionType, as Microsoft's page lists none. Security products and debuggers do this too, so a rare pair is a lead to read in memory, not proof.

KQLDefender XDR, Advanced hunting
// One process creating, queuing work to or steering a thread in another, rarest pairs firstDeviceEvents| where Timestamp > ago(30d)| where ActionType in ("CreateRemoteThreadApiCall", "QueueUserApcRemoteApiCall", "SetThreadContextRemoteApiCall")| where ProcessId != InitiatingProcessId| summarize Devices = dcount(DeviceId), Events = count(), Actions = make_set(ActionType), FirstSeen = min(Timestamp), LastSeen = max(Timestamp) by InitiatingProcessFileName, InitiatingProcessFolderPath, FileName| order by Devices asc, Events asc

Sources

MITRE ATT&CK is a free public catalogue of the techniques attackers use. Each technique has an ID, such as T1055 for Process Injection, so defenders can name the same behaviour the same way. Reading this part helps find these techniques.