目录
4. 初始化和退出函数(Initialization and finalization)
5. 加载函数和退出加载函数(Load and unload)
6. 访问扫描数据(Accessing the scanned data)
7. 设置变量的值(Setting variable's values)
8. 存储数据以供后续使用(Storing data for later use)
10. 函数返回值(Function return values)
前面几章展示了Yara自带的第三方模块,由Yara官方发布与维护,这些模块使得Yara规则更加灵活与丰富,极大提升了Yara的可用性。如果想了解Yara所支持的模块详细信息,可参考以下章节。
- YARA:第七章-模块使用之PE
- YARA:第八章-模块使用之ELF
- YARA:第九章-模块使用之Magic
- YARA:第十章-模块使用之Hash
- YARA:第十一章-模块使用之Math
- YARA:第十二章-模块使用之Time、Console和String
如果想了解Yara规则如何编写,可以参考以下章节:
- YARA:第二章-字符串之十六进制字符串(一)
- YARA:第三章-字符串之文本字符串(二)
- YARA:第四章-字符串之正则表达式(三)
- YARA:第五章-条件块_yara条件快详解
- YARA:第六章-更多关于规则
自Yara 3.0版本起,用户可以自主开发定制化模块,以满足其特定的检测需求。这些模块可以无缝集成到Yara的检测引擎中,为用户提供更加个性化和高效的威胁检测解决方案。
Yara自定义模块要求使用C语言编写,并作为Yara引擎的一部分进行编译。自定义模块的源码要位于Yara源码下libyara/modules文件夹中,并且建议模块源文件名称与模块名一致。例如新增模块名称为json,则其源文件建议应为json.c。
1. 模块模板
在Yara源码libyara/modules/demo文件夹下可以找到一个demo.c文件,这个文件中包含了实现Yara模块的基础框架,我们的自定义模块可以基于这个文件进行编写。
下面是demo.c的内容:
#include <yara/modules.h>
#define MODULE_NAME demo
begin_declarations
declare_string("greeting");
end_declarations
int module_initialize(YR_MODULE* module)
{
return ERROR_SUCCESS;
}
int module_finalize(YR_MODULE* module)
{
return ERROR_SUCCESS;
}
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
yr_set_string("Hello World!", module_object, "greeting");
return ERROR_SUCCESS;
}
int module_unload(YR_OBJECT* module_object)
{
return ERROR_SUCCESS;
}
其中#include <yara/modules.h>代码在模块源码中必须存在。此头文件在libyara/include/yara文件夹下,如下图所示:
其中#define MODULE_NAME demo定义了模块名称,其中demo即为模块名称,用户可根据自定义模块名称进行替换。此行代码是模块源码中必须存在的,并且模块名称要在Yara中具有唯一性。
接下来是begin_declarations和end_declarations两个宏定义以及中间包含了一个字符串的声明。其中begin_declarations和end_declarations两个宏定义在modules.h文件中定义,详细信息如下:
#define begin_declarations \
int module_declarations(YR_OBJECT* module) \
{ \
YR_OBJECT* stack[64]; \
int stack_top = 0; \
stack[stack_top] = module;
#define end_declarations \
return ERROR_SUCCESS; \
}
因此这块代码展开后的内容为:
int module_declarations(YR_OBJECT* module)
{
YR_OBJECT* stack[64];
int stack_top = 0;
stack[stack_top] = module;
declare_string("greeting");
return ERROR_SUCCESS;
}
接下来是模块初始化函数module_initialize和模块退出函数module_finalize。初始化函数在Yara引擎初始化期间被调用,退出函数在Yara引擎结束并退出时调用,这两个函数在其它时候例如扫描检测时不再被调用。初始化函数可以用于初始化模块依赖的变量或者申请空间等。模块退出函数则负责将模块额外申请的空间释放。这两个函数也是模块中必须存在的。
后面接着是模块加载函数module_load和模块退出加载函数module_unload。Yara检测引擎每次扫描新的目标(文件或进程)时都会调用module_load函数,在完成当前目标扫描并退出时调用module_unload函数。module_load函数可以加载正在扫描的目标信息,以自己的方式解析它的内容,并可以将分析结果填充到自定义的数据结构或者声明部分的变量中。在demo.c文件中module_load函数并没有检查文件的内容,而只是将字符串"Hello World!"赋值给之前声明的名字为"greeting"的字符串变量当中。module_unload在当前目标扫描完成退出时调用,用于释放模块在扫描当前目标时所申请的空间以及各种资源。这两个函数如果返回ERROR_SUCCESS说明函数执行成功,否则返回任意其它值则表明扫描失败,Yara将终止扫描并向用户返回错误信息。
2. 模块集成
接下来是介绍如何将自定义模块集成到Yara引擎当中。主要分为以下四个步骤:
(1)将自定义模块添加到模块列表(module_list)文件中。模块列表文件在Yara源码中libyara/modules/文件夹下,其内容如下所示,只需要参照上面的格式将demo模块加入到模块列表即可,下面第9行是我们所添加的配置:
MODULE(tests)
MODULE(pe)
MODULE(elf)
MODULE(math)
MODULE(time)
MODULE(console)
MODULE(string)
MODULE(zip)
MODULE(demo)
... ...
(2)修改Makefile.am文件,在编译Yara源码的同时也编译自定义模块的源码。Makefile.am文件在yara文件夹下。其内容如下所示,只需要参照上面的格式将demo模块源文件路径添加到编译路径即可,下面第8行是我们所添加的配置:
MODULES = libyara/modules/tests/tests.c
MODULES += libyara/modules/pe/pe.c
if CUCKOO_MODULE
MODULES += libyara/modules/cuckoo/cuckoo.c
endif
MODULES += libyara/modules/demo/demo.c
... ...
(3)重新编译Yara源码。
./bootstrap.sh
./configure
make
sudo make install
(4)编译完成后,我们可以在Yara规则中导入demo模块并使用,如下所示:
import "demo"
rule HelloWorld
{
condition:
// 由于demo模块中greeting字符串被定义为"Hello World!",因此下面这个条件将一直成立。
// 也就是说规则HelloWorld会一直被命中。
demo.greeting == "Hello World!"
}
3. 声明部分(Declaration section)
在声明部分,用户可以声明在Yara规则中使用的变量、结构和函数,每个模块中都要包含声明部分。声明部分具体格式如下:
begin_declarations;
<your declarations here>
end_declarations;
3.1 基础类型(Basic types)
在声明部分,我们可以声明三种基本的数据类型,包括字符串、整形和浮点型,其对应的声明函数分别是declare_string(<variable name>)、declare_integer(<variable name>)和declare_float(<variable name>)。使用示例如下:
begin_declarations;
// 声明一个整形变量,名称为"test1"。
declare_integer("test1");
// 声明一个字符串变量,名称为"test2"。
declare_string("test2");
// 声明一个浮点型变量,名称为"test3"。
declare_float("test3");
end_declarations;
<variable name>即为所声明的变量名。需要注意的是,变量名只能包含数字、字母和下划线这三种字符。
在Yara规则中,导入我们自定义模块就可以访问这些变量值,假若自定义模块名为testModule,那么这些变量在Yara规则中可以这样使用:
testModule.test1 == 10
testModule.test2 == "Hello World!"
testModule.test3 > 2
3.2 结构(Structures)
结构类似于结构体,结构中既可以包含基础类型数据,也可以包含其它结构类型。下面是使用示例:
begin_declarations;
declare_integer("test1");
declare_string("test2");
declare_float("test3");
begin_struct("structure1");
declare_integer("test1");
begin_struct("structure1_1");
declare_integer("test1_1");
end_struct("structure1_1");
end_struct("tructure1");
begin_struct("tructure2");
declare_integer("test1");
declare_string("test2");
declare_float("test3");
end_struct("tructure2");
end_declarations;
上面示例中我们使用begin_struct(<structure name>)和end_struct(<structure name>)来定义一个结构,每个结构中也可以包含其它的结构。需要注意的是,不同结构对象中包含的成员变量名可以是相同的,但是同一个结构中成员变量名是唯一的。
我们将上面示例转化成C语言结构格式如下所示:
struct _structure1 {
uint64_t test1;
struct _structure1_1 structure1_1;
};
struct _structure2 {
uint64_t test1;
uint8_t test2;
double test3;
};
struct _structure {
uint64_t test1;
uint8_t test2;
double test3;
struct _structure1 structure1;
struct _structure2 structure2;
};
假若自定义模块名为testModule,那么在Yara规则中结构对象的成员变量可以通过下面形式访问:
testModule.test1
testModule.test2
testModule.structure1.test1
testModule.structure1.structure1_1.test1
testModule.structure2.test1
3.3 数组(Arrays)
Yara模块中支持声明数组对象,根据数组中元素类型可以分为整型数组、字符串数组、浮点型数组和结构数组,其分别对应的声明函数是declare_integer_array(<array name>)、declare_string_array(<array name>)、declare_float_array(<array name>)、begin_struct_array(<array name>)和end_struct_array(<array name>)。使用示例如下:
begin_declarations;
declare_integer_array("int_array");
declare_string_array("string_array");
declare_float_array("float_array");
begin_struct_array("struct_array");
declare_integer("int_object");
declare_string("string_object");
end_struct_array("struct_array");
end_declarations;
和大多数编程语言一样,对于数组对象可以使用"[]"符号来随机访问成员变量。假若自定义模块名为testModule,那么在Yara规则中数组对象的随机访问可以通过下面形式:
testModule.int_array[0]
testModule.string_array[1]
testModule.float_array[0]
testModule.struct_array[3].int_object
testModule.struct_array[3].string_object
需要注意的是,数组的索引从0开始,在声明时不需要指定数组的大小。当后面例如module_load函数对数组进行初始化赋值时,数组对象长度会根据实际数据量自动增长。
3.4 字典(Dictionaries)
Yara模块中支持声明字典对象,根据字典中值(value)类型可以分为整型字典、字符串字典、浮点型字典和结构字典,其分别对应的声明函数是declare_integer_dictionary(<dict name>)、declare_string_dictionary(<dict name>)、declare_float_dictionary(<dict name>)、begin_struct_dictionary(<dict name>)和end_struct_dictionary(<dict name>)。使用示例如下:
begin_declarations;
declare_integer_dictionary("int_dict");
declare_string_dictionary("string_dict");
declare_float_dictionary("float_dict")
begin_struct_dictionary("struct_dict");
declare_integer("int_object");
declare_string("string_object");
end_struct_dictionary("struct_dict");
end_declarations;
字典对象可通过键(key)来访问值,假若自定义模块名为testModule,那么在Yara规则中字典对象的键值可以通过下面形式访问:
testModule.int_dict["key_1"]
testModule.string_dict["key_2"]
testModule.float_dict["key_3"]
testModule.struct_dict["key_4"].int_object
testModule.struct_dict["key_4"].string_object
3.5 方法(Function)
在Yara规则中不仅可以调用模块中变量,也可以调用模块中方法。在声明部分中,方法的声明语句如下所示:
declare_function(<function name>, <argument types>, <return tuype>, <C function>);
<function name>是方法名,字符串类型,此名称也是Yara规则中模块调用方法的名称。
<argument types>是参数类型,字符串类型。模块中方法支持四种不同类型的参数,分别是字符串、整型、浮点型和正则表达式。这四种类型分别使用字符"s"、"i"、"f"和"r"表示。例如,如果方法支持两个整型参数,那么则为"ii";如果支持两个参数分别是整型和字符串,那么为"is";如果方法支持三个参数分别是整型、字符串和浮点型,那么为"isf"。
<return tuype>是返回值类型,字符串类型,同一样,使用字符"s"、"i"和"f"标识不同的返回类型。
<C function>是函数标识符,标识此方法实际对应的源码中的函数名称。
方法的声明以及定义示例如下:
// 方法的定义
define_function(isum)
{
// Yara中定义整形为8字节,因此这里在接收整形变量时使用int64_t。
int64_t a = integer_argument(1);
int64_t b = integer_argument(2);
return_integer(a + b);
}
// 方法的定义
define_function(fsum)
{
// Yara中定义浮点型为double类型,因此这里在接收浮点型变量时使用double。
double a = float_argument(1);
double b = float_argument(2);
return_integer(a + b);
}
begin_declarations;
// 方法的声明
declare_function("sum", "ii", "i", isum);
declare_function("sum", "ff", "f", fsum);
end_declarations;
Yara模块中支持函数的重载,因此在声明函数时,<function name>名称可以相同,只是参数在类型或者数量上不同即可。例如Hash模块的md5方法支持两种传参形式,其函数声明部分如下所示:
begin_declarations;
// hash.md5(offset, size)
declare_function("md5", "ii", "s", data_md5);
// hash.md5(string)
declare_function("md5", "s", "s", string_md5);
end_declarations;
4. 初始化和退出函数(Initialization and finalization)
每个模块中都必须实现模块初始化函数module_initialize和模块退出函数module_finalize。前者是Yara初始化时调用,后者为Yara退出时调用。
模块初始化函数可以初始化后续使用的全局变量或者申请空间,而退出函数则负责释放初始化时所申请的空间。但是一般情况下,这两个函数都是空函数,如下所示:
int module_initialize(
YR_MODULE* module)
{
return ERROR_SUCCESS;
}
int module_finalize(
YR_MODULE* module)
{
return ERROR_SUCCESS;
}
5. 加载函数和退出加载函数(Load and unload)
除了模块初始化函数和模块退出函数外,每个模块中必须包含模块加载函数module_load和模块退出加载函数module_unload。这两个函数在每次扫描不同的目标(文件或进程)开始和结束时调用,且每个目标只调用一次。
module_load函数定义如下所示:
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
context参数包含当前扫描的上下文信息,也包含当前正在扫描的数据信息。
module_object是一个YR_OBJECT类型的结构体指针,指向当前模块顶层YR_OBJECT对象。所有在声明部分中声明的基础类型变量、函数和结构都包含在YR_OBJECT结构体中。这个些属性以树(Tree)的结构存储,假若自定义模块名为testModule,在该模块中有以下声明:
begin_declarations;
declare_integer("int_object");
begin_struct("struct_object");
declare_string("string_object");
end_struct("struct_object");
end_declarations;
那么这棵树看起来像下面这样:
YR_OBJECT(type=OBJECT_TYPE_STRUCT, name="testModule")
|
|_ YR_OBJECT(type=OBJECT_TYPE_INTEGER, name="int_object")
|
|_ YR_OBJECT(type=OBJECT_TYPE_STRUCT, name="struct_object")
|
|_ YR_OBJECT(type=OBJECT_TYPE_STRING, name="string_object")
struct_object和testModule在YR_OBJECT对象中属于同一类型,这说明YR_OBJECT定义的module_object是一个结构对象,类似与struct_object。因此当我们调用model中的int_object属性时(testModule.int_object)的查找方式和在struct_object对象中查找string_object对象时(struct_object.string_object)查找方式一样。
你可以通过module_object对象访问声明部分中所有属性、方法和结构变量。module_object指针指向了YR_OBJECT结构的根节点。
module_data参数指向一段数据,用于向模块中传递额外的数据。
module_data_size参数标识module_data指针指向数据的大小。并不是所有的模块都需要传入额外数据,根据需要可自己配置。
module_data可以在使用Yara客户端程序时,使用"-x"参数传入。
6. 访问扫描数据(Accessing the scanned data)
一般情况下,Yara的模块需要提前获取目标(文件或者进程)内容,这些内容可通过module_load函数中context参数获取。有时如果目标较大,那么数据将会被分成多个块(block),可以使用foreach_memory_block宏函数以迭代的形式依次获取被分片的数据块。
使用示例如下:
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
YR_MEMORY_BLOCK* block;
foreach_memory_block(context->iterator, block)
{
..do something with the current memory block
}
}
其中foreach_memory_block宏函数的定义在modules.h中,定义内容如下:
#define foreach_memory_block(iterator, block) \
for (block = iterator->first(iterator); block != NULL; \
block = iterator->next(iterator))
将宏函数替换后上面内容如下:
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
YR_MEMORY_BLOCK* block;
// foreach_memory_block(context->iterator, block)
for (block = context->iterator->first(context->iterator); block != NULL;
block = context->iterator->next(context))
{
..do something with the current memory block
}
}
YR_MEMORY_BLOCK* block对象指向迭代时读取的每一个数据块对象,每个数据块中包含的属性如下所示:
struct YR_MEMORY_BLOCK
{
// 数据块的大小
size_t size;
// 如果扫描目标是文件,那么此属性表示当前块据文件开头的偏移值,如果扫描
// 目标是进程,那么此属性表示当前块开始部分在进程中虚拟地址。
uint64_t base;
void* context;
// 函数指针,该函数返回指向块的数据的指针。
YR_MEMORY_BLOCK_FETCH_DATA_FUNC fetch_data;
};
foreach_memory_block总是按照文件或者进程内容顺序依次迭代数据块。如果扫描目标为文件,第一个数据块将包含文件的开头部分,例如能标识文件类型的序列。在实际扫描中,绝大多数情况下,一个数据块能包含文件所有的内容,但是编写自定义模块时需要加上容错机制,考虑到一个数据块不能包含所有内容的可能,对于较大的文件可能被分割成两个或者更多的数据块。
如果扫描目标为进程,进程内容将会被分割成大量的数据块,每个数据块对应着进程空间的一段区域。
如果说我们只需要解析文件类型,并且文件类型的标识序列包含在文件的开始部分,那么我们可以使用first_memory_block函数只读取第一个数据块,示例如下:
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
YR_MEMORY_BLOCK* block;
const uint8_t* block_data;
// 获取文件的第一个数据块对象
block = first_memory_block(context);
if (block != NULL)
{
// 通过数据块对象中fetch_data函数获取执行数据内容的指针。
block_data = block->fetch_data(block)
if (block_data != NULL)
{
..do something with the memory block
}
}
}
7. 设置变量的值(Setting variable's values)
在模块加载函数中,我们可以为声明部分声明的变量进行赋值。赋值函数包含yr_set_float、yr_set_integer和yr_set_string。
下表是三个赋值函数的定义:
void yr_set_float(double value, YR_OBJECT *object, const char *field, ...)
void yr_set_integer(int64_t value, YR_OBJECT *object, const char *field, ...)
void yr_set_string(const char *value, YR_OBJECT *object, const char *field, ...)
前面在绘制YR_OBJECT的树形图时可以知道,不管是基础变量还是结构,都是YR_OBJECT类型。因此,上面三个赋值函数中第二个参数object可以是所赋值变量自己的YR_OBJECT对象,也可以是其父节点的YR_OBJECT对象。第三个参数field标识属性名称。第一个参数为所赋的值。
如果object对象指向所赋值变量自己的YR_OBJECT对象,那么field参数必须是NULL。如果说object对象指向所赋值变量的父节点或者更高节点的YR_OBJECT对象,那么field需要描述所赋值变量与object之间的关系,示例如下,假设当前testModule模块中声明如下:
begin_declarations;
begin_struct("struct_1_obj");
declare_string("string_obj");
begin_struct("struct_2_obj");
declare_integer("int_obj");
end_struct("struct_2_obj");
end_struct("struct_1_obj");
end_declarations;
上面声明部分转换成YR_OBJECT属性结构如下所示:
YR_OBJECT(type=OBJECT_TYPE_STRUCT, name="testModule")
|
|_ YR_OBJECT(type=OBJECT_TYPE_STRUCT, name="struct_1_obj")
|
|_ YR_OBJECT(type=OBJECT_TYPE_STRING, name="string_obj")
|
|_ YR_OBJECT(type=OBJECT_TYPE_STRING, name="struct_2_obj")
|
|_ YR_OBJECT(type=OBJECT_TYPE_INTEGER, name="int_obj")
如果说赋值函数中,参数object为struct_1_obj结构对应的YR_OBJECT结构对象,那么对string_obj属性可以使用如下方法赋值:
// object指向struct_1_obj,string_obj只是其结构中的成员变量。
yr_set_string("只道当时是寻常", object, "string_obj");
如果说赋值函数中,参数object为struct_1_obj父结构对应的YR_OBJECT结构对象testModule的YR_OBJECT对象,即,那么对string_obj属性可以使用如下方法赋值:
yr_set_string("只道当时是寻常", object, "struct_1_obj.string_obj");
同理,如果object对象为struct_1_obj结构对应的YR_OBJECT结构,即那么对int_obj属性需要使用如下方法赋值:
// object指向struct_1_obj,int_obj是其struct_2_obj成员中的一个正向变量。
yr_set_integer(20, object, "struct_2_obj.int_obj");
在module_load函数中,module_object也是一个YR_OBJECT类型对象,之前也提到过此对象指向了数的根节点,z在本例中module_object对象与testModule对应的YR_OBJECT是一个,因此如果说赋值函数中object参数为module_object,那么对于string_obj和int_obj两个属性赋值如下所示:
yr_set_string("只道当时是寻常", module_object, "struct_1_obj.string_obj");
yr_set_integer(<value>, module_object, "struct_1_obj.struct_2_obj.int_obj");
前面提到,声明部分中可以声明数组类型的变量,假设当前testModule模块中声明如下:
begin_declarations;
declare_integer_array("int_array");
begin_struct_array("struct_array")
declare_string("string_obj");
declare_int_array("int_array");
end_struct_array("struct_array");
end_declarations;
那么对于数组的赋值可以参照下面示例:
yr_set_integer(20, module_object, "int_array[0]");
yr_set_integer(20, module_object, "int_array[%i]", 2);
yr_set_string("只道当时是寻常", module_object, "struct_array[%i].string_obj", 5);
yr_set_integer(20, module_object, "struct_array[%i].int_array[0]");
yr_set_integer(20, module_object, "struct_array[0].int_array[%i]", 0);
yr_set_integer(20, module_object, "bstruct_array[%i].int_array[%i]", 100, 200);
其中在field参数中,可以使用"%i"标识一个整型值,而此整型值可以通过后面的参数依次替换,类似与C语言中格式化字符串,不同的是C语言中格式化字符串使用"%d"表示整型,Yara中使用"%i",除此之外,Yara中使用"%s"表示字符串。
Yara中使用"%s"表示字符串一般用于对字段对象中键(key)使用,示例如下:
yr_set_integer(<value>, module, "foo[\"key\"]");
yr_set_integer(<value>, module, "foo[%s]", "key");
yr_set_string(<value>, module, "bar[%s].baz", "another_key");
如果你没有对声明的变量赋值,那么这些变量属于未定义状态。但这并没有关系,在很多情况下会使用变量未定义状态是合法的。例如模块需要解析特定的文件类型并按照文件格式解析内容并将数据赋值给声明的变量。如果说这个文件的并不是模块期望的类型,那么我们可以直接返回变量未定义,而不是非要将变量初始化成一个特定没有意义的值并将这个值返回。
模块除了支持赋值函数外,还支持取值函数,包括yr_get_float、yr_get_integer和yr_get_string。这些函数可以从YR_OBJECT对象中获取其存储的值。例如在module_load函数中模块解析文件或者进程内容并赋值到声明对象中的变量,而在其它函数调用时使用这些取值函数读取声明变量中的值。
下面是取值函数的定义:
double yr_get_float(YR_OBJECT *object, const char *field, ...)
int64_t yr_get_integer(YR_OBJECT *object, const char *field, ...)
SIZED_STRING *yr_get_string(YR_OBJECT *object, const char *field, ...)
取值函数的使用方法和赋值函数相似,其参数包含YR_OBJECT对象,指定取值变量所在的YR_OBJECT对象或者父类YR_OBJECT对象,field用于标识取值变量与YR_OBJECT对象之间的关系。
除了取值函数外,模块还支持获取YR_OBJECT对象的函数,定义如下:
YR_OBJECT *yr_get_object(YR_OBJECT *object, const char *field, ...)
8. 存储数据以供后续使用(Storing data for later use)
前面提到过声明部分的变量是可以在Yara规则中通过模块名访问到变量值的,例如testModule.greeting,因此这些变量具有公开性。除此之外,模块可能需要存储更加复杂或者自定义类型的数据,例如模块在module_load函数中读取并解析文件或进程内容,并提取关键信息存储。如果只是在声明部分声使用变量和使用前面提到的赋值函数是不能满足数据隐私性以及数据多样性需求的。因此会迫使开发者使用全局变量来存储这些信息。但是全局变量是线程不安全的,还需要加锁等操作。最优的方法是使用Yara模块中YR_OBJECT对象包含的data属性来存储这些数据。
每一个YR_OBJECT对象都包含一个void* data成员变量,你可以使用data指针指向任何用户自定义数据结构来存储。使用示例如下:
// 定义一个结构体包含自定义数据。
typedef struct _MY_DATA
{
int some_integer;
} MY_DATA;
int module_load(
YR_SCAN_CONTEXT* context,
YR_OBJECT* module_object,
void* module_data,
size_t module_data_size)
{
// 申请空间,这里可以使用Yara提供的yr_malloc函数。
module_object->data = yr_malloc(sizeof(MY_DATA));
((MY_DATA*) module_object->data)->some_integer = 0;
return ERROR_SUCCESS;
}
需要注意的是,在module_load函数中我们申请了空间,需要记得在模块的退出加载函数中释放这段空间,否则会造成内存泄露。
int module_unload(YR_OBJECT* module_object)
{
yr_free(module_object->data);
return ERROR_SUCCESS;
}
注意:在模块中尽量不使用全局变量来存储数据,模块中的函数可能同时被不同线程调用,如果处理不当会导致访问临界区数据风险。
9. 函数参数(Function arguments)
模块支持函数在规则中调用,而模块支持的函数中需要使用参数获取函数获取传入的参数值,参数获取函数包括integer_argument(n)、float_argument(n)、regexp_argument(n)、string_argument(n)和sized_string_argument(n)。其中参数n标识第几个参数,从数字1开始。
string_argument(n)函数期望获取一个字符串以NULL结尾。如果说期望接收一段二进制序列,并且其中包含NULL字符,那么可以使用sized_string_argument(n)函数来接收。
下面是使用示例:
int64_t arg_1 = integer_argument(1);
RE* arg_2 = regexp_argument(2);
char* arg_3 = string_argument(3);
SIZED_STRING* arg_4 = sized_string_argument(4);
double arg_5 = float_argument(1);
整型需要定义为int64_t类型,浮点型定义为double类型,正则表达式定义为RE*类型,对于二进制序列定义为SIZED_STRING*类型。
SIZED_STRING结构包含两个属性分别是length和c_string。其中length标识二进制序列的长度,c_string指针指向序列的开头。
10. 函数返回值(Function return values)
Yara模块中支持函数返回三种类型的值,分别是字符串、整型和浮点型。与C语言编程不同的是,Yara模块中要求返回值使用return_string(x)、return_integer(x)和return_float(x)函数来返回执行结果。其中参数x可以是常量、变量或者表达式,参数x类型为char*、int64_t或者double。使用示例如下:
define_function(string_md5)
{
... ...
return_string(digest_ascii);
}
除此之外,返回函数中可以使用YR_UNDEFINED作为参数返回,例如return_string(YR_UNDEFINED)、return_float(YR_UNDEFINED)和return_integer(YR_UNDEFINED)。此情况一般用于当前扫描文件格式不支持或者函数的传入参数无效或者函数在执行过程中出现错误执行失败等。
需要注意的是,不要像C语言一样直接使用return将结果返回。
11. 访问对象(Accessing objects)
在实现模块函数时我们可能需要访问声明部分变量值,或者YR_OBJECT对象中data指针指向的用户自定义数据。因此我们需要一个方法来获取对应的YR_OBJECT对象。Yara模块支持两个函数获取YR_OBJECT对象对象,分别是yr_module()和yr_parent()。yr_module()函数返回一个顶层的YR_OBJECT对象指针,此指针指向的YR_OBJECT对象就是module_load函数中module_object参数对象。yr_parent()函数返回的是函数所属父类的YR_OBJECT对象。参考示例如下:
define_function(f1)
{
YR_OBJECT* module = yr_module();
YR_OBJECT* parent = yr_parent();
// 由于函数f1都属于顶层YR_OBJECT对象,因此module和parent函数返回的
// YR_OBJECT对象相同。
}
define_function(f2)
{
YR_OBJECT* module = yr_module();
YR_OBJECT* parent = yr_parent();
// 由于函数f2属于结构foo对应的YR_OBJECT对象,因此module和parent函数返回的
// YR_OBJECT对象相同,module()返回的是顶层YR_OBJECT对象,parent()返回的是
// 结构foo所属的YR_OBJECT对象。
}
begin_declarations;
// 声明一个函数对象f1
declare_function("f1", "i", "i", f1);
begin_struct("foo");
// 在结构对象中声明一个函数对象f2
declare_function("f2", "i", "i", f2);
end_struct("foo");
end_declarations;
12. 扫描上下文(Scan context)
在模块函数中可以通过YR_SCAN_CONTEXT结构访问前面扫描过的数据。这个对象对于正在扫描文件或者进程时很有用。我们可以使用yr_scan_context()函数获取此对象,示例如下:
define_function(search_context)
{
// 在模块函数中使用yr_scan_context()函数获取YR_SCAN_CONTEXT对象。
YR_SCAN_CONTEXT* context = yr_scan_context();
YR_MEMORY_BLOCK* block;
foreach_memory_block(context->iterator, block)
{
..do something with the current memory block
}
return_integer(0);
}
14 注意
编写这篇文章时使用的是yara 4.5.0,最新版本为4.5.1。在参考Yara官方文档并开发Yara模块时发现有些模块中函数的命名和实际使用是不一致的,因此本篇文档中在介绍接口时更正为Yara 4.5.0版本提供的接口的名称。大家在参考开发时需要注意。
下面是这篇文档接口命名与官方文档中的不一致的地方。
// 访问扫描数据章节
foreach_memory_block(context, block) 更正为 foreach_memory_block(context->iterator, block)
// 设置变量值章节
set_float(...) 更正为 yr_set_float(...)
set_integer(...) 更正为 yr_set_integer(...)
set_string(...) 更正为 yr_set_string(...)
get_float(...) 更正为 yr_get_float(...)
get_intege(...) 更正为 yr_get_integer(...)
get_string(...) 更正为 yr_get_string(...)
// 访问对象章节
module(...) 更正为 yr_module(...)
parent(...) 更正为 yr_parent(...)
// 扫描上下文章节
scan_context(...) 更正为 yr_scan_context(...)
15. 未完待续
如果您觉得这篇文章对您有所帮助,或者在阅读过程中有所启发,我将非常感激您的支持。您的打赏不仅是对我努力的认可,也是对知识分享精神的一种鼓励。如果您愿意,可以通过以下方式给予支持: