From 9e5499018cd18e83642ba76eb0098a745a838c5f Mon Sep 17 00:00:00 2001 From: maxdcb <40819564+maxDcb@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:49:51 +0200 Subject: [PATCH] Functional tests --- TESTING.md | 58 ++++++ modules/AssemblyExec/AssemblyExec.cpp | 4 +- modules/AssemblyExec/CMakeLists.txt | 61 ++++++- .../testsAssemblyExecFunctional.cpp | 167 +++++++++++++++++ modules/DotnetExec/CMakeLists.txt | 30 +++- modules/DotnetExec/DotnetExec.cpp | 6 +- .../functional/testsDotnetExecFunctional.cpp | 144 +++++++++++++++ modules/Evasion/CMakeLists.txt | 11 ++ modules/Evasion/Evasion.cpp | 2 +- .../functional/testsEvasionFunctional.cpp | 74 ++++++++ modules/Inject/CMakeLists.txt | 34 +++- modules/Inject/Inject.cpp | 2 +- .../functional/testsInjectFunctional.cpp | 169 ++++++++++++++++++ modules/PsExec/CMakeLists.txt | 28 ++- modules/PsExec/PsExec.cpp | 4 +- .../functional/testsPsExecFunctional.cpp | 89 +++++++++ modules/PwSh/CMakeLists.txt | 20 ++- modules/PwSh/PwSh.cpp | 6 +- .../tests/functional/testsPwShFunctional.cpp | 138 ++++++++++++++ 19 files changed, 1019 insertions(+), 28 deletions(-) create mode 100644 modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp create mode 100644 modules/DotnetExec/tests/functional/testsDotnetExecFunctional.cpp create mode 100644 modules/Evasion/tests/functional/testsEvasionFunctional.cpp create mode 100644 modules/Inject/tests/functional/testsInjectFunctional.cpp create mode 100644 modules/PsExec/tests/functional/testsPsExecFunctional.cpp create mode 100644 modules/PwSh/tests/functional/testsPwShFunctional.cpp diff --git a/TESTING.md b/TESTING.md index a866e6e..6a1166b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -80,9 +80,15 @@ This is important because many modules keep the default `errorCode == -1` on suc Current module-level functional tests: +- `testsAssemblyExecFunctional` - `testsCimExecFunctional` - `testsDcomExecFunctional` +- `testsDotnetExecFunctional` +- `testsEvasionFunctional` - `testsEnumerateRdpSessionsFunctional` +- `testsInjectFunctional` +- `testsPsExecFunctional` +- `testsPwShFunctional` - `testsSpawnAsFunctional` - `testsSshExecFunctional` - `testsTaskSchedulerFunctional` @@ -320,6 +326,58 @@ Notes: - `kerberos` maps to `wmiExec -k` - this test is Windows-only for real execution +### AssemblyExec + +- `C2_FUNC_ASSEMBLYEXEC_PAYLOAD` +- `C2_FUNC_ASSEMBLYEXEC_KIND` +- `C2_FUNC_ASSEMBLYEXEC_MODE` +- `C2_FUNC_ASSEMBLYEXEC_METHOD` +- `C2_FUNC_ASSEMBLYEXEC_ARGS` +- `C2_FUNC_ASSEMBLYEXEC_PROCESS` +- `C2_FUNC_ASSEMBLYEXEC_SPOOFED_PARENT` + +### DotnetExec + +- `C2_FUNC_DOTNETEXEC_ASSEMBLY` +- `C2_FUNC_DOTNETEXEC_KIND` +- `C2_FUNC_DOTNETEXEC_NAME` +- `C2_FUNC_DOTNETEXEC_TYPE` +- `C2_FUNC_DOTNETEXEC_METHOD` +- `C2_FUNC_DOTNETEXEC_ARGS` + +### Evasion + +- `C2_FUNC_EVASION_ACTION` +- `C2_FUNC_EVASION_VALUE` +- `C2_FUNC_EVASION_EXTRA` + +### Inject + +- `C2_FUNC_INJECT_PAYLOAD` +- `C2_FUNC_INJECT_KIND` +- `C2_FUNC_INJECT_PID` +- `C2_FUNC_INJECT_METHOD` +- `C2_FUNC_INJECT_ARGS` +- `C2_FUNC_INJECT_PROCESS` +- `C2_FUNC_INJECT_SYSCALL` + +### PsExec + +- `C2_FUNC_PSEXEC_AUTH` +- `C2_FUNC_PSEXEC_TARGET` +- `C2_FUNC_PSEXEC_SERVICE` +- `C2_FUNC_PSEXEC_USER` +- `C2_FUNC_PSEXEC_PASSWORD` + +### PwSh + +- `C2_FUNC_PWSH_MODE` +- `C2_FUNC_PWSH_RUNNER` +- `C2_FUNC_PWSH_TYPE` +- `C2_FUNC_PWSH_COMMAND` +- `C2_FUNC_PWSH_IMPORT` +- `C2_FUNC_PWSH_SCRIPT` + ## Module-Specific Intent ### SpawnAs diff --git a/modules/AssemblyExec/AssemblyExec.cpp b/modules/AssemblyExec/AssemblyExec.cpp index 02a07f5..425b1ca 100644 --- a/modules/AssemblyExec/AssemblyExec.cpp +++ b/modules/AssemblyExec/AssemblyExec.cpp @@ -109,7 +109,7 @@ std::string AssemblyExec::getInfo() int AssemblyExec::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if(splitedCmd.size() == 2) { if(splitedCmd[1]=="thread") @@ -263,7 +263,7 @@ int AssemblyExec::init(std::vector &splitedCmd, C2Message &c2Messag int AssemblyExec::initConfig(const nlohmann::json &config) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) for (auto& it : config.items()) { if(it.key()=="process") diff --git a/modules/AssemblyExec/CMakeLists.txt b/modules/AssemblyExec/CMakeLists.txt index b90fcc7..aacd023 100644 --- a/modules/AssemblyExec/CMakeLists.txt +++ b/modules/AssemblyExec/CMakeLists.txt @@ -39,7 +39,7 @@ endif() set_property(TARGET AssemblyExec PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") -if(C2CORE_BUILD_TESTS) +if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) target_link_libraries(AssemblyExec PRIVATE Donut ) endif() c2core_disable_safeseh_for_x86(AssemblyExec) @@ -47,10 +47,14 @@ c2core_disable_safeseh_for_x86(AssemblyExec) add_custom_command(TARGET AssemblyExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") -if(C2CORE_BUILD_TESTS) +if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) if(WIN32) add_executable(assemblyExecDummyExe tests/dummyAssemblyExec.cpp) + endif() +endif() +if(C2CORE_BUILD_TESTS) + if(WIN32) if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") add_executable(testsAssemblyExec tests/testsAssemblyExec.cpp @@ -100,6 +104,53 @@ if(C2CORE_BUILD_TESTS) add_custom_command(TARGET testOutputWriter POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") - - add_test(NAME testsAssemblyExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") -endif() + + add_test(NAME testsAssemblyExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") +endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + if(WIN32) + if (CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") + add_executable(testsAssemblyExecFunctional + tests/functional/testsAssemblyExecFunctional.cpp + AssemblyExec.cpp + ../ModuleCmd/syscall.cpp + ${SYSCALL_OBJ} + ../ModuleCmd/peb.cpp + ../ModuleCmd/hwbp.cpp + ) + elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) + add_executable(testsAssemblyExecFunctional + tests/functional/testsAssemblyExecFunctional.cpp + AssemblyExec.cpp + ../ModuleCmd/syscall.cpp + ${SYSCALL_OBJ} + ../ModuleCmd/peb.cpp + ../ModuleCmd/hwbp.cpp + ) + else() + add_executable(testsAssemblyExecFunctional + tests/functional/testsAssemblyExecFunctional.cpp + AssemblyExec.cpp + ../ModuleCmd/syscall.cpp + ${SYSCALL_OBJ} + ../ModuleCmd/peb.cpp + ../ModuleCmd/hwbp.cpp + ) + endif() + target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut) + c2core_disable_safeseh_for_x86(testsAssemblyExecFunctional) + add_dependencies(testsAssemblyExecFunctional assemblyExecDummyExe) + target_compile_definitions(testsAssemblyExecFunctional PRIVATE C2_ASSEMBLYEXEC_DUMMY_EXE="$") + else() + add_executable(testsAssemblyExecFunctional tests/functional/testsAssemblyExecFunctional.cpp AssemblyExec.cpp) + target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut ${aplib64}) + endif() + + target_include_directories(testsAssemblyExecFunctional PRIVATE ../tests) + add_custom_command(TARGET testsAssemblyExecFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsAssemblyExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsAssemblyExecFunctional PROPERTIES LABELS "functional;assemblyexec;local-exec" SKIP_RETURN_CODE 77) +endif() diff --git a/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp b/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp new file mode 100644 index 0000000..f0a6514 --- /dev/null +++ b/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp @@ -0,0 +1,167 @@ +#include "../../AssemblyExec.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +#include +#include +#include +#include + +using namespace functional_tests; + +namespace +{ +std::string defaultPayloadPath() +{ +#ifdef C2_ASSEMBLYEXEC_DUMMY_EXE + return C2_ASSEMBLYEXEC_DUMMY_EXE; +#else + return {}; +#endif +} + +std::string normalizeMode(const std::string& mode) +{ + if (mode == "thread") + { + return "thread"; + } + if (mode == "process-spoofed" || mode == "processWithSpoofedParent") + { + return "processWithSpoofedParent"; + } + return "process"; +} + +std::string normalizeKind(const std::string& kind) +{ + if (kind == "dll") + { + return "dll"; + } + if (kind == "raw") + { + return "raw"; + } + return "exe"; +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--payload", "C2_FUNC_ASSEMBLYEXEC_PAYLOAD", "Assembly or shellcode path", false, false, defaultPayloadPath()}, + {"--kind", "C2_FUNC_ASSEMBLYEXEC_KIND", "Payload kind: exe, dll, or raw", false, false, "exe"}, + {"--mode", "C2_FUNC_ASSEMBLYEXEC_MODE", "Execution mode: thread, process, or process-spoofed", false, false, "process"}, + {"--method", "C2_FUNC_ASSEMBLYEXEC_METHOD", "DLL method name for dll mode", false}, + {"--args", "C2_FUNC_ASSEMBLYEXEC_ARGS", "Arguments passed to the assembly entrypoint", false}, + {"--process", "C2_FUNC_ASSEMBLYEXEC_PROCESS", "Optional process to spawn for process mode", false}, + {"--spoofed-parent", "C2_FUNC_ASSEMBLYEXEC_SPOOFED_PARENT", "Optional spoofed parent process path", false}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsAssemblyExecFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsAssemblyExecFunctional skipped: execution is only supported on Windows in this harness.\n"; + return skipReturnCode; + } +#endif + + const std::string payload = readInput(inputs[0], argc, argv, interactive); + const std::string kind = normalizeKind(readInput(inputs[1], argc, argv, interactive)); + const std::string mode = normalizeMode(readInput(inputs[2], argc, argv, interactive)); + const std::string method = readInput(inputs[3], argc, argv, interactive); + const std::string arguments = readInput(inputs[4], argc, argv, interactive); + const std::string processToSpawn = readInput(inputs[5], argc, argv, interactive); + const std::string spoofedParent = readInput(inputs[6], argc, argv, interactive); + + std::vector missing; + if (payload.empty()) + { + missing.push_back(inputs[0]); + } + if (kind == "dll" && method.empty()) + { + missing.push_back(inputs[3]); + } + if (!missing.empty()) + { + return skipMissing("testsAssemblyExecFunctional", missing); + } + + AssemblyExec module; + + C2Message modeMessage; + std::vector modeCommand = {"assemblyExec", mode}; + if (module.init(modeCommand, modeMessage) != -1) + { + std::cerr << "testsAssemblyExecFunctional mode selection failed\n"; + return 1; + } + + std::vector command = {"assemblyExec"}; + if (kind == "raw") + { + command.push_back("-r"); + command.push_back(payload); + } + else if (kind == "dll") + { + command.push_back("-d"); + command.push_back(payload); + command.push_back(method); + } + else + { + command.push_back("-e"); + command.push_back(payload); + } + if (!arguments.empty()) + { + command.push_back(arguments); + } + + C2Message message; + if (module.init(command, message) != 0) + { + std::cerr << "testsAssemblyExecFunctional init failed:\n" << message.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsAssemblyExecFunctional init OK\n"; + std::cout << "instruction: " << message.instruction() << '\n'; + std::cout << "input: " << message.inputfile() << '\n'; + + if (!execute) + { + std::cout << "execution skipped; pass --execute to run the module process path.\n"; + return 0; + } + + nlohmann::json config; + if (!processToSpawn.empty()) + { + config["process"] = processToSpawn; + } + if (!spoofedParent.empty()) + { + config["spoofedParent"] = spoofedParent; + } + if (!config.empty()) + { + module.initConfig(config); + } + + C2Message result; + module.process(message, result); + std::cout << result.returnvalue() << '\n'; + return 0; +} diff --git a/modules/DotnetExec/CMakeLists.txt b/modules/DotnetExec/CMakeLists.txt index ab0078f..0eb25b0 100644 --- a/modules/DotnetExec/CMakeLists.txt +++ b/modules/DotnetExec/CMakeLists.txt @@ -15,7 +15,7 @@ target_link_libraries(DotnetExec ) add_custom_command(TARGET DotnetExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") -if(C2CORE_BUILD_TESTS) +if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) if(WIN32) find_program(C2_CSC_EXECUTABLE NAMES csc.exe @@ -34,7 +34,11 @@ if(C2CORE_BUILD_TESTS) VERBATIM) add_custom_target(dummyDotnetExecAssembly DEPENDS "${C2_DOTNETEXEC_DUMMY_EXE}") endif() + endif() +endif() +if(C2CORE_BUILD_TESTS) + if(WIN32) add_executable(testsDotnetExec tests/testsDotnetExec.cpp DotnetExec.cpp HostControl.cpp AssemblyManager.cpp AssemblyStore.cpp HostMalloc.cpp MemoryManager.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ} tests/project.rc) if(C2_CSC_EXECUTABLE) add_dependencies(testsDotnetExec dummyDotnetExecAssembly) @@ -48,6 +52,26 @@ if(C2CORE_BUILD_TESTS) set_property(TARGET testsDotnetExec PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") add_custom_command(TARGET testsDotnetExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") - - add_test(NAME testsDotnetExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsDotnetExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") +endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + if(WIN32) + add_executable(testsDotnetExecFunctional tests/functional/testsDotnetExecFunctional.cpp DotnetExec.cpp HostControl.cpp AssemblyManager.cpp AssemblyStore.cpp HostMalloc.cpp MemoryManager.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) + if(C2_CSC_EXECUTABLE) + add_dependencies(testsDotnetExecFunctional dummyDotnetExecAssembly) + target_compile_definitions(testsDotnetExecFunctional PRIVATE C2_DOTNETEXEC_DUMMY_EXE="${C2_DOTNETEXEC_DUMMY_EXE}") + endif() + else() + add_executable(testsDotnetExecFunctional tests/functional/testsDotnetExecFunctional.cpp DotnetExec.cpp) + endif() + target_include_directories(testsDotnetExecFunctional PRIVATE ../tests) + target_link_libraries(testsDotnetExecFunctional ) + set_property(TARGET testsDotnetExecFunctional PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") + add_custom_command(TARGET testsDotnetExecFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsDotnetExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsDotnetExecFunctional PROPERTIES LABELS "functional;dotnetexec;local-exec" SKIP_RETURN_CODE 77) endif() diff --git a/modules/DotnetExec/DotnetExec.cpp b/modules/DotnetExec/DotnetExec.cpp index 539ee06..16e12c8 100644 --- a/modules/DotnetExec/DotnetExec.cpp +++ b/modules/DotnetExec/DotnetExec.cpp @@ -193,7 +193,7 @@ std::string DotnetExec::getInfo() #define runDll "00003" -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) bool endsWithDLL(const std::string& str) { @@ -221,7 +221,7 @@ bool endsWithEXE(const std::string& str) int DotnetExec::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if((splitedCmd.size()==4 || splitedCmd.size()==5) && splitedCmd[1]=="load") { @@ -1187,7 +1187,7 @@ int DotnetExec::invokeMethodDll(const std::string name, const string& method, co int DotnetExec::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) int errorCode = c2RetMessage.errorCode(); if(errorCode>0) { diff --git a/modules/DotnetExec/tests/functional/testsDotnetExecFunctional.cpp b/modules/DotnetExec/tests/functional/testsDotnetExecFunctional.cpp new file mode 100644 index 0000000..73fa30d --- /dev/null +++ b/modules/DotnetExec/tests/functional/testsDotnetExecFunctional.cpp @@ -0,0 +1,144 @@ +#include "../../DotnetExec.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +#include +#include +#include + +using namespace functional_tests; + +namespace +{ +std::string defaultAssemblyPath() +{ +#ifdef C2_DOTNETEXEC_DUMMY_EXE + return C2_DOTNETEXEC_DUMMY_EXE; +#else + return {}; +#endif +} + +std::string normalizeKind(const std::string& kind) +{ + return kind == "dll" ? "dll" : "exe"; +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--assembly", "C2_FUNC_DOTNETEXEC_ASSEMBLY", "Managed assembly path", false, false, defaultAssemblyPath()}, + {"--kind", "C2_FUNC_DOTNETEXEC_KIND", "Assembly kind: exe or dll", false, false, "exe"}, + {"--name", "C2_FUNC_DOTNETEXEC_NAME", "Short in-memory module name", false, false, "dummy"}, + {"--type", "C2_FUNC_DOTNETEXEC_TYPE", "Fully-qualified type name for dll mode", false}, + {"--method", "C2_FUNC_DOTNETEXEC_METHOD", "Method to invoke for dll mode", false}, + {"--args", "C2_FUNC_DOTNETEXEC_ARGS", "Arguments passed to the managed entrypoint", false, false, "alpha beta"}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsDotnetExecFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsDotnetExecFunctional skipped: execution is only supported on Windows.\n"; + return skipReturnCode; + } +#endif + + const std::string assembly = readInput(inputs[0], argc, argv, interactive); + const std::string kind = normalizeKind(readInput(inputs[1], argc, argv, interactive)); + const std::string name = readInput(inputs[2], argc, argv, interactive); + const std::string type = readInput(inputs[3], argc, argv, interactive); + const std::string method = readInput(inputs[4], argc, argv, interactive); + const std::string arguments = readInput(inputs[5], argc, argv, interactive); + + std::vector missing; + if (assembly.empty()) + { + missing.push_back(inputs[0]); + } + if (kind == "dll" && type.empty()) + { + missing.push_back(inputs[3]); + } + if (kind == "dll" && method.empty()) + { + missing.push_back(inputs[4]); + } + if (!missing.empty()) + { + return skipMissing("testsDotnetExecFunctional", missing); + } + + DotnetExec module; + std::vector loadCommand = {"dotnetExec", "load", name, assembly}; + if (kind == "dll") + { + loadCommand.push_back(type); + } + + C2Message loadMessage; + if (module.init(loadCommand, loadMessage) != 0) + { + std::cerr << "testsDotnetExecFunctional load init failed:\n" << loadMessage.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsDotnetExecFunctional load init OK\n"; + std::cout << "instruction: " << loadMessage.instruction() << '\n'; + + std::vector runCommand = {"dotnetExec", kind == "dll" ? "runDll" : "runExe", name}; + if (kind == "dll") + { + runCommand.push_back(method); + } + if (!arguments.empty()) + { + runCommand.push_back(arguments); + } + + C2Message runMessage; + if (module.init(runCommand, runMessage) != 0) + { + std::cerr << "testsDotnetExecFunctional run init failed:\n" << runMessage.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsDotnetExecFunctional run init OK\n"; + + if (!execute) + { + std::cout << "execution skipped; pass --execute to run the module process path.\n"; + return 0; + } + + C2Message loadResult; + module.process(loadMessage, loadResult); + if (loadResult.errorCode() > 0) + { + std::string error; + module.errorCodeToMsg(loadResult, error); + std::cerr << "testsDotnetExecFunctional load failed: " << error << '\n'; + return 1; + } + + C2Message runResult; + module.process(runMessage, runResult); + if (runResult.errorCode() > 0) + { + std::string error; + module.errorCodeToMsg(runResult, error); + std::cerr << "testsDotnetExecFunctional run failed: " << error << '\n'; + return 1; + } + + std::cout << runResult.returnvalue() << '\n'; + return 0; +} diff --git a/modules/Evasion/CMakeLists.txt b/modules/Evasion/CMakeLists.txt index 68a30b4..d454cef 100644 --- a/modules/Evasion/CMakeLists.txt +++ b/modules/Evasion/CMakeLists.txt @@ -13,3 +13,14 @@ if(C2CORE_BUILD_TESTS) add_test(NAME testsEvasion COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + add_executable(testsEvasionFunctional tests/functional/testsEvasionFunctional.cpp Evasion.cpp) + target_include_directories(testsEvasionFunctional PRIVATE ../tests) + target_link_libraries(testsEvasionFunctional ) + add_custom_command(TARGET testsEvasionFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsEvasionFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsEvasionFunctional PROPERTIES LABELS "functional;evasion;windows-local" SKIP_RETURN_CODE 77) +endif() diff --git a/modules/Evasion/Evasion.cpp b/modules/Evasion/Evasion.cpp index 1523d61..cf69df9 100644 --- a/modules/Evasion/Evasion.cpp +++ b/modules/Evasion/Evasion.cpp @@ -92,7 +92,7 @@ std::string Evasion::getInfo() int Evasion::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if (splitedCmd.size() >= 2) { std::string cmd = splitedCmd[1]; diff --git a/modules/Evasion/tests/functional/testsEvasionFunctional.cpp b/modules/Evasion/tests/functional/testsEvasionFunctional.cpp new file mode 100644 index 0000000..22a68a5 --- /dev/null +++ b/modules/Evasion/tests/functional/testsEvasionFunctional.cpp @@ -0,0 +1,74 @@ +#include "../../Evasion.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +using namespace functional_tests; + +namespace +{ +bool requiresValue(const std::string& action) +{ + return action == "Introspection" || action == "ReadMemory" || action == "PatchMemory"; +} + +bool requiresExtra(const std::string& action) +{ + return action == "ReadMemory" || action == "PatchMemory"; +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--action", "C2_FUNC_EVASION_ACTION", "Evasion action, e.g. CheckHooks, DisableETW, Unhook, AmsiBypass, Introspection, ReadMemory, PatchMemory, RemotePatch", false, false, "CheckHooks"}, + {"--value", "C2_FUNC_EVASION_VALUE", "Primary value for actions such as Introspection, ReadMemory, or PatchMemory", false}, + {"--extra", "C2_FUNC_EVASION_EXTRA", "Secondary value for actions such as ReadMemory size or PatchMemory bytes", false}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsEvasionFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsEvasionFunctional skipped: execution is only supported on Windows.\n"; + return skipReturnCode; + } +#endif + + const std::string action = readInput(inputs[0], argc, argv, interactive); + const std::string value = readInput(inputs[1], argc, argv, interactive); + const std::string extra = readInput(inputs[2], argc, argv, interactive); + + std::vector missing; + if (requiresValue(action) && value.empty()) + { + missing.push_back(inputs[1]); + } + if (requiresExtra(action) && extra.empty()) + { + missing.push_back(inputs[2]); + } + if (!missing.empty()) + { + return skipMissing("testsEvasionFunctional", missing); + } + + std::vector command = {"evasion", action}; + if (!value.empty()) + { + command.push_back(value); + } + if (!extra.empty()) + { + command.push_back(extra); + } + + Evasion module; + return runModuleScenario("testsEvasionFunctional", module, command, execute); +} diff --git a/modules/Inject/CMakeLists.txt b/modules/Inject/CMakeLists.txt index b7ad0fa..afadbaf 100644 --- a/modules/Inject/CMakeLists.txt +++ b/modules/Inject/CMakeLists.txt @@ -19,7 +19,7 @@ endif() set_property(TARGET Inject PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") -if(C2CORE_BUILD_TESTS) +if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) target_link_libraries(Inject PRIVATE Donut ) endif() c2core_disable_safeseh_for_x86(Inject) @@ -27,10 +27,14 @@ c2core_disable_safeseh_for_x86(Inject) add_custom_command(TARGET Inject POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") -if(C2CORE_BUILD_TESTS) +if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) if(WIN32) add_executable(dummyInject tests/dummyInject.cpp) + endif() +endif() +if(C2CORE_BUILD_TESTS) + if(WIN32) add_executable(testsInject tests/testsInject.cpp Inject.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) target_link_libraries(testsInject PRIVATE Donut ) c2core_disable_safeseh_for_x86(testsInject) @@ -43,6 +47,26 @@ if(C2CORE_BUILD_TESTS) add_custom_command(TARGET testsInject POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") - - add_test(NAME testsInject COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") -endif() + + add_test(NAME testsInject COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") +endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + if(WIN32) + add_executable(testsInjectFunctional tests/functional/testsInjectFunctional.cpp Inject.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) + target_link_libraries(testsInjectFunctional PRIVATE Donut) + c2core_disable_safeseh_for_x86(testsInjectFunctional) + add_dependencies(testsInjectFunctional dummyInject) + target_compile_definitions(testsInjectFunctional PRIVATE C2_INJECT_DUMMY_EXE="$") + else() + add_executable(testsInjectFunctional tests/functional/testsInjectFunctional.cpp Inject.cpp) + target_link_libraries(testsInjectFunctional PRIVATE Donut ${aplib64}) + endif() + + target_include_directories(testsInjectFunctional PRIVATE ../tests) + add_custom_command(TARGET testsInjectFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsInjectFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsInjectFunctional PROPERTIES LABELS "functional;inject;local-exec" SKIP_RETURN_CODE 77) +endif() diff --git a/modules/Inject/Inject.cpp b/modules/Inject/Inject.cpp index a15c9c9..a5dfc0e 100644 --- a/modules/Inject/Inject.cpp +++ b/modules/Inject/Inject.cpp @@ -81,7 +81,7 @@ std::string Inject::getInfo() int Inject::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if (splitedCmd.size() >= 4) { bool donut=false; diff --git a/modules/Inject/tests/functional/testsInjectFunctional.cpp b/modules/Inject/tests/functional/testsInjectFunctional.cpp new file mode 100644 index 0000000..6572d6b --- /dev/null +++ b/modules/Inject/tests/functional/testsInjectFunctional.cpp @@ -0,0 +1,169 @@ +#include "../../Inject.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +#include +#include +#include + +using namespace functional_tests; + +namespace +{ +std::string defaultPayloadPath() +{ +#ifdef C2_INJECT_DUMMY_EXE + return C2_INJECT_DUMMY_EXE; +#else + return {}; +#endif +} + +std::string defaultSpawnProcess() +{ +#ifdef C2_INJECT_DUMMY_EXE + return C2_INJECT_DUMMY_EXE; +#else + return {}; +#endif +} + +std::string normalizeKind(const std::string& kind) +{ + if (kind == "dll") + { + return "dll"; + } + if (kind == "raw") + { + return "raw"; + } + return "exe"; +} + +bool isTruthy(const std::string& value) +{ + return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES"; +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--payload", "C2_FUNC_INJECT_PAYLOAD", "Payload path", false, false, defaultPayloadPath()}, + {"--kind", "C2_FUNC_INJECT_KIND", "Payload kind: exe, dll, or raw", false, false, "exe"}, + {"--pid", "C2_FUNC_INJECT_PID", "Target pid, or -1 to spawn a new process", false, false, "-1"}, + {"--method", "C2_FUNC_INJECT_METHOD", "DLL method name for dll mode", false}, + {"--args", "C2_FUNC_INJECT_ARGS", "Arguments passed to the payload", false}, + {"--spawn-process", "C2_FUNC_INJECT_PROCESS", "Process path to spawn when pid is negative", false, false, defaultSpawnProcess()}, + {"--syscall", "C2_FUNC_INJECT_SYSCALL", "Enable syscall path (1/0)", false, false, "0"}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsInjectFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsInjectFunctional skipped: execution is only supported on Windows in this harness.\n"; + return skipReturnCode; + } +#endif + + const std::string payload = readInput(inputs[0], argc, argv, interactive); + const std::string kind = normalizeKind(readInput(inputs[1], argc, argv, interactive)); + const std::string pid = readInput(inputs[2], argc, argv, interactive); + const std::string method = readInput(inputs[3], argc, argv, interactive); + const std::string arguments = readInput(inputs[4], argc, argv, interactive); + const std::string processToSpawn = readInput(inputs[5], argc, argv, interactive); + const std::string useSyscall = readInput(inputs[6], argc, argv, interactive); + + std::vector missing; + if (payload.empty()) + { + missing.push_back(inputs[0]); + } + if (kind == "dll" && method.empty()) + { + missing.push_back(inputs[3]); + } + if (!missing.empty()) + { + return skipMissing("testsInjectFunctional", missing); + } + + std::vector command = {"inject"}; + if (kind == "raw") + { + command.push_back("-r"); + command.push_back(payload); + } + else if (kind == "dll") + { + command.push_back("-d"); + command.push_back(payload); + command.push_back(pid); + command.push_back(method); + if (!arguments.empty()) + { + command.push_back(arguments); + } + } + else + { + command.push_back("-e"); + command.push_back(payload); + command.push_back(pid); + if (!arguments.empty()) + { + command.push_back(arguments); + } + } + + if (kind == "raw") + { + command.push_back(pid); + } + + Inject module; + C2Message message; + if (module.init(command, message) != 0) + { + std::cerr << "testsInjectFunctional init failed:\n" << message.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsInjectFunctional init OK\n"; + std::cout << "instruction: " << message.instruction() << '\n'; + std::cout << "pid: " << message.pid() << '\n'; + + if (!execute) + { + std::cout << "execution skipped; pass --execute to run the module process path.\n"; + return 0; + } + + nlohmann::json config; + if (!processToSpawn.empty()) + { + config["process"] = processToSpawn; + } + if (isTruthy(useSyscall)) + { + config["syscall"] = true; + } + if (!config.empty()) + { + module.initConfig(config); + } + + C2Message result; + module.process(message, result); + std::cout << result.returnvalue() << '\n'; + return 0; +} diff --git a/modules/PsExec/CMakeLists.txt b/modules/PsExec/CMakeLists.txt index 9bdd0e3..cc68f03 100644 --- a/modules/PsExec/CMakeLists.txt +++ b/modules/PsExec/CMakeLists.txt @@ -29,4 +29,30 @@ if(C2CORE_BUILD_TESTS) endif (WIN32) -endif() \ No newline at end of file +endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + add_executable(testsPsExecFunctional tests/functional/testsPsExecFunctional.cpp PsExec.cpp) + target_include_directories(testsPsExecFunctional PRIVATE ../tests) + target_link_libraries(testsPsExecFunctional ) + + add_custom_command(TARGET testsPsExecFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + if (WIN32) + if (NOT TARGET TestService) + add_executable(TestService tests/TestService.cpp) + target_link_libraries(TestService PRIVATE advapi32) + add_custom_command(TARGET TestService POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + $ + "${C2_TEST_BIN_OUTPUT_DIR}/$" + ) + endif() + add_dependencies(testsPsExecFunctional TestService) + target_compile_definitions(testsPsExecFunctional PRIVATE C2_PSEXEC_TEST_SERVICE="$") + endif() + + add_test(NAME testsPsExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsPsExecFunctional PROPERTIES LABELS "functional;psexec;windows-network" SKIP_RETURN_CODE 77) +endif() diff --git a/modules/PsExec/PsExec.cpp b/modules/PsExec/PsExec.cpp index c6c1edb..9e096eb 100644 --- a/modules/PsExec/PsExec.cpp +++ b/modules/PsExec/PsExec.cpp @@ -133,7 +133,7 @@ std::string PsExec::getInfo() int PsExec::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if (splitedCmd.size() >= 2) { @@ -522,7 +522,7 @@ int PsExec::process(C2Message &c2Message, C2Message &c2RetMessage) int PsExec::errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) int errorCode = c2RetMessage.errorCode(); if (errorCode > 0) { diff --git a/modules/PsExec/tests/functional/testsPsExecFunctional.cpp b/modules/PsExec/tests/functional/testsPsExecFunctional.cpp new file mode 100644 index 0000000..da55200 --- /dev/null +++ b/modules/PsExec/tests/functional/testsPsExecFunctional.cpp @@ -0,0 +1,89 @@ +#include "../../PsExec.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +using namespace functional_tests; + +namespace +{ +std::string defaultServicePath() +{ +#ifdef C2_PSEXEC_TEST_SERVICE + return C2_PSEXEC_TEST_SERVICE; +#else + return {}; +#endif +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--auth", "C2_FUNC_PSEXEC_AUTH", "Authentication mode: no-cred, kerberos, or userpass", false, false, "no-cred"}, + {"--target", "C2_FUNC_PSEXEC_TARGET", "Remote hostname or IP", true}, + {"--service", "C2_FUNC_PSEXEC_SERVICE", "Windows service executable to deploy", false, false, defaultServicePath()}, + {"--user", "C2_FUNC_PSEXEC_USER", "Username for userpass mode, optionally DOMAIN\\user", false}, + {"--password", "C2_FUNC_PSEXEC_PASSWORD", "Password for userpass mode", false, true}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsPsExecFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsPsExecFunctional skipped: execution is only supported on Windows.\n"; + return skipReturnCode; + } +#endif + + const std::string auth = readInput(inputs[0], argc, argv, interactive); + const std::string target = readInput(inputs[1], argc, argv, interactive); + const std::string service = readInput(inputs[2], argc, argv, interactive); + const std::string user = readInput(inputs[3], argc, argv, interactive); + const std::string password = readInput(inputs[4], argc, argv, interactive); + + std::vector missing; + if (target.empty()) + { + missing.push_back(inputs[1]); + } + if (service.empty()) + { + missing.push_back(inputs[2]); + } + if (auth == "userpass" && user.empty()) + { + missing.push_back(inputs[3]); + } + if (auth == "userpass" && password.empty()) + { + missing.push_back(inputs[4]); + } + if (!missing.empty()) + { + return skipMissing("testsPsExecFunctional", missing); + } + + std::vector command = {"psExec"}; + if (auth == "userpass") + { + command.insert(command.end(), {"-u", user, password, target, service}); + } + else if (auth == "kerberos") + { + command.insert(command.end(), {"-k", target, service}); + } + else + { + command.insert(command.end(), {"-n", target, service}); + } + + PsExec module; + return runModuleScenario("testsPsExecFunctional", module, command, execute); +} diff --git a/modules/PwSh/CMakeLists.txt b/modules/PwSh/CMakeLists.txt index 260de75..7f0dc78 100644 --- a/modules/PwSh/CMakeLists.txt +++ b/modules/PwSh/CMakeLists.txt @@ -26,6 +26,22 @@ if(C2CORE_BUILD_TESTS) set_property(TARGET testsPwSh PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") add_custom_command(TARGET testsPwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") - - add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") +endif() + +if(C2CORE_BUILD_FUNCTIONAL_TESTS) + if(WIN32) + add_executable(testsPwShFunctional tests/functional/testsPwShFunctional.cpp PwSh.cpp HostControl.cpp AssemblyManager.cpp AssemblyStore.cpp HostMalloc.cpp MemoryManager.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) + else() + add_executable(testsPwShFunctional tests/functional/testsPwShFunctional.cpp PwSh.cpp) + endif() + target_include_directories(testsPwShFunctional PRIVATE ../tests) + target_link_libraries(testsPwShFunctional ) + set_property(TARGET testsPwShFunctional PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") + add_custom_command(TARGET testsPwShFunctional POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + $ "${C2_TEST_BIN_OUTPUT_DIR}/$") + + add_test(NAME testsPwShFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") + set_tests_properties(testsPwShFunctional PROPERTIES LABELS "functional;pwsh;local-exec" SKIP_RETURN_CODE 77) endif() diff --git a/modules/PwSh/PwSh.cpp b/modules/PwSh/PwSh.cpp index 5956d8b..ab91fa3 100644 --- a/modules/PwSh/PwSh.cpp +++ b/modules/PwSh/PwSh.cpp @@ -198,7 +198,7 @@ std::string PwSh::getInfo() #define scriptPS "00005" -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) bool endsWithDLL(const std::string& str) { @@ -226,7 +226,7 @@ bool endsWithEXE(const std::string& str) int PwSh::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if ((splitedCmd.size() == 2 || splitedCmd.size() == 4) && splitedCmd[1] == "init") { @@ -970,7 +970,7 @@ int PwSh::invokeMethodDll(const string& argument, std::string& result) int PwSh::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) int errorCode = c2RetMessage.errorCode(); if(errorCode>0) { diff --git a/modules/PwSh/tests/functional/testsPwShFunctional.cpp b/modules/PwSh/tests/functional/testsPwShFunctional.cpp new file mode 100644 index 0000000..b9d0817 --- /dev/null +++ b/modules/PwSh/tests/functional/testsPwShFunctional.cpp @@ -0,0 +1,138 @@ +#include "../../PwSh.hpp" +#include "../../../tests/FunctionalTestHelpers.hpp" + +#include +#include +#include +#include + +using namespace functional_tests; + +namespace +{ +std::string defaultRunnerPath() +{ + return (std::filesystem::path(__FILE__).parent_path().parent_path() / "rdm.dll").string(); +} +} + +int main(int argc, char** argv) +{ + const std::vector inputs = { + {"--mode", "C2_FUNC_PWSH_MODE", "Execution mode: run, import, or script", false, false, "run"}, + {"--runner", "C2_FUNC_PWSH_RUNNER", "PowerShell runner dll path", false, false, defaultRunnerPath()}, + {"--type", "C2_FUNC_PWSH_TYPE", "Fully-qualified runner type", false, false, "rdm.rdm"}, + {"--command", "C2_FUNC_PWSH_COMMAND", "PowerShell command to run", false, false, "Write-Output \"c2core-pwsh-functional\""}, + {"--import", "C2_FUNC_PWSH_IMPORT", "PowerShell script path to import as a module", false}, + {"--script", "C2_FUNC_PWSH_SCRIPT", "PowerShell script path to execute", false}, + }; + + if (hasFlag(argc, argv, "--help")) + { + printUsage("testsPwShFunctional", inputs); + return 0; + } + + const bool interactive = hasFlag(argc, argv, "--interactive"); + const bool execute = hasFlag(argc, argv, "--execute"); + +#ifndef _WIN32 + if (execute) + { + std::cout << "testsPwShFunctional skipped: execution is only supported on Windows.\n"; + return skipReturnCode; + } +#endif + + const std::string mode = readInput(inputs[0], argc, argv, interactive); + const std::string runner = readInput(inputs[1], argc, argv, interactive); + const std::string type = readInput(inputs[2], argc, argv, interactive); + const std::string commandText = readInput(inputs[3], argc, argv, interactive); + const std::string importPath = readInput(inputs[4], argc, argv, interactive); + const std::string scriptPath = readInput(inputs[5], argc, argv, interactive); + + std::vector missing; + if (runner.empty()) + { + missing.push_back(inputs[1]); + } + if (mode == "import" && importPath.empty()) + { + missing.push_back(inputs[4]); + } + if (mode == "script" && scriptPath.empty()) + { + missing.push_back(inputs[5]); + } + if (mode == "run" && commandText.empty()) + { + missing.push_back(inputs[3]); + } + if (!missing.empty()) + { + return skipMissing("testsPwShFunctional", missing); + } + + PwSh module; + std::vector loadCommand = {"pwSh", "init", runner, type}; + C2Message loadMessage; + if (module.init(loadCommand, loadMessage) != 0) + { + std::cerr << "testsPwShFunctional init failed:\n" << loadMessage.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsPwShFunctional runner init OK\n"; + + std::vector actionCommand = {"pwSh"}; + if (mode == "import") + { + actionCommand.insert(actionCommand.end(), {"import", importPath}); + } + else if (mode == "script") + { + actionCommand.insert(actionCommand.end(), {"script", scriptPath}); + } + else + { + actionCommand.insert(actionCommand.end(), {"run", commandText}); + } + + C2Message actionMessage; + if (module.init(actionCommand, actionMessage) != 0) + { + std::cerr << "testsPwShFunctional action init failed:\n" << actionMessage.returnvalue() << '\n'; + return 1; + } + + std::cout << "testsPwShFunctional action init OK\n"; + + if (!execute) + { + std::cout << "execution skipped; pass --execute to run the module process path.\n"; + return 0; + } + + C2Message loadResult; + module.process(loadMessage, loadResult); + if (loadResult.errorCode() > 0) + { + std::string error; + module.errorCodeToMsg(loadResult, error); + std::cerr << "testsPwShFunctional load failed: " << error << '\n'; + return 1; + } + + C2Message actionResult; + module.process(actionMessage, actionResult); + if (actionResult.errorCode() > 0) + { + std::string error; + module.errorCodeToMsg(actionResult, error); + std::cerr << "testsPwShFunctional action failed: " << error << '\n'; + return 1; + } + + std::cout << actionResult.returnvalue() << '\n'; + return 0; +}