9.4 数据包过滤的幕后

WinPcap通过用户空间与内核空间各函数之间的相互配合,来完成数据包的过滤功能。图9-6为与数据包过滤相关的重要函数调用关系图。后续各节将依据从用户空间到内核空间的顺序,来详细描述各重要函数的具体实现。

9.4 数据包过滤的幕后 - 图1

图 9-6 与数据包过滤相关的重要函数调用关系图

9.4.1 wpcap.dll库中相应函数的实现

1.关键结构体

wpcap.dll库所导出的函数都使用了bpf_program结构体,该结构体表示BPF数组形式的指令(汇编形式的指令),其具体定义如下:


struct bpf_program{

u_int bf_len;/BPF代码中谓词判断指令的数目/

struct bpf_insnbf_insns;/指向第一个谓词判断指令的指针*/

};


上述代码中,结构体bpf_insn表示谓词判断指令的结构体,其具体定义如下:


struct bpf_insn{

u_short code;//操作码

u_char jt;//真跳转

u_char jf;//假跳转

bpf_int32 k;//通用字段

}


2.pcap_compile函数

pcap_compile函数将一个高层的、容易理解的过滤表达式编译成了一个能够被过滤模块执行的低层字节码,其原型如下:


int pcap_compile(pcap_tp,struct bpf_programprogram,const char*buf,

int optimize,bpf_u_int32 mask)


上述函数中,参数buf为一个过滤表达式字符串;参数program是一个指向bpf_program结构体的指针,该结构体由该函数负责填充;参数optimize控制是否对最终生成的字节码执行优化;参数mask规定需要进行数据包捕获的网络的IPv4网络掩码,它仅在过滤程序中检查IPv4广播地址时使用。

如果pcap_compile函数执行成功,则返回0;否则返回-1,可以调用pcap_geterr函数显示所发生的错误。

pcap_compile函数的主要实现代码如下:


int pcap_compile(pcap_tp,struct bpf_programprogram,

const char*buf,int optimize,bpf_u_int32 mask)

{

int result;

EnterCriticalSection(&g_PcapCompileCriticalSection);

result=pcap_compile_unsafe(p,program,buf,optimize,mask);

LeaveCriticalSection(&g_PcapCompileCriticalSection);

return result;

}

static int pcap_compile_unsafe(pcap_tp,struct bpf_programprogram,

const char*buf,int optimize,bpf_u_int32 mask)

{

extern int n_errors;

const char*volatile xbuf=buf;

int len;

no_optimize=0;

n_errors=0;

root=NULL;

bpf_pcap=p;

/初始化被使用寄存器与当前寄存器的表/

init_regs();

/如果出现错误,调用bpf_error函数,程序最终跳转到此处释放资源/

if(setjmp(top_ctx))

{

lex_cleanup();//在解析后做必要的清理工作

freechunks();//释放内存资源

return(-1);

}

netmask=mask;//设置网络掩码

snaplen=pcap_snapshot(p);//设置捕获数据包的长度

if(snaplen==0){//如果为0,则丢弃所有的数据包

snprintf(p->errbuf,PCAP_ERRBUF_SIZE,

"snaplen of 0 rejects all packets");

return-1;

}

lex_init(xbuf?xbuf:"");//初始化词法分析

init_linktype(p);//初始化网络数据链路层类型

/生成虚拟机指令的中间表达形式/

(void)pcap_parse();

if(n_errors)

syntax();//过滤表达式中有语法错误

if(root==NULL)

{//生成接收snaplen字节数的返回指令的中间表达形式

root=gen_retblk(snaplen);

}

/决定是否优化/

if(optimize&&!no_optimize)

{

bpf_optimize(&root);//优化过滤器代码

if(root==NULL||(root->s.code==(BPF_RET|BPF_K)&&root->s.k==0))

{//过滤表达式丢弃所有的数据包,程序跳转到setjmp指令处

bpf_error("expression rejects all packets");

}

}

/*

*把控制流图的中间表示形式转换为BPF数组形式的指令,

*并设置bf_len为指令的条数

*/

program->bf_insns=icode_to_fcode(root,&len);

program->bf_len=len;

/清除资源/

lex_cleanup();

freechunks();

return(0);

}


从上面的代码可以看出,函数首先会进行各种初始化与设置,然后调用pcap_parse函数生成虚拟机指令的中间表达形式,该函数其实是调用bison生成的词法分析函数yyparse,而yyparse函数又会调用flex生成的语法分析函数yylex。

接着程序分析是否需要对所生成的虚拟机指令的中间表达形式进行优化处理,如果需要优化,就会调用bpf_optimize函数进行优化。

然后icode_to_fcode函数把控制流图的中间表示形式转换为BPF数组表示形式的指令,并将指令的条数设置到bpf_program结构体的bf_len成员中。

最后执行资源清除操作。

(1)setjmp与longjmp函数

在C语言中goto语句是不能跨越函数的,所以执行这类跳转功能的是setjmp与longjmp函数。这两个函数对于处理发生在深层嵌套函数调用中的出错情况非常有用,它们的原型如下:


include<setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env,int value);


在希望返回的位置调用setjmp,在pcap_compile函数中通过如下代码进行设置:


if(setjmp(top_ctx))

{

lex_cleanup();

freechunks();

return(-1);

}


如果直接调用setjmp函数,则返回0值。若调用longjmp函数,则返回非0值(该值通过longjmp的第二个参数value设置)。

setjmp函数的参数env是一个jmp_buf类型,在调用longjmp函数时用来存放与恢复栈状态的所有信息。因为需要在另一个函数中引用evn变量,所以规范的处理方式是把evn定义为全局变量。例如在wpcap\libpcap\gencode.c文件的开始部分进行如下定义:


static jmp_buf top_ctx;


longjmp函数的第一个参数是在调用setjmp函数时所用的参数evn,第二个参数是一个非0值。使用第二个参数的原因是对于一个setjmp函数可以有多个longjmp函数,通过该参数不同的值进行区分。例如要在wpcap\libpcap\gencode.c文件的bpf_error函数中调用longjmp函数,实现代码如下:


Void bpf_error(const char*fmt,…)

{

……

longjmp(top_ctx,1);//跳转到setjmp处

/不可能执行到此处/

}


任何调用bpf_error的函数最后都将跳转到setjmp处执行资源清除操作,并使函数pcap_compile返回-1。

(2)icode_to_fcode函数

该函数用于把控制流图的中间表达结果转换为BPF数组形式的指令,并设置lenp参数为指令条数,其原型如下:


struct bpf_insnicode_to_fcode(struct blockroot,int*lenp);


上述函数中,参数lenp记录指令的条数,在pcap_compile函数中用来设置program->bf_len成员的值。

icode_to_fcode函数返回指向BPF数组形式指令的数组指针。

此函数的主要实现代码如下:


struct bpf_insn*icode_to_fcode(root,lenp)

struct block*root;

int*lenp;

{

int n;

struct bpf_insn*fp;

/循环调用convert_code_r函数,直到没有太大偏移(大于256)的分支待处理/

while(1)

{

//count_stmts函数返回通过root可达的指令数目

//在调用count_stmts之前该结点root需要解除标记

unMarkAll();

n=*lenp=count_stmts(root);

//分配存储指令的空间(n个bpf_insn结构体),并清零

fp=(struct bpf_insn)malloc(sizeof(fp)*n);

if(fp==NULL)

bpf_error("malloc");

memset((char)fp,0,sizeof(fp)*n);

//设置指令数组的起点与终点

fstart=fp;

ftail=fp+n;

//开始转换,如果执行成功则结束循环,否则重新开始执行转换

unMarkAll();

if(convert_code_r(root))

break;//成功结束循环

free(fp);

}

/返回指向指令数组的指针/

return fp;

}


注意,该函数中fp所指的内存并没有泄露,在返回fp之前一定不要调用free(fp)。因为icode_to_fcode返回的BPF指令数组应该指向一个有效的内存空间,返回值在bpf_program结构体中使用,如下所示:


program->bf_insns=icode_to_fcode(root,&len);

program->bf_len=len;


如果icode_to_fcode函数真出现了内存泄露问题,应该是使用pcap_compile函数的程序释放bpf_program中的内存失败造成的。也就是说内存泄露是程序使用函数不当导致的,并不是在分配该内存的函数中发生的(类似的,如果一个程序对一个FILE*调用了fopen函数,而没有调用fclose函数,就会导致FILE结构体的资源泄露,不过泄露并不是在fopen函数中发生的,而是程序对这些函数的使用不当造成的)。

所以,在使用完pcap_compile函数后,应该调用配对的pcap_freecode函数来释放该内存空间。

其中convert_code_r函数的原型如下:


static int convert_code_r(struct block*p);


如果此函数执行成功,则返回true。如果一个分支具有非常大的偏移(实际实现为256)则返回false,在这种情况下,将标记该分支,以便在后续的一个迭代中被合适的处理,代码如下所示:


if(off>=256)

{

/该分支的偏移太大,必须添加一次跳转/

if(p->longjt==0)

{

//标记该指令并重试

p->longjt++;

return(0);

}

……

}


3.pcap_compile_nopcap函数

pcap_compile_nopcap函数在不需要打开适配器的情况下编译过滤器。这个函数能将程序中高级语言描述的过滤表达式转换成能被内核级过滤模块所处理的字节码。pcap_compile_nopcap函数与pcap_compile函数类似,不同之处在于它不是传入一个pcap结构,而是显式地传递snaplen_arg与linktype_arg参数。pcap_compile_nopcap函数是对pcap_open_dead、pcap_compile与pcap_close等函数的封装。

pcap_compile_nopcap函数的原型如下:


int pcap_compile_nopcap(int snaplen_arg,int linktype_arg,

struct bpf_programprogram,const charbuf,

int optimize,bpf_u_int32 mask)


上述函数中,参数snaplen_arg为需要捕获的数据包的长度;参数linktype_arg为数据链路层的类型;参数program为指向一个bpf_program结构体的指针,该结构体由本函数完成填充;参数buf为一个过滤表达式字符串;参数optimize用于控制是否对最终生成的字节码执行优化;参数mask规定需要进行数据包捕获的网络的IPv4网络掩码。

如果函数pcap_compile_nopcap执行成功则返回0,失败则返回-1。

该函数的主要实现代码如下:


int pcap_compile_nopcap(int snaplen_arg,int linktype_arg,

struct bpf_programprogram,const charbuf,

int optimize,bpf_u_int32 mask)

{

pcap_t*p;

int ret;

/创建一个pcap_t结构体,而不是开始一个捕获操作/

p=pcap_open_dead(linktype_arg,snaplen_arg);

if(p==NULL)

return(-1);

/将一个高层的、容易理解的布尔过滤表达式编译成一个底层的字节码/

ret=pcap_compile(p,program,buf,optimize,mask);

/关闭适配器,释放资源/

pcap_close(p);

return(ret);

}


该函数主要用于编译过滤器表达式,不必要调用pcap_open函数,可直接使用。

4.pcap_setfilter函数

pcap_setfilter函数用于把一个已编译好的过滤表达式的指令集与内核捕获实例相关联。当pcap_setfilter函数被调用时,该过滤器将被应用到来自网络的所有数据包上,并且所有符合要求的数据包(即那些经过过滤器以后,布尔表达式为真的包)将会被存储到内核缓冲区中,以供应用程序使用。其原型如下:


int pcap_setflter(pcap_tp,struct bpf_programfp);


上述函数中,参数fp是指向bpf_program结构体的指针,通常是调用pcap_compile函数返回的结果。

如果pcap_setfilter函数执行成功则返回0,失败则返回-1,调用pcap_geterr可显示错误信息。

有关的代码实现如下:


int pcap_setflter(pcap_tp,struct bpf_programfp)

{

return p->setflter_op(p,fp);

}

static int pcap_activate_win32(pcap_t*p)

{

……

p->setflter_op=pcap_setflter_win32_npf;

……

}


pcap_setfilter函数内部主要是调用pcap_setfilter_win32_npf函数,pcap_setfilter_win32_npf函数的主要实现代码如下:


static int pcap_setflter_win32_npf(pcap_t*p,

struct bpf_program*fp)

{

/调用Packet.dll库的PacketSetBpf函数设置过滤器/

if(PacketSetBpf(p->adapter,fp)==FALSE)

{

//内核过滤器安装失败

snprintf(p->errbuf,PCAP_ERRBUF_SIZE,

"Driver error:cannot set bpf flter:%s",

pcap_win32strerror());

return(-1);

}

/*

*丢弃前面接收的所有数据包,

*BIOCSETF命令码的IOCTL操作将丢弃内核缓冲区中的数据包

*/

p->cc=0;

return(0);

}


pcap_setfilter_win32_npf函数主要调用Packet.dll库中的PacketSetBpf函数来给内核设置过滤器。

5.pcap_freecode函数

当程序不再需要过滤表达式已编译好的指令集时,pcap_freecode函数释放调用pcap_compile函数或pcap_compile_nopcap函数所获得的bpf_program结构体。比如过滤指令集已通过pcap_setfilter函数设置给了内核,则用户空间的bpf_program结构体就没用了。

该函数的主要实现代码如下:


void pcap_freecode(struct bpf_program*program)

{

program->bf_len=0;

if(program->bf_insns!=NULL){

free((char*)program->bf_insns);

program->bf_insns=NULL;

}

}


从上面的代码可知,pcap_freecode函数主要调用free函数来释放在icode_to_fcode函数中分配的、用来存储指令的内存空间。