Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/command/sessions/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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",
Expand Down
144 changes: 144 additions & 0 deletions svcgen/main.lua
Original file line number Diff line number Diff line change
@@ -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 <implant.yaml> --dst <dir> --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.
]])
8 changes: 8 additions & 0 deletions svcgen/mal.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: svcgen
type: lua
author: kimi
version: 1.0.0
entry: main.lua
lib: false
depend_modules: []
depend_armory: []
88 changes: 88 additions & 0 deletions svcgen/svcgen-guide.md
Original file line number Diff line number Diff line change
@@ -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,阻塞等待完成(建议带上) |

**产物**

- `<dst>/base.exe` — docker 全量编译的基础 beacon
- `<dst>/svc_beacon_<ip_port>.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 模板 | 服务端 `<ServerRoot>/templates/` 下放带**固定 obf_seed** 的模板 exe,命名 `malefic-<transport>-<target>[.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-<target>.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)
Loading