Functional tests

This commit is contained in:
maxdcb
2026-04-30 13:49:51 +02:00
parent 3ecea92b23
commit 9e5499018c
19 changed files with 1019 additions and 28 deletions
+58
View File
@@ -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
+2 -2
View File
@@ -109,7 +109,7 @@ std::string AssemblyExec::getInfo()
int AssemblyExec::init(std::vector<std::string> &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<std::string> &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")
+56 -5
View File
@@ -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
$<TARGET_FILE:AssemblyExec> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:AssemblyExec>")
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
$<TARGET_FILE:testOutputWriter> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testOutputWriter>")
add_test(NAME testsAssemblyExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsAssemblyExec>")
endif()
add_test(NAME testsAssemblyExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsAssemblyExec>")
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="$<TARGET_FILE:assemblyExecDummyExe>")
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
$<TARGET_FILE:testsAssemblyExecFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsAssemblyExecFunctional>")
add_test(NAME testsAssemblyExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsAssemblyExecFunctional>")
set_tests_properties(testsAssemblyExecFunctional PROPERTIES LABELS "functional;assemblyexec;local-exec" SKIP_RETURN_CODE 77)
endif()
@@ -0,0 +1,167 @@
#include "../../AssemblyExec.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
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<Input> 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<Input> 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<std::string> modeCommand = {"assemblyExec", mode};
if (module.init(modeCommand, modeMessage) != -1)
{
std::cerr << "testsAssemblyExecFunctional mode selection failed\n";
return 1;
}
std::vector<std::string> 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;
}
+27 -3
View File
@@ -15,7 +15,7 @@ target_link_libraries(DotnetExec )
add_custom_command(TARGET DotnetExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:DotnetExec> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:DotnetExec>")
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
$<TARGET_FILE:testsDotnetExec> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsDotnetExec>")
add_test(NAME testsDotnetExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsDotnetExec>")
add_test(NAME testsDotnetExec COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsDotnetExec>")
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
$<TARGET_FILE:testsDotnetExecFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsDotnetExecFunctional>")
add_test(NAME testsDotnetExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsDotnetExecFunctional>")
set_tests_properties(testsDotnetExecFunctional PROPERTIES LABELS "functional;dotnetexec;local-exec" SKIP_RETURN_CODE 77)
endif()
+3 -3
View File
@@ -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<std::string> &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)
{
@@ -0,0 +1,144 @@
#include "../../DotnetExec.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <iostream>
#include <string>
#include <vector>
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<Input> 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<Input> 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<std::string> 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<std::string> 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;
}
+11
View File
@@ -13,3 +13,14 @@ if(C2CORE_BUILD_TESTS)
add_test(NAME testsEvasion COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsEvasion>")
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
$<TARGET_FILE:testsEvasionFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsEvasionFunctional>")
add_test(NAME testsEvasionFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsEvasionFunctional>")
set_tests_properties(testsEvasionFunctional PROPERTIES LABELS "functional;evasion;windows-local" SKIP_RETURN_CODE 77)
endif()
+1 -1
View File
@@ -92,7 +92,7 @@ std::string Evasion::getInfo()
int Evasion::init(std::vector<std::string> &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];
@@ -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<Input> 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<Input> 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<std::string> 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);
}
+29 -5
View File
@@ -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
$<TARGET_FILE:Inject> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:Inject>")
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
$<TARGET_FILE:testsInject> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsInject>")
add_test(NAME testsInject COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsInject>")
endif()
add_test(NAME testsInject COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsInject>")
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="$<TARGET_FILE:dummyInject>")
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
$<TARGET_FILE:testsInjectFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsInjectFunctional>")
add_test(NAME testsInjectFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsInjectFunctional>")
set_tests_properties(testsInjectFunctional PROPERTIES LABELS "functional;inject;local-exec" SKIP_RETURN_CODE 77)
endif()
+1 -1
View File
@@ -81,7 +81,7 @@ std::string Inject::getInfo()
int Inject::init(std::vector<std::string> &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;
@@ -0,0 +1,169 @@
#include "../../Inject.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <iostream>
#include <string>
#include <vector>
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<Input> 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<Input> 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<std::string> 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;
}
+27 -1
View File
@@ -29,4 +29,30 @@ if(C2CORE_BUILD_TESTS)
endif (WIN32)
endif()
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
$<TARGET_FILE:testsPsExecFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPsExecFunctional>")
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
$<TARGET_FILE:TestService>
"${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:TestService>"
)
endif()
add_dependencies(testsPsExecFunctional TestService)
target_compile_definitions(testsPsExecFunctional PRIVATE C2_PSEXEC_TEST_SERVICE="$<TARGET_FILE:TestService>")
endif()
add_test(NAME testsPsExecFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPsExecFunctional>")
set_tests_properties(testsPsExecFunctional PROPERTIES LABELS "functional;psexec;windows-network" SKIP_RETURN_CODE 77)
endif()
+2 -2
View File
@@ -133,7 +133,7 @@ std::string PsExec::getInfo()
int PsExec::init(std::vector<std::string> &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)
{
@@ -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<Input> 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<Input> 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<std::string> 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);
}
+18 -2
View File
@@ -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
$<TARGET_FILE:testsPwSh> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwSh>")
add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwSh>")
add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwSh>")
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
$<TARGET_FILE:testsPwShFunctional> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwShFunctional>")
add_test(NAME testsPwShFunctional COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwShFunctional>")
set_tests_properties(testsPwShFunctional PROPERTIES LABELS "functional;pwsh;local-exec" SKIP_RETURN_CODE 77)
endif()
+3 -3
View File
@@ -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<std::string> &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)
{
@@ -0,0 +1,138 @@
#include "../../PwSh.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
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<Input> 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<Input> 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<std::string> 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<std::string> 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;
}