Merge pull request #35 from vxcontrol/sync_modules

sync modules 20.12.22
This commit is contained in:
Dmitry Ng
2022-12-22 10:34:34 +03:00
committed by GitHub
15 changed files with 480 additions and 167 deletions
+1
View File
@@ -74,6 +74,7 @@ globals = {
"__mock",
-- utils classes
"CSystemInfo",
"CFileReader",
"CReader",
+10 -10
View File
@@ -37,8 +37,8 @@
"reason",
"version"
],
"last_module_update": "2022-12-08 00:00:00",
"last_update": "2022-12-08 00:00:00"
"last_module_update": "2022-12-20 00:00:00",
"last_update": "2022-12-20 00:00:00"
},
{
"group_id": "",
@@ -181,8 +181,8 @@
"subject.fullpath",
"subject.process.fullpath"
],
"last_module_update": "2022-12-08 00:00:00",
"last_update": "2022-12-08 00:00:00"
"last_module_update": "2022-12-20 00:00:00",
"last_update": "2022-12-20 00:00:00"
},
{
"group_id": "",
@@ -249,8 +249,8 @@
"subject.process.id",
"subject.process.name"
],
"last_module_update": "2022-12-08 00:00:00",
"last_update": "2022-12-08 00:00:00"
"last_module_update": "2022-12-20 00:00:00",
"last_update": "2022-12-20 00:00:00"
},
{
"group_id": "",
@@ -419,8 +419,8 @@
"subject.process.parent.id",
"subject.process.path"
],
"last_module_update": "2022-12-08 12:52:12",
"last_update": "2022-12-08 12:52:12"
"last_module_update": "2022-12-20 00:00:00",
"last_update": "2022-12-20 00:00:00"
},
{
"group_id": "",
@@ -511,7 +511,7 @@
"fields": [
"reason"
],
"last_module_update": "2022-12-08 00:00:00",
"last_update": "2022-12-08 00:00:00"
"last_module_update": "2022-12-20 00:00:00",
"last_update": "2022-12-20 00:00:00"
}
]
+126 -142
View File
@@ -1,163 +1,147 @@
<template>
<div>
<el-tabs tab-position="left" v-model="leftTab">
<el-tab-pane
name="api"
:label="locale[$i18n.locale]['api']"
class="layout-fill overflow-hidden"
v-if="viewMode === 'agent'"
>
<div id="exec_actions" class="layout-margin-xl limit-length">
<el-select v-model="actionName" slot="prepend" :placeholder="locale[$i18n.locale]['actionSelectPl']">
<el-option v-for="(id, idx) in module.info.actions"
:label="module.locale.actions[id][$i18n.locale].title"
:value="id"
:key="idx"
></el-option>
</el-select>
<div v-if="actionName">
<div id="inp_actions"
v-for="(id, idx) in module.current_action_config[actionName].fields"
:key="idx">
<el-input
:placeholder="module.locale.fields[id][$i18n.locale].description"
v-model="actionDataModel[id]">
</el-input>
<div>
<div id="exec_actions" class="layout-margin-bottom-xl">
<el-select v-model="actionName" slot="prepend" :placeholder="locale[$i18n.locale]['actionSelectPl']">
<el-option v-for="(id, idx) in module.info.actions"
:label="module.locale.actions[id][$i18n.locale].title"
:value="id"
:key="idx"
></el-option>
</el-select>
<div v-if="actionName">
<div id="inp_actions"
v-for="(id, idx) in module.current_action_config[actionName].fields"
:key="idx">
<el-input
:placeholder="module.locale.fields[id][$i18n.locale].description"
v-model="actionDataModel[id]">
</el-input>
</div>
<el-button @click="submitReqToExecAction" slot="append"
>{{ locale[$i18n.locale]["buttonExecAction"] }}
</el-button>
</div>
<el-button @click="submitReqToExecAction" slot="append"
>{{ locale[$i18n.locale]["buttonExecAction"] }}
</el-button>
</div>
</div>
<div class="layout-fill layout-column layout-align-space-between scrollable">
<ul>
<li :key="line" v-for="line in lines">{{ line }}</li>
</ul>
<div class="layout-column layout-align-space-between scrollable">
<ul>
<li :key="line" v-for="line in lines">{{ line }}</li>
</ul>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
const name = "responder";
module.exports = {
name,
props: ["protoAPI", "hash", "module", "api", "components", "viewMode"],
data: () => ({
leftTab: undefined,
connection: {},
lines: [],
actionName: undefined,
actionDataModel: {},
locale: {
ru: {
api: "Удаление файлов",
buttonExecAction: "Выполнить действие",
connected: "— подключение к серверу установлено",
connError: "Не удалось подключиться к серверу",
recvError: "Не удалось выполнить операцию",
checkError: "Данные введены некорректно",
actionError: "Выберите действие из списка",
actionSelectPl: "Выбрать действие"
},
en: {
api: "File remover",
buttonExecAction: "Exec action",
connected: "— connection to the server established",
connError: "Failed to connect to the server",
recvError: "Unable to perform the operation",
checkError: "Data entered incorrectly",
actionError: "Please choose action from list",
actionSelectPl: "Select action"
}
}
}),
created() {
if (this.viewMode === 'agent') {
this.protoAPI.connect().then(
connection => {
const date = new Date().toLocaleTimeString();
this.connection = connection;
this.connection.subscribe(this.recvData, "data");
this.$root.NotificationsService.success(`${date} ${this.locale[this.$i18n.locale]['connected']}`);
},
error => {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]['connError']);
console.log(error);
},
);
}
},
mounted() {
this.leftTab = this.viewMode === 'agent' ? 'api' : undefined;
},
methods: {
recvData(msg) {
const date = new Date();
const date_ms = date.toLocaleTimeString() + `.${date.getMilliseconds()}`;
this.lines.push(
`${date_ms} RECV DATA: ${new TextDecoder(
"utf-8"
).decode(msg.content.data)}`
);
},
submitReqToExecAction() {
const date = new Date();
const date_ms = date.toLocaleTimeString() + `.${date.getMilliseconds()}`;
if (!this.actionName) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["actionError"]);
return;
}
const defActCfg = this.module.default_action_config[this.actionName]
if (typeof(defActCfg) !== "object" || !Array.isArray(defActCfg.fields) || defActCfg.fields.length === 0) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["checkError"]);
return;
}
let actionData = {};
try {
for (let fieldID in this.module.default_action_config[this.actionName].fields) {
const fieldName = this.module.default_action_config[this.actionName].fields[fieldID];
switch (this.module.fields_schema.properties[fieldName]["type"]) {
case "number":
actionData[fieldName] = parseInt(this.actionDataModel[fieldName], 10);
break;
case "string":
actionData[fieldName] = this.actionDataModel[fieldName].toString();
break;
case "array":
case "object":
actionData[fieldName] = JSON.parse(this.actionDataModel[fieldName].toString());
break;
}
if (!actionData[fieldName]) {
throw "empty field value";
}
name,
props: ["protoAPI", "hash", "module", "api", "components", "viewMode"],
data: () => ({
connection: {},
lines: [],
actionName: undefined,
actionDataModel: {},
locale: {
ru: {
buttonExecAction: "Выполнить действие",
connected: "— подключение к серверу установлено",
connError: "Не удалось подключиться к серверу",
recvError: "Не удалось выполнить операцию",
checkError: "Данные введены некорректно",
actionError: "Выберите действие из списка",
actionSelectPl: "Выбрать действие"
},
en: {
buttonExecAction: "Exec action",
connected: "— connection to the server established",
connError: "Failed to connect to the server",
recvError: "Unable to perform the operation",
checkError: "Data entered incorrectly",
actionError: "Please choose action from list",
actionSelectPl: "Select action"
}
}
}),
created() {
if (this.viewMode === 'agent') {
this.protoAPI.connect().then(
connection => {
const date = new Date().toLocaleTimeString();
this.connection = connection;
this.connection.subscribe(this.recvData, "data");
this.$root.NotificationsService.success(`${date} ${this.locale[this.$i18n.locale]['connected']}`);
},
error => {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]['connError']);
console.log(error);
},
);
}
},
methods: {
recvData(msg) {
const date = new Date();
const date_ms = date.toLocaleTimeString() + `.${date.getMilliseconds()}`;
this.lines.push(
`${date_ms} RECV DATA: ${new TextDecoder(
"utf-8"
).decode(msg.content.data)}`
);
},
submitReqToExecAction() {
const date = new Date();
const date_ms = date.toLocaleTimeString() + `.${date.getMilliseconds()}`;
if (!this.actionName) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["actionError"]);
return;
}
const defActCfg = this.module.default_action_config[this.actionName]
if (typeof (defActCfg) !== "object" || !Array.isArray(defActCfg.fields) || defActCfg.fields.length === 0) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["checkError"]);
return;
}
let actionData = {};
try {
for (let fieldID in this.module.default_action_config[this.actionName].fields) {
const fieldName = this.module.default_action_config[this.actionName].fields[fieldID];
switch (this.module.fields_schema.properties[fieldName]["type"]) {
case "number":
actionData[fieldName] = parseInt(this.actionDataModel[fieldName], 10);
break;
case "string":
actionData[fieldName] = this.actionDataModel[fieldName].toString();
break;
case "array":
case "object":
actionData[fieldName] = JSON.parse(this.actionDataModel[fieldName].toString());
break;
}
if (!actionData[fieldName]) {
throw "empty field value";
}
}
} catch (e) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["checkError"]);
return;
}
let data = JSON.stringify({
data: actionData,
actions: [`${this.module.info.name}.${this.actionName}`]
});
this.lines.push(
`${date_ms} SEND ACTION: ${data}`
);
this.connection.sendAction(data, this.actionName);
}
}
catch (e) {
this.$root.NotificationsService.error(this.locale[this.$i18n.locale]["checkError"]);
return;
}
let data = JSON.stringify({
data: actionData,
actions: [`${this.module.info.name}.${this.actionName}`]
});
this.lines.push(
`${date_ms} SEND ACTION: ${data}`
);
this.connection.sendAction(data, this.actionName);
}
}
};
</script>
<style scoped>
#exec_actions .el-select, #inp_actions .el-input {
#exec_actions .el-select, #inp_actions .el-input {
max-width: 800px;
min-width: 400px;
width: 100%;
margin-bottom: 12px;
}
}
</style>
+7 -10
View File
@@ -5,9 +5,10 @@ local cjson = require("cjson.safe")
local luapath = require("path")
local process_api = require("process_api")
local lk32
local win_const = {}
if ffi.os == "Windows" then
lk32 = require("waffi.windows.kernel32")
lk32.STILL_ALIVE = 259
win_const.STILL_ALIVE = 259
else
ffi.cdef[[
typedef uint32_t pid_t;
@@ -285,10 +286,6 @@ local function exec_action(action_name, action_data)
action_data = set_action_data_fields(action_data, object_type)
if ffi.os == "OSX" and (glue.ends(action_name, "by_image") or glue.ends(action_name, "by_file_path")) then
return set_osx_unsupported()
end
if action_name == "pt_kill_object_process_by_file_path" then
object_value = action_data.data[object_type .. ".fullpath"]
dyn_handlers.kill_process_by_name(action_name, action_data, object_type, object_value, false)
@@ -338,7 +335,7 @@ if ffi.os == "Windows" then
elseif lk32.GetExitCodeProcess(proc_handle, exitCode) ~= 0 then
__log.debugf("exit code from requested kill process: %d", tonumber(exitCode[0]))
if tonumber(exitCode[0]) ~= lk32.STILL_ALIVE then
if tonumber(exitCode[0]) ~= win_const.STILL_ALIVE then
action_data.data.result = true
action_data.data.reason = "already terminating"
push_event("pt_" .. object_type .. "_process_killed_successful", action_name, action_data)
@@ -560,13 +557,13 @@ else
-- current way of getting process info does not guarantee getting full process name
-- instead it gets argv[0] of running process
if ffi.os == "OSX" then
if proc_info.path == "" or ( not glue.ends(proc_info.name, name)) then
if proc_info.path == "" or ( not glue.ends(proc_info.name, name) and not glue.ends(proc_info.path, name)) then
return false
end
else
if proc_info.path == "" or (proc_info.path ~= name and proc_info.name ~= name) then
return false
end
if proc_info.path == "" or (proc_info.path ~= name and proc_info.name ~= name) then
return false
end
end
proc_found = true
action_data.data = update_action_data(action_data, object_type, proc_info.pid, proc_info.path)
@@ -4,10 +4,11 @@ local ffi = require("ffi")
local lfs = require("lfs")
local luapath = require("path")
local lk32
local win_const = {}
if ffi.os == "Windows" then
lk32 = require("waffi.windows.kernel32")
lk32.TH32CS_SNAPPROCESS = 0x00000002
lk32.SYNCHRONIZE = 0x00100000
win_const.TH32CS_SNAPPROCESS = 0x00000002
win_const.SYNCHRONIZE = 0x00100000
else
ffi.cdef[[
typedef uint32_t pid_t;
@@ -53,7 +54,7 @@ if ffi.os == "Windows" then
local proc_entry = ffi.new("PROCESSENTRY32[1]")
proc_entry[0].dwSize = ffi.sizeof("PROCESSENTRY32")
local snap_handle = lk32.CreateToolhelp32Snapshot(lk32.TH32CS_SNAPPROCESS, 0)
local snap_handle = lk32.CreateToolhelp32Snapshot(win_const.TH32CS_SNAPPROCESS, 0)
if (lk32.Process32First(snap_handle, proc_entry[0]) == 1) then
while (lk32.Process32Next(snap_handle, proc_entry[0]) == 1) do
@@ -85,7 +86,7 @@ if ffi.os == "Windows" then
function api.windows.get_process_handle(pid)
local handle = lk32.OpenProcess(bit.bor(lk32.PROCESS_QUERY_LIMITED_INFORMATION, lk32.PROCESS_TERMINATE,
lk32.PROCESS_VM_READ, lk32.SYNCHRONIZE
lk32.PROCESS_VM_READ, win_const.SYNCHRONIZE
), false, pid)
if handle == ffi.NULL then
return nil, api.windows.get_last_error()
+1 -1
View File
@@ -33,7 +33,7 @@ local version = "unknown"
-- Module immutable global variables
local arch = __api.get_arch()
local def_sysmon_prefix = "sysmon_pt"
local def_sysmon_prefix = "sysmon_vx"
local def_sysmon_binary_name = def_sysmon_prefix .. "_" .. arch .. ".exe"
local def_sysmon_config_name = "config.xml"
local data_sysmon_binary_path = tostring(__tmpdir) .. "\\data\\binaries\\" .. def_sysmon_binary_name
+302
View File
@@ -0,0 +1,302 @@
require 'busted.runner'()
---------------------------------------------------
local ffi = require('ffi')
local lfs = require('lfs')
local path = require('path')
local cjson = require("cjson.safe")
local strings = require('strings')
local process_api
---------------------------------------------------
-- helper functions
---------------------------------------------------
local test_process_name
local test_process_path
local test_process_args
local test_subprocess_name
local test_subprocess_path
local function get_luajit_path()
local i_min = 0
while (arg[i_min]) do i_min = i_min - 1 end
return path.normalize(arg[i_min + 1], nil, {sep=true})
end
local function file_exists(p)
local f = io.open(p, "r")
if f == nil then
return false
end
io.close(f)
return true
end
local function copy_file(src, dst)
local copy_bin = ffi.os == 'Windows' and 'copy' or 'cp'
local cmd = ('%s "%s" "%s"'):format(copy_bin, src, dst)
__log.debug(cmd)
os.execute(cmd)
assert(file_exists(dst), 'failed to copy luajit')
end
local function prepare_test_process_files()
local ext = ffi.os == 'Windows' and '.exe' or ''
local cwd = lfs.currentdir()..path.default_sep()
local luajit_path = get_luajit_path()
if (ffi.os == 'Windows') then
local lua51dll = 'lua51.dll'
local luajit_dir = path.dir(luajit_path)..path.default_sep()
copy_file(luajit_dir..lua51dll, cwd..lua51dll)
end
test_process_name = "proc_terminator_test_process"..ext
test_process_path = cwd..test_process_name
copy_file(luajit_path, test_process_path)
test_subprocess_name = "proc_terminator_test_subprocess"..ext
test_subprocess_path = cwd..test_subprocess_name
copy_file(luajit_path, test_subprocess_path)
local test_process_script_name = "proc_terminator_start_subprocess.lua"
local test_process_script_path = cwd..test_process_script_name
os.remove(test_process_script_path)
local script = assert(io.open(test_process_script_path, "w"), 'failed to create script-file for subprocess')
local script_code = ("os.execute('%s -e \"while true do end\"')"):format(strings.escape_path(test_subprocess_path))
script:write(script_code)
script:close()
test_process_args = ' '..test_process_script_path
end
local function sleep(sec)
local socket = require("socket")
socket.sleep(sec)
end
local function make_set(list)
local set = {}
for _, l in ipairs(list) do
set[l] = true
end
return set
end
local function get_process_info(process_name)
local result_info
process_api.for_each_process(function(proc_info)
if (proc_info.name == process_name) then
result_info = proc_info
return true -- stop iterations
end
end)
return result_info
end
local function create_test_processes()
local cmd = ('%s %s'):format(test_process_path, test_process_args)
if (ffi.os ~= 'Windows') then
cmd = cmd .. ' &'
end
__log.debug('Starting process: ' .. cmd)
assert(io.popen(cmd), 'failed to start process')
sleep(0.3)
return get_process_info(test_process_name)
, get_process_info(test_subprocess_name)
end
local function kill_test_processes()
local names_set = make_set{test_subprocess_name, test_process_name}
process_api.for_each_process(function(proc_info)
if (names_set[proc_info.name]) then
process_api.kill_process(proc_info.pid)
return false
end
end)
end
---------------------------------------------------
---------------------------------------------------
describe('proc_terminator agent', function()
local module_actions
setup(function()
_G.__mock = {
vars = {},
timeout = 2, -- in seconds
cwd = "tmpcwd",
module = "proc_terminator",
version = "1.0.0",
side = "agent", -- server
log_level = os.getenv("LOG_LEVEL") or "debug", -- error, warn, info, debug, trace
sec = {siem="{}", waf="{}", nad="{}", sandbox="{}"},
}
-- load mocked environment
require("mock")
-- wait until module initialization is finished
assert.is_true(__mock:expect("event", function(o)
return o.event.name == "pt_module_started"
end), "pt_module_started event not arrived")
process_api = require("process_api")
module_actions = require("actions")
prepare_test_process_files()
end)
teardown(function()
if process_api then
kill_test_processes()
end
-- stop module actually wait for module coroutine to finish execution
__mock:module_stop()
-- check last expected events
assert.is_true(__mock:expect("event", function(o)
return o.event and o.event.name == "pt_module_stopped"
end))
end)
describe('process terminator', function()
local src, dst = __mock.mock_token, __mock.module_token
local function kill_process_test(kill_process_action)
local object_type = select(3, kill_process_action:find("^pt_kill_(.-)_process"))
local need_kill_subprocess = kill_process_action:match("_tree_") ~= nil
assert.is_true(object_type ~= nil, "unsupported action")
kill_test_processes()
local process_info, subprocess_info = create_test_processes()
assert(process_info ~= nil and process_info.name == test_process_name, "test process was not started")
assert(subprocess_info ~= nil and subprocess_info.name == test_subprocess_name, "test subprocess was not started")
local action_data = {}
action_data[object_type..'.process.id'] = process_info.pid
action_data[object_type..'.process.parent.id'] = process_info.parent_pid
action_data[object_type..'.process.name'] = test_process_name
action_data[object_type..'.process.fullpath'] = test_process_path
action_data[object_type..'.fullpath'] = test_process_path
local action_data_json = cjson.encode({data=action_data})
local process_killed_successful_event = ("pt_%s_process_killed_successful"):format(object_type)
-- ask module to kill the process
assert(__mock:send_action(src, dst, action_data_json, kill_process_action), "failed to send kill process action")
-- wait for expected result to arrive (in any order)
assert.is_true(__mock:expect("event", function(o) return o.event and o.event.name == process_killed_successful_event end))
assert.is_true(__mock:expect("data", function(o) return o.data and o.data.name == kill_process_action end))
-- check that process was actually killed
process_info = get_process_info(test_process_name)
subprocess_info = get_process_info(test_subprocess_name)
assert.is_true(process_info == nil, "test process was not terminated by a module")
if (need_kill_subprocess) then
assert(subprocess_info == nil, "test subprocess was not terminated by a tree-killing action")
else
assert(subprocess_info ~= nil, "test subprocess was terminated by a non-tree-killing action")
end
-- kill process manually
if (process_info or subprocess_info) then
kill_test_processes()
end
-- ask module to kill the process again
assert(__mock:send_action(src, dst, action_data_json, kill_process_action), "failed to send kill process action")
-- wait for expected result to arrive (in any order)
assert.is_true(__mock:expect("event", function(o) return o.event and o.event.name == "pt_process_not_found" end))
assert.is_true(__mock:expect("data", function(o) return o.data and o.data.name == kill_process_action end))
end
--[[ Kill object single process ]]
it('should kill object process by file path', function()
kill_process_test(module_actions.pt_kill_object_process_by_file_path)
end)
it('should kill object process by name', function()
kill_process_test(module_actions.pt_kill_object_process_by_name)
end)
it('should kill object process by name and pid', function()
kill_process_test(module_actions.pt_kill_object_process_by_name_and_id)
end)
it('should kill object process by image', function()
kill_process_test(module_actions.pt_kill_object_process_by_image)
end)
it('should kill object process by image and pid', function()
kill_process_test(module_actions.pt_kill_object_process_by_image_and_id)
end)
--[[ Kill object process tree ]]
it('should kill object process tree by file path', function()
kill_process_test(module_actions.pt_kill_object_process_tree_by_file_path)
end)
it('should kill object process tree by name', function()
kill_process_test(module_actions.pt_kill_object_process_tree_by_name)
end)
it('should kill object process tree by name and pid', function()
kill_process_test(module_actions.pt_kill_object_process_tree_by_name_and_id)
end)
it('should kill object process tree by image', function()
kill_process_test(module_actions.pt_kill_object_process_tree_by_image)
end)
it('should kill object process tree by image and pid', function()
kill_process_test(module_actions.pt_kill_object_process_tree_by_image_and_id)
end)
--[[ Kill subject single process ]]
it('should kill subject process by name', function()
kill_process_test(module_actions.pt_kill_subject_process_by_name)
end)
it('should kill subject process by name and pid', function()
kill_process_test(module_actions.pt_kill_subject_process_by_name_and_id)
end)
it('should kill subject process by image', function()
kill_process_test(module_actions.pt_kill_subject_process_by_image)
end)
it('should kill subject process by image and pid', function()
kill_process_test(module_actions.pt_kill_subject_process_by_image_and_id)
end)
--[[ Kill subject process tree ]]
it('should kill subject process tree by name', function()
kill_process_test(module_actions.pt_kill_subject_process_tree_by_name)
end)
it('should kill subject process tree by name and pid', function()
kill_process_test(module_actions.pt_kill_subject_process_tree_by_name_and_id)
end)
it('should kill subject process tree by image', function()
kill_process_test(module_actions.pt_kill_subject_process_tree_by_image)
end)
it('should kill subject process tree by image and pid', function()
kill_process_test(module_actions.pt_kill_subject_process_tree_by_image_and_id)
end)
end)
end)
+28
View File
@@ -0,0 +1,28 @@
local ffi = require('ffi')
local strings = {}
function strings.named_format(String, Args)
local i = 1
local result = ""
while (true) do
local start_i, end_i, arg_key = String:find("%%{(.-)}", i)
if (start_i == nil) then
break
end
local arg_value = Args[arg_key]
if (arg_value == nil) then
assert(arg_value ~= nil, ("Format error: no argument '%s' found"):format(arg_key))
end
result = result..String:sub(i, start_i - 1)..tostring(arg_value)
i = end_i + 1
end
result = result..String:sub(i, String:len())
return result
end
function strings.escape_path(p)
return ffi.os == "Windows" and p:gsub('\\', '\\\\') or p
end
return strings