zeek语法

教程网址

初识zeek

event zeek_init()
	{
	print "Hello, World!";
	}

event zeek_done()
	{
	print "Goodbye, World!";
	}

zeek hello.zeek运行之后打印输出:

Hello, World!
Goodbye, World!

目录

 1.@load:使用该指令load scripts,从其他文件加载脚本代码

2.function

3.Declarations and Statements

4.基本类型

5.运算

6.控制语句

练习

7.Event

8.Hook

 9.Table

10.Set

11.Vector

12.Record

13.Redefinitions for records

14.Zeek Datatypes

练习

15.Writing a Module conventions

16.Writing a Module: Export

17.Writing a Module: Logging

18.Filtering Logs

19.Rename Logs

20.Raising a Notice

21.The Summary Statistics Framework(Sumstats)

SumStats Exercise(1)

SumStats Exercise(2)

22.The Intel Framework


 1.@load:使用该指令load scripts,从其他文件加载脚本代码

@load misc/dump-events

zeek自动定义了一些默认路径,这些路径使加载zeek自带的脚本更加方便。默认的路径如下(基于已安装的前缀目录表示为<prefix>):

  • <prefix>/share/bro
  • <prefix>/share/bro/policy
  • <prefix>/share/bro/site

 在base/中的所有内容都是默认加载的:

2.function

# Function implementation.
function emphasize(s: string, p: string &default = "*"): string
	{
	return p + s + p;
	}
function void()
{
  ;  #相当于pass语句
}

event zeek_init() 
	{ 
	# Function calls.
	print emphasize("yes");  # 输出 *yes*
	print emphasize("no", "_");  # 输出 _no_
	}

格式:其中 argument* 是一个可能为空)逗号分隔的参数列表,而 type 是一个可选的返回类型。

function( argument* ): type

# greeting是具有特定功能类型的标识符。函数主体尚未定义,并且可以具有不同的函数主体
global greeting: function(name: string): string;
# 要定义一个包包含主体值的函数,语法如下:
function greeting(name: string): string
    {
    return "Hello, " + name;
    }
# 无参数无返回值的函数
function my_func()
    {
    print "my_func";
    }
# 函数类型可以不需要名称,匿名分配:
greeting = function(name: string): string { return "Hi, " + name; };
print greeting("Dave");
# 函数参数可以指定默认值,只要它们出现在参数列表的最后即可:
global foo: function(s: string, t: string &default="abc", u: count &default=0);

上面的@default是属性,zeek总共支持的属性如下:

3.Declarations and Statements

declaration:  是声明,变量声明 函数声明 类声明...,因此declaration是定义变量.
statement:   是表达式或者语句.

module:  module关键字用于更改当前模块。这会影响任何后续声明的全局标识符(global identifiers)的范围。如果在模块声明之后声明了全局标识符,则其作用域在当前Zeek脚本的末尾或在下一个模块声明(以先到者为准)结束。如果在模块声明(module declaration)之后但在导出块(export block)中声明了全局标识符,则其作用域在最后加载的Zeek脚本的末尾结束,但是必须在其他模块中使用名称空间运算符(::)对其进行引用。

export:导出块(export block)包含当前模块(module)正在导出的一个或多个声明(declarations)(注意:导出块export block 中不允许使用任何statements语句)。请注意,始终需要在导出块中使用花括号即大括号(它们并不表示复合语句)。同样,不需要使用分号来终止导出块。如:

export {
    redef enum Log::ID += { LOG };

    type Info: record {
        ts: time &log;
        uid: string &log;
    };
    const conntime = 30sec &redef;
}

global:使用global关键字声明的变量将具有全局作用域。如果未指定类型,则需要初始化,以便可以推断该类型;同样,如果未提供初始化,则必须指定类型;在某些情况下,当无法正确推断类型时,即使存在初始值设定项,也必须指定该类型。

任何函数、钩子或事件程序之外的变量声明都需要使用此关键字(除非它们是用const关键字声明的)。

函数、钩子和事件处理程序的定义不允许使用global关键字。但是,函数声明(即不提供函数体)可以使用global关键字。

全局变量的范围从声明所在的位置开始,并扩展到加载的所有剩余Zeek脚本(但是,有关模块如何更改全局标识符可见性的解释,请参见module关键字)。

global pi = 3.14;  # 未指定类型,则需要初始化,可以推断出类型是double
global pi:double = 3.14;  # 初始化指定类型
global hosts: set[addr]; # 未初始化,则需要指定类型。
global ciphers: table[string] of string = table();

const:用const关键字声明的变量将是常量。声明为常量的变量需要在声明时进行初始化。通常,从初始化程序中推断类型,但是可以显式指定类型。常数的值不能更改。唯一的例外是,如果变量是全局常量并且具有&redef属性,但是即使如此,也只能使用redef更改其值。请注意,const关键字不能与local或global关键字一起使用(即const可以替代local或global)。

const pi = 3.14; # 一般方式初始化
const ssh_port: port = 22/tcp; # 也可以加上类型

option:用option关键字声明的变量是配置选项。选项option必须在声明时进行初始化。通常,从初始化程序中推断类型,但是可以显式指定类型。可以通过redef重新定义初始值。option的值不能由赋值语句更改,但可以通过Config :: set_value函数或更改Config :: config_files中指定的配置文件来更改。option的范围是全局的。请注意,option声明也不能使用local,global或const关键字。

option hostname = "host-1";
option peers: set[addr] = {};

type:type关键字用于声明用户定义的类型。此新类型的名称具有全局范围,可在可能出现内置类型名称的任何地方使用。type关键字在定义record或enum时最常用,但在处理更复杂的类型时也很有用。

redef:可以使用几种方法来使用redef:1)去重新定义global变量初始值或运行中option的初始值;2)去扩展record类型或enum类型;3)去指定一个新的事件处理程主体(new event handler body),该主体将替换以前定义的event handlers。如果您使用redef重新定义全局变量(使用const或global定义)的初始值,则要更改的变量必须具有&redef属性。您可以使用redef重新定义运行时选项(使用option定义)的初始值,即使该选项没有&redef属性。
如果您要更改的变量是table,set,vector或pattern,则可以使用+ =添加新元素,也可以使用=指定新值(对象的所有先前内容均已删除)。如果您要更改的变量是table或set,则可以使用-=运算符删除指定的元素(对于不存在的指定元素,则不会发生任何事情)。如果要更改的变量不是table,set,vector或pattern,则必须使用=运算符。如果您使用redef扩展record或enum,则必须使用+ =赋值运算符。注意:redef中的每个record字段都必须具有&optional或&default属性。

redef pi = 3.14; # 必须且仅可以使用=
redef set_of_ports += { 22/tcp, 53/udp }; # 增加元素使用+=
redef enum color += { Blue, Red };
redef record MyRecord += { n2:int &optional; s2:string &default=""; }; # record增加的元素必须至少使用两个属性之一

 add:add语句用于将元素添加到set中。如果指定的元素已经存在于集合中,则什么也不会发生。

local myset: set[string];
add myset["test"];  # 增加一个元素 “test”

break:break语句用于中断switch,for或while语句。

delete:delete语句用于从set、table或vector删除元素,或从具有&optional属性的记录字段中删除值。删除元素时,如果指定的索引不存在,则不会发生任何事情。总之就是急事要删除的不存在,不会报错。

event:event语句会立刻在event handler调用排队。

fallthrough:fallthrough语句可用于case块中,以指示应在下一个case或默认标签处继续执行。

for:for循环遍历字符串,集合,向量或表中的每个元素,并为每次迭代执行一条语句,请注意,循环在集合或表中的元素上迭代的顺序是不确定的。如果字符串,集合,向量或表为空,则不会发生循环迭代。set和string的循环变量是元素,table和vector的循环变量是索引。如果表达式是表或具有多个索引的集合,则必须将循环变量指定为以逗号分隔的不同循环变量列表(每个索引一个),并用方括号括起来break语句终止循环,next语句跳到下一个循环。for语句的条件使用小括号,循环体如果只有一句可以不使用大括号,多句则一定要使用大括号,推荐都使用大括号。

local x1 = 5.7; # 不声明类型但是要指定值
local x2: double;  # 只声明类型,不初始化
local x3: double = 5.7; # 初始化值
local myset = set(80/tcp, 81/tcp);
local mytable = table([10.0.0.1, 80/tcp]="s1", [10.0.0.2, 81/tcp]="s2");
for ( p in myset )
    print p;
for ( [i,j], val in mytable ) # 索引有两个,故使用中括号,每个用逗号分开;val是字段值
    {
    if (val == "done")
        break; # 结束循环
    if (val == "skip")
        next; # 跳到下一个循环
    print i,j, val;
    }

if:if语句的条件使用小括号,主体如果只有一句可以不使用大括号,多句则一定要使用大括号,推荐都使用大括号。

local:用local关键字声明的变量将是local。如果未指定类型,则需要初始值设定项,以便可以推断该类型。同样,如果未提供初始值设定项,则必须指定类型。局部变量的范围从声明它的位置开始,并持续到声明它的函数、钩子或事件处理程序的结尾(即使局部变量是在复合语句中声明的,或者是for语句中的循环变量,也是如此)。

next:只能出现在for和while循环中,表示跳到下一次循环。

print:print语句采用逗号分隔的一个或多个表达式列表。评估列表中的每个表达式,然后将其转换为字符串。然后打印每个字符串,每个字符串在输出中用逗号分隔。默认情况下,print语句写入标准输出(stdout)。但是,如果第一个表达式的类型为file,则print将写入该文件。

4.基本类型

大多数类型应该是其他编程语言所熟悉的,但是Zeek作为网络监视系统也引入了一组特定于域的类型,例如 timeintervalportaddr, and subnet.

event zeek_init() 
	{
	local x : string = "two";
	local y : int = 10000000000000000000000000000000000000000000000000;
	print "y is a large int:", y;  # y is a large int:, -1
	x="one";  
	print "x is a short string:", x; # x is a short string:, one
	
	#pattern matching 
	print /one|two|three/ == "two";  # T
	print /one|two|three/ == "ones"; # F (exact matching)
	print /one|two|three/ in "ones"; # T (embedded matching)
	print /[123].*/ == "2 two";  # T
	print /[123].*/ == "4 four"; # F
	}

bool: 取值为T/F,支持这些运算:equality/inequality (==!=), logical and/or (&&||), logical negation (!), and absolute value (where |T| is 1, and |F| is 0, and in both cases the result type is count).

int:表示64位有符号整数的数字类型,一个int常数是一串数字,前面带有+或-号,如-42、+42,其中+号是可选的。支持这些运算:rithmetic operators (+-*/%), comparison operators (==!=<<=>>=), assignment operators (=+=-=), pre-increment (++), pre-decrement (--), unary plus and minus (+-), and absolute value (e.g., |-3| is 3, but the result type is count).
注意:在使用类型推断时,请谨慎使用,例如  local size_difference = 0 will infer count, while local size_difference = +0 will infer int.

count:表示64位无符号整数的数字类型,一个int常数是一串数字,如42、0。支持的运算和int是一样的。但是一元操作符(unary plus and minus)会将count类型转换为int类型,除此之外还支持按位运算符&、|、^

double:表示双精度浮点数的数字类型。浮点常数以数字字符串形式编写,带有可选的小数点,科学计数法的可选比例因子和可选的+或-号,如-1234-1234e03.14159, and .003E-23。支持的运算和int是一样的。使用类型推断的时候,请谨慎使用,例如:local size_difference = 5 will infer count, while local size_difference = 5.0 will infer double.

time:表示绝对时间的时间类型。当前无法指定时间常数,但是可以使用double_to_time,current_timenetwork_time内置函数将值分配给time类型的变量。

interval:表示相对时间的时间类型。可以写成数字常量,后接一个时间单位(时间单位usec、msec、sec、min、hr、day分别表示微秒、毫秒、秒、分钟、小时、天)。数字常数和时间单位之间的空格是可选的。将字母s附加到时间单位以使其复数也是可选的,如3.5min和3.5mins。间隔也可以取负数,例如-12 hr表示“过去十二个小时”。对于该变量的绝对值运算的结果是秒数,如|-1 min| is 60.0

string:字符串常量是通过将文本括在一对双引号(“)中来创建的。字符串常量不能跨越Zeek脚本中的多行。反斜杠字符(\)引入转义序列【Zeek识别以下转义序列:\\,\ n,\ t,\ v,\ b,\ r,\ f,\ a,\ ooo(其中每个“ o”是八进制数字),\ xhh(其中每个“ h”是一个十六进制数字)】。如果Zeek无法识别转义序列,则Zeek将忽略反斜杠(\\ g变为g)。可以通过将字符串用竖线字符括起来来找到字符串中的字符数(例如|“ abc” |为3)。可以使用in或!in运算符执行子字符串搜索(例如,“ bar” in “ foobar”产生true)。请注意,下标运算符不能用于修改字符串(即,不能位于赋值运算符的左侧)。

local orig = "0123456789";
local second_char = orig[1];         # "1"
local last_char = orig[-1];          # "9"
local first_two_chars = orig[:2];    # "01"
local last_two_chars = orig[8:];     # "89"
local no_first_and_last = orig[1:9]; # "12345678"
local no_first = orig[1:];           # "123456789"
local no_last = orig[:-1];           # "012345678"
local copy_orig = orig[:];           # "0123456789"

pattern:表示可用于快速文本搜索操作的正则表达式模式的类型。模式常量是通过将文本括在正斜杠(/)中来创建的,并且使用与flex词法分析器支持的模式相同的语法。pattern match支持两种类型:exact、embedded。   

例如:local a:pattern; a=/foo/;
           (1)在精准匹配(exact matching)中,将==相等关系运算符与一个模式操作数和一个字符串操作数(操作数的顺序无关紧要)一起使用,以检查完整的字符串是否与模式完全匹配,如果不完全匹配则返回F,正确则返回T,如  /foo|bar/ == "foo" 返回T,/foo|bar/ == "foobar"返回F,!=表示==的反。注意行首符^和行尾符$在精准匹配中是不起作用的。
           (2)在嵌入式匹配(embedded matching)中,in运算符与一个模式操作数(必须在左侧)和一个字符串操作数一起使用,和==不同的是目的在于测试该模式是否出现在给定字符串内的任何位置,如 /foo|bar/ in "foobar"返回T,/^oob/ in "foobar",!in表示in的反。注意行首符^和行尾符$是起作用的。内置函数,sub(字符串,pattern,替换的字符串),例如sub("\x00\x01show databases",  /\x00\x01/,  "")返回的结果"show databases"。

port:表示传输层端口号的类型(除了tcp和udp端口,还有icmp端口)。

端口常量被写成无符号整数后跟/tcp、/udp、/icmp或者/unknown之一,例如443/tcp、80/udp、1000/unknown、90/icmp。

端口支持比较运算符(==、!=、>、<、>=、<=),当不同协议传输层之间比较时,优先级顺序为unknow<tcp<udp<icmp,例如65535/tcp<80/udp。

get_port_transport_proto内置函数获取port的传输级协议类型,返回值为enumerated type,如果要转换成字符串使用cat函数;使用port_to_count内置函数获得port的数值,返回值为count类型。

addr:表示IP地址的类型。
IPv4地址常数以“点四分组”格式A1.A2.A3.A4写入,其中A1-A4每一组使用8bit地址,取值都位于十进制0和255之间,总共32bit。例如:192.168.1.50

IPv6地址常数被写成以冒号分隔的十六进制形式放在方括号中(还有嵌入ipv4的混合表示法,在低32位中以点四分组表示),它分为8个16bit块,总共128bit。例如:[2001:db8::1]、[::ffff:192.168.1.100]或[aaaa:bbbb:cccc:dddd:eeee:ffff:11111:2222]。

请注意,IPv4映射的IPv6地址(即,前80位为0,后16位为1,最后32位为IPv4地址)在内部被视为IPv6地址(例如,[::ffff:192.168.1.100]等于192.168.1.100),实际上是ipv4的地址。

可以比较地址的相等性(==,!=)和排序(<,<=,>,>=)。地址的绝对值|addr|得到地址的位数(IPv4为32,IPv6为128)。也可以使用/掩码地址以生成子网:

subnet:用CIDR表示法表示IP地址块的类型。子网常数写为地址后跟斜杠(/),然后是指定为十进制数的网络前缀大小。例如,192.168.0.0/16或[fe80::]/64。掩码是为了划分主机可用数量,总位数-掩码数。
可以比较子网是否相等(==,!=)。可以使用in或! in操作。

# 不需要event zeek_init也可以输出结果
 local pp:port=80/tcp;
 local cnt:count;
 local str:string;
 str=cat(get_port_transport_proto(pp)); # 强行将其他类型转成string类型
 cnt=port_to_count(pp);
 local mm=get_port_transport_proto(pp);  # 不指定类型,根据值判断,实际上是enumerated type
 print str;
 print cnt;
 print mm;

# ipv6,冒号分隔,总共8段,每一段都是16bit
# 下面同一个变量的值都是等价的
local a:addr=[1002:003B:456C:678D:890E:0012:234F:56G7]
a=[1002:3B:456C:678D:890E:0012:234F:56G7] # 每一段中开始的0是可以省略的
local b=[1002:003B:456C:678D:890E:0000:0000:56G7]
b=[1002:3B:456C:678D:890E:0:0:56G7] # 如果这一段中全是0的话,可以直接用一个0代替
b=[1002:3B:456C:678D:890E::56G7] # 如果有一段或连续几段都是0,那么这些段可以用一个::来标识,::这个标识只能出现一次!
local c=[::ffff.192.168.20.5] # 虽然是ipv6的写法,实际上就是ipv4地址,并不是ipv6地址
c=192.168.20.5

# 使用绝对值
local aa=[::ffff:192.168.20.1];
local bb=[::bc:A:192.168.20.1];
local cc=192.168.20.1;
local dd=[B:bc:A:C:D:E:F:901];
print fmt("aa:%d,bb:%s,cc:%d,dd:%d",|aa|,|bb|,|cc|,|dd|); # aa:32,bb:128,cc:32,dd:128
print is_v4_addr(aa),is_v6_addr(aa),is_v6_addr(dd); # T,F,T

# 使用/变成子网subnet
local a: addr = 192.168.1.100;
local s: subnet = 192.168.0.0/16;
if ( a/16 == s && a in s)
    print "true";  # true  

enum:一种类型,它允许指定一组没有进一步结构的相关值。枚举没有关联的值或者顺序。声明示例:  type color: enum { Red, White, Blue, };

table:见下面

set:见下面

vector:见下面

record:见下面

function:见上面

event:见下面

hook:见下面

file:Zeek支持写入文件,但不读取文件,如果需要读取文件,可参考 Input Framework;对于写入文件,不建议写入此文件进行日志记录,而是参考Logging Framework.

opaque:一种数据类型,其实际表示/实现被有意隐藏。

any:用于绕过强类型。例如,一个函数可以具有任意类型的参数,只要它可能具有不同的类型。对any类型的变量唯一允许的操作是赋值。请注意,不希望用户使用此类型。它主要是供Zeek附带的一些内置函数和脚本使用的。

void:内部Zeek类型(即void不是Zeek脚本语言中的保留关键字),表示不存在函数的返回类型。

5.运算

运算类型如下










6.控制语句

event zeek_init(){  # if语句
	local x = "3";
	for ( c in "12345" ){
		if ( c == x ){
			print "Found it.";
			# A preview of functions: fmt() does substitutions, outputs result.
			print fmt("And by 'it', I mean %s.", x);
			}
		else{
			# A quick way to print multiple things on one line.
			print "I'm looking for", x, "not", c;
		     }
	}
}

event zeek_init(){  # for语句
	for ( character in "abc" ){
		print character;
	}
}

event zeek_init(){  # while语句
    local i = 0;
    while ( i < 5 )
        print ++i;
    while ( i % 2 != 0 ){
        local finish_up = F;
        if ( finish_up == F )
    	    print "nope";
        ++i;
        next;
        if ( finish_up )
            break;
    }
    print i;
}

event zeek_init(){  # switch语句
    local x = 4;
    switch ( x ){
        case 0:
            # This block only executes if x is 0.
            print "case 0";
            break;
        case 1, 2, 3:
            # This block executes if any of the case labels match.
            print "case 1, 2, 3";
            break;
        case 4:
            print "case 4 and ...";
            # Block ending in the "fallthrough" also execute subsequent case.
            fallthrough;
        case 5:
            # This block may execute if x is 4 or 5.
            print "case 5";
            break;
        default:
            # This block executed if no other case matches.
            print "default case";
            break;
    }
}

练习

现在已经接触了types、operators、functions、loops、logic control。两个要求:(1)剔除字符串中所有的e;(2)判断从1到100的数字,如果是3的倍数则打印Fizz,5的倍数打印Buzz,两者的倍数打印FizzBuzz;(3)使用switch语句,输出元音的数目。

分别写在zeek_init、zeek_done 事件中。

function removeE(s:string){
    local result = "";
    for ( c in s ){
        if ( c != "e" )
	    {
            result = result + c;
            # Compound assignment, ``result += c``, also works.
            }
     }
    print result;
}
event zeek_init(){ 
    removeE("messege");
}

#Recursive approach w/ string concatenation.   
function fizzbuzz(i: count){
    # Modulo, string concatenation approach.
    local s = "";
    if ( i % 3 == 0 )
        s += "Fizz";
    if ( i % 5 == 0 )
        s += "Buzz";
    if ( i==10 )
        ;  #相当于pass,占位作用
    if ( i==10 )
    {}  #主体是一个空的大括号,也相当于pass,占位作用
    if ( i==10 ){}  #或者写在上面,也相当于pass,占位作用
    if ( s == "" )
        print i;
    else
        print s;
    if ( i < 100 )
        fizzbuzz(i + 1);
}
event zeek_done(){
    fizzbuzz(1);
}

# switch exercise
event zeek_init(){ 
    local result = 0;
    local input = "The Zeek Network Security Monitor";
    for ( c in input ){
        switch ( c ){
            case "a", "e", "i", "o", "u":
                ++result;
                break;
        }
    }
    print result;
}

7.Event

events实际上是一种特殊的functions,但对于Zeek的工作方式却至关重要。在以下方面和functions功能不同:

(1)可能会在以后的时间进行scheduled和executed,所以在调用他们之后可能无法直接实现他们的效果。

(2)他们没有返回值。因为他们没有被直接调用而是安排在以后执行。

(3)可以为同一个event定义多个body,每一个body都被视为“event handler”。当需要去执行一个event的时候,该event的所有handler bodies以@priority优先级顺序执行。@priority值越大,那么优先级越高,如果值相同,那么定义在前的先执行。

点击这里可以查看zeek和脚本之间如何交互,以及事件在zeek脚本中扮演什么角色。和协议或者文件分析无关的预定义事件,可以参考这里。

global myevent: event(s: string);  # 定义一个myevent声明,参数是string的变量s。也可以不写,效果一样。

global n = 0;

event myevent(s: string) &priority = -10 {  # myevent的实现,其中&priority属性是可选的,并且巨顶的是执行顺序优先权,默认是0 
	++n;
        print "i am executed";
}

event myevent(s: string) &priority = 10	{ # &priority数值越高,优先级越高。
	print "myevent", s, n;
}

event zeek_init(){ # zeek启动的时候会执行这个事件
	print "zeek_init()";
	event myevent("hi");
	schedule 5 sec { myevent("bye") };
}

event zeek_done(){
	print "zeek_done()";
}

里面有自定义的事件,并且设置了优先级。打印结果如下:

zeek_init()  首先不管优先级,执行的就是zeek_init event
myevent, hi, 0   该事件被调用,选择优先级最大的先执行,接着执行优先级较低的body
i am executed
myevent, bye, 1  该事件再一次被调用,选择优先级最大的先执行,接着执行优先级较低的body
i am executed
zeek_done()

 event handler bodies当通过三种不同方法之一调用事件处理程序主体(event handler bodies)时,将执行它们,而不是直接从脚本中调用事件处理程序(event handler):

(1)从事件引擎(event engine):当事件引擎检测到您已为其定义了相应事件处理程序的事件时,它会将该处理程序的事件排队。事件引擎完成对当前数据包的处理并刷新首先排队的其他事件处理程序后,将立即调用该处理程序。

(2)使用脚本中的事件语句:立即对事件处理程序进行排队调用,如下所示:假定password_exposed之前已声明为具有兼容参数的事件处理程序类型。

event password_exposed(user, password);  

(3)通过脚本中的事件表表达式(the schedule expression):这将事件处理程序的调用延迟到将来的某个时间。例如:

schedule 5 secs { password_exposed(user, password) };

8.Hook

Hooks是另外一种functions。和events相似,因为也可以有多个bodies,每一个body都被视为“hook handler”。但是在以下两个方面不一样:

(1)在调用时会被立刻执行,并不会像events一样的调度。

(2)hook handler的终止方式确定了是否进一步执行其他handlers。如果能reach the end of body,or a return statement,那么下一个hook handler将会被执行; 但是如果hook handler是以break statement作为结尾,那么不会执行其余的hook handlers。

Hooks对于modules提供定制点(custom points)非常有用,因为它们允许将决策(decisions)外包(outsource)给站点特定代码(site-specific code)。

可以从下面代码看出,如果终止方式是break,那么返回的是F,如果终止方式是正常结束,那么返回的是T。

global myhook: hook(s: string);

hook myhook(s: string) &priority = 10{
	print "priority 10 myhook handler", s;
	s = "bye";
}

hook myhook(s: string){
	print "break out of myhook handling", s;
	break;
}

hook myhook(s: string) { # 优先级和上面的一样,定义顺序就起了作用,由于上个hook以break结尾,那么不会执行该hook。
	print "not going to happen", s;
}
hook normal(s: string) { # 定义一个新的hook
	print "normal return must is T", s;
}
	
event zeek_init(){
        print  hook myhook("jupy");
	local ret: bool = hook myhook("hi");  # 因为是以break结尾终止了,所以返回的F,无法执行下面的打印
	if ( ret ){
		print "all handlers ran";
	}
	local res: bool = hook normal("just do it"); # 因为是正常结束的,所以返回的T,能执行下面的打印
	if (res){
		print "correctly,normal end return T";
	}
}

上面的hook程序运行结果如下;

priority 10 myhook handler, jupy
break out of myhook handling, jupy
F
priority 10 myhook handler, hi
break out of myhook handling, hi
normal return must is T, just do it
correctly,normal end return T

 9.Table

table是关联集合,将一组唯一索引值映射到其他值。

从一组值映射到另一组的关联数组,例如A->B,被映射的值称为index 或indices,即A,映射的结果称为yield,即B。声明语法:  table [ type^+ ] of type  ,其中type^+表示一种或多种类型,以逗号分隔。index类型(也就是)不能为以下:pattern, table, set, vector, file, opaque, any。但是yield不受限制。可以通过将表标识符放在垂直竖线字符之间来获得表中元素的数量,和string一样的作用。例子如下:既可以将一组初始化程序放在花括号内,也可以使用构造函数创建表。打印出来是大括号括起来的。赋值操作符,如果存在则更改值的作用,如果不存在则增加元素的作用;
增加元素使用赋值,如a[6]="aaa",删除元素使用delete,如delete a[6]。

# 仅声明
local a: table[count] of string;
# 这个是错误的赋值
a={
[2]="a",
[4]="v"
};
# 这个才是正确的赋值
a=table(
[2]="a",
[4]="v"  # 最后一个元素不能有逗号
);
# 初始化方式一,最后一个元素逗号是可选的
global t: table[count] of string = {
    [11] = "eleven",
    [5] = "five",
};
# 初始化方式二,最后一个元素不能有逗号
global t2 = table(
    [192.168.0.2, 22/tcp] = "ssh",
    [192.168.0.3, 80/tcp] = "http"
);

# 表构造函数也可以由类型显式命名,这在更复杂的索引类型可能不明确的情况下非常有用:
type MyRec: record {
    a: count &optional;
    b: count;
};
type MyTable: table[MyRec] of string;
global t3 = MyTable([[$b=5]] = "b5", [[$b=7]] = "b7");
event zeek_init(){ 
	local x: table[count] of string = { [1] = "one", 
	                                    [3] = "three",
	                                    [5] = "five" };
	x[7] = "seven"; # 增加一个元素
	print"7 in x", 7 in x; # T
#	print"x[7] in x", x[7] in x; # 两个错误(1)error: type mismatch (x[7] and count).(2)error:not an index type (x[7] in x)
	delete x[3]; # 删除一个元素
	print "3 !in x",3 !in x; # T
	x[1] = "jupy"; # 更改一个元素
        print x;
	for ( key in x ){
		print key,x[key];
	}
}

输出:

7 in x, T
3 !in x, T
{[5] = five, [7] = seven, [1] = 1}   是大括号
5, five
1, jupy
7, seven

10.Set

Set是集合,使用add和delete运算符对元素进行添加和删除,使用in运算符测试成员。
set类似table,但是没有yield。声明语法:set [ type^+ ] ,其他和table一样,type^+里面不能是以下类型: patterntablesetvectorfileopaqueany.。也是有两种方式初始化。打印出来是大括号括起来的。获取集合长度使用绝对值符号。
增加元素使用add,如add x["aa"]增加了元素“aa”,删除元素使用delete,delete x["aa"]删除了元素"aa"。

# 初始化方法一
local s: set[port] = { 21/tcp, 23/tcp, 80/tcp, 443/tcp };
global s1: set[port, string] = { [21/tcp, "ftp"], [23/tcp, "telnet"] };
# 初始化方法二
global s2 = set(21/tcp, 23/tcp, 80/tcp, 443/tcp);

# Set构造函数也可以由类型显式命名,这在更复杂的索引类型可能不明确的情况下很有用
type MyRec: record {
    a: count &optional;
    b: count;
};
type MySet: set[MyRec];
global s4 = MySet([$b=1], [$b=2]);
event zeek_init(){
    local x: set[string] = { "one", "two", "three" };
    add x["four"]; # 增加元素
    print "four" in x; # T
    delete x["two"]; # 删除元素
    print "two" !in x; # T
    add x["one"]; # x is unmodified since 1 is already a member.
    print x;
    for ( e in x ){
        print e;
    }
}

输出:

T
T
{four, one, three}  打印出来是大括号括起来的
four
one
three

11.Vector

vector is a collection of values with 0-based indexing. 和set相比,值可以不唯一。
vector类似于table,不同之处在于索引是从0开始的非负整数。也是有两种方式初始化。打印出来是方括号括起来的。赋值操作符,如果存在则更改值的作用,如果不存在则增加元素的作用;
增加元素使用+=末尾添加,如a+="aaa"等价于a[|a|]="aaa"(也可以使用这个方式,table的增加值方式,如果索引重复那就覆盖);删除元素不能使用delete,而是使用赋值,比如a=a[:|a|],后面的值整个会覆盖求前面变量的值。

# 初始化方式一
local v1: vector of string={"a", "b", "c"};
# 初始化方式二
local v2 = vector("one", "two", "three");
event zeek_init(){ 
	local x: vector of string = { "one", "two", "three" };
	print x;   # [one, two, three]
	print x[1]; # two
        # x += "one";等价于下面语句,表示在后面添加一个元素
	x[|x|] = "one"; # 如果x是set、table、string、vector,那么|x|表示元素的个数,也就是长度。
	print x; # [one, two, three, one] 打印出来是中括号
        x[0:1] = vector("a", "b", "c"); # 作用就是后面替换掉前面的操作数,那么x变成了[a,b,c,two, three, one]
	for ( i in x ){
		print i;  # Iterates over indices.依次输出上述内容
	}
}

12.Record

a record is a collection of values。每个值都有一个字段名称和一个类型。每个值类型可以不一样,而且也没有类型的限制。字段名称必须遵循与常规变量名称相同的语法(但允许字段名称与局部变量或全局变量相同)

# 声明
type MyRecordType: record {
    c: count;  # 每一个value都是由字段名和字段类型组成
    s: string &optional;  # value可以有不同的类型
};
# 初始化变量方法一
local r: MyRecordType = record($c = 7);
# 初始化变量方法二(比较常用的)
local r = MyRecordType($c = 42);
# 初始化变量方法三
local r: MyRecordType = [$c = 13, $s = "thirteen"];

a record是用户定义的异构类型命名值的集合,类似于C中的结构(struct)。字段(fields)通过$操作符,被间接引用(dereferenced)。可选字段(optional field)的存在(existence)可以通过 ?$ 操作符检查(check)返回T或者F。

type MyRecord: record {
	a: string;
	b: count;
	c: bool &default = T;
	d: int &optional;
};

event zeek_init(){ 
	local x = MyRecord($a = "vvvvvv", $b = 6, $c = F, $d = -13);
	if ( x?$d ){
		print x$d;
	}
	
	x = MyRecord($a = "abc", $b = 3);
	print x$c;  # T (default value of the field)
	print x?$d; # F (optional field was not set)
}

输出:

-13
T
F

13.Redefinitions for records

redef(也就是重定义redefine)这个declaration不仅适用于值,而且适用于某些类型,即record(and enum)类型进行扩展。

@redef是一个属性:

redef和type都是declarations:

扩展,即增加record字段,该record定义的时候不需要具有&redef属性,但是操作上必须使用redef关键字重新定义该record,增加字段使用+=,后面接上大括号,里面再定义字段。

删减字段,

type MyRecord: record {
	a: string &default="hi";
	b: count  &default=7;
} &redef;  # 加不加属性,下面的结果都一样

redef record MyRecord += {
	c: bool &optional;
	d: bool &default=F;
	#e: bool; # Not allowed, must be &optional or &default.
};

event zeek_init(){
	print MyRecord();
	print MyRecord($c=T);
}

输出:

[a=hi, b=7, c=<uninitialized>, d=F]
[a=hi, b=7, c=T, d=F]

14.Zeek Datatypes

Zeek是network monitoring system,专注于网络,包括一些在使用网络时特别有用的数据类型:

  • time:绝对时间点(an absolute point time)。内建函数network_time返回zeek对现在状态的观念(是从zeek分析的数据包中得出)。而创建任意时间值的唯一方法是通过double_to_time(d)函数,其中ddouble类型的变量,表示来自UNIX时代的秒数。
  • interval:相对时间单位(a relative Unit of time)。已知单位是usec、msec、sec、min、hr、day(可以通过在末尾加s表示复数形式)。
  • port:传输级别的端口号(a transport-level port number)。 Examples: 80/tcp53/udp.
  • addr:IP地址。 Examples: 1.2.3.4[2001:db8::1].
  • subnet:使用CDIR表示法的一组具有公共前缀的IP地址。Example: 192.168.0.0/16.   表示,IP地址为192.168.0.0;子网掩码为:255.255.0.0(二进制的前16位全为1,即11111111.11111111.00000000.00000000)

练习

192.168.1.0/24192.68.2.0/24172.16.0.0/20172.16.16.0/20172.16.32.0/20172.16.48.0/20.看作给定的本地子网。

  • 第一需要告诉前10个新的源IP和端口、目的IP和端口、连接ID、连接开始的时间;
  • 第二计算看到的所有连接,并且打印它们;
  • 第三打印出每一个唯一IP,不管是本地的还是外部的。
  • 备注:使用exercise_traffic.pcap作为加载流量样本。

solution:使用 the event new_connection

# 将本地子网存进set数据变量local_subnets中,本地子网的数据类型是subnet。
global local_subnets: set[subnet] = { 192.168.1.0/24, 192.68.2.0/24, 172.16.0.0/20, 172.16.16.0/20, 172.16.32.0/20, 172.16.48.0/20 };
# 为了要计数所有的连接,声明全局变量my_count,初始化为0.这里没有指定该变量的类型,但是通过类型推断,推出是count类型。如果是+0,则推断出是int类型(有符号64位整数类型)。
global my_count = 0;
global inside_networks: set[addr];
global outside_networks: set[addr];

# 要了解每一个新连接,只需要使用new_connection event。每次触发该事件时,就增加计数器。
event new_connection(c: connection){
    ++my_count;
    # 对于前10个连接,将会打印源IP和端口、目的IP和端口、以及连接ID和时间。其中连接ID就是获取的连接字段uid,要用易读的形式打印开始时间,使用的是Zeek bif strftime。
    if ( my_count <= 10 ){
        print fmt("The connection %s from %s on port %s to %s on port %s started at %s.", c$uid, c$id$orig_h, c$id$orig_p, c$id$resp_h, c$id$resp_p, strftime("%D %H:%M", c$start_time)); 
    }
    # 如果源IP是本地IP那么就存到inside_networks集合中,否则存到outside_networks集合中
    if ( c$id$orig_h in local_subnets){
		add inside_networks[c$id$orig_h];
    }else
        add outside_networks[c$id$orig_h];
    # 如果目的IP是本地IP那么就存到inside_networks集合中,否则存到outside_networks集合中    
    if ( c$id$resp_h in local_subnets){
        add inside_networks[c$id$resp_h];
    }else
        add outside_networks[c$id$resp_h];
}

# 连接结束的时候,可以获取该连接持续的时间。当一个连接即将从内存中删除的时候,connection_state_remove event就会被触发,那么就可以得到该连接的持续时间。
event connection_state_remove(c: connection){
    if ( my_count <= 10 ){
    	print fmt("Connection %s took %s seconds", c$uid, c$duration);	
    }
}

event zeek_done(){
    # 展示的是所有连接数目
    print fmt("Saw %d new connections", my_count);
    print "These IPs are considered local";
    for (a in inside_networks){
        print a;
    }
    print "These IPs are considered external";
    for (a in outside_networks){
        print a;
    }
}

输出:

15.Writing a Module conventions

a module in zeek is a semantic entity(语义实体) that defines a new name space。通常上一个模块对应一个问题,a module可以是file、a bundle of files、a package。

注意一点:只能以不同的方式对zeek中已实现的event做出反应(react),却不能编写(write)新的协议事件(protocol events)。扩展Zeek,比如使用新的分析工具(new analyzers),创建新的事件(new events)等,是Try.zeek谈论范围之外的主题。

通常,模块会对来自Zeek事件引擎的现有内部事件作出反应。也可以在zeek脚本中创建对流量(traffic)不起作用的事件。

Zeek脚本约定( Zeek Scripting Conventions):

Module Names:驼峰式,无下划线(缩写均为大写)。Examples: FTP, SSH, Notice, Remote, Signatures

Types:驼峰式,无下划线。Examples: Log, Connection, EntropyTestResult, GeoLocation, Packet

Variables:小写并带有下划线。Examples: example_variable, ftp_sessions

Enum Values:大写并带有下划线。Examples: KNOWN_HOSTS, SSH, NFS3_REG

Notice Names:(这是命名枚举值的特殊情况)驼峰式并带有下划线。Examples: Off_Port_Protocol_Found, Malware_Hash_Registry_Match, Site_Exec_Success。

zeek脚本必须遵守的规则(和标准库一致):

  • 不能打印,而是使用reporter代替。
  • 框架不应仅通过它们的行为就造成任何性能开销。
  • 协议型脚本(protocol script)不应该生成任何通知。
  • 导出部分(export sections)的所有consts(常量)必须具有标记属性&redef
  • 无需使用redef重新配置变量。如果默认设置在默写情况下不正确,则可能需要对默认值进行讨论。

 Zeek Script Packages 可以发现每一个模块都包含两个文件__load__.zeek and main.zeek,模块名就是收集这些文件的目录名。

比如 http module,是支持超文本传输​​协议(HTTP)分析。

该模块名字为http里面有5个文件,就包含了__load__.zeek and main.zeek

其中__load__.zeek 如下:将上面的其余文件都导入进去了,包含main.zeek文件。因为加载模块的时候,Zeek首先查看__load__.zeek文件。

16.Writing a Module: Export

一个模块就是一个语义实体,意味着你想在外部使用实体的所有变量和函数就需要使得它们可用(available)。在Zeek中是通过an export block实现的。

export块中

(1)每个record、variable、function要被其他脚本访问,都需要放在block里面;并且注意导出的值都需要设置为global,否则将不能被访问使用。

(2)被其他脚本调用时,请注意该函数的调用语法:Module::function,或者Module::variable,或者Module::record。在函数名称之前需要指定模块名称(是Factor而不是factorial),并且每次使用export-section中的参数或者函数时,都必须指定模块名称。
下面是两个zeek文件,一个是factorial.zeek(实现的阶乘n!)

# factorial.zeek,该文件名字,内容如下

module Factor;  # 声明这是一个名为Factor的模块
# 下面是 a export block
export {
    global factorial: function(n: count): count; # 包含了一个函数声明,一个count参数,以及返回值也是count类型。
}

# 阶乘函数的具体实现    
function factorial(n: count): count{
    if ( n == 0 )
        return 1;
    else
        return ( n * factorial(n - 1) );
}

 另外一个是main.zeek, 使用了前面一个zeek文件产生的值。 

# main.zeek

@load factorial  # 导入(load)factorial.zeek

event zeek_done(){
    # 定义一个长度为9类型为vector的numbers变量
    local numbers: vector of count = vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);    
    # 对向量调用iterate
    for ( n in numbers )
        # 调用函数并且输出结果。请注意该函数的调用语法:在函数名称之前需要指定模块名称(是Factor而不是factorial),并且每次使用export-section中的参数或者函数时,都必须指定模块名称。
    	print fmt("%d", Factor::factorial(numbers[n]));
}

输出:

1
2
6
24
120
720
5040
40320
362880
3628800

17.Writing a Module: Logging

通常,新模块会创造新数据,并且希望收集到的这些数据放在新的日志文件中。在zeek中,操作日志是一个非常有用的工具,不仅仅是添加更多的日志文件或字段。

logging framework:Zeek带有一个灵活的基于键值的日志记录接口(logging interface),该接口允许对所有记录的内容和记录方式进行细粒度的控制。

日志记录接口主要由以下三个抽象(abstractions)构建(build)

(1)Streams:一个日志流(log stream)对应一个单独的日志(single log)。它定义了日志所包含的字段集(set of fields)及其名称(names)和类型(types)

(2)Filters:每一个流都附带有一组筛选器,这些筛选器确定要写出哪些信息。默认情况下,每个流都有一个默认过滤器,该过滤器会将所有内容全部记录到磁盘中。但是,可以添加其他过滤器来记录日志记录的一个子集,写到不同的输出,或者设置轮换间隔(rotation interval)。如果一个流删除了所有的过滤器,那么该流的输出将会被禁用。

(3)Writers:每一个流(stream)都有一个编写器(writer)。writer定义了记录信息的实际输出格式。默认的writer是ASCII writer,它产生制表符分隔的(tab-separated)ASCII files。其他的writers也可以使用(available),比如用于二进制输出或直接登入到数据库的writer。

自定义Zeek日志的方法有很多种:你可以创建一个新的日志流,你可以是用新字段(new fields)来扩展现有日志,你可以将过滤器应用于现有的日志流,或者你可以通过设置log writer options来自定义输出格式。所有的这些方法都可以参考logging framework

在这里依旧有两个zeek文件,一个是factorial.zeek,另一个是main.zeek。

首先看前者:

在export section部分,使用 redef 添加了名为LOG的值到enumerable变量Log::ID中。关于Log的详细信息可以参考这里。接着创建了一个record变量Info,里面包含的是log文件中的列,并且创造了两列num和factorial_num,其中num表示给出的数字n,factorial_num表示阶乘结果n!,同时这两个变量都添加了属性@log,该属性表示告诉Zeek给出的字段名字京会被添加进日志流中(logging stream)。

# factorial.zeek

module Factor;

export {
    # Append the value LOG to the Log::ID enumerable.
    redef enum Log::ID += { LOG };

    # Define a new type called Factor::Info.
    type Info: record {
        num: count &log;
        factorial_num: count &log;
    };
    global factorial: function(n: count): count;
}
    
function factorial(n: count): count{
    if ( n == 0 )
        return 1;
    
    else
        return ( n * factorial(n - 1) );
}

然后是后者:

首先需要使用 Log::create_stream 创建新的日志流,其中必要的参数是module的lLOG值和保存日志字段的record,变量 $path 告诉Zeek如何命名新的日志文件。注意一点,日志流需要在zeek_init事件中初始化。

下一步看起来与之前的步骤非常相似,但是现在我们不再使用将结果打印到Stdout的方式,而是使用Log::write函数写入新日志。注意一点,下面的例子是在zeek_done event中写入了所有结果,实际中,通常是在与日志文件有关的事件处理程序中完成的。

# main.zeek

@load factorial

event zeek_init(){
    # Create the logging stream.
    Log::create_stream(Factor::LOG, [$columns=Factor::Info, $path="factor"]);
}

event zeek_done(){
    local numbers: vector of count = vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);    
    for ( n in numbers )
        Log::write( Factor::LOG, [$num=numbers[n], $factorial_num=Factor::factorial(numbers[n])]);
}

18.Filtering Logs

自定义Zeek日志的方法有很多种,使用filter是一种方法。例如,将日志流分成两个日志文件。

在这里依旧有两个zeek文件,一个是factorial.zeek,另一个是main.zeek。

首先看前者:增加了一个函数mod5,函数会判断数字是否是5的模然后返回对应的字符串,返回的字符串是两个新日志文件的名称。这个模动态函数决定将哪个记录发送到哪个日志文件,关于动态确定日志路径的更多详细信息可查看这里

# factorial.zeek

module Factor;

export {
    # Append the value LOG to the Log::ID enumerable.
    redef enum Log::ID += { LOG };

    # Define a new type called Factor::Info.
    type Info: record {
        num: count &log;
        factorial_num: count &log;
    };
    global factorial: function(n: count): count;
    global mod5: function(id: Log::ID, path: string, rec: Factor::Info) : string;
}
    
function factorial(n: count): count{
    if ( n == 0 )
        return 1;
    
    else
        return ( n * factorial(n - 1) );
}
    
function mod5(id: Log::ID, path: string, rec: Factor::Info) : string {
    if ( rec$factorial_num % 5 == 0 )
        return "factor-mod5";
    
    else
        return "factor-non5";
}

另一个文件:

在zeek_init中加了很多行,创建了一个名叫my_filter的filter,添加该filter同时移除default filter。

# main.zeek

@load factorial

event zeek_init(){
    Log::create_stream(Factor::LOG, [$columns=Factor::Info, $path="factor"]);
    
    local my_filter: Log::Filter = [$name="split-mod5s", $path_func=Factor::mod5];
    Log::add_filter(Factor::LOG, my_filter);
    Log::remove_filter(Factor::LOG, "default");
}

event zeek_done(){
    local numbers: vector of count = vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);    
    for ( n in numbers )
        Log::write( Factor::LOG, [$num=numbers[n], $factorial_num=Factor::factorial(numbers[n])]);
}

输出

但是当注释掉删除default的那行,结果如下,从中可以看出,默认的filter也会作用,生成了factor日志文件

19.Rename Logs

创建日志文件是很重要的,同时Zeek的另一功能还允许修改现有日志。这个功能称为过滤(filtering),不仅仅可以过滤某些字段。日志流具有一个或者多个过滤器(没有任何过滤器的日志流将不会产生任何日志输出)。流创建出来后,将自动为其附加一个default filter。可以删除或者替换过滤器,也可以将其他过滤器添加到流中。可以使用 Log::add_filter or Log::remove_filter 函数来进行增删操作。默认过滤器(default filter)会将所有带有 &log 属性的字段写入日志文件。以下将会完成:使用过滤器完成重命名日志文件,将输出拆分成多个文件,控制写入的记录以及设置自定义轮换间隔(rotation interval)之类的任务。

下面的代码不全,只需要关注如何修改日志名字就可以。

event zeek_init(){
    # Replace default filter for the Conn::LOG stream in order to
    # change the log filename.
    # 首先该函数将新的过滤器f与connection analyzer的日志流关联
    local f = Log::get_filter(Conn::LOG, "default");
    # 设置新名称myconn
    f$path = "myconn";
    # 必须将此新过滤器添加到日志流中
    Log::add_filter(Conn::LOG, f);
}

20.Raising a Notice

定制Zeek的最简单方法之一就是编写本地通知策略(local notice policy)。除了查看日志文件之外,您还可以要求Zeek向您发送电子邮件,以用于特定情况或有关警告等的汇总电子邮件。此功能是通过通告框架(Notice Framework)提供的。在下面代码中并未提供email功能,只是将通知做成了日志文件。处理消息本身之外,它还告诉您通知的名称这对于汇,总汇总特别有用。

包含了两个文件,一个是factorial.zeek,另一个是main.zeek

# factorial.zeek

module Factor;

export {
    redef enum Log::ID += { LOG };
    #Append a new notice value to the Notice::Type enumerable.
    redef enum Notice::Type += { Interesting_Result };
    # 对于该例子,120是一个感兴趣的值,希望收到该值。因此将其设置为const,并且加入@redf属性以便于更改其他数值。
    const interesting_result = 120 &redef;

    type Info: record {
        num:           count &log;
        factorial_num: count &log;
    };
    global factorial: function(n: count): count;
    global mod5: function(id: Log::ID, path: string, rec: Factor::Info) : string;
    global result : count  = 0;
}

function factorial(n: count): count {
    if ( n == 0 )
        {
    	result = 1;
        return 1;
        }
    
    else
        {
    	result = n * factorial(n - 1);
        return result;
        }
}
    
function mod5(id: Log::ID, path: string, rec: Factor::Info) : string {
    if ( rec$factorial_num % 5 == 0 )
        return "factor-mod5";
    
    else
        return "factor-non5";
}

另一个文件:

raise a notice,使用的是NOTICE,该通知框架(Notice Framework)可能有一点一混乱(confusing),我们可以简单的把它看成一个函数,那么操作起来就会更加的简单。下面中已经给得出一些字段,比如msg、sub等。若需要通过Notice Framework发送电子邮件,则在系统上要有效的sendEmail配置。

@load factorial

event zeek_init() {
    Log::create_stream(Factor::LOG, [$columns=Factor::Info, $path="factor"]);
    
    local filter: Log::Filter = [$name="split-mod5s", $path_func=Factor::mod5];
    Log::add_filter(Factor::LOG, filter);
    Log::remove_filter(Factor::LOG, "default");
}

event zeek_done(){
    local numbers: vector of count = vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);    
    for ( i in numbers ){
    	local result = Factor::factorial(numbers[i]);
        Log::write( Factor::LOG, [$num=numbers[i], $factorial_num=result]);
        # 判断是不是感性兴趣的值,如果是,则发出通知(the notice is raised)
    	if ( result == Factor::interesting_result){
	    	NOTICE([$note=Factor::Interesting_Result, $msg = "Something happened!", $sub = fmt("result = %d", result)]);
    	}
    }
}

21.The Summary Statistics Framework(Sumstats)

(没理解,点击具体查看)

摘要统计框架(The Summary Statistics Framework简称sumsats)这个工具可以帮助深入了解网络中正在发生的事情。一个很好的;例子是扫描检测器(scan-detector),它把许多短连接,甚至看起来不相关的失败连接关联在一起,并发出有关扫描仪活动(scanner activity)的警告。关于Summary Statistics更多信息参考这里

Sumstats中所有定义的详细字段和函数可参考base/frameworks/sumstats/main.zeek

@load base/frameworks/sumstats
event zeek_init()
    {
    local r1 = SumStats::Reducer($stream="dns.lookup", $apply=set(SumStats::UNIQUE));
    SumStats::create([$name="dns.requests.unique",
                      $epoch=6hrs,
                      $reducers=set(r1),
                      $epoch_result(ts: time, key: SumStats::Key, result: SumStats::Result) =
                        {
                        local r = result["dns.lookup"];
                        print fmt("%s did %d total and %d unique DNS requests in the last 6 hours.", 
                        			key$host, r$num, r$unique);
                        }
                      ]);
    }

event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
    {
    # 如果不添加一些限制比如端口、非空请求、本地主机等,那么Zeek系统内存将会被占满,因为它会收集看到的每个主机每个唯一的DNS请求。
    if ( c$id$resp_p == 53/udp && query != "" )
    	# 如果这是一个真正的请求,我们要观察该请求,那么就在这个地方使用sumstats observer。
        SumStats::observe("dns.lookup", [$host=c$id$orig_h], [$str=query]);
    }

上面代码加载了

输出:
192.168.1.102 did 17 total and 6 unique DNS requests in the last 6 hours.
192.168.1.104 did 20 total and 10 unique DNS requests in the last 6 hours.
192.168.1.103 did 18 total and 6 unique DNS requests in the last 6 hours.
192.168.1.105 did 37 total and 20 unique DNS requests in the last 6 hours.
192.168.1.102 did 6 total and 3 unique DNS requests in the last 6 hours.
192.168.1.104 did 89 total and 86 unique DNS requests in the last 6 hours.
192.168.1.103 did 48 total and 42 unique DNS requests in the last 6 hours.


输出的日志文件outputlogs:
capture_loss
conn
dhcp
dns
files
http
known_certs
known_hosts
known_services
notice
ntp
smtp
software
ssl
stats
weird
x509

SumStats Exercise(1)

修改21中的代码,使用唯一计算的启发式版本HLL_UNIQUE,您可以在文档中找到它。然后设置sample size为5.

@load base/frameworks/sumstats
event zeek_init()
    {
    # 相比于21中的代码,这里需要修改reducer,修改了apply字段的内容,并且添加了num_samples字段并设置为5.
    local r1 = SumStats::Reducer($stream="dns.lookup", $apply=set(SumStats::HLL_UNIQUE, SumStats::SAMPLE), $num_samples=5);
    SumStats::create([$name="dns.requests.unique",
                      $epoch=6hrs,
                      $reducers=set(r1),
                      $epoch_result(ts: time, key: SumStats::Key, result: SumStats::Result) =
                        {
                        local r = result["dns.lookup"];
                        # 还修改了打印中的字段,将r$unique修改为r$hll_unique
                        print fmt("%s did %d total and %d unique DNS requests in the last 6 hours.", 
                        			key$host, r$num, r$hll_unique);
                        }]);
    }

event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
    {
    if ( c$id$resp_p == 53/udp && query != "" )
        SumStats::observe("dns.lookup", [$host=c$id$orig_h], [$str=query]);
    }

代码加载文件和21一致,输出也是一致。

SumStats Exercise(2)

结合thresholds使用,sumstats会更加强大,编写一个sumstats脚本,该脚本将对DNS查找进行计数,并在主机进行10个以上的DNS查找时写一条通知。

修改21中的代码,如下,在这个例子中,我们只能将其写入notice.log,但实际上,它当然可以作为电子邮件发送,以警告安全管理员有关可疑的行为。

@load base/frameworks/sumstats

redef enum Notice::Type += 
	{
    ExcessiveRequests
	};
# 设置数目为10,虽然是一个很小的数字
const excessive_limit: double = 10  &redef;

event zeek_init()
    {
    # 修改reducer中的apply字段内容,因为要计算所有请求的总和
    local r1 = SumStats::Reducer($stream="dns.lookup", $apply=set(SumStats::SUM));
    SumStats::create([$name="dns.requests",
                      $epoch=6hrs,
                      # 设置阈值
                      $threshold = excessive_limit,
                      $reducers=set(r1),
                      # 这次我们不使用epoch_result,而是对交叉阈值(crossed threshold)做出反应。
                      # 为此,我们需要首先评估阈值,一旦超过阈值,我们将发出通知。
                      $threshold_val(key: SumStats::Key, result: SumStats::Result) = 
                      	{
                        return result["dns.lookup"]$sum;
                      	},
                      $threshold_crossed(key: SumStats::Key, result: SumStats::Result) = 
                      	{
                        local r = result["dns.lookup"];
                        NOTICE([
                            $note=ExcessiveRequests,
                            $src=key$host,
                            $msg=fmt("%s has made more than %.0f DNS requests.", key$host, r$sum),
                            $sub=cat(r$sum),
                            $identifier=cat(key$host)
                          ]);
                      	}
                    ]);
    }

event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count)
    {
    if ( c$id$resp_p == 53/udp && query != "" )
        SumStats::observe("dns.lookup", [$host=c$id$orig_h], [$str=query]);
    }

其中加载的文件和上面的代码中加载的文件不一样。

输出日志文件output logs:
capture_loss
conn
dns
files
http
known_hosts
known_services
notice
software
stats
weird

22.The Intel Framework

The Intel Framework提供了一个接口,可将您自己的情报数据馈入Zeek。

“出于安全目的,情报数据对于监视过程至关重要。始终有通过事件响应过程发现的数据,以及通过私有社区共享的数据。Zeek情报框架的目标是使用这些数据,使其可用于匹配,可提供高性能,高内存利用率的基础架构,使得所有这些工作变得更加容易。”

下面的脚本去读intel数据文件,zeek使得在开始时就能发现匹配项。结果会产生一个intel.log的日志文件,其中包含了所有可见的匹配项。和往常一样,可以利用UID进一步调查,此匹配中涉及哪些连接,以及这些连接以哪种方式进行连接。

@load frameworks/intel/seen

redef Intel::read_files += {
	fmt("%s/intel-1.dat", @DIR)
};
print @DIR;
  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值