YARA:第十五章-libyara使用(威胁检测)
YARA是一个流行的开源项目,用于恶意软件检测和分析。它允许用户定义规则,这些规则可以识别和分类恶意软件样本。YARA 的强大之处在于其灵活性和易用性,尤其是在 C/C++ 项目中。本文将详细介绍如何通过 YARA 的 C API 集成和使用 YARA,从而将 YARA 无缝集成到您的安全解决方案中。
文章目录
1. libyara初始化和退出函数(Initializing and finalizing libyara)
3. 定义额外变量(Defining external variables)
4. 保存和获取已编译的规则(Saving and retrieving compiled rules)
1. libyara初始化和退出函数(Initializing and finalizing libyara)
你的程序在使用libyara时,第一件必须做的事就是调用初始化函数yr_initialize(),该函数负责初始化libyara执行所需要的各种资源等。对应的,当程序退出时,即不在使用libyara库时,需要调用退出函数yr_finalize(),释放libyara执行过程中申请的资源。
如果说你的程序包含多个线程,且多个线程会调用libyara库函数,那么只需要在主线程调用一次初始化或者退出函数即可,而其它子线程无需重复执行此操作。
2. 编译规则(Compiling rules)
在使用Yara规则扫描文件或者进程之前,首先要将Yara规则编译成特定格式的二进制文件。使用函数yr_compiler_create()可以创建编译器对象,编译器使用完后需要使用yr_compiler_destroy()函数销毁编译器对象。创建和销毁函数定义如下:
int yr_compiler_create(YR_COMPILER **compiler)
void yr_compiler_destroy(YR_COMPILER *compiler)
你可以使用 yr_compiler_add_file(), yr_compiler_add_fd()或者yr_compiler_add_string()函数添加一个或者多个规则源。这些函数中都包含一个参数为"namespace",可以在添加不同编译对象时指定不同的"namespace"名称,类似与C++的命名空间,同一namespace下规则名必须唯一,不同namespace下规则名可以重复,如果使用默认的namespace,则传入NULL即可。规则源可以是文件名或者字符串。
int yr_compiler_add_file(YR_COMPILER *compiler,
FILE *file, const char *namespace, const char *file_name)
int yr_compiler_add_fd(YR_COMPILER *compiler,
YR_FILE_DESCRIPTOR rules_fd, const char *namespace, const char *file_name)
int yr_compiler_add_string(YR_COMPILER *compiler,
const char *string, const char *namespace_)
yr_compiler_add_file(), yr_compiler_add_fd()或者yr_compiler_add_string()函数的返回值是在解析规则源(规则文件或者字符串)时,出现格式错误规则的数量,正常情况下返回0。如果在执行编译规则函数时返回了错误,那么这个编译器对象不能继续使用,即不能再使用编译规则函数添加新的规则进行编译。
如果编译规则时想要获取错误信息或者针对编译错误想要程序做出相应的处理那么可以使用 yr_compiler_set_callback() 函数注册一个编译回调函数。注册函数接口定义如下:
void yr_compiler_set_callback(YR_COMPILER *compiler,
YR_COMPILER_CALLBACK_FUNC callback, void *user_data)
回调函数的格式如下所示:
void callback_function(
int error_level,
const char* file_name,
int line_number,
const YR_RULE* rule,
const char* message,
void* user_data)
参数error_level表示错误等级,在libyara/include/yara/compiler.h文件中定义两个不同的错误等级如下所示:
#define YARA_ERROR_LEVEL_ERROR 0
#define YARA_ERROR_LEVEL_WARNING 1
参数file_name和line_number表示编译出错文件名和行号。如果使用 yr_compiler_add_file() or yr_compiler_add_fd()函数指定文件作为目标规则源,那么file_name为文件名,如果使用 yr_compiler_add_string()函数指定字符串作为规则源,那么file_name为NULL。
参数rule是一个YR_RULE 结构对象指向包含错误规则的结构,如果说编译错误并不是因为某个特定的规则格式错误导致的,那么此参数为NULL。
参数user_data为注册回调函数时由用户指定的传入回调函数的数据。
默认情况下,libyara在解析规则源时,如果规则源中包含对其它Yara规则文件的引用(Yara规则中支持"include"关键字用于包含其它规则文件中规则,例如include "filename.yar"),Yara则尝试在磁盘上找到这些引用的规则文件并解析。如果说你想要引入的其它的规则文件需要特殊处理(例如从数据库中读取或者通过网络下载等形式),那么你可以通过 yr_compiler_set_include_callback()函数注册回调函数,当libyara在解析规则源时检测到引用其它的规则信息,则直接调用所注册的回调函数。注册函数定义如下:
void yr_compiler_set_include_callback(YR_COMPILER *compiler,
YR_COMPILER_INCLUDE_CALLBACK_FUNC callback,
YR_COMPILER_INCLUDE_FREE_FUNC include_free,
void *user_data)
回调函数的定义如下所示:
const char* include_callback(
const char* include_name,
const char* calling_rule_filename,
const char* calling_rule_namespace,
void* user_data);
参数include_name表示别引用的规则文件名称。
参数calling_rule_filename表示包含引用规则文件的规则文件名称,假若规则源是一段字符串且字符串中包含引用规则,那么此参数为NULL。
参数user_data为注册回调函数时由用户指定的传入回调函数的数据。
此回调函数应该返回以NULL为结尾的字符串且返回的字符串中包含外部引用的规则信息。该字符串由回调函数内部申请空间存储。一旦此规则内容不再使用,则需要安全的释放这段申请的空间,在调用注册函数时用户需要传入include_free()回调函数作为执行函数,如果不需要释放空间,则调用注册函数时include_free参数传入NULL即可。
include_free回调函数定义如下:
void include_free(
const char* callback_result_ptr,
void* user_data);
参数callback_result_ptr指向include_callback函数返回的字符串指针。
参数user_data为注册回调函数时由用户指定的传入回调函数的数据。
规则编译完成后,可以使用yr_compiler_get_rules()函数获取规则对象。该函数返回一个YR_RULES类型的指针。一旦调用了yr_compiler_get_rules()函数获取到了规则对象,那么此编译器对象则不能再添加新的规则源。yr_compiler_get_rules()可以在不同的地方多次调用获取规则对象,需要注意的是每次调用返回的规则对象指向同一段内存。规则获取函数定义如下:
int yr_compiler_get_rules(YR_COMPILER *compiler, YR_RULES **rules)
最后需要使用 yr_rules_destroy()函数销毁规则对象。销毁函数定义如下:
void yr_rules_destroy(YR_RULES *rules)
3. 定义额外变量(Defining external variables)
Yara允许在规则中使用变量,在执行检测时需要指定变量具体的值。
假若Yara规则中包含一个名字位value_var的变量,如下所示:
import "console"
rule test_rule
{
meta:
description = "external variables"
condition:
console.log(value_var)
}
在执行Yara程序加载此规则并检测时,必须指定变量value_var的具体指才可正常执行,执行命令如下:
yara -d test_value=20 demo.yar demo.txt
// 终端输出结果为
/*
20
test_rule demo.txt
*/
因此,如果你的Yara规则中也包含这样的变量时,在使用libyara编译规则前需要使用变量定义函数对这些变量进行赋值操作,否则编译失败。libyara中变量定义函数有yr_compiler_define_integer_variable()、yr_compiler_define_float_variable()、yr_compiler_define_boolean_variable()和yr_compiler_define_string_variable()。这些函数的定义如下:
int yr_compiler_define_integer_variable(YR_COMPILER *compiler,
const char *identifier, int64_t value)
int yr_compiler_define_float_variable(YR_COMPILER *compiler,
const char *identifier, double value)
int yr_compiler_define_boolean_variable(YR_COMPILER *compiler,
const char *identifier, int value)
int yr_compiler_define_string_variable(YR_COMPILER *compiler,
const char *identifier, const char *value)
使用前面的函数会将变量值嵌入到规则当中使得规则能够正常编译。在规则编译之后,可以使用yr_rules_define_integer_variable()、yr_rules_define_boolean_variable()、yr_rules_define_float_variable()和yr_rules_define_string_variable()动态修改规则中变量的值。这些函数的定义如下:
int yr_rules_define_integer_variable(YR_RULES *rules,
const char *identifier, int64_t value)
int yr_rules_define_boolean_variable(YR_RULES *rules,
const char *identifier, int value)
int yr_rules_define_float_variable(YR_RULES *rules,
const char *identifier, double value)
int yr_rules_define_string_variable(YR_RULES *rules,
const char *identifier, const char *value)
4. 保存和获取已编译的规则(Saving and retrieving compiled rules)
libyara支持将编译好的规则对象以文件的形式保存到磁盘中,也支持从磁盘读取规则对象文件重新加载成规则对象。保存和加载函数分别是yr_rules_save() and yr_rules_load()。函数定义如下:
int yr_rules_save(YR_RULES *rules, const char *filename)
int yr_rules_load(const char *filename, YR_RULES **rules)
编译好的规则对象文件支持跨机器使用,只要这两台机器有着相同的大小端即可,无论两台机器对应的操作系统是什么,也无论是32位还是64位机器。但是需要注意规则对象文件加载时需要注意libyara版本,很有可能规则对象文件是旧版本libyara编译保存的,新版本的libyara在加载规则对象文件时可能因为有些字段已不再支持导致加载失败。
当然,如果说规则对象以其它的形式存储(例如存储在数据库中或者存储在其它设备上),用户可以使用 yr_rules_save_stream() and yr_rules_load_stream()函数来保存或者获取规则对象。这两个函数的定义如下:
int yr_rules_save_stream(YR_RULES *rules, YR_STREAM *stream)
int yr_rules_load_stream(YR_STREAM *stream, YR_RULES **rules)
其中参数YR_STREAM *stream的结构如下所示:
typedef struct _YR_STREAM
{
void* user_data;
YR_STREAM_READ_FUNC read;
YR_STREAM_WRITE_FUNC write;
} YR_STREAM;
其中成员变量read和write需要用户自己实现并赋值,这两个函数可以根据规则对象实际存储场景实现读写数据库或者读写远程设备磁盘等。read函数会在yr_rules_load_stream()函数调用时调用,write函数会在yr_rules_save_stream()函数调用时调用。
read和write函数定义格式如下:
size_t read(void* ptr, size_t size, size_t count, void* user_data);
size_t write(const void* ptr, size_t size, size_t count, void* user_data);
参数ptr是字符指针,指向一个内存块,在read函数中内存块保存读取的规则对象数据,write函数中内存块保存需要存储的规则对象数据。
参数size表示要读取或者写入数据元素的大小。
参数count表示数据元素的个数。即读取或写入信息的总大小为size * count。
参数user_data是YR_STREAM中的user_data,可以向write、read函数中传入用户自定义指定的数据。
read函数返回成功读取的元素数量,如果这个数值小于count,可能是因为发生了错误或到达了文件末尾。write函数返回成功写入的元素数量。如果这个数量小于count,可能是因为发生了错误或写入操作被中断。
5. 扫描数据(Scanning data)
一旦成功获取了规则的实例对象YR_RULES,您就可以继续对目标进行扫描,这些目标可以是文件或进程内存。您可以选择以下三种函数之一来执行扫描操作:
- yr_rules_scan_file():用于扫描文件。
- yr_rules_scan_fd():用于扫描文件描述符。
- yr_rules_scan_mem():用于扫描内存区域。
下面是这三个函数的定义:
int yr_rules_scan_file(YR_RULES *rules, const char *filename,
int flags, YR_CALLBACK_FUNC callback, void *user_data, int timeout)
int yr_rules_scan_fd(YR_RULES *rules, YR_FILE_DESCRIPTOR fd,
int flags, YR_CALLBACK_FUNC callback, void *user_data, int timeout)
int yr_rules_scan_mem(YR_RULES *rules, const uint8_t *buffer, size_t buffer_size,
int flags, YR_CALLBACK_FUNC callback, void *user_data, int timeout)
扫描结果会以回调函数的形式返回给用户,回调函数的定义如下:
int callback_function(YR_SCAN_CONTEXT* context,
int message, void* message_data, void* user_data);
参数context保存扫描上下文结构。
参数message表示调用回调函数时消息类型,其定义如下:
#define CALLBACK_MSG_RULE_MATCHING 1
#define CALLBACK_MSG_RULE_NOT_MATCHING 2
#define CALLBACK_MSG_SCAN_FINISHED 3
#define CALLBACK_MSG_IMPORT_MODULE 4
#define CALLBACK_MSG_MODULE_IMPORTED 5
#define CALLBACK_MSG_TOO_MANY_MATCHES 6
#define CALLBACK_MSG_CONSOLE_LOG 7
#define CALLBACK_MSG_TOO_SLOW_SCANNING 8
当消息类型为CALLBACK_MSG_RULE_MATCHING或CALLBACK_MSG_RULE_NOT_MATCHING时,message_data将指向一个YR_RULE结构体对象,该对象包含了匹配或不匹配规则的详细信息。
当Yara规则文件中包含import关键字以导入模块时,每当扫描新的目标,回调函数会被触发,并且消息类型会设置为CALLBACK_MSG_IMPORT_MODULE。需要注意的是,这仅在规则文件中检测到import关键字时发生,并不意味着模块已经被实际加载(即尚未调用模块的module_load函数)。在这种情况下,message_data会指向一个YR_MODULE_IMPORT结构体对象。以下是YR_MODULE_IMPORT对象的详细定义,
struct YR_MODULE_IMPORT
{
const char* module_name;
void* module_data;
size_t module_data_size;
};
- module_name:标识触发回调函数的模块名称。
- module_data:初始值为NULL。您可以在回调函数中为这个指针赋值,以便模块使用用户自定义的数据。
- module_data_size:初始值为0。如果在回调函数中为module_data赋值,那么module_data_size应设置为指向的数据的大小。
这种机制允许你在模块加载之前,通过回调函数提供必要的初始化数据。具体module_data的含义以及作用可参考文章YARA:第十三章-编写定制化模块和YARA:第十四章-基于JSON文件的威胁分析。
在Yara规则文件中的每个模块成功导入并加载完成后,回调函数会被自动触发。此时,消息类型将设置为CALLBACK_MSG_MODULE_IMPORTED,而message_data将指向一个YR_OBJECT_STRUCTURE结构体对象如下所示,此结构包含模块针对当前扫描目标所包含的所有信息。
struct YR_OBJECT_STRUCTURE
{
OBJECT_COMMON_FIELDS
YR_STRUCTURE_MEMBER* members;
};
在扫描过程中,如果某个规则中定义的字符串在当前目标中匹配的次数超过了预设的阈值,Yara将触发此回调函数调用。这时,消息类型会被设置为CALLBACK_MSG_TOO_MANY_MATCHES,并且message_data会指向一个YR_STRING结构体对象,此对象指向造成当前告警的规则字符串信息,如果回调函数返回CALLBACK_CONTINUE将忽略该字符串并继续扫描,否则扫描终止。
如果在规则中导入了Console模块并且使用该模块提供的打印函数打印信息时(Console模块的使用和介绍可参考YARA:第十二章-模块使用之Time、Console和String),回调函数会被调用且message值为CALLBACK_MSG_CONSOLE_LOG,且message_data指向由Console模块生成的char *类型的字符串。你可以在回调函数中任意操作这段message_data信息,例如输出到终端、stdout、写入到文件或者发送给其它程序。
如果扫描结束,即程序将要退出时,回调函数会被调用且message值为CALLBACK_MSG_SCAN_FINISHED,且message_data为空。
如果在扫描当前目标时,规则中某个字符串规则在目标中匹配的数量达到指定阈值或者目标内容超过0.2M大小时,回调函数会被调用且message值为CALLBACK_MSG_TOO_SLOW_SCANNING,且message_data指向YR_STRING结构的对象。
需要注意的是官方文档中给出的message类型中没有包含CALLBACK_MSG_TOO_SLOW_SCANNING类型,但是在Yara4.5.0源码中定义了此类型并且说明了在什么情况下触发此类型的回调,如下所示:
// The number of matches before detecting slow scanning. If more matches are found
// the scan will have a CALLBACK_MSG_TOO_SLOW_SCANNING.
#ifndef YR_SLOW_STRING_MATCHES
#define YR_SLOW_STRING_MATCHES 600000
#endif
// If size of the input is bigger then 0.2 MB and 0-length atoms are used
// the scan will have a CALLBACK_MSG_TOO_SLOW_SCANNING.
#ifndef YR_FILE_SIZE_THRESHOLD
#define YR_FILE_SIZE_THRESHOLD 200000
#endif
// Maximum number of matches allowed for a string. If more matches are found
// the scan will have a CALLBACK_MSG_TOO_MANY_MATCHES.
#ifndef YR_MAX_STRING_MATCHES
#define YR_MAX_STRING_MATCHES 1000000
#endif
回调函数的返回值必须是下面三个值中的一个。
CALLBACK_CONTINUE
CALLBACK_ABORT
CALLBACK_ERROR
如果返回CALLBACK_CONTINUE,Yara将继续正常运行。返回CALLBACK_ABORT,Yara将终止扫描,但是扫描函数(例如yr_rules_scan_file)将会返回ERROR_SUCCESS。返回CALLBACK_ERROR,Yara不但终止扫描,规则扫描函数将返回ERROR_CALLBACK_ERROR。
回调函数中user_data指针指向的数据就是扫描函数(例如yr_rules_scan_file)中user_data指针指向的数据。
规则扫描函数中参数flag支持调整扫描动作,其支持的标志为如下:
//
// Flags used with yr_scanner_set_flags and yr_rules_scan_xxx functions.
//
#define SCAN_FLAGS_FAST_MODE 1
#define SCAN_FLAGS_PROCESS_MEMORY 2
#define SCAN_FLAGS_NO_TRYCATCH 4
#define SCAN_FLAGS_REPORT_RULES_MATCHING 8
#define SCAN_FLAGS_REPORT_RULES_NOT_MATCHING 16
启用SCAN_FLAGS_FAST_MODE标志位可以显著提升Yara的扫描效率,通过避免对已匹配的字符串进行重复搜索。一旦Yara在文档中识别出某个字符串,它将不再在文档的其余部分中搜索该字符串。这意味着即使该规则字符串在文档中多次出现,也只会记录一次匹配。要激活这一功能,可以在运行Yara程序时使用-f选项来启动快速扫描模式。
SCAN_FLAGS_REPORT_RULES_MATCHING and SCAN_FLAGS_REPORT_RULES_NOT_MATCHING标志位分别控制Yara在命中规则和没有命中规则的情况是否调用回调函数。
扫描函数(例如yr_rules_scan_file)包含timeout参数,此参数指定了Yara扫描时间,单位为秒,如果超时则强制停止扫描并退出。如果此参数设置为0,则永不超时。
6. 使用scanner(Using a scanner)
多数情况下使用扫描函数(例扫如yr_rules_scan_file)就能满足大多数情况,但是有时你想要更加灵活的扫描,需要使用yr_scanner_create()函数创建一个scanner对象。scanner对象是对YR_RULES结构的包装,每个scanner对象可以进行单独配置,例如规则中的变量所赋的值,不同scanner之间不会互相影响。
创建scanner适用于你想创建多个worker(可以是多个线程或者携程等)使用同一份规则进行扫描,即每个worker中使用yr_scanner_create函数创建一个scanner,且创建时使用同一个YR_RULES对象。每个scanner对象可以使用变量赋值函数(例如yr_compiler_define_integer_variable)对规则中变量赋不同的值,且互不影响。
如果Yara规则数量特别多,那么YR_RULES对象占用的内存也会很大,这样在多线程的情况下每个线程创建一个scanner且每个scanner使用同一个YR_RULE对象,所以scanner其实是轻量级的。
如果您觉得这篇文章对您有所帮助,或者在阅读过程中有所启发,我将非常感激您的支持。您的打赏不仅是对我努力的认可,也是对知识分享精神的一种鼓励。如果您愿意,可以通过以下方式给予支持:
原文地址:https://blog.csdn.net/qq_29490749/article/details/140559836
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!