How to Run a Windows Service Inside Svchost.exe

Author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

Pavel Yosifovich, co-author of “Windows Internals 7th Edition, Part 1” and author of “Windows Native API Programming”, takes on a service model most developers only ever see from the outside: the DLL hosted inside svchost.exe. If you open Process Explorer and sort by name, you find a large crowd of svchost.exe processes, each one a container for one or more Windows services. Third parties are not really meant to use this hosting model, and yet nothing stops them, which is precisely why it matters to anyone doing security research: a service that lives inside svchost is a service that is very easy to overlook. This post is for Windows systems programmers and malware analysts who want to understand the mechanism end to end, and by the end you will be able to build a service DLL, register it, and watch svchost load it.

Why Do Most Services Live Inside Svchost in the First Place?

Open Process Explorer and the service host processes stand out, usually tinted pink. Each svchost.exe exists to host services, and the whole point is that it can host more than one. For a service to be loaded into a shared process like that, the service cannot be an executable. It has to be a DLL, so it can be mapped into the address space of an existing process. Double click a service host and you see the service or services it is carrying, along with the DLLs that implement them.

The number of svchost processes on your machine is a story in itself. Before Windows 10 version 1703, the system deliberately bundled many services into each svchost to conserve processes, since every process costs resources: address space, a handle table, and so on. Bundle a hundred services into groups of ten and you pay for ten processes instead of a hundred. The downside is isolation, or the lack of it: one misbehaving service that throws an unhandled exception can take down every other service sharing that process. Starting with 1703, on systems with at least 3.5 GB of RAM, the default flipped, and most services now get their own svchost so a crash stays contained. The old behavior is still a registry value away if you want it back. Either way, the hosting mechanism itself is unchanged, and that is what we are going to drive by hand.

What Does Svchost Actually Require From Your DLL?

A DLL gets you a DllMain, and DllMain is fine, but it is nowhere near enough. What svchost looks for is a specific exported function, ServiceMain. That is the entry point svchost calls for the service, and it has a fixed prototype: an argument count and an argument vector, the classic argc/argv shape, even though this particular service ignores both. Two things about the export are not optional. It has to be marked __declspec(dllexport) (a .def file works just as well), and it has to be declared extern "C" so the name is not C++ mangled. Svchost is looking up a plain, undecorated symbol, and if the exported name does not match exactly, svchost never finds your entry point. Technically the function can be named something other than ServiceMain, as long as you tell the registry where to look, but there is no good reason to fight the default – unless you want the DLL to be more obscure.

Inside ServiceMain you do not call StartServiceCtrlDispatcher. That call belongs to a standalone service executable, and here svchost is the one that already made it. What your ServiceMain does is register a control handler with RegisterServiceCtrlHandler, passing the service name (simplesvc2 in the demo, the same name you will use in the registry) and a handler function. The call hands back a SERVICE_STATUS_HANDLE, an opaque value you keep in a global, because you need it every time you report status. This is exactly the machinery covered in the earlier walkthrough of building a plain standalone service executable, just reshaped for a DLL.

How Do You Report Status and Do Real Work Without Blocking?

Status goes back to the Service Control Manager through SetServiceStatus, which takes your status handle and a SERVICE_STATUS structure. Two fields carry the weight. dwCurrentState is the state you are reporting, SERVICE_START_PENDING while you are initializing and SERVICE_RUNNING once you are up. dwControlsAccepted declares which control requests you can handle, and at the very least you want SERVICE_ACCEPT_STOP, because a service that cannot be stopped is a problem. There is one more field that looks harmless and is not, and it is where a good chunk of the video goes: SERVICE_WIN32_SHARE_PROCESS has to be set here too in dwSetviceType. Leave it out and the service refuses to transition to RUNNING and sits in START_PENDING, which is the bug you see Pavel chase down before things finally behave.

Now the awkward part. Whatever the service is actually for, listening on a socket, watching a queue, anything with a wait in it, cannot happen directly inside ServiceMain. Svchost calls that function and expects it to return promptly. So the real work goes on a separate CreateThread. In the demo the worker does something deliberately modest: every second it prints the elapsed time since it started, using GetTickCount64 and std::format from C++20, pushed out with OutputDebugString so you can watch it in DebugView. Marginally useful, on purpose, so nothing distracts from the plumbing.

How Should the Service Stop Cleanly?

The clean way to shut a worker thread down is an event. Create one with CreateEvent, keep it in a global, and have the worker loop on WaitForSingleObject with a one second timeout instead of a bare Sleep. A return of WAIT_TIMEOUT means no stop has arrived, so do one unit of work and wait again. Any other return means the event was signaled, so the loop exits and the thread returns. The control handler is the other half: when the Service Control Manager sends a control, the handler inspects it, and on the stop request it signals the event with SetEvent and then reports SERVICE_STOPPED. This is the same threads, events, and waiting model that anyone injecting a DLL into a live process ends up leaning on, and it is worth being fluent in it.

There is a second live bug here worth calling out, because it is the kind that hides in plain sight. The handler compared against the wrong constant, a bare value with no type identity, instead of SERVICE_CONTROL_STOP. Everything looked right and the stop command simply did nothing. Constants without a type to anchor them are exactly where this happens.

What Does the Registry Configuration Look Like?

Writing the code is only half of it. The other half lives in the registry, and this is the part that trips people up. You register the service with an sc create command from an elevated prompt, but the binPath is the twist. It does not point at your DLL, because a service binPath has to be an executable, and the executable here is svchost itself: something like %SystemRoot%\System32\svchost.exe -k SimpleGroup. That -k switch names the service group, the label svchost uses to decide which services share a process. In the same command you can set the account to LocalSystem, the account svchost services typically run under, and a type that marks the service as willing to share its process.

sc create writes the service key under HKLM\SYSTEM\CurrentControlSet\Services, but that key alone does not tell svchost where the DLL is. For that you create a Parameters subkey and, inside it, a value named ServiceDll holding the full path to your DLL. The catch that Pavel calls out from experience: ServiceDll must be a REG_EXPAND_SZ, an expandable string, even when the path contains no environment variables at all. Svchost specifically expects that type, and a plain string quietly fails. Finally the group itself has to exist. Under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost you add a REG_MULTI_SZ value named after your group (SimpleGroup), listing the service names that belong to it. With all three pieces in place, the service key, the ServiceDll under Parameters, and the group under Svchost, sc start launches a fresh svchost, which loads your DLL and calls ServiceMain. Process Explorer confirms it: a new service host, your DLL mapped into its address space, and DebugView showing the worker’s heartbeat.

Why Does This Matter for Security?

Nothing here is exotic, and that is exactly the point. A service DLL inside svchost inherits svchost’s camouflage. There are usually dozens of service host processes on a machine, most of them legitimate, and one more blends in far better than a strange standalone executable would. That is why the technique shows up in offensive tradecraft, and why defenders benefit from understanding the registry footprint it leaves: a ServiceDll pointing somewhere unusual, a fresh group under the Svchost key, a service whose binPath is svchost but whose DLL is not a Microsoft binary. If you know exactly which registry values svchost consults and in what order, you know exactly where to look.

What This Means Practically

  • Export ServiceMain from your DLL with extern "C" and __declspec(dllexport), since svchost looks up an undecorated symbol and DllMain alone will never be called as a service entry point
  • Set SERVICE_ACCEPT_SHARE_PROCESS alongside SERVICE_ACCEPT_STOP in dwControlsAccepted, or the service hangs in START_PENDING and never reports RUNNING
  • Move any waiting or listening off ServiceMain onto a worker thread, because svchost expects ServiceMain to return promptly
  • Signal shutdown with an event and WaitForSingleObject with a timeout rather than a fixed Sleep, so a stop request is handled within the loop’s next tick
  • Write ServiceDll as a REG_EXPAND_SZ even with no environment variables in the path, because svchost rejects a plain string
  • Remember the three registry pieces: the service key from sc create, a Parameters\ServiceDll value, and the group listed under the Svchost key
  • When hunting for hidden services, check for a binPath of svchost paired with a ServiceDll that is not a signed Microsoft DLL, and for unfamiliar groups under the Svchost key

Keep Learning

If you want to go deeper on the threading, synchronization, and DLL mechanics this service is built on, these TrainSec courses cover the ground in full:

Related reading in the knowledge library: DLL Injection with Windows Application Verifier, another way a DLL ends up running inside a process it did not start life in, and VMMap Basics if you want to see the loaded modules inside a process like svchost for yourself.


blue depth

About the author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.