9.4.3 驱动程序中对应的函数

1.NPF_IoControl函数

NPF_IoControl函数的BIOCSETF命令码用来在驱动程序中设置一个新的数据包过滤器,其主要实现代码如下:


/#defne BIOCSETF 9030/

case BIOCSETF:

/获得新的过滤器指令集/

NewBpfProgram=(struct bpf_insn*)Irp->AssociatedIrp.SystemBuffer;

if(NewBpfProgram==NULL)

{

SET_FAILURE_BUFFER_SMALL();

break;

}

/获得自旋锁/

NdisAcquireSpinLock(&Open->MachineLock);

/生成数据包过滤器/

do

{

//释放旧的过滤器

if(Open->bpfprogram!=NULL)

{

TmpBPFProgram=Open->bpfprogram;

Open->bpfprogram=NULL;

ExFreePool(TmpBPFProgram);

}

//x86架构的32位平台即时(Just-In-Time,JIT)编译技术

//注意:JIT编译器只被x86(32位)架构支持*/

if(Open->Filter!=NULL)

{

//删除由BPF_jitter函数原先设置的一个旧过滤程序

BPF_Destroy_JIT_Filter(Open->Filter);

Open->Filter=NULL;

}

//获得新过滤器的指令条数

insns=(IrpSp->Parameters.DeviceIoControl.InputBufferLength)

/sizeof(struct bpf_insn);

//计算操作指令的条数

for(cnt=0;(cnt<insns)&&(NewBpfProgram[cnt].code!=BPF_SEPARATION);

cnt++);

if((cnt!=insns)&&(insns!=cnt+1)&&

(NewBpfProgram[cnt].code==BPF_SEPARATION))

{//如果存在初始化的指令,则调用bpf_filter_init函数,

//初始化NPF虚拟机

IsExtendedFilter=TRUE;

initprogram=&NewBpfProgram[cnt+1];

if(bpf_flter_init(initprogram,&(Open->mem_ex),

&(Open->tme),&G_Start_Time)!=INIT_OK)

{//初始化NPF虚拟机失败

SET_FAILURE_INVALID_REQUEST();

break;

}

}

//NPF虚拟机已被初始化,接着必须确认指令的有效性

insns=cnt;

//注意:TME指令的确认代码检查,如果在64位机上遇见TME指令,则返回失败

if(bpf_validate(NewBpfProgram,cnt,Open->mem_ex.size)==0)

{

SET_FAILURE_INVALID_REQUEST();

break;

}

//分配包含新过滤器程序的缓冲区,如果被强制使用bpf_filter_with_2_buffers

//函数,可能需要原始的BPF程序

TmpBPFProgram=(PUCHAR)ExAllocatePoolWithTag(

NonPagedPool,cnt*sizeof(struct bpf_insn),'4PWA');

if(TmpBPFProgram==NULL)

{//错误,没有内存可分配

SET_FAILURE_NOMEM();

break;

}

//创建新的JIT过滤器函数

if(!IsExtendedFilter)

{

if((Open->Filter=BPF_jitter(NewBpfProgram,cnt))==NULL)

{//失败

ExFreePool(TmpBPFProgram);

SET_FAILURE_UNSUCCESSFUL();

break;

}

}

//复制程序到新的缓冲区中

RtlCopyMemory(TmpBPFProgram,NewBpfProgram,cnt*sizeof(struct bpf_insn));

Open->bpfprogram=TmpBPFProgram;

SET_RESULT_SUCCESS(0);

}while(FALSE);

/释放自旋锁,清空当前实例存储数据包的内核环形缓冲区/

NdisReleaseSpinLock(&Open->MachineLock);

NPF_ResetBufferContents(Open);

break;


在对新的过滤器分配任何内存空间前,调用bpf_validate函数检查过滤器的正确性。如果函数返回TRUE,则该过滤器就会被复制到驱动程序的内存中,它的地址将存储在与当前驱动实例相关联的OPEN_INSTANCE结构体的bpfprogram字段中,该过滤器将对每个进来的数据包执行过滤。

执行该命令码也将会清空当前实例存储数据包的内核环形缓冲区,这样可防止缓冲区中还存储着不符合过滤条件的数据包。

BPF_Destroy_JIT_Filter函数用于删除由BPF_jitter函数设置的一个旧过滤器,并释放各种与旧过滤程序相关的内存空间,其原型如下:


void BPF_Destroy_JIT_Filter(JIT_BPF_Filter*Filter)


上述函数中,参数Filter就是需要清除的结构体JIT_BPF_Filter,该结构体描述一个由即时编译器所创建的x86平台下的过滤器,JIT_BPF_Filter结构体的定义如下:


typedef struct JIT_BPF_Filter

{

//x86平台下过滤器的机器码,采用BPF_filter_function格式

BPF_flter_function Function;

//过滤器的内存

PINT mem;

}

JIT_BPF_Filter;


上述代码中,函数指针BPF_filter_function的定义如下:


//JIT编译器创建的过滤函数的原型,该原型的参数与bpf_filter函数类似

//该过滤器是由硬件直接执行的,它不再通过解释器来执行了

typedef UINT(__cdeclBPF_flter_function)(PVOID,ULONG,UINT);


内核需要能够检查一个应用程序传递的过滤器指令,否则,一个伪造的指令能够很容易地摧毁系统。bpf_validate函数用来确认一个用户层应用程序传递的过滤器程序的合法性,其原型如下:


int bpf_validate(struct bpf_insn*f,int len,uint32 mem_ex_size);


上述函数中,参数f为过滤指令数组;参数len为过滤指令数组的长度,即伪指令的条数;参数mem_ex_size是扩展内存的长度,用来确认LD/ST指令对该内存的使用是否合法。

如果是一个有效的过滤器程序,则bpf_validate函数返回true。

BPF_jitter函数从一个BPF指令集构建一个x86平台下能直接执行的机器码,其原型如下:


JIT_BPF_FilterBPF_jitter(struct bpf_insnfp,INT nins)


上述函数中,参数fp为过滤器的BPF伪编译指令,将会被转换为x86平台下的机器码;参数nins为输入的指令条数。

BPF_jitter函数返回一个JIT_BPF_Filter结构体指针,其中包含了一个x86平台下的机器码函数指针Function。该函数的主要实现代码如下:


JIT_BPF_FilterBPF_jitter(struct bpf_insnfp,INT nins)

{

JIT_BPF_Filter*Filter;

/分配过滤器JIT_BPF_Filter结构体/

Filter=(struct JIT_BPF_Filter*)ExAllocatePoolWithTag(

NonPagedPool,sizeof(struct JIT_BPF_Filter),'2JWA');

if(Filter==NULL)

{

return NULL;

}

/分配过滤器的内存/

Filter->mem=(INT*)ExAllocatePoolWithTag(

NonPagedPool,BPF_MEMWORDS*sizeof(INT),'3JWA');

if(Filter->mem==NULL)

{

ExFreePool(Filter);

return NULL;

}

/创建x86平台的过滤函数(JIT编译器创建机器码)/

if((Filter->Function=BPFtoX86(fp,nins,Filter->mem))

==NULL)

{

ExFreePool(Filter->mem);

ExFreePool(Filter);

return NULL;

}

return Filter;

}


函数为新的过滤器指令分配缓冲区,它会调用BPFtoX86函数把BPF过滤器指令转换为x86机器码。

2.BPFtoX86函数

BPFtoX86函数把一个BPF指令集转换为了在x86平台下能够直接被执行的机器码。函数返回在x86平台下能够被直接执行的过滤函数,BPFtoX86函数原型如下:


BPF_flter_function BPFtoX86(struct bpf_insn*ins,

UINT nins,INT*mem);


上述函数中,参数ins指向BPF指令集,其将被转换为x86平台的机器码;参数nins为需要被转换的指令的条数;参数mem为x86平台过滤函数,用来仿真BPF伪处理器RAM的内存。

该函数所涉及的binary_stream结构体描述了x86平台的机器码的信息流,此结构体的具体定义如下:


typedef struct binary_stream{

INT cur_ip;//当前X86指令的指针

INT bpf_pc;//当前BPF指令的指针,比如jitter在BPF程序中达到的位置

PCHAR ibuf;//指令的缓冲区,包含生成的x86指令码

PUINT refs;//跳转引用表

}binary_stream;


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


BPF_flter_function BPFtoX86(struct bpf_insnprog,UINT nins,INTmem)

{

struct bpf_insn*ins;

UINT i,pass;

binary_stream stream;

/*

*注意:不要修改emitm的名称,在后续的宏定义中使用该名称

*如:#define RET()\

*emitm(&stream,12<<4|0<<3|3,1);

*/

emit_func emitm;

/给跳转引用表分配内存空间/

stream.refs=(UINT*)ExAllocatePoolWithTag(NonPagedPool,

(nins+1)*sizeof(UINT),'0JWA');

if(stream.refs==NULL)

{

return NULL;

}

/对引用表清零/

for(i=0;i<nins+1;i++)

stream.refs[i]=0;

/对指针指令清零/

stream.cur_ip=0;

stream.bpf_pc=0;

/*

*第一次for循环时,获得指令的长度,来创建引用表

*所以设置emitm为emit_lenght函数

*/

emitm=emit_lenght;

for(pass=0;;)

{

ins=prog;

/创建过程头/

PUSH(EBP)

MOVrd(EBP,ESP)

PUSH(EBX)

PUSH(ECX)

PUSH(EDX)

PUSH(ESI)

PUSH(EDI)

MOVodd(EBX,EBP,8)

for(i=0;i<nins;i++){

stream.bpf_pc++;

switch(ins->code){

default:

return NULL;

case BPF_RET|BPF_K://表示接收累加器中值的字节数

MOVid(EAX,ins->k)

POP(EDI)

POP(ESI)

POP(EDX)

POP(ECX)

POP(EBX)

POP(EBP)

RET()

break;

……

case BPF_LD|BPF_MEM:

MOVid(ECX,(INT)mem)

MOVid(ESI,ins->k*4)

MOVobd(EAX,ECX,ESI)

break;

……

}

ins++;

}

/修改循环次数,计数为2时退出循环/

pass++;

if(pass==2)break;

/分配生成的x86指令码的缓冲区/

stream.ibuf=(CHAR*)ExAllocatePoolWithTag(NonPagedPool,

stream.cur_ip,'1JWA');

if(stream.ibuf==NULL)

{

ExFreePool(stream.refs);

return NULL;

}

/修改引用表,包含偏移,并不是指令的长度/

for(i=1;i<nins+1;i++)

stream.refs[i]+=stream.refs[i-1];

/复位指令计数器为0/

stream.cur_ip=0;

stream.bpf_pc=0;

/*

*第二次for循环时,创建实际的机器码

*所以设置emitm为emit_code函数

*/

emitm=emit_code;

}

/引用表仅在编译的时候使用,现在释放它/

ExFreePool(stream.refs);

return(BPF_flter_function)stream.ibuf;

}


该函数为JIT编译器执行硬件部分的工作。它通过定义在jitter.h中的宏指令把一批BPF伪指令创建为一个能被硬件直接执行的机器码。

比如,MOVid、POP与RET的宏定义如下:


///mov r32,i32

defne MOVid(r32,i32)\

emitm(&stream,11<<4|1<<3|r32&0x7,1);emitm(&stream,i32,4);

///pop r32

defne POP(r32)\

emitm(&stream,5<<4|1<<3|r32&0x7,1);

///ret

defne RET()\

emitm(&stream,12<<4|0<<3|3,1);


emit_func函数指针原型如下:


typedef void(emit_func)(binary_streamstream,ULONG value,UINT n);


上述代码中,参数stream用来接收输入数据的信息流;参数value是一个包含输入数据的变量;参数length是长度,可能的值为1、2或4,分别代表1字节、2字节或1字(4字节)。

因为可以赋予该函数指针不同的函数以实现不同的功能,所以参数的实际意义随着赋予的函数而改变。

更新跳转引用表的emit_lenght函数的主要实现代码如下:


void emit_lenght(binary_stream*stream,ULONG value,UINT len)

{

(stream->refs)[stream->bpf_pc]+=len;

stream->cur_ip+=len;

}


生成实际二进制机器码的emit_code函数的主要实现代码如下:


void emit_code(binary_stream*stream,ULONG value,UINT len)

{

switch(len){

case 1:

stream->ibuf[stream->cur_ip]=(UCHAR)value;

stream->cur_ip++;

break;

case 2:

((USHORT)(stream->ibuf+stream->cur_ip))=(USHORT)value;

stream->cur_ip+=2;

break;

case 4:

((ULONG)(stream->ibuf+stream->cur_ip))=value;

stream->cur_ip+=4;

break;

default:;

}

return;

}


我们从POP、RET等宏定义与emit_lenght、emit_code函数的定义可以很清楚地知道BPFtoX86函数第一次循环与第二次循环的区别。第一次for循环时设置emitm为emit_lenght函数,来获得指令的长度,创建生成机器码时需要的引用表。第二次for循环时设置emitm为emit_code函数,来创建实际的代码。

3.NPF_ResetBufferContents函数

NPF_ResetBufferContents函数用于清空当前实例内核环形缓冲区中存储的数据包,这可防止缓冲区中还存储着不符合过滤条件的数据包,其主要实现代码如下:


VOID NPF_ResetBufferContents(POPEN_INSTANCE Open)

{

UINT i;

/获得保护缓冲区的自旋锁/

for(i=0;i<g_NCpu;i++)

{

NdisAcquireSpinLock(&Open->CpuData[i].BufferLock);

}

/读写序号复位为0/

Open->ReaderSN=0;

Open->WriterSN=0;

/复位各CpuData的成员值/

for(i=0;i<g_NCpu;i++)

{

Open->CpuData[i].C=0;

Open->CpuData[i].P=0;

Open->CpuData[i].Free=Open->Size;

Open->CpuData[i].Accepted=0;

Open->CpuData[i].Dropped=0;

Open->CpuData[i].Received=0;

}

/释放保护缓冲区的自旋锁,与获取的顺序相反/

i=g_NCpu;

do

{

i—;

NdisReleaseSpinLock(&Open->CpuData[i].BufferLock);

}

while(i!=0);

}


上述代码中,结构体__CPU_Private_Data包含了数据包捕获需要的内核缓冲区(与其他各CPU相关的成员),此结构体的具体定义如下:


typedef struct__CPU_Private_Data

{

ULONG P;//缓冲区中生产者基于0的索引。指明了第一个可以写的

//空闲字节

ULONG C;//缓冲区中消费者基于0的索引。指明了第一个可以读的字节

ULONG Free;//缓冲区中的空闲字节数

PUCHAR Buffer;//指向数据包捕获所使用的内核缓冲区指针

//当前捕获实例所接收的数据包数,从打开网络适配器开始计数

//如果一个数据包通过了过滤器并被填充到了缓冲区中,它就是被接收的

//被接收的数据包是已经到达应用程序的数据包

//该数目与该结构体引用的特定CPU相关

ULONG Accepted;

//当前捕获实例所接收的数据包数,也就是该捕获/监视/转储会话从一开始所接收的网络数据包的个数

//该数目与该结构体引用的特定CPU相关

ULONG Received;

//当前捕获实例所丢弃的数据包的数量,从打开网络适配器开始计数

//丢弃数据包是因为当前实例相关联的驱动程序的环形缓冲区中没有足够的空间

//该数目与该结构体引用的特定CPU相关

ULONG Dropped;

NDIS_SPIN_LOCK BufferLock;//保护与该CPU相关联的缓冲区的

//自旋锁

//第一个内存描述表(Memory descriptor list,MDL)用来映射部分缓冲区

//该缓冲区包含一个进入NIC的数据包

PMDL TransferMdl1;

//第二个MDL用来映射部分缓冲区,该缓冲区包含一个进入NIC的数据包

PMDL TransferMdl2;

//被NdisTransferData函数使用

//(当调用NdisTransferData时,仅须在TransferDataComplete函数中

//更新p索引)

ULONG NewP;

}CpuPrivateData;