Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Vectored Syscalls #22

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The help shows all the available commands and features of the tool:
```
C:\>python syswhispers.py -h

usage: syswhispers.py [-h] [-p PRESET] [-a {x86,x64}] [-m {embedded,egg_hunter,jumper,jumper_randomized}] [-f FUNCTIONS] -o OUT_FILE [--int2eh] [--wow64] [-v] [-d]
usage: syswhispers.py [-h] [-p PRESET] [-a {x86,x64}] [-m {embedded,egg_hunter,jumper,jumper_randomized,vectored}] [-f FUNCTIONS] -o OUT_FILE [--int2eh] [--wow64] [-v] [-d]

SysWhispers3 - SysWhispers on steroids

Expand All @@ -70,7 +70,7 @@ optional arguments:
Architecture
-c {msvc,mingw,all}, --compiler {msvc,mingw,all}
Compiler
-m {embedded,egg_hunter,jumper,jumper_randomized}, --method {embedded,egg_hunter,jumper,jumper_randomized}
-m {embedded,egg_hunter,jumper,jumper_randomized,vectored}, --method {embedded,egg_hunter,jumper,jumper_randomized,vectored}
Syscall recovery method
-f FUNCTIONS, --functions FUNCTIONS
Comma-separated functions
Expand Down
114 changes: 107 additions & 7 deletions data/base.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
#include <stdio.h>

//#define DEBUG

// JUMPER
// VECTORED

#ifdef _M_IX86

Expand All @@ -15,6 +15,63 @@ EXTERN_C PVOID internal_cleancall_wow64_gate(VOID) {

#endif

#ifdef VECTORED

static PVOID EXCP_ADDR = NULL;
static PVOID SYSC_ADDR = NULL;
static DWORD SSN = 0x0;
static HANDLE mutex = NULL;

//From: https://gist.github.com/CCob/fe3b63d80890fafeca982f76c8a3efdf
unsigned long long SetBits(unsigned long long dw, int lowBit, int bits, unsigned long long newValue)
{
unsigned long long mask = (1UL << bits) - 1UL;
dw = (dw & ~(mask << lowBit)) | (newValue << lowBit);
return dw;
}

void UpdateAddresses(PVOID excpAddr, PVOID syscAddr, DWORD ssn)
{
WaitForSingleObject(mutex, INFINITE);
EXCP_ADDR = excpAddr;
SYSC_ADDR = syscAddr;
SSN = ssn;
ReleaseMutex(mutex);
}

void EnableBreakpoint(CONTEXT* ctx, PVOID address) {
ctx->Dr0 = (ULONG_PTR)address;
ctx->Dr7 = SetBits(ctx->Dr7, 16, 16, 0);
ctx->Dr7 = SetBits(ctx->Dr7, 0, 1, 1);
ctx->Dr6 = 0;
}

void ClearBreakpoint(CONTEXT* ctx)
{
ctx->Dr0 = 0;
ctx->Dr7 = SetBits(ctx->Dr7, 0, 1, 0);
ctx->Dr6 = 0;
ctx->EFlags = 0;
}

LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS exceptions)
{
if (exceptions->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP &&
exceptions->ExceptionRecord->ExceptionAddress == EXCP_ADDR)
{
WaitForSingleObject(mutex, INFINITE);
exceptions->ContextRecord->R10 = exceptions->ContextRecord->Rcx;
exceptions->ContextRecord->Rax = SSN;
exceptions->ContextRecord->Rip = (DWORD64) SYSC_ADDR;
ReleaseMutex(mutex);
ClearBreakpoint(exceptions->ContextRecord);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

#endif

// Code below is adapted from @modexpblog. Read linked article for more details.
// https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams

Expand All @@ -40,12 +97,30 @@ DWORD SW3_HashSyscall(PCSTR FunctionName)
return Hash;
}

#ifndef JUMPER
PVOID SC_Address(PVOID NtApiAddress)
#ifndef VECTORED
BOOL SW3_Init()
{
return NULL;
return TRUE;
}
#else
BOOL SW3_Init()
{
static HANDLE hExHandler = NULL;

if(mutex == NULL)
mutex = CreateMutex(NULL, FALSE, NULL);

if(hExHandler == NULL)
hExHandler = AddVectoredExceptionHandler(1, ExceptionHandler);

if(hExHandler == NULL)
return FALSE;

return TRUE;
}
#endif

#if defined(JUMPER) || defined(VECTORED)
PVOID SC_Address(PVOID NtApiAddress)
{
DWORD searchLimit = 512;
Expand Down Expand Up @@ -123,9 +198,13 @@ PVOID SC_Address(PVOID NtApiAddress)

return NULL;
}
#else
PVOID SC_Address(PVOID NtApiAddress)
{
return NULL;
}
#endif


BOOL SW3_PopulateSyscallList()
{
// Return early if the list is already populated.
Expand Down Expand Up @@ -175,12 +254,13 @@ BOOL SW3_PopulateSyscallList()
{
PCHAR FunctionName = SW3_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);

// Is this a system call?
// Is this a system call?
if (*(USHORT*)FunctionName == 0x775a)
{
Entries[i].Hash = SW3_HashSyscall(FunctionName);
Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];
Entries[i].SyscallAddress = SC_Address(SW3_RVA2VA(PVOID, DllBase, Entries[i].Address));
Entries[i].pAddress = SW3_RVA2VA(PVOID, DllBase, Entries[i].Address);
Entries[i].SyscallAddress = SC_Address(Entries[i].pAddress);

i++;
if (i == SW3_MAX_ENTRIES) break;
Expand All @@ -202,22 +282,42 @@ BOOL SW3_PopulateSyscallList()

TempEntry.Hash = Entries[j].Hash;
TempEntry.Address = Entries[j].Address;
TempEntry.pAddress = Entries[j].pAddress;
TempEntry.SyscallAddress = Entries[j].SyscallAddress;

Entries[j].Hash = Entries[j + 1].Hash;
Entries[j].Address = Entries[j + 1].Address;
Entries[j].pAddress = Entries[j + 1].pAddress;
Entries[j].SyscallAddress = Entries[j + 1].SyscallAddress;

Entries[j + 1].Hash = TempEntry.Hash;
Entries[j + 1].Address = TempEntry.Address;
Entries[j + 1].pAddress = TempEntry.pAddress;
Entries[j + 1].SyscallAddress = TempEntry.SyscallAddress;

}
}
}

return TRUE;
}

PVOID SW3_GetFunctionAddress(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
if (!SW3_PopulateSyscallList()) return NULL;

for (DWORD i = 0; i < SW3_SyscallList.Count; i++)
{
if (FunctionHash == SW3_SyscallList.Entries[i].Hash)
{
return SW3_SyscallList.Entries[i].pAddress;
}
}

return NULL;
}

EXTERN_C DWORD SW3_GetSyscallNumber(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
Expand Down
11 changes: 8 additions & 3 deletions data/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ typedef NTSTATUS* PNTSTATUS;

typedef struct _SW3_SYSCALL_ENTRY
{
DWORD Hash;
DWORD Address;
PVOID SyscallAddress;
DWORD Hash;
DWORD Address;
PVOID pAddress;
PVOID SyscallAddress;
} SW3_SYSCALL_ENTRY, *PSW3_SYSCALL_ENTRY;

typedef struct _SW3_SYSCALL_LIST
Expand Down Expand Up @@ -56,8 +57,12 @@ typedef struct _SW3_PEB {
PSW3_PEB_LDR_DATA Ldr;
} SW3_PEB, *PSW3_PEB;

BOOL SW3_Init();
DWORD SW3_HashSyscall(PCSTR FunctionName);
BOOL SW3_PopulateSyscallList();
PVOID SW3_GetFunctionAddress(DWORD FunctionHash);
EXTERN_C DWORD SW3_GetSyscallNumber(DWORD FunctionHash);
EXTERN_C PVOID SW3_GetSyscallAddress(DWORD FunctionHash);
EXTERN_C PVOID internal_cleancall_wow64_gate(VOID);


62 changes: 60 additions & 2 deletions syswhispers.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class SyscallRecoveryType(Enum):
EGG_HUNTER = 1
JUMPER = 2
JUMPER_RANDOMIZED = 3
VECTORED = 4

@classmethod
def from_name_or_default(cls, name):
Expand Down Expand Up @@ -281,6 +282,9 @@ def populate_defined_types(self):


def validate(self):
if self.recovery == SyscallRecoveryType.VECTORED and self.arch != Arch.x64:
exit("[-] Vectored compatible only with x64 arch")

if self.recovery == SyscallRecoveryType.EGG_HUNTER:
if self.compiler in [Compiler.All, Compiler.MINGW]:
# TODO: try to make the 'db' instruction work in MinGW
Expand Down Expand Up @@ -315,6 +319,8 @@ def generate(self, function_names: list = (), basename: str = 'syscalls'):
base_source_contents = base_source_contents.replace('<BASENAME>', os.path.basename(basename), 1)
if self.recovery in [SyscallRecoveryType.JUMPER, SyscallRecoveryType.JUMPER_RANDOMIZED]:
base_source_contents = base_source_contents.replace("// JUMPER", "#define JUMPER")
elif self.recovery == SyscallRecoveryType.VECTORED:
base_source_contents = base_source_contents.replace("// VECTORED", "#define VECTORED")

if self.wow64:
base_source_contents = base_source_contents.replace('// JUMP_TO_WOW32Reserved',
Expand Down Expand Up @@ -346,9 +352,14 @@ def generate(self, function_names: list = (), basename: str = 'syscalls'):
output_source.write((self._get_function_asm_code_mingw(function_name) + '\n').encode())
output_source.write('#endif\n'.encode())

if self.recovery == SyscallRecoveryType.VECTORED:
for function_name in function_names:
output_source.write((self._get_function_vectored_code(function_name) + '\n\n').encode())


basename_suffix = ''
basename_suffix = basename_suffix.capitalize() if os.path.basename(basename).istitle() else basename_suffix
if self.compiler in [Compiler.All, Compiler.MSVC]:
if self.recovery != SyscallRecoveryType.VECTORED and self.compiler in [Compiler.All, Compiler.MSVC]:
if self.arch in [Arch.Any, Arch.x64]:
# Write x64 ASM file
basename_suffix = f'_{basename_suffix}' if '_' in basename else basename_suffix
Expand Down Expand Up @@ -498,6 +509,53 @@ def _fix_type(self, _type: str) -> str:
# return "P" + self.prefix + "_" + _type[1:]
#
# return _type

def _get_function_vectored_code(self, function_name: str) -> str:
# Check if given function is in syscall map.
if function_name not in self.prototypes:
raise ValueError('Invalid function name provided.')

function_hash = self._get_function_hash(function_name)
num_params = len(self.prototypes[function_name]['params'])
func_body = f'NTSTATUS {self.prefix.capitalize()}{function_name}('
if num_params:
for i in range(num_params):
param = self.prototypes[function_name]['params'][i]

_type = self._fix_type(param['type'])

func_body += '\n\t'
func_body += f'{_type} {param["name"]}'
func_body += ',' if i < num_params - 1 else ')'
else:
func_body += ')'

func_body += '\n{\n'
func_body += '\tPVOID funcAddr, syscAddr;\n'
func_body += '\tCONTEXT threadCtx;\n'
func_body += '\tthreadCtx.ContextFlags = CONTEXT_ALL;\n'
func_body += '\tif (!GetThreadContext((HANDLE)-2, &threadCtx))\n'
func_body += '\t\treturn 1;\n'
func_body += f'\tfuncAddr = SW3_GetFunctionAddress(0x{function_hash:08X});\n'
func_body += f'\tsyscAddr = SW3_GetSyscallAddress(0x{function_hash:08X});\n'
func_body += f'\tUpdateAddresses((BYTE*)funcAddr, syscAddr, SW3_GetSyscallNumber(0x{function_hash:08X}));\n'
func_body += '\tEnableBreakpoint(&threadCtx, EXCP_ADDR);\n'
func_body += '\tSetThreadContext((HANDLE)-2, &threadCtx);\n'

func_body += f'\treturn ((NTSTATUS (*)('
if num_params:
for i in range(num_params):
param = self.prototypes[function_name]['params'][i]
func_body += f'{param["type"]}'
func_body += ',' if i < num_params - 1 else ')'
func_body += ')(funcAddr))('
for i in range(num_params):
param = self.prototypes[function_name]['params'][i]
func_body += f'{param["name"]}'
func_body += ',' if i < num_params - 1 else ');'
func_body += '\n}'

return func_body

def _get_function_prototype(self, function_name: str) -> str:
# Check if given function is in syscall map.
Expand Down Expand Up @@ -811,7 +869,7 @@ def _get_function_asm_code_msvc(self, function_name: str, arch: Arch) -> str:
parser.add_argument('-c', '--compiler', default="msvc", choices=["msvc", "mingw", "all"], help='Compiler',
required=False)
parser.add_argument('-m', '--method', default="embedded",
choices=["embedded", "egg_hunter", "jumper", "jumper_randomized"],
choices=["embedded", "egg_hunter", "jumper", "jumper_randomized", "vectored"],
help='Syscall recovery method', required=False)
parser.add_argument('-f', '--functions', help='Comma-separated functions', required=False)
parser.add_argument('-o', '--out-file', help='Output basename (w/o extension)', required=True)
Expand Down