diff --git a/.github/ci/tasks.yaml b/.github/ci/tasks.yaml index 848684e..1ff3130 100644 --- a/.github/ci/tasks.yaml +++ b/.github/ci/tasks.yaml @@ -74,10 +74,7 @@ tasks: # copy-wildcard-match - cmdline: "copy C:\\Temp\\fsbof-test\\*.log C:\\Temp\\fsbof-test\\dest\\" - expected: | - C:\Temp\fsbof-test\file1.log - C:\Temp\fsbof-test\file2.log - 2 file(s) copied. + expected_regex: "(?s)C:\\\\Temp\\\\fsbof-test\\\\file1\\.log.*C:\\\\Temp\\\\fsbof-test\\\\file2\\.log|C:\\\\Temp\\\\fsbof-test\\\\file2\\.log.*C:\\\\Temp\\\\fsbof-test\\\\file1\\.log" # copy-wildcard-no-match - cmdline: "copy C:\\Temp\\fsbof-test\\*.xyz C:\\Temp\\fsbof-test\\dest\\" @@ -106,10 +103,7 @@ tasks: # move-wildcard-match - cmdline: "move C:\\Temp\\fsbof-test\\*.log C:\\Temp\\fsbof-test\\moved\\" - expected: | - C:\Temp\fsbof-test\file1.log - C:\Temp\fsbof-test\file2.log - 2 file(s) moved. + expected_regex: "(?s)C:\\\\Temp\\\\fsbof-test\\\\file1\\.log.*C:\\\\Temp\\\\fsbof-test\\\\file2\\.log|C:\\\\Temp\\\\fsbof-test\\\\file2\\.log.*C:\\\\Temp\\\\fsbof-test\\\\file1\\.log" # move-wildcard-no-match - cmdline: "move C:\\Temp\\fsbof-test\\*.xyz C:\\Temp\\fsbof-test\\moved\\" @@ -173,3 +167,39 @@ tasks: # pwd-happy-path-step2 - cmdline: "pwd" expected: "C:\\Temp\\fsbof-test" + + # ── PS-BOF ── + + # ps-list + - cmdline: "ps list" + expected: "System" + + # ps-list-lsass + - cmdline: "ps list" + expected: "lsass.exe" + + # ps-run-spawn-ping (CI-02/04/05/06 fixture — ping runs indefinitely on server SKUs) + - cmdline: 'ps run --command "ping -n 999 127.0.0.1"' + expected_regex: "Process started: PID \\d+" + capture: + pid: "Process started: PID (\\d+)" + + # ps-run-pipe-whoami (CI-03) + - cmdline: 'ps run --command "cmd.exe /c whoami" --pipe' + expected_regex: "(?i)ci_runner" + + # ps-grep (CI-04) + - cmdline: "ps grep {{pid}}" + expected_regex: "(?s)\\[Token\\].*\\[Modules\\].*\\[Cmdline\\].*\\[Threads\\]" + + # ps-suspend (CI-05) + - cmdline: "ps suspend {{pid}}" + not_expected: "error" + + # ps-resume (CI-06) + - cmdline: "ps resume {{pid}}" + not_expected: "error" + + # ps-kill (CI-02) + - cmdline: "ps kill {{pid}}" + not_expected: "error" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1d81ee3..0709967 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -264,6 +264,10 @@ jobs: cp /workspace/FS-BOF/_bin/*.o /tmp/adaptixc2/dist/BOF-Collection/FS-BOF/_bin/ cp /workspace/FS-BOF/fs.axs /tmp/adaptixc2/dist/BOF-Collection/FS-BOF/fs.axs cp /workspace/Exit-BOF/_bin/*.o /tmp/adaptixc2/dist/BOF-Collection/Exit-BOF/_bin/ + mkdir -p /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin + cp /workspace/PS-BOF/_bin/*.o /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin/ + cp /workspace/PS-BOF/ps.axs /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/ + cp /workspace/bof-collection.axs /tmp/adaptixc2/dist/BOF-Collection/ # ── Build verification ─────────────────────────────────────── echo "=== Build verification ===" @@ -273,8 +277,16 @@ jobs: count=$(ls /tmp/adaptixc2/dist/BOF-Collection/FS-BOF/_bin/*.x64.o | wc -l) [ "$count" -eq 8 ] && echo "✓ All 8 FS-BOF x64 objects deployed to container" || \ { echo "✗ Expected 8 FS-BOF x64 objects in container bin, got $count"; exit 1; } + count=$(ls /workspace/PS-BOF/_bin/*.x64.o | wc -l) + [ "$count" -eq 6 ] && echo "✓ All 6 PS-BOF x64 objects compiled" || \ + { echo "✗ Expected 6 PS-BOF x64 objects, got $count"; exit 1; } + count=$(ls /tmp/adaptixc2/dist/BOF-Collection/PS-BOF/_bin/*.x64.o | wc -l) + [ "$count" -eq 6 ] && echo "✓ All 6 PS-BOF x64 objects deployed to container" || \ + { echo "✗ Expected 6 PS-BOF x64 objects in container bin, got $count"; exit 1; } echo "=== Build verification passed ===" + uv tool install --reinstall "git+https://github.com/TheGr3atJosh/Testing-Kit@c53a47d" + # ── Server startup ─────────────────────────────────────────── echo "Generating required TLS certificate..." openssl req -x509 -nodes -newkey rsa:2048 \ diff --git a/Makefile b/Makefile index c619300..8cb9c18 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SUBDIRS := FS-BOF Exit-BOF +SUBDIRS := FS-BOF Exit-BOF PS-BOF .PHONY: all $(SUBDIRS) clean docker-build diff --git a/PS-BOF/Makefile b/PS-BOF/Makefile new file mode 100644 index 0000000..bf0d858 --- /dev/null +++ b/PS-BOF/Makefile @@ -0,0 +1,25 @@ +CC64 = x86_64-w64-mingw32-gcc +CC86 = i686-w64-mingw32-gcc +STRIP64 = x86_64-w64-mingw32-strip --strip-unneeded +STRIP86 = i686-w64-mingw32-strip --strip-unneeded +CFLAGS = -I ../_include -I _include -w -Wno-incompatible-pointer-types -Os -DBOF -c + +all: bof + +bof: clean + @(mkdir _bin 2>/dev/null) && echo 'creating _bin directory' || echo '_bin directory exists' + @($(CC64) $(CFLAGS) list/list.c -o _bin/list.x64.o && $(STRIP64) _bin/list.x64.o) && echo '[+] list x64' || echo '[!] list x64' + @($(CC86) $(CFLAGS) list/list.c -o _bin/list.x32.o && $(STRIP86) _bin/list.x32.o) && echo '[+] list x32' || echo '[!] list x32' + @($(CC64) $(CFLAGS) kill/kill.c -o _bin/kill.x64.o && $(STRIP64) _bin/kill.x64.o) && echo '[+] kill x64' || echo '[!] kill x64' + @($(CC86) $(CFLAGS) kill/kill.c -o _bin/kill.x32.o && $(STRIP86) _bin/kill.x32.o) && echo '[+] kill x32' || echo '[!] kill x32' + @($(CC64) $(CFLAGS) run/run.c -o _bin/run.x64.o && $(STRIP64) _bin/run.x64.o) && echo '[+] run x64' || echo '[!] run x64' + @($(CC86) $(CFLAGS) run/run.c -o _bin/run.x32.o && $(STRIP86) _bin/run.x32.o) && echo '[+] run x32' || echo '[!] run x32' + @($(CC64) $(CFLAGS) grep/grep.c -o _bin/grep.x64.o && $(STRIP64) _bin/grep.x64.o) && echo '[+] grep x64' || echo '[!] grep x64' + @($(CC86) $(CFLAGS) grep/grep.c -o _bin/grep.x32.o && $(STRIP86) _bin/grep.x32.o) && echo '[+] grep x32' || echo '[!] grep x32' + @($(CC64) $(CFLAGS) suspend/suspend.c -o _bin/suspend.x64.o && $(STRIP64) _bin/suspend.x64.o) && echo '[+] suspend x64' || echo '[!] suspend x64' + @($(CC86) $(CFLAGS) suspend/suspend.c -o _bin/suspend.x32.o && $(STRIP86) _bin/suspend.x32.o) && echo '[+] suspend x32' || echo '[!] suspend x32' + @($(CC64) $(CFLAGS) resume/resume.c -o _bin/resume.x64.o && $(STRIP64) _bin/resume.x64.o) && echo '[+] resume x64' || echo '[!] resume x64' + @($(CC86) $(CFLAGS) resume/resume.c -o _bin/resume.x32.o && $(STRIP86) _bin/resume.x32.o) && echo '[+] resume x32' || echo '[!] resume x32' + +clean: + @(rm -rf _bin) diff --git a/PS-BOF/README.md b/PS-BOF/README.md new file mode 100644 index 0000000..aa4d47e --- /dev/null +++ b/PS-BOF/README.md @@ -0,0 +1,60 @@ +# PS-BOF + +Process management operations: ps list, ps kill, ps run, ps grep, ps suspend, ps resume. + +## ps list + +List all running processes. Output columns: PID, PPID, session ID, owner (domain\user), architecture. + +``` +ps list +``` + +## ps kill + +Terminate a process by PID. Optional exit code argument (defaults to 1 if omitted). + +``` +ps kill [exit_code] +``` + +## ps run + +Launch a new process. Supports default CreateProcess, credential-based launch (WithLogon), and token-based launch (WithToken), with optional PPID spoofing and stdout/stderr pipe capture. + +``` +ps run --command [--pipe] [--ppid ] [--state suspended] [--domain --username --password ] [--token ] +``` + +- `--command ` — Command line to execute (required) +- `--pipe` — Capture stdout/stderr via anonymous pipe +- `--ppid ` — Spoof parent PID +- `--state suspended` — Launch process in suspended state +- `--domain ` — Domain for CreateProcessWithLogon +- `--username ` — Username for CreateProcessWithLogon +- `--password ` — Password for CreateProcessWithLogon +- `--token ` — Token handle for CreateProcessWithToken + +## ps grep + +Inspect a process by PID. Output sections: token (owner, elevated flag, integrity level), modules (name, base address, entry point, size), command line, threads (TIDs). + +``` +ps grep +``` + +## ps suspend + +Suspend a process by PID. + +``` +ps suspend +``` + +## ps resume + +Resume a suspended process by PID. + +``` +ps resume +``` diff --git a/PS-BOF/grep/grep.c b/PS-BOF/grep/grep.c new file mode 100644 index 0000000..c1f11e8 --- /dev/null +++ b/PS-BOF/grep/grep.c @@ -0,0 +1,216 @@ +#include +#include +#include "bofdefs.h" +#include "beacon.h" + +#ifndef ProcessCommandLineInformation +#define ProcessCommandLineInformation ((PROCESSINFOCLASS)60) +#endif + +static char* WideToUtf8(LPCWSTR wide, int wide_chars) { + int needed = KERNEL32$WideCharToMultiByte(CP_UTF8, 0, wide, wide_chars, NULL, 0, NULL, NULL); + if (needed <= 0) return NULL; + char *buf = (char*)MSVCRT$malloc(needed + 1); + if (!buf) return NULL; + KERNEL32$WideCharToMultiByte(CP_UTF8, 0, wide, wide_chars, buf, needed, NULL, NULL); + buf[needed] = '\0'; + return buf; +} + +static void get_tokens(HANDLE process_handle) { + HANDLE token_handle = NULL; + TOKEN_USER *token_user_ptr = NULL; + TOKEN_MANDATORY_LABEL *integrity = NULL; + TOKEN_ELEVATION elevation = {0}; + ULONG return_len = 0; + NTSTATUS status; + + BeaconPrintf(CALLBACK_OUTPUT, "\n[Token]\n"); + + if (!ADVAPI32$OpenProcessToken(process_handle, TOKEN_QUERY, &token_handle)) { + BeaconPrintf(CALLBACK_ERROR, "OpenProcessToken failed: %d\n", KERNEL32$GetLastError()); + return; + } + + status = NTDLL$NtQueryInformationToken(token_handle, TokenUser, NULL, 0, &return_len); + if (return_len > 0) { + token_user_ptr = (TOKEN_USER*)MSVCRT$malloc(return_len); + if (token_user_ptr) { + status = NTDLL$NtQueryInformationToken(token_handle, TokenUser, + token_user_ptr, return_len, &return_len); + if (NT_SUCCESS(status)) { + WCHAR domain[MAX_PATH] = {0}; + WCHAR username[MAX_PATH] = {0}; + DWORD dom_len = MAX_PATH, usr_len = MAX_PATH; + SID_NAME_USE sid_type; + if (ADVAPI32$LookupAccountSidW(NULL, token_user_ptr->User.Sid, + username, &usr_len, domain, &dom_len, &sid_type)) { + char *dom_n = WideToUtf8(domain, -1); + char *usr_n = WideToUtf8(username, -1); + BeaconPrintf(CALLBACK_OUTPUT, " User: %s\\%s\n", + dom_n ? dom_n : "?", usr_n ? usr_n : "?"); + if (dom_n) MSVCRT$free(dom_n); + if (usr_n) MSVCRT$free(usr_n); + } + } + MSVCRT$free(token_user_ptr); + } + } + + return_len = sizeof(elevation); + status = NTDLL$NtQueryInformationToken(token_handle, TokenElevation, + &elevation, return_len, &return_len); + if (NT_SUCCESS(status)) { + BeaconPrintf(CALLBACK_OUTPUT, " Elevated: %s\n", + elevation.TokenIsElevated ? "Yes" : "No"); + } + + NTDLL$NtQueryInformationToken(token_handle, TokenIntegrityLevel, NULL, 0, &return_len); + if (return_len > 0) { + integrity = (TOKEN_MANDATORY_LABEL*)MSVCRT$malloc(return_len); + if (integrity) { + status = NTDLL$NtQueryInformationToken(token_handle, TokenIntegrityLevel, + integrity, return_len, &return_len); + if (NT_SUCCESS(status)) { + SID *isid = (SID*)integrity->Label.Sid; + ULONG level = isid->SubAuthority[isid->SubAuthorityCount - 1]; + const char *lvl_str = "Untrusted"; + if (level >= SECURITY_MANDATORY_SYSTEM_RID) lvl_str = "System"; + else if (level >= SECURITY_MANDATORY_HIGH_RID) lvl_str = "High"; + else if (level >= SECURITY_MANDATORY_MEDIUM_RID) lvl_str = "Medium"; + else if (level >= SECURITY_MANDATORY_LOW_RID) lvl_str = "Low"; + BeaconPrintf(CALLBACK_OUTPUT, " Integrity: %s\n", lvl_str); + } + MSVCRT$free(integrity); + } + } + + KERNEL32$CloseHandle(token_handle); +} + +static void get_modules(HANDLE process_handle) { + HMODULE modules[256]; + DWORD needed = 0; + WCHAR wide_name[MAX_PATH]; + MODULEINFO mod_info; + DWORD mod_count, i; + + BeaconPrintf(CALLBACK_OUTPUT, "\n[Modules]\n"); + + if (!PSAPI$EnumProcessModulesEx(process_handle, modules, sizeof(modules), &needed, 3)) { + BeaconPrintf(CALLBACK_ERROR, "EnumProcessModulesEx failed: %d\n", KERNEL32$GetLastError()); + return; + } + + mod_count = needed / sizeof(HMODULE); + { + DWORD buf_capacity = sizeof(modules) / sizeof(HMODULE); + if (mod_count > buf_capacity) mod_count = buf_capacity; + } + for (i = 0; i < mod_count; i++) { + char *name_n = NULL; + if (PSAPI$GetModuleFileNameExW(process_handle, modules[i], wide_name, MAX_PATH)) { + name_n = WideToUtf8(wide_name, -1); + } + if (PSAPI$GetModuleInformation(process_handle, modules[i], &mod_info, sizeof(mod_info))) { + BeaconPrintf(CALLBACK_OUTPUT, " %-60s base=0x%p entry=0x%p size=0x%lx\n", + name_n ? name_n : "?", + mod_info.lpBaseOfDll, + mod_info.EntryPoint, + (unsigned long)mod_info.SizeOfImage); + } + if (name_n) MSVCRT$free(name_n); + } +} + +static void get_cmdline(HANDLE process_handle) { + PVOID buffer = NULL; + PUNICODE_STRING cmdline = NULL; + ULONG return_len = 0; + NTSTATUS status; + + BeaconPrintf(CALLBACK_OUTPUT, "\n[Cmdline]\n"); + + NTDLL$NtQueryInformationProcess(process_handle, ProcessCommandLineInformation, + NULL, 0, &return_len); + if (return_len > 0) { + buffer = MSVCRT$malloc(return_len); + if (!buffer) { + BeaconPrintf(CALLBACK_ERROR, "get_cmdline: malloc failed\n"); + return; + } + status = NTDLL$NtQueryInformationProcess(process_handle, ProcessCommandLineInformation, + buffer, return_len, &return_len); + if (NT_SUCCESS(status)) { + char *narrow; + cmdline = (PUNICODE_STRING)buffer; + narrow = WideToUtf8(cmdline->Buffer, -1); + if (narrow) { + BeaconPrintf(CALLBACK_OUTPUT, " %s\n", narrow); + MSVCRT$free(narrow); + } + } else { + BeaconPrintf(CALLBACK_ERROR, "NtQueryInformationProcess(cmdline) failed: 0x%lx\n", + (unsigned long)status); + } + MSVCRT$free(buffer); + } else { + BeaconPrintf(CALLBACK_ERROR, "get_cmdline: size probe returned 0\n"); + } +} + +static void get_threads(DWORD pid) { + HANDLE snapshot = INVALID_HANDLE_VALUE; + THREADENTRY32 te; + + BeaconPrintf(CALLBACK_OUTPUT, "\n[Threads]\n"); + + snapshot = KERNEL32$CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + BeaconPrintf(CALLBACK_ERROR, "CreateToolhelp32Snapshot failed: %d\n", KERNEL32$GetLastError()); + return; + } + + te.dwSize = sizeof(THREADENTRY32); + if (KERNEL32$Thread32First(snapshot, &te)) { + do { + if (te.th32OwnerProcessID == pid) { + BeaconPrintf(CALLBACK_OUTPUT, " TID: %lu\n", (unsigned long)te.th32ThreadID); + } + } while (KERNEL32$Thread32Next(snapshot, &te)); + } + + KERNEL32$CloseHandle(snapshot); +} + +void go(char *args, int len) { + HANDLE process_handle = NULL; + datap data_parser = {0}; + INT32 pid; + + BeaconDataParse(&data_parser, args, len); + pid = BeaconDataInt(&data_parser); + + process_handle = KERNEL32$OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + FALSE, (DWORD)pid); + if (!process_handle) { + DWORD open_err = KERNEL32$GetLastError(); + process_handle = KERNEL32$OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, + FALSE, (DWORD)pid); + if (!process_handle) { + BeaconPrintf(CALLBACK_ERROR, "ps grep: OpenProcess failed for PID %d (error %d)\n", + pid, open_err); + return; + } + } + + get_tokens(process_handle); + get_modules(process_handle); + get_cmdline(process_handle); + get_threads((DWORD)pid); + + goto cleanup; + +cleanup: + if (process_handle) KERNEL32$CloseHandle(process_handle); +} diff --git a/PS-BOF/kill/kill.c b/PS-BOF/kill/kill.c new file mode 100644 index 0000000..a9df58a --- /dev/null +++ b/PS-BOF/kill/kill.c @@ -0,0 +1,32 @@ +#include +#include "bofdefs.h" +#include "beacon.h" + +void go(char *args, int len) +{ + datap data_parser = {0}; + INT32 process_id; + INT32 process_exitcode; + HANDLE h; + BOOL ok; + + BeaconDataParse(&data_parser, args, len); + process_id = BeaconDataInt(&data_parser); + process_exitcode = BeaconDataInt(&data_parser); + + h = KERNEL32$OpenProcess(PROCESS_TERMINATE, FALSE, (DWORD)process_id); + if (!h) { + BeaconPrintf(CALLBACK_ERROR, "OpenProcess failed: %d\n", KERNEL32$GetLastError()); + return; + } + + ok = KERNEL32$TerminateProcess(h, (UINT)process_exitcode); + KERNEL32$CloseHandle(h); + + if (!ok) { + BeaconPrintf(CALLBACK_ERROR, "TerminateProcess failed: %d\n", KERNEL32$GetLastError()); + return; + } + + BeaconPrintf(CALLBACK_OUTPUT, "Process killed\n"); +} diff --git a/PS-BOF/list/list.c b/PS-BOF/list/list.c new file mode 100644 index 0000000..8ba8eae --- /dev/null +++ b/PS-BOF/list/list.c @@ -0,0 +1,170 @@ +#include +#include "bofdefs.h" +#include "beacon.h" + +#ifndef STATUS_BUFFER_TOO_SMALL +#define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023) +#endif +#ifndef STATUS_INFO_LENGTH_MISMATCH +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004) +#endif + +static WCHAR* GetUserByToken(HANDLE token_handle) { + TOKEN_USER *token_user_ptr = NULL; + SID_NAME_USE sid_name = SidTypeUnknown; + NTSTATUS status; + WCHAR *user_domain = NULL; + WCHAR *domain = NULL; + WCHAR *username = NULL; + ULONG total_len = 0, return_len = 0, domain_len = 0, username_ln = 0; + BOOL success = FALSE; + + status = NTDLL$NtQueryInformationToken(token_handle, TokenUser, NULL, 0, &return_len); + if (status != STATUS_BUFFER_TOO_SMALL) + goto cleanup; + + token_user_ptr = (TOKEN_USER*)MSVCRT$malloc(return_len); + if (!token_user_ptr) goto cleanup; + + status = NTDLL$NtQueryInformationToken(token_handle, TokenUser, + token_user_ptr, return_len, &return_len); + if (!NT_SUCCESS(status)) goto cleanup; + + ADVAPI32$LookupAccountSidW(NULL, token_user_ptr->User.Sid, NULL, + &username_ln, NULL, &domain_len, &sid_name); + if (KERNEL32$GetLastError() != ERROR_INSUFFICIENT_BUFFER) + goto cleanup; + + total_len = username_ln + domain_len + 2; + user_domain = (WCHAR*)MSVCRT$malloc(total_len * sizeof(WCHAR)); + if (!user_domain) goto cleanup; + + domain = (WCHAR*)MSVCRT$malloc(domain_len * sizeof(WCHAR)); + username = (WCHAR*)MSVCRT$malloc(username_ln * sizeof(WCHAR)); + if (!domain || !username) goto cleanup; + + success = ADVAPI32$LookupAccountSidW(NULL, token_user_ptr->User.Sid, + username, &username_ln, domain, &domain_len, &sid_name); + if (!success) goto cleanup; + + { + ULONG di = 0, ui = 0; + while (domain[di]) { user_domain[di] = domain[di]; di++; } + user_domain[di++] = L'\\'; + while (username[ui]) { user_domain[di] = username[ui]; di++; ui++; } + user_domain[di] = L'\0'; + } + +cleanup: + if (token_user_ptr) MSVCRT$free(token_user_ptr); + if (domain) MSVCRT$free(domain); + if (username) MSVCRT$free(username); + if (!success && user_domain) { + MSVCRT$free(user_domain); + user_domain = NULL; + } + return user_domain; +} + +static char* WideToUtf8(LPCWSTR wide, int wide_chars) { + int needed = KERNEL32$WideCharToMultiByte(CP_UTF8, 0, wide, wide_chars, NULL, 0, NULL, NULL); + if (needed <= 0) return NULL; + char *buf = (char*)MSVCRT$malloc(needed + 1); + if (!buf) return NULL; + KERNEL32$WideCharToMultiByte(CP_UTF8, 0, wide, wide_chars, buf, needed, NULL, NULL); + buf[needed] = '\0'; + return buf; +} + +void go(char *args, int len) { + SYSTEM_PROCESS_INFORMATION *system_proc_info = NULL; + PVOID base_sysproc = NULL; + ULONG return_length = 0; + NTSTATUS status; + BOOL IsWow64 = FALSE; + WCHAR *user_token = NULL; + HANDLE token_handle = NULL; + HANDLE proc_handle = NULL; + formatp output_buf; + + do { + NTDLL$NtQuerySystemInformation(SystemProcessInformation, NULL, 0, &return_length); + return_length += 4096; + if (base_sysproc) MSVCRT$free(base_sysproc); + base_sysproc = MSVCRT$malloc(return_length); + if (!base_sysproc) return; + status = NTDLL$NtQuerySystemInformation(SystemProcessInformation, + base_sysproc, return_length, &return_length); + } while (status == STATUS_INFO_LENGTH_MISMATCH); + + if (!NT_SUCCESS(status)) { + BeaconPrintf(CALLBACK_ERROR, "Failed to get system process information, error: %d\n", + KERNEL32$GetLastError()); + MSVCRT$free(base_sysproc); + return; + } + + system_proc_info = (SYSTEM_PROCESS_INFORMATION*)base_sysproc; + + BeaconFormatAlloc(&output_buf, 65536); + + BeaconFormatPrintf(&output_buf, "%-50s %6s %6s %7s %-35s %s\n", + "Name", "PID", "PPID", "Session", "User", "Arch"); + BeaconFormatPrintf(&output_buf, "%-50s %6s %6s %7s %-35s %s\n", + "----", "---", "----", "-------", "----", "----"); + + do { + proc_handle = NULL; + token_handle = NULL; + user_token = NULL; + IsWow64 = FALSE; + + proc_handle = KERNEL32$OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, + HandleToUlong(system_proc_info->UniqueProcessId)); + + if (proc_handle) { + KERNEL32$IsWow64Process(proc_handle, &IsWow64); + if (ADVAPI32$OpenProcessToken(proc_handle, TOKEN_QUERY, &token_handle) && token_handle) { + user_token = GetUserByToken(token_handle); + KERNEL32$CloseHandle(token_handle); + } + KERNEL32$CloseHandle(proc_handle); + } + + char *name = NULL; + char *user = NULL; + + if (system_proc_info->ImageName.Buffer && system_proc_info->ImageName.Length > 0) { + name = WideToUtf8(system_proc_info->ImageName.Buffer, + system_proc_info->ImageName.Length / sizeof(WCHAR)); + } + + if (user_token) { + user = WideToUtf8(user_token, KERNEL32$lstrlenW(user_token)); + MSVCRT$free(user_token); + } + + BeaconFormatPrintf(&output_buf, "%-50s %6lu %6lu %7lu %-35s %s\n", + name ? name : "[System]", + HandleToUlong(system_proc_info->UniqueProcessId), + HandleToUlong(system_proc_info->InheritedFromUniqueProcessId), + (ULONG)system_proc_info->SessionId, + user ? user : "N/A", + IsWow64 ? "x86" : "x64"); + + if (name) MSVCRT$free(name); + if (user) MSVCRT$free(user); + + if (system_proc_info->NextEntryOffset == 0) + break; + system_proc_info = (SYSTEM_PROCESS_INFORMATION*)((UINT_PTR)system_proc_info + system_proc_info->NextEntryOffset); + + } while (1); + + int out_len = 0; + char *out = BeaconFormatToString(&output_buf, &out_len); + BeaconOutput(CALLBACK_OUTPUT, out, out_len); + BeaconFormatFree(&output_buf); + + MSVCRT$free(base_sysproc); +} diff --git a/PS-BOF/ps.axs b/PS-BOF/ps.axs new file mode 100644 index 0000000..eb1936d --- /dev/null +++ b/PS-BOF/ps.axs @@ -0,0 +1,89 @@ +var metadata = { + name: "PS-BOF", + description: "Process management: ps list, ps kill, ps run, ps grep, ps suspend, ps resume", +}; + +var cmd_ps_list = ax.create_command("list", "List all running processes", "ps list"); +cmd_ps_list.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let bof_path = ax.script_dir() + "_bin/list." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}"`, "BOF: ps list"); +}); + +var cmd_ps_kill = ax.create_command("kill", "Terminate a process by PID", "ps kill 1234"); +cmd_ps_kill.addArgInt("pid", true); +cmd_ps_kill.addArgInt("exit_code", false); +cmd_ps_kill.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let pid = parsed_json["pid"]; + let bof_path = ax.script_dir() + "_bin/kill." + ax.arch(id) + ".o"; + let bof_params; + if (parsed_json["exit_code"] !== undefined && parsed_json["exit_code"] !== null) { + bof_params = ax.bof_pack("int,int", [pid, parsed_json["exit_code"]]); + } else { + bof_params = ax.bof_pack("int,int", [pid, 1]); + } + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: ps kill"); +}); + +var cmd_ps_run = ax.create_command("run", "Create a new process", "ps run --command \"cmd.exe /c whoami\" --pipe"); +cmd_ps_run.addArgFlagString("--command", "command", true, "Command line to execute"); +cmd_ps_run.addArgFlagString("--state", "state", false, "Process state: suspended or standard"); +cmd_ps_run.addArgBool("--pipe", "Capture stdout/stderr via anonymous pipe", false); +cmd_ps_run.addArgFlagInt("--ppid", "ppid", "Parent PID for PPID spoofing", 0); +cmd_ps_run.addArgFlagString("--domain", "domain", false, "Domain (CreateProcessWithLogon)"); +cmd_ps_run.addArgFlagString("--username", "username", false, "Username (CreateProcessWithLogon)"); +cmd_ps_run.addArgFlagString("--password", "password", false, "Password (CreateProcessWithLogon)"); +cmd_ps_run.addArgFlagInt("--token", "token", "Token handle (CreateProcessWithToken)", 0); +cmd_ps_run.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let cmd = parsed_json["command"] || ""; + let state = parsed_json["state"] === "suspended" ? 1 : 0; + // addArgBool stores its key with the full flag name including leading dashes, + // unlike addArgFlagString which strips the dashes and uses the separate key arg. + let pipe = parsed_json["--pipe"] ? 1 : 0; + let ppid = parsed_json["ppid"] || 0; + let domain = parsed_json["domain"] || ""; + let user = parsed_json["username"] || ""; + let pass = parsed_json["password"] || ""; + let token = parsed_json["token"] || 0; + + let method = 0; + if (token && token !== 0) { method = 2; } + else if (domain || user || pass) { method = 1; } + + let bof_params = ax.bof_pack("int,wstr,int,int,int,wstr,wstr,wstr,int", + [method, cmd, state, pipe, ppid, domain, user, pass, token]); + let bof_path = ax.script_dir() + "_bin/run." + ax.arch(id) + ".o"; + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: ps run"); +}); + +var cmd_ps_grep = ax.create_command("grep", "Inspect a process by PID", "ps grep 1234"); +cmd_ps_grep.addArgInt("pid", true); +cmd_ps_grep.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let pid = parsed_json["pid"]; + let bof_path = ax.script_dir() + "_bin/grep." + ax.arch(id) + ".o"; + let bof_params = ax.bof_pack("int", [pid]); + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: ps grep"); +}); + +var cmd_ps_suspend = ax.create_command("suspend", "Suspend a process by PID", "ps suspend 1234"); +cmd_ps_suspend.addArgInt("pid", true); +cmd_ps_suspend.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let pid = parsed_json["pid"]; + let bof_path = ax.script_dir() + "_bin/suspend." + ax.arch(id) + ".o"; + let bof_params = ax.bof_pack("int", [pid]); + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: ps suspend"); +}); + +var cmd_ps_resume = ax.create_command("resume", "Resume a suspended process by PID", "ps resume 1234"); +cmd_ps_resume.addArgInt("pid", true); +cmd_ps_resume.setPreHook(function (id, cmdline, parsed_json, ...parsed_lines) { + let pid = parsed_json["pid"]; + let bof_path = ax.script_dir() + "_bin/resume." + ax.arch(id) + ".o"; + let bof_params = ax.bof_pack("int", [pid]); + ax.execute_alias(id, cmdline, `execute bof "${bof_path}" ${bof_params}`, "BOF: ps resume"); +}); + +var cmd_ps = ax.create_command("ps", "Process management"); +cmd_ps.addSubCommands([cmd_ps_list, cmd_ps_kill, cmd_ps_run, cmd_ps_grep, cmd_ps_suspend, cmd_ps_resume]); + +var group_ps = ax.create_commands_group("PS-BOF", [cmd_ps]); +ax.register_commands_group(group_ps, ["beacon", "gopher", "kharon"], ["windows"], []); diff --git a/PS-BOF/resume/resume.c b/PS-BOF/resume/resume.c new file mode 100644 index 0000000..984aa26 --- /dev/null +++ b/PS-BOF/resume/resume.c @@ -0,0 +1,24 @@ +#include +#include "bofdefs.h" +#include "beacon.h" + +void go(char *args, int len) { + datap data_parser = {0}; + BeaconDataParse(&data_parser, args, len); + INT32 pid = BeaconDataInt(&data_parser); + + HANDLE h = KERNEL32$OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, (DWORD)pid); + if (!h) { + BeaconPrintf(CALLBACK_ERROR, "OpenProcess failed: %d\n", KERNEL32$GetLastError()); + return; + } + + NTSTATUS status = NTDLL$NtResumeProcess(h); + KERNEL32$CloseHandle(h); + + if (!NT_SUCCESS(status)) { + BeaconPrintf(CALLBACK_ERROR, "NtResumeProcess failed: 0x%08x\n", status); + return; + } + BeaconPrintf(CALLBACK_OUTPUT, "Process resumed\n"); +} diff --git a/PS-BOF/run/run.c b/PS-BOF/run/run.c new file mode 100644 index 0000000..bb84fcc --- /dev/null +++ b/PS-BOF/run/run.c @@ -0,0 +1,313 @@ +#include +#include "bofdefs.h" +#include "beacon.h" + +/* ------------------------------------------------------------------------- + * Method constants (matches Phase 26 ps.axs packing order) + * -----------------------------------------------------------------------*/ +#define CREATE_METHOD_DEFAULT 0 /* CreateProcessW */ +#define CREATE_METHOD_LOGON 1 /* CreateProcessWithLogonW */ +#define CREATE_METHOD_TOKEN 2 /* CreateProcessWithTokenW */ + +/* Fallback defines for older MinGW toolchains */ +#ifndef EXTENDED_STARTUPINFO_PRESENT +#define EXTENDED_STARTUPINFO_PRESENT 0x00080000 +#endif +#ifndef PROC_THREAD_ATTRIBUTE_PARENT_PROCESS +#define PROC_THREAD_ATTRIBUTE_PARENT_PROCESS ((DWORD_PTR)(0x00020002)) +#endif +#ifndef LOGON_WITH_PROFILE +#define LOGON_WITH_PROFILE 0x00000001 +#endif +#ifndef CREATE_NO_WINDOW +#define CREATE_NO_WINDOW 0x08000000 +#endif + +/* ------------------------------------------------------------------------- + * Argument struct — local to run.c only + * -----------------------------------------------------------------------*/ +typedef struct { + int method; /* CREATE_METHOD_* */ + DWORD state; /* 0 or CREATE_SUSPENDED */ + int pipe; /* 0=no capture, 1=capture stdout/stderr */ + int ppid; /* 0=no spoofing, nonzero=target parent PID */ + HANDLE token; /* WithToken method */ + WCHAR *argument; /* lpCommandLine (raw pointer into args buffer) */ + WCHAR *domain; /* WithLogon */ + WCHAR *username; /* WithLogon */ + WCHAR *password; /* WithLogon */ +} PS_RUN_ARGS; + +/* ------------------------------------------------------------------------- + * fmt_err — human-readable Win32 error via FormatMessageA (D-05) + * -----------------------------------------------------------------------*/ +static void fmt_err(const char *prefix, DWORD code) +{ + LPSTR msg = NULL; + KERNEL32$FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&msg, 0, NULL); + BeaconPrintf(CALLBACK_ERROR, "%s (%lu): %s\n", + prefix, (unsigned long)code, msg ? msg : ""); + if (msg) KERNEL32$LocalFree(msg); +} + +/* ------------------------------------------------------------------------- + * read_pipe_output — simple blocking ReadFile loop (D-04) + * Streams captured output directly to beacon; no growing buffer needed. + * -----------------------------------------------------------------------*/ +static void read_pipe_output(HANDLE pipe_read) +{ + char *buf = (char*)MSVCRT$malloc(4096); + DWORD bytes_read = 0; + if (!buf) return; + while (KERNEL32$ReadFile(pipe_read, buf, 4096, &bytes_read, NULL) && bytes_read > 0) + BeaconOutput(CALLBACK_OUTPUT, buf, (int)bytes_read); + MSVCRT$free(buf); +} + +/* ------------------------------------------------------------------------- + * go — BOF entry point + * Arg parse order (must match ps.axs packing in Phase 26): + * method (int), command (wstr), state (int), pipe (int), ppid (int), + * domain (wstr), username (wstr), password (wstr), token (int/HANDLE) + * -----------------------------------------------------------------------*/ +void go(char *args, int len) +{ + datap parser; + PS_RUN_ARGS a; + PROCESS_INFORMATION pi; + STARTUPINFOEXW siex; + STARTUPINFOW si; + SECURITY_ATTRIBUTES sa; + HANDLE pipe_read = NULL; + HANDLE pipe_write = NULL; + HANDLE pipe_dup = NULL; + HANDLE parent_handle= NULL; + PVOID attribute_buff = NULL; + SIZE_T attribute_size = 0; + DWORD creation_flags; + DWORD err; + BOOL success = FALSE; + LPSTARTUPINFOW psi = NULL; + /* Writable command-line copy (CreateProcessW may modify lpCommandLine). + * Heap-allocated: 64 KB on the stack would overflow BOF thread stacks. */ + WCHAR *cmd_buf = NULL; + + /* Zero-init everything */ + cmd_buf = (WCHAR*)MSVCRT$malloc(32768 * sizeof(WCHAR)); + if (!cmd_buf) { + BeaconPrintf(CALLBACK_ERROR, "ps run: malloc failed\n"); + return; + } + + intZeroMemory(&a, sizeof(a)); + intZeroMemory(&pi, sizeof(pi)); + intZeroMemory(&siex, sizeof(siex)); + intZeroMemory(&si, sizeof(si)); + intZeroMemory(&sa, sizeof(sa)); + intZeroMemory(cmd_buf, 32768 * sizeof(WCHAR)); + + /* ---- Parse beacon args ---- */ + BeaconDataParse(&parser, args, len); + a.method = BeaconDataInt(&parser); + a.argument = (WCHAR*)BeaconDataExtract(&parser, NULL); + a.state = BeaconDataInt(&parser); /* nonzero → CREATE_SUSPENDED */ + a.pipe = BeaconDataInt(&parser); + a.ppid = BeaconDataInt(&parser); + a.domain = (WCHAR*)BeaconDataExtract(&parser, NULL); + a.username = (WCHAR*)BeaconDataExtract(&parser, NULL); + a.password = (WCHAR*)BeaconDataExtract(&parser, NULL); + a.token = (HANDLE)(ULONG_PTR)BeaconDataInt(&parser); + + /* Copy command to writable buffer */ + if (a.argument) { + int wlen = KERNEL32$lstrlenW(a.argument); + if (wlen >= 32768) { + BeaconPrintf(CALLBACK_ERROR, "ps run: command too long\n"); + return; + } + memcpy(cmd_buf, a.argument, (wlen + 1) * sizeof(WCHAR)); + } + + /* ---- Base creation flags ---- */ + creation_flags = CREATE_NO_WINDOW; + if (a.state) creation_flags |= CREATE_SUSPENDED; + + /* ---- Setup startup info based on method ---- */ + if (a.method == CREATE_METHOD_DEFAULT) { + /* cb must match creation_flags: EXTENDED_STARTUPINFO_PRESENT is only + * added when PPID spoofing is active, so use sizeof(STARTUPINFOW) + * for the plain case — Windows validates cb strictly. */ + siex.StartupInfo.cb = a.ppid ? sizeof(STARTUPINFOEXW) : sizeof(STARTUPINFOW); + siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; + siex.StartupInfo.wShowWindow = SW_HIDE; + psi = &siex.StartupInfo; + + if (a.ppid) { + /* Size probe */ + KERNEL32$InitializeProcThreadAttributeList(NULL, 1, 0, &attribute_size); + attribute_buff = MSVCRT$malloc(attribute_size); + if (!attribute_buff) { + BeaconPrintf(CALLBACK_ERROR, "ps run: failed to allocate attribute list\n"); + goto cleanup; + } + if (!KERNEL32$InitializeProcThreadAttributeList( + (LPPROC_THREAD_ATTRIBUTE_LIST)attribute_buff, 1, 0, &attribute_size)) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: InitializeProcThreadAttributeList failed", err); + goto cleanup; + } + + /* Open the target parent — needs PROCESS_DUP_HANDLE for pipe dup */ + parent_handle = KERNEL32$OpenProcess( + PROCESS_CREATE_PROCESS | PROCESS_DUP_HANDLE, FALSE, (DWORD)a.ppid); + if (!parent_handle) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: OpenProcess (PPID) failed", err); + goto cleanup; + } + + if (!KERNEL32$UpdateProcThreadAttribute( + (LPPROC_THREAD_ATTRIBUTE_LIST)attribute_buff, 0, + PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, + &parent_handle, sizeof(HANDLE), NULL, NULL)) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: UpdateProcThreadAttribute failed", err); + goto cleanup; + } + + siex.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)attribute_buff; + creation_flags |= EXTENDED_STARTUPINFO_PRESENT; + } + } else { + /* Plain STARTUPINFOW — WithLogon and WithToken do not support attribute lists */ + si.cb = sizeof(STARTUPINFOW); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + psi = &si; + } + + /* ---- Setup anonymous pipe if capture requested ---- */ + if (a.pipe) { + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = FALSE; /* create non-inheritable; we mark only pipe_write below */ + + if (!KERNEL32$CreatePipe(&pipe_read, &pipe_write, &sa, 0)) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: CreatePipe failed", err); + goto cleanup; + } + /* Make only the write end inheritable so no other parent handles leak into the child */ + KERNEL32$SetHandleInformation(pipe_write, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); + + if (a.method == CREATE_METHOD_DEFAULT && a.ppid && parent_handle) { + /* + * PPID + pipe combined: with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS + * active, the child inherits handles from the *spoofed parent's* + * handle table, not ours. Duplicate pipe_write into the parent + * so the child can inherit it. (Confirmed from Kharon lines 268-277) + */ + if (!KERNEL32$DuplicateHandle( + KERNEL32$GetCurrentProcess(), pipe_write, + parent_handle, &pipe_dup, + 0, TRUE, DUPLICATE_SAME_ACCESS)) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: DuplicateHandle (pipe into parent) failed", err); + goto cleanup; + } + KERNEL32$CloseHandle(pipe_write); + pipe_write = pipe_dup; + pipe_dup = NULL; + } + + /* Wire pipe into startup info */ + if (a.method == CREATE_METHOD_DEFAULT) { + siex.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + siex.StartupInfo.hStdOutput = pipe_write; + siex.StartupInfo.hStdError = pipe_write; + siex.StartupInfo.hStdInput = KERNEL32$GetStdHandle(STD_INPUT_HANDLE); + } else { + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdOutput = pipe_write; + si.hStdError = pipe_write; + si.hStdInput = KERNEL32$GetStdHandle(STD_INPUT_HANDLE); + } + } + + /* ---- Launch the process ---- */ + switch (a.method) { + case CREATE_METHOD_DEFAULT: + success = KERNEL32$CreateProcessW( + NULL, cmd_buf, NULL, NULL, (BOOL)a.pipe, + creation_flags, NULL, NULL, + psi, &pi); + break; + + case CREATE_METHOD_LOGON: + success = ADVAPI32$CreateProcessWithLogonW( + a.username, a.domain, a.password, + LOGON_WITH_PROFILE, + NULL, cmd_buf, creation_flags, + NULL, NULL, + psi, &pi); + break; + + case CREATE_METHOD_TOKEN: + success = ADVAPI32$CreateProcessWithTokenW( + a.token, LOGON_WITH_PROFILE, + NULL, cmd_buf, creation_flags, + NULL, NULL, + psi, &pi); + break; + + default: + BeaconPrintf(CALLBACK_ERROR, + "ps run: unknown method %d (0=default,1=logon,2=token)\n", + a.method); + goto cleanup; + } + + if (!success) { + err = KERNEL32$GetLastError(); + fmt_err("ps run: CreateProcess failed", err); + goto cleanup; + } + + /* + * Close write end BEFORE the ReadFile loop — otherwise ReadFile never + * gets EOF because the write end remains open in our handle table. + */ + if (pipe_write) { + KERNEL32$CloseHandle(pipe_write); + pipe_write = NULL; + } + + /* Capture stdout/stderr if pipe was requested */ + if (a.pipe && pipe_read) { + read_pipe_output(pipe_read); + } + + BeaconPrintf(CALLBACK_OUTPUT, + "Process started: PID %lu, TID %lu\n", + (unsigned long)pi.dwProcessId, + (unsigned long)pi.dwThreadId); + + KERNEL32$CloseHandle(pi.hProcess); + KERNEL32$CloseHandle(pi.hThread); + +cleanup: + if (attribute_buff) { + KERNEL32$DeleteProcThreadAttributeList( + (LPPROC_THREAD_ATTRIBUTE_LIST)attribute_buff); + MSVCRT$free(attribute_buff); + } + if (pipe_read) KERNEL32$CloseHandle(pipe_read); + if (pipe_write) KERNEL32$CloseHandle(pipe_write); + if (parent_handle) KERNEL32$CloseHandle(parent_handle); + if (cmd_buf) MSVCRT$free(cmd_buf); +} diff --git a/PS-BOF/suspend/suspend.c b/PS-BOF/suspend/suspend.c new file mode 100644 index 0000000..1816c8a --- /dev/null +++ b/PS-BOF/suspend/suspend.c @@ -0,0 +1,24 @@ +#include +#include "bofdefs.h" +#include "beacon.h" + +void go(char *args, int len) { + datap data_parser = {0}; + BeaconDataParse(&data_parser, args, len); + INT32 pid = BeaconDataInt(&data_parser); + + HANDLE h = KERNEL32$OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, (DWORD)pid); + if (!h) { + BeaconPrintf(CALLBACK_ERROR, "OpenProcess failed: %d\n", KERNEL32$GetLastError()); + return; + } + + NTSTATUS status = NTDLL$NtSuspendProcess(h); + KERNEL32$CloseHandle(h); + + if (!NT_SUCCESS(status)) { + BeaconPrintf(CALLBACK_ERROR, "NtSuspendProcess failed: 0x%08x\n", status); + return; + } + BeaconPrintf(CALLBACK_OUTPUT, "Process suspended\n"); +} diff --git a/README.md b/README.md index 0dfa5a1..1028055 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,20 @@ Terminate the beacon process or thread. [More details](Exit-BOF/README.md) |exit process|`exit process`|Terminate beacon process via `RtlExitUserProcess(0)`. Terminal.| |exit thread|`exit thread`|Terminate beacon thread via `RtlExitUserThread(0)`. Terminal.| +## PS-BOF + +Process management: ps list, ps kill, ps run, ps grep, ps suspend, ps resume. [More details](PS-BOF/README.md) + +|Commands|Usage|Notes| +|--------|-----|-----| +|ps list|`ps list`|List all running processes (PID, PPID, session, owner, arch)| +|ps kill|`ps kill [exit_code]`|Terminate a process; optional exit code| +|ps run|`ps run --command [--pipe] [--ppid ] [--state suspended] [--domain --username --password ] [--token ]`|Launch a process (CreateProcess/WithLogon/WithToken); optional PPID spoofing and pipe capture| +|ps grep|`ps grep `|Inspect a process: token, modules, cmdline, threads| +|ps suspend|`ps suspend `|Suspend a process| +|ps resume|`ps resume `|Resume a suspended process| + ## Credits - [Extension-Kit](https://github.com/Adaptix-Framework/Extension-Kit): Project structure and README +- [Kharon](https://github.com/entropy-z/Kharon): PS-BOF command implementations diff --git a/_include/bofdefs.h b/_include/bofdefs.h index deac5ad..bcb841f 100644 --- a/_include/bofdefs.h +++ b/_include/bofdefs.h @@ -14,6 +14,8 @@ #include #include +#include +#include // ============================================================================= // KERNEL32 — memory management @@ -70,7 +72,9 @@ WINBASEAPI WINBOOL WINAPI KERNEL32$DeleteFileW(LPCWSTR lpFileName); // MSVCRT (used by base.c shared by all FS-BOF, and by fserror.h fallback) // ============================================================================= WINBASEAPI void *__cdecl MSVCRT$calloc(size_t _NumOfElements, size_t _SizeOfElements); +WINBASEAPI void *__cdecl MSVCRT$malloc(size_t _Size); WINBASEAPI void __cdecl MSVCRT$free(void *_Memory); +WINBASEAPI void * __cdecl MSVCRT$memset(void *dest, int c, size_t count); WINBASEAPI int __cdecl MSVCRT$vsnprintf(char * __restrict__ d, size_t n, const char * __restrict__ format, va_list arg); WINBASEAPI int __cdecl MSVCRT$_snprintf(char * __restrict__ _Dest, size_t _Count, const char * __restrict__ _Format, ...); @@ -80,6 +84,71 @@ WINBASEAPI int __cdecl MSVCRT$_snprintf(char * __restrict__ _Dest, size_t _Count WINBASEAPI VOID NTAPI NTDLL$RtlExitUserProcess(NTSTATUS Status); WINBASEAPI VOID NTAPI NTDLL$RtlExitUserThread(NTSTATUS Status); +// ============================================================================= +// NTDLL — process control (PS-BOF) +// ============================================================================= +WINBASEAPI NTSTATUS NTAPI NTDLL$NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength); +WINBASEAPI NTSTATUS NTAPI NTDLL$NtSuspendProcess(HANDLE ProcessHandle); +WINBASEAPI NTSTATUS NTAPI NTDLL$NtResumeProcess(HANDLE ProcessHandle); + +// ============================================================================= +// KERNEL32 — process management (PS-BOF) +// ============================================================================= +WINBASEAPI HANDLE WINAPI KERNEL32$OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId); +WINBASEAPI BOOL WINAPI KERNEL32$TerminateProcess(HANDLE hProcess, UINT uExitCode); +WINBASEAPI BOOL WINAPI KERNEL32$IsWow64Process(HANDLE hProcess, PBOOL Wow64Process); +WINBASEAPI HANDLE WINAPI KERNEL32$GetCurrentProcess(VOID); + +// ============================================================================= +// KERNEL32 — process creation (PS-BOF run) +// ============================================================================= +WINBASEAPI BOOL WINAPI KERNEL32$CreateProcessW(LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +WINBASEAPI HANDLE WINAPI KERNEL32$GetStdHandle(DWORD nStdHandle); +WINBASEAPI BOOL WINAPI KERNEL32$CreatePipe(PHANDLE, PHANDLE, LPSECURITY_ATTRIBUTES, DWORD); +WINBASEAPI BOOL WINAPI KERNEL32$SetHandleInformation(HANDLE, DWORD, DWORD); +WINBASEAPI BOOL WINAPI KERNEL32$InitializeProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD, PSIZE_T); +WINBASEAPI BOOL WINAPI KERNEL32$UpdateProcThreadAttribute(LPPROC_THREAD_ATTRIBUTE_LIST, DWORD, DWORD_PTR, PVOID, SIZE_T, PVOID, PSIZE_T); +WINBASEAPI VOID WINAPI KERNEL32$DeleteProcThreadAttributeList(LPPROC_THREAD_ATTRIBUTE_LIST); +WINBASEAPI BOOL WINAPI KERNEL32$DuplicateHandle(HANDLE, HANDLE, HANDLE, LPHANDLE, DWORD, BOOL, DWORD); + +// ============================================================================= +// ADVAPI32 — process creation with credentials/token (PS-BOF run) +// ============================================================================= +WINADVAPI BOOL WINAPI ADVAPI32$CreateProcessWithLogonW(LPCWSTR, LPCWSTR, LPCWSTR, DWORD, LPCWSTR, LPWSTR, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION); +WINADVAPI BOOL WINAPI ADVAPI32$CreateProcessWithTokenW(HANDLE, DWORD, LPCWSTR, LPWSTR, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION); + +// ============================================================================= +// ADVAPI32 — token / privilege (PS-BOF) +// ============================================================================= +WINADVAPI BOOL WINAPI ADVAPI32$OpenProcessToken(HANDLE ProcessHandle, DWORD DesiredAccess, PHANDLE TokenHandle); +WINADVAPI BOOL WINAPI ADVAPI32$LookupAccountSidW(LPCWSTR lpSystemName, PSID Sid, LPWSTR Name, LPDWORD cchName, LPWSTR ReferencedDomainName, LPDWORD cchReferencedDomainName, PSID_NAME_USE peUse); +WINADVAPI BOOL WINAPI ADVAPI32$AdjustTokenPrivileges(HANDLE TokenHandle, BOOL DisableAllPrivileges, PTOKEN_PRIVILEGES NewState, DWORD BufferLength, PTOKEN_PRIVILEGES PreviousState, PDWORD ReturnLength); +WINADVAPI BOOL WINAPI ADVAPI32$LookupPrivilegeValueW(LPCWSTR lpSystemName, LPCWSTR lpName, PLUID lpLuid); + +// ============================================================================= +// NTDLL — token query (PS-BOF) +// ============================================================================= +WINBASEAPI NTSTATUS NTAPI NTDLL$NtQueryInformationToken(HANDLE TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, PVOID TokenInformation, ULONG TokenInformationLength, PULONG ReturnLength); + +// ============================================================================= +// PSAPI — module enumeration (PS-BOF grep) +// ============================================================================= +WINBASEAPI WINBOOL WINAPI PSAPI$EnumProcessModulesEx(HANDLE hProcess, HMODULE *lphModule, DWORD cb, LPDWORD lpcbNeeded, DWORD dwFilterFlag); +WINBASEAPI DWORD WINAPI PSAPI$GetModuleFileNameExW(HANDLE hProcess, HMODULE hModule, LPWSTR lpFilename, DWORD nSize); +WINBASEAPI WINBOOL WINAPI PSAPI$GetModuleInformation(HANDLE hProcess, HMODULE hModule, LPMODULEINFO lpmodinfo, DWORD cb); + +// ============================================================================= +// KERNEL32 — toolhelp (PS-BOF grep) +// ============================================================================= +WINBASEAPI HANDLE WINAPI KERNEL32$CreateToolhelp32Snapshot(DWORD dwFlags, DWORD th32ProcessID); +WINBASEAPI BOOL WINAPI KERNEL32$Thread32First(HANDLE hSnapshot, LPTHREADENTRY32 lpte); +WINBASEAPI BOOL WINAPI KERNEL32$Thread32Next(HANDLE hSnapshot, LPTHREADENTRY32 lpte); + +// ============================================================================= +// NTDLL — process info (PS-BOF grep) +// ============================================================================= +WINBASEAPI NTSTATUS NTAPI NTDLL$NtQueryInformationProcess(HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength); + // ============================================================================= // Helper macros (used by base.c shared across FS-BOF sources) // ============================================================================= diff --git a/bof-collection.axs b/bof-collection.axs index 3c0946e..9bf0869 100644 --- a/bof-collection.axs +++ b/bof-collection.axs @@ -7,3 +7,4 @@ var metadata = { var path = ax.script_dir(); ax.script_load(path + "FS-BOF/fs.axs"); ax.script_load(path + "Exit-BOF/exit.axs"); +ax.script_load(path + "PS-BOF/ps.axs");