首页> 中国专利> 基于编译器代码注入的Linux系统调用监控方法

基于编译器代码注入的Linux系统调用监控方法

摘要

本发明涉及网络安全技术领域,公开的一种基于编译器代码注入的Linux系统调用监控方法,包括:监控系统模块总体设计、监控系统方案流程设计、监控系统各模块的具体设计、系统调用监控系统的实现,系统调用监控系统的测试,用于对实现系统的功能和系统损耗方面进行验证和测试;本发明通过调用监控方案提供的调用监控系统能够对从监控系统环境的搭建和功能测试进行完整的高效工作及测试,并且还可以作为运行时系统保护机制的监控机制,当不可信程序请求执行敏感操作时,系统可以及时发现并进行安全分析或拦截。

著录项

说明书

技术领域

本发明涉及网络安全技术领域,尤其涉及一种基于编译器代码注入的Linux系统调用监控方法。

背景技术

随着互联网的飞速发展,信息安全事件也随之频发,人们对暴露在互联网中的服务器系统安全的重视程度也在不断增加。Linux作为最常用的一种服务器操作系统,其面临的恶意代码威胁也十分严峻。

信息安全届对恶意代码分析的研究已经持续多年,其中动态分析是目前针对高度混淆的恶意代码分析中效果较好的分析模式,而系统调用监控是动态分析中十分重要的一个分析信息来源。通过系统调用监控,安全人员可以直观的发现应用程序的所有敏感操作行为,并且不会受到代码混淆的影响。除了作为恶意代码分析工具,系统调用监控还可以作为运行时系统保护机制的监控机制,当不可信程序请求执行敏感操作时,系统可以及时发现并进行安全分析或拦截。当前主流的系统调用监控方案都需要对系统环境或内核进行修改,针对大规模部署在微处理器系统并不十分友好,因此需要一种能够实现系统调用监控方案通过编译器代码注入的方式,将监控逻辑直接整合到编译完成的可执行文件中,这样将编译完成的可执行文件分发给各系统中即可完成对系统调用的监控。

发明内容

为克服现有技术的不足,本发明提供一种基于编译器代码注入的Linux系统调用监控方法。

为实现上述发明目的,本发明采用如下技术方案:

一种基于编译器代码注入的Linux系统调用监控方法,包括:监控系统模块总体设计、监控系统方案流程设计、监控系统各模块的具体设计、系统调用监控系统的实现,系统调用监控系统的测试,用于对实现系统的功能和系统损耗方面进行验证和测试;其步骤如下:

1).监控系统模块总体设计,构建一套用于在应用程序运行时实时搜索系统调用执行,并将其调用相关信息提供给安全人员,使得安全人员能够对应用程序的系统调用行为有直观的监控;因此系统的主要功能需求有:

(1)能够搜索到应用程序中所有的系统调用函数监控系统的起点在于对系统调用函数的发现,由于LLVM IR阶段时,C库函数仍是以函数接口的形式出现,其具体实现仍在之后的链接过程中装入,因此监控系统将对系统调用对应的用户API函数作为监控的对象;

(2)对调用函数的实时信息进行收集,函数的执行时信息主要包含了函数名,参数,返回值等信息,需要对这些运行时信息进行以及可执行文件的进程相关信息进行全面收集;

(3)将收集到的信息以用户可读形式输出,由于系统调用执行时的参数多为指针的形式出现,所指向内容不会永久保存,且

可读性不强,因此我们需要将收集到的信息以用户可读的形式

输出,便于安全人员的日志记录;

监控方案是在LLVM编译器IR优化阶段的注入来实现整个监控效果,系统总体由三个模块组成,即系统调用搜索模块、调用信息收集模块以及监控信息格式化输出模块;

系统调用搜索模块负责寻找监控系统的监控目标,其搜索粒度为系统调用API函数级别;调用信息收集模块负责收集一个函数调用中的全部信息及进程相关信息;监控信息格式化输出模块负责程序运行过程中监控系统所收集信息的格式化输出,提供进行分析;

2).监控系统方案流程设计,监控系统的核心处理时机为LLVM编译器的LLVM IR中间优化阶段,将编写的所有处理逻辑通过注册Pass的方式加载到LLVM IR的中间优化逻辑中,操作对象为LLVM IR比特码文件,其根据时序可以划分为搜索阶段、信息收集阶段及信息输出阶段。这四个阶段也与方案的四个功能模块相对应。处理后的内容为注入监控逻辑的LLVM IR比特码文件,使用LLVM项目提供的汇编工具集将注入后的比特码文件生成最终的可执行文件;

3).监控系统各模块的具体设计,即将对各个模块涉及到的的具体操作细节进行处理;

A.系统调用搜索模块,系统调用搜索模块是整个监控系统的出发点,也是整个监控系统细粒度划分的部分;根据LLVM的编译流程特征,以及系统调用在LLVM IR中的出现形式,设置以系统调用API函数为监控起始位置;系统调用搜索模块的功能是解析出LLVM IR比特码文件中的函数调用指令,并判断此次函数调用是否为系统调用API函数,根据其判断结果执行进一步监控逻辑;

(1)解析LLVM IR比特码文件,搜索其中的函数调用指令;

(2)如果本次函数调用不是系统调用API函数调用,则跳过该调用指令,继续寻找下一条函数调用指令;

(3)如果本次函数调用属于系统调用API函数调用则:

a.从该函数调用指令中获取所调API函数;

b.将所调API函数实例传入系统调用信息收集模块;

c.等待信息收集模块处理完毕后继续搜索下一条调用指令;

B.调用信息收集模块,调用信息收集模块是整个系统调用监控系统的核心功能,是获取系统调用所有程序运行时信息的部分;调用信息收集模块的功能是处理传入的系统调用API函数,对本次系统调用的调用号、参数进行解析;

这里需要获得的信息分为系统调用信息、运行时API函数信息和相关进程信息;系统调用信息包括系统调用名称及系统调用号;运行时API函数信息包括此次系统调用所传入的参数内容和返回值信息;相关进程信息为发起本次系统调用的进程号;其逻辑为:

(1)等待系统调用搜索模块传入系统调用API函数;

(2)解析调用函数中的系统调用信息,取出系统调用并获取对应的系统调用号;

(3)解析调用函数中的运行时信息,将传入参数及返回值进行保存;

(4)注入获取发起调用函数的进程信息,将获取的进程号保存;

由于运行时API函数信息及相关进程信息需要在程序运行时实时获取,因此在这里需要将需要获取的以上两种信息以形参的形式进行保存,等待程序运行时获取其真实值;

C.监控信息格式化输出模块,监控信息格式化输出模块是监控系统的结果展示部分,负责所收集到的监控信息,以用户可读的形式从程序运行时输出,并持久化保存,便于后续对应用程序系统调用行为的进一步分析;

监控信息的格式化输出,分为格式化文本生成和文件输出部分;为了方便用户对监控结果的分析,首先需要所获取信息的类型,并根据其类型生成对应的格式化文本,将格式化后的文本以文件的形式持久化存储到程序所在的的运行目录中;

4).系统调用监控系统的实现

4.1系统调用搜索模块的实现,通过上述系统调用搜索模块的功能是搜索LLVM IR比特码文件中发起函数调用的call指令,并判断所调用的函数是否属于系统调用API函数,如果是则将该被调函数实例传给调用信息收集模块,并进行后续的流程;如果不是则继续搜索下一条指令,直到遍历完LLVM IR文件中的所有指令;所以系统调用搜索模块的任务有:

a.遍历LLVM IR比特码文件中的所有指令

b.获取函数调用指令中所调用函数的实例

c.判断所调用函数是否为系统调用API函数

通过以上每个任务的实现,实施系统调用模块的具体步骤:

(1)对于任务1,在LLVM中,Pass处理的对象为由一个或多个LLVM IR比特码组成的Module,在Pass文件中,需要通过声明PassManger Interface方法将所编写的Pass注册在中间优化工具opt中;在该过程中,需要向ModulePassManager类的addPass方法中传入实现了run成员方法的Pass子类,LLVM IR系统函数会使opt工具的输入,即待处理的Module,传入run方法中;

至此,获得了待处理比特码文件的整个Module实数,然后将在该Module实数中对比特码文件中的指令进行搜索;在/llvm/IR/Module.h文件,能够看到在Module类内部实现了遍历其内部Function的迭代器,因此使用for语句即可对所传入Module中的Function进行遍历;同样的在Function类实现了对内部BasicBlock的迭代器,BasicBlock类实现了对内部Instruction类的迭代器;因此从传入的Module实数开始,使用三层for循环,即可对LLVM IR比特码文件中的所有指令进行遍历;

特别的,由于在LLVM IR阶段,还包含一些C标准库函数,这些函数在IR中仅以声明的形式出现,并没有内部函数实现,因此在遍历Function时需要通过Function::isDeclaration()方法跳过这些仅声明的函数;

(2)对于任务2,在LLVM IR中,Instruction类是所有指令类的基类,不同类型的指令又分别对应不同的指令类子类;函数调用指令call所对应的指令类子类为CallInst类;

在Pass开发中,提供了dyn_cast()方法来进行实数的类型转换,如果能够转换成功则返回转换后的类实数,如果失败则返回空指针;因此,通过对每一条Instruction实例尝试向CallInst实例的转换,来完成对函数调用指令的识别;在获取到函数调用指令对应的CallInst实例后,使用CallInst::getCalledFunction()方法能够获取该函数调用指令所调用的函数实例;

(3)对于任务3,在获取到所调用函数的Function实数后,通过Function::getName()方法得到该函数的函数名,需要搜索的是系统调用API函数,并且这些函数的函数名是已知的;因此,在这里设置一个系统调用API名称集,使用MAP数据结构,在监控系统初始化时将系统调用API函数列表传入名称集中;将所调用函数的函数名在其中搜索,如果搜索成功,则说明所调用函数为系统调用API函数,需要将该函数实例传入调用信息收集模块;反之则不是所搜索的目标函数,继续下一次指令遍历;

4.2.调用信息收集模块的实现,调用信息收集模块的功能是将传入的系统调用API函数进行处理分析,将用户感兴趣的所有相关信息收集整合,以及之后输出模块注入点的定位;需要收集的信息分为系统调用信息、运行时函数信息和相关进程信息;因此调用信息收集模块的任务为:

a.获取本次系统调用的属性信息,包括系统调用名称和系统调用号;

b.获取系统调用API函数的运行时信息,包括所传入的参数、系统调用返回值以及调用指令指针;

c.获取发起本次系统调用的相关进程信息,执行本次系统调用API函数的进程号在调用信息收集模块中,定义一个SystemCallInfo类,用来代表每一次系统调用所需要收集的信息,使用成员变量来保存所需要收集的全部信息,这样在对一次系统调用函数实例进行处理时,处理结束后获取的所有信息将会保存在一个SystemCallInfo实例中;

(1)对于任务1,在系统调用搜索模块中,使用Map数据结构存储了一个预定义的系统调用API函数集,其中函数集的键名为系统调用名称,键值为其对应的系统调用号;因此通过在系统调用API函数集中进行查找键值对,就能够获取本次调用所使用的系统调用名称及对应的系统调用号,分别将其保存在SystemCAllInfo实例的callName成员变量和callNo成员变量中;

(2)对于任务2,在LLVM IR中,函数调用的参数是以call指令内的操作码的形式出现,因此需要对所定位到的call指令中的操作码进行遍历,以获取传入系统调用API函数的所有参数;在函数调用类CallInst类中提供了op_begin()和op_end()两个方法,分别会返回第一个操作数形参和最后一个操作数形参,以这两个返回值为边界进行遍历,即可获得本次系统调用函数的所有运行时参数;由于不同系统调用API函数的参数是不等长的,因此使用C++中的vector数据结构来声明存储调用参数的callArgs成员变量,将遍历的参数结果依次存入;

同时在LLVM pass编程中,指令实例指针也是Value的子类,能够直接作为指令返回值指针来使用,能够直接将所定位到的call指令指针作为系统调用返回值保存到SystemCallInfo实例的callRet成员变量中;由于本任务的所有信息均是以行参的形式保存,需要在后续输出时在源LLVM IR比特码中注入获取参数的逻辑,因此在信息收集模块中还需要保存格式化输出模块注入点指针,这里将系统调用所在指令的下一指令指针作为注入点;选择此位置的原因有两点:一是在系统调用执行完毕之后,其返回值才会确定,所以注入点需要在系统调用指令之后;二是由于所保存的运行时信息均为形参,为了尽可能的避免后续执行中形参内容的改变,所以应该将注入点选的尽可能靠前;

(3)对于任务3,本系统调用监控运行在Linux系统的用户态中,因此对于进程相关信息的获取只能通过getpid()系统调用函数,为了尽可能准确的获取系统调用API函数所在call指令的所属进程,在系统调用API函数所在call指令处注入getpid()系统调用来获取当前call指令所在的进程号;

在LLVM pass编程中,向IR中注入内容的核心函数为IRBuilder::CreateCall()方法,具体步骤为先实例化一个IRBuilder类的Builder注入节点实数,Builder初始化时有两种方法:一是传入一个BasicBlock实数,这将使注入点指针指向该基本块的末尾处;二是传入一个Instruction实例,这将使所注入内容插入该指令之前;使用第二种初始化方法,使getpid()系统调用注入到所监控系统调用指令之前;CreateCall()接收一个FunctionCallee类的注入函数实数,注入函数调用时的其他实现细节见下一模块任务2的实现处;

4.3.监控信息格式化输出模块的实现,监控信息格式化输出模块的功能是将调用信息收集模块所记录的形参信息进行实时获取,并以可读性强的格式化文本持久化存储;因此该模块的任务有:

对调用信息收集模块采集的每个数据进行类型判断,并生成合适的格式化文本进行保存;

将转换完成的格式化文本进行持久化存储;

(1)对于任务1在调用信息收集模块中我们所保存系统调用名称和系统调用号是预定义的,因此其数据类型已知,分别为字符串类型和整形数类型;所以此处的格式化文本内容为固定格式;

系统调用参数、系统调用返回值以及发起系统调用指令所属进程的进程号的数据均为LLVM IR中的值指针;在LLVM pass编程中,一切值实例均为Value类的子类,通过Value::getType()方法获得该实例的值类型,然后通过Type::isIntegerTy()、Type::isFloatTy()、Type::isPointerTy()类型判别方法构造一个选择分支,将数据指针与对应的格式化文本按序映射,实现格式化文本内容动态适应不同数据大小和数据类型;

(2)对于任务2,选择使用写文件的方式将格式化后的可读文本进行持久化存储,实现内容为文件操作相关函数的注入,分为两部分,分别为函数声明注入和函数调用注入;

在LLVM pass编程中,注入一个对C语言标准库函数声明所使用的核心函数为Module::getOrInsertFunction()方法,需要向该方法中传入所声明函数的函数类型;这里的函数类型指的是函数的签名信息,包含函数的参数类型以及返回值类型,通过Function::get()方法进行构造即可;在该模块中注入了对fopen()、fprintf()和fclose()函数的声明,以便于之后文件操作的调用注入;

格式化输出模块的函数调用注入,是将调用日志文件操作函数的指令注入到源LLVM IR比特码文件中;和调用信息收集模块注入getpid()函数的操作同理,首先对每一个注入点声明对应的注入Builder,其中fopen()函数的调用注入点为main函数的起始位置,fclose()函数的调用注入点为main函数的return指令之前,fprintf()函数的调用注入点为信息收集模块中所保存的信息输出注入点;然后通过IRBuilder::CreateCall()方法注入对文件操作函数的调用;与调用getpid()函数不同的是,文件操作函数的注入还需要提供函数调用所传入的参数内容,这些参数按序放入一个ArrayRef类型变量,作为CreateCall()函数的第二个参数传入;所传入的参数内容必须是LLVM IR比特码中已有的值内容,通过Module::getOrInsertGlobal()方法以插入全局变量的方式生成新的参数内容;

5).系统调用监控系统的测试,将对实现系统的功能和系统损耗方面进行验证和测试;

5.1.监控系统测试的环境配置,CPU Intel(R)Core(TM)i5 CPU@2.70GHz,,

(1)LLVM项目,实现监控系统核心为一个LLVM编译器中的Pass组件,所使用LLVM项目源码版本为11.0,配套前端clang版本为6.0,需要编译之后的LLVM可执行程序,还需要使用LLVM项目中的库文件,因此还需要整个LLVM项目源码;由于LLVM项目版本迭代较多,各版本间差异也较大,因此这里推荐使用git工具从github上的LLVM官方仓库中下载对应版本的源码;

(2)cmake,将其构建为动态共享库文件,以提供给LLVM中的中间代码优化Pass加载工具使用,来完成对监控逻辑的注入,这里使用的构建工具为cmake,版本号为3.20;

cmake使用过程化描述文件来指定编译过程中所使用的链接库文件、编译参数,该描述文件通常命名为CMakeLists.txt;通过Ubuntu开源软件源安装或通过git工具下载开源库中的对应版本源码进行本地构建;

5.2.监控系统的功能测试

(1)监控系统Pass的构建,本Pass菜单下有include、lib、build、test四个文件夹;include下存放了Pass所使用的部分头文件,lib下存放了Pass的主体源代码,所有的监控逻辑主体均在其中,build用来存放编译后的输出文件,为Pass编译后的动态共享库文件,test菜单下提供了一个用于测试监控效果的c语言测试样例代码;

其中Pass菜单及lib菜单下提供了相应的CMakeLists.txt文件,用于将LLVM项目提供的库文件引入以及指定输出位置,因此在主目录下使用cmake工具进行构建;它将自动根据所提供的CMakeLists.txt配置生成编译文件,之后在使用make命令来完成所需动态链接库的编译;最终生成的动态共享库文件位于build/lib/libScout.so;

(2)监控系统实现效果,为了测试监控系统对系统调用函数的监控效果,在这里选取了10种系统调用作为测试样例程序进行测试。

首先需要通过clang前端将测试源码编译为比特码IR文件,再使用LLVM中提供的opt工具将本文所提供的监控Pass加载;在opt命令中需要指定所注册的Pass名称,这里为scout和Pass所在动态链接库位置;加载结束后会获得Pass处理完成之后的比特码IR文件,这时使用lli工具将处理后的比特码IR文件以JIT的形式解释执行,也能够使用llc工具将IR转换为汇编文件并最终生成可执行文件来编译执行;由于测试brk()函数在使用LLVMJIT执行方式时会出现堆栈错误,因此选择编译执行;

将程序执行后,所有的监控信息将输出到测试样例目录下的monitor.log这里测试样例中所使用到的time、open、read、write、close系统调用函数,均被监控系统捕获并将其运行时信息输出至日志文件monitor.log中。

由于采用如上所述的技术方案,本发明具有如下优越性:

一种基于编译器代码注入的操作系统调用监控方法,是通过系统调用监控模块总体设计、方案流程设计及各模块设计方案,对当前系统调用监控部署时的局限性分析,提出了本发明系统调用监控系统的实现细节以及系统测试的设计需求,并针对这些需求对应的设计出对应的功能模块。

在设计部分,公开了各模块之间的关系及各模块内部的处理流程,通过流程具体的描述了监控逻辑的具体工作方案。包括系统模块中涉及到的如何编写处理LLVM IR比特码文件的Pass组件,如何遍历IR文件中的调用指令,如何注入函数调用逻辑等技术细节。在监控系统测试中,分别从监控系统环境搭建和功能测试两方面,对所实现系统进行了完整测试,本发明监控系统的环境搭建模块逻辑过程和监控功能,均具有很高的监控效果,验证了系统调用监控系统是可行的。基于LLVM编译器代码注入的Linux系统调用监控方案的具体设计,是本发明的核心,因此本发明将监控逻辑直接整合到编译完成的可执行文件中,这样将编译完成的可执行文件分发给各系统中即可完成对系统调用的监控。当不可信程序请求执行敏感操作时,系统可以及时发现并进行安全分析或拦截。

附图说明

图1基于编译器代码注入的系统调用监控系统的流程图;

图2编译器代码注入的系统调用搜索流程图;

图3格式化输出流程图。

具体实施方式

如图1至图3所示,一种基于编译器代码注入的Linux系统调用监控方法,包括:监控系统模块总体设计、监控系统方案流程设计、监控系统各模块的具体设计、系统调用监控系统的实现,系统调用监控系统的测试,步骤如下:

1.监控系统模块总体设计,构建一套用于在应用程序运行时实时搜索系统调用执行,并将其调用相关信息提供给安全人员,使得安全人员能够对应用程序的系统调用行为有直观的监控。因此系统的主要功能需求有:

(1)能够搜索到应用程序中所有的系统调用函数监控系统的起点在于对系统调用函数的发现,由于LLVM IR阶段时,C库函数仍是以函数接口的形式出现,其具体实现仍在之后的链接过程中装入,因此我们的监控系统将对系统调用对应的用户API函数作为监控的对象。

(2)对调用函数的实时信息进行收集,函数的执行时信息主要包含了函数名,参数,返回值等信息,我们需要对这些运行时信息进行以及可执行文件的进程相关信息进行全面收集,以便安全人员更清晰的了解应用程序使用系统调用的情况。

(3)将收集到的信息以用户可读形式输出,由于系统调用执行时的参数多为指针的形式出现,所指向内容不会永久保存,且可读性不强,因此我们需要将收集到的信息以用户可读的形式输出,便于安全人员的日志记录。

监控方案主要是在LLVM编译器IR优化阶段的注入来实现整个监控效果,系统总体由三个模块组成,即系统调用搜索模块、调用信息收集模块以及监控信息格式化输出模块。

系统调用搜索模块负责寻找监控系统的监控目标,其搜索粒度为系统调用API函数级别;调用信息收集模块负责收集一个函数调用中的全部信息及进程相关信息;监控信息格式化输出模块负责程序运行过程中监控系统所收集信息的格式化输出,提供给安全人员进行分析。

2.监控系统方案流程设计,监控系统的核心处理时机为LLVM编译器的LLVM IR中间优化阶段,将编写的所有处理逻辑通过注册Pass的方式加载到LLVM IR的中间优化逻辑中,操作对象为LLVM IR比特码文件,其根据时序可以划分为搜索阶段、信息收集阶段及信息输出阶段。这四个阶段也与方案的四个功能模块相对应。处理后的内容为注入监控逻辑的LLVM IR比特码文件,可使用LLVM项目提供的汇编工具集将注入后的比特码文件生成最终的可执行文件。

3.监控系统各模块的具体设计方法,即将对各个模块涉及到的的具体操作细节进行描述。

A.系统调用搜索模块,系统调用搜索模块是整个监控系统的出发点,也是整个监控系统细粒度划分的部分。这里我们根据LLVM的编译流程特征,以及系统调用在LLVM IR中的出现形式,设置以系统调用API函数为监控起始位置。系统调用搜索模块的主要功能是解析出LLVM IR比特码文件中的函数调用指令,并判断此次函数调用是否为系统调用API函数,根据其判断结果执行进一步监控逻辑。

(1)解析LLVM IR比特码文件,搜索其中的函数调用指令。

(2)如果本次函数调用不是系统调用API函数调用,则跳过该调用指令,继续寻找下一条函数调用指令。

(3)如果本次函数调用属于系统调用API函数调用则:

①从该函数调用指令中获取所调API函数实例。

②将所调API函数实例传入系统调用信息收集模块。

③等待信息收集模块处理完毕后继续搜索下一条调用指令。

B.调用信息收集模块,调用信息收集模块是整个系统调用监控系统的核心功能,是获取系统调用所有程序运行时信息的部分。调用信息收集模块的主要功能是处理传入的系统调用API函数实例,对本次系统调用的调用号、参数等进行解析。

这里我们需要获得的信息主要分为系统调用信息、运行时API函数信息和相关进程信息。系统调用信息包括系统调用名称及系统调用号;运行时API函数信息包括此次系统调用所传入的参数内容和返回值信息;相关进程信息为发起本次系统调用的进程号。其主要逻辑为:

(1)等待系统调用搜索模块传入系统调用API函数实例。

(2)解析调用函数中的系统调用信息,取出系统调用并获取对应的系统调用号。

(3)解析调用函数中的运行时信息,将传入参数及返回值进行保存。

(4)注入获取发起调用函数的进程信息,将获取的进程号保存。

由于运行时API函数信息及相关进程信息需要在程序运行时实时获取,因此在这里我们需要将需要获取的以上两种信息以形参的形式进行保存,等待程序运行时获取其真实值。

C.监控信息格式化输出模块,监控信息格式化输出模块是监控系统的结果展示部分,负责所收集到的监控信息,以用户可读的形式从程序运行时输出,并持久化保存,便于后续对应用程序系统调用行为的进一步分析。

监控信息的格式化输出,主要分为格式化文本生成和文件输出部分。为了方便用户对监控结果的分析,我们不能将所获取到的信息直接以元数据的形式保存。因此我们首先需要所获取信息的类型,并根据其类型生成对应的格式化文本,将格式化后的文本以文件的形式持久化存储到程序所在的的运行目录中。

4.系统调用监控系统的实现

4.1系统调用搜索模块的实现

通过上述模块设计部分的描述,系统调用搜索模块的主要功能是搜索LLVM IR比特码文件中发起函数调用的call指令并判断所调用的函数是否属于系统调用API函数,如果是则将该被调函数实例传给调用信息收集模块,并进行后续的流程;如果不是则继续搜索下一条指令,直到遍历完LLVM IR文件中的所有指令。所以系统调用搜索模块的主要任务有:

①遍历LLVM IR比特码文件中的所有指令

②获取函数调用指令中所调用函数的实例

③判断所调用函数是否为系统调用API函数

通过以上每个任务的实现方法,以此介绍系统调用模块的具体实现细节。

(1)对于任务1

在LLVM中,Pass处理的对象为由一个或多个LLVM IR比特码组成的Module。在Pass文件中,需要通过声明PassManger Interface方法将所编写的Pass注册在中间优化工具opt中。在该过程中,需要向ModulePassManager类的addPass方法中传入实现了run成员方法的Pass子类,LLVM IR系统函数会使opt工具的输入,即待处理的Module,传入run方法中。

至此,获得了待处理比特码文件的整个Module实例,然后将在该Module实例中对比特码文件中的指令进行搜索。在/llvm/IR/Module.h文件第607行开始,看到在Module类内部实现了遍历其内部Function的迭代器,因此可以使用for语句即可对所传入Module中的Function进行遍历。同样的在Function类实现了对内部BasicBlock的迭代器,BasicBlock类实现了对内部Instruction类的迭代器。因此可以从传入的Module实例开始,使用三层for循环,即可对LLVM IR比特码文件中的所有指令进行遍历。

特别的,由于在LLVM IR阶段,还包含一些C标准库函数,这些函数在IR中仅以声明的形式出现,并没有内部函数实现,因此在遍历Function时需要通过Function::isDeclaration()方法跳过这些仅声明的函数。

(2)对于任务2

在LLVM IR中,Instruction类是所有指令类的基类,不同类型的指令又分别对应不同的指令类子类。函数调用指令call所对应的指令类子类为CallInst类。在Pass开发中,提供了dyn_cast()方法来进行实例的类型转换,如果能够转换成功则返回转换后的类实例,如果失败则返回空指针。因此,我们可以通过对每一条Instruction实例尝试向CallInst实例的转换,来完成对函数调用指令的识别。在获取到函数调用指令对应的CallInst实例后,使用CallInst::getCalledFunction()方法可以获取该函数调用指令所调用的函数实例。

(3)对于任务3

在获取到所调用函数的Function实例后,可以通过Function::getName()方法得到该函数的函数名,这里需要搜索的是系统调用API函数,并且这些函数的函数名我们是已知的。因此,在这里设置一个系统调用API名称集,使用MAP数据结构,在监控系统初始化时将系统调用API函数列表传入名称集中。将所调用函数的函数名在其中搜索,如果搜索成功,则说明所调用函数为系统调用API函数,需要将该函数实例传入调用信息收集模块;反之则不是所搜索的目标函数,继续下一次指令遍历。

4.2.调用信息收集模块的实现

调用信息收集模块的主要功能是将传入的系统调用API函数进行处理分析,将用户感兴趣的所有相关信息收集整合,以及之后输出模块注入点的定位。这里需要收集的信息主要分为系统调用信息、运行时函数信息和相关进程信息。因此调用信息收集模块的主要任务为:

①获取本次系统调用的属性信息,包括系统调用名称和系统调用号。

②获取系统调用API函数的运行时信息,包括所传入的参数、系统调用返回值以及调用指令指针。

③获取发起本次系统调用的相关进程信息,如执行本次系统调用API函数的进程号在调用信息收集模块中,定义了一个SystemCallInfo类,用来代表每一次系统调用所需要收集的信息,使用成员变量来保存所需要收集的全部信息,这样在对一次系统调用函数实例进行处理时,处理结束后获取的所有信息将会保存在一个SystemCallInfo实例中。

(1)对于任务1

在系统调用搜索模块中提到了使用Map数据结构存储了一个预定义的系统调用API函数集,其中函数集的键名为系统调用名称,键值为其对应的系统调用号。因此通过在系统调用API函数集中进行查找键值对,就可以获取本次调用所使用的系统调用名称及对应的系统调用号,分别将其保存在SystemCAllInfo实例的callName成员变量和callNo成员变量中。

(2)对于任务2

在LLVM IR中,函数调用的参数是以call指令内的操作码的形式出现,因此我们需要对所定位到的call指令中的操作码进行遍历,以获取传入系统调用API函数的所有参数。在函数调用类CallInst类中提供了op_begin()和op_end()两个方法,分别会返回第一个操作数形参和最后一个操作数形参,以这两个返回值为边界进行遍历,即可获得本次系统调用函数的所有运行时参数。由于不同系统调用API函数的参数是不等长的,因此我们使用C++中的vector数据结构来声明存储调用参数的callArgs成员变量,将遍历的参数结果依次存入。

同时在LLVM pass编程中,指令实例指针也是Value的子类,可以直接作为指令返回值指针来使用,可以直接将所定位到的call指令指针作为系统调用返回值保存到SystemCallInfo实例的callRet成员变量中。由于本任务的所有信息均是以行参的形式保存,需要在后续输出时在源LLVM IR比特码中注入获取参数的逻辑,因此在信息收集模块中还需要保存格式化输出模块注入点指针,这里将系统调用所在指令的下一指令指针作为注入点。选择此位置的原因有两点:一是在系统调用执行完毕之后,其返回值才会确定,所以注入点需要在系统调用指令之后;二是由于所保存的运行时信息均为形参,为了尽可能的避免后续执行中形参内容的改变,所以应该将注入点选的尽可能靠前。

(3)对于任务3

设计的系统调用监控运行在Linux系统的用户态中,因此对于进程相关信息的获取只能通过getpid()系统调用函数,为了尽可能准确的获取系统调用API函数所在call指令的所属进程,在系统调用API函数所在call指令处注入getpid()系统调用来获取当前call指令所在的进程号。

在LLVM pass编程中,向IR中注入内容的核心函数为IRBuilder::CreateCall()方法,具体步骤为先实例化一个IRBuilder类的Builder注入节点实例,Builder初始化时有两种方法:一是传入一个BasicBlock实例,这将使注入点指针指向该基本块的末尾处;二是传入一个Instruction实例,这将使所注入内容插入该指令之前。这里使用第二种初始化方法,使getpid()系统调用注入到所监控系统调用指令之前。CreateCall()接收一个FunctionCallee类的注入函数实例,注入函数调用时的其他实现细节见下一模块任务2的实现处。

4.3.监控信息格式化输出模块的实现

监控信息格式化输出模块的主要功能是将调用信息收集模块所记录的形参信息进行实时获取,并以可读性强的格式化文本持久化存储。因此该模块的主要任务有:

①对调用信息收集模块采集的每个数据进行类型判断,并生成合适的格式化文本进行保存

(1)将转换完成的格式化文本进行持久化存储

(2)对于任务1在调用信息收集模块中我们所保存系统调用名称和系统调用号是预定义的,因此其数据类型已知,分别为字符串类型和整形数类型。所以此处的格式化文本内容为固定格式。

系统调用参数、系统调用返回值以及发起系统调用指令所属进程的进程号的数据均为LLVM IR中的值指针。在LLVM pass编程中,一切值实例均为Value类的子类,我们通过Value::getType()方法可以获得该实例的值类型,然后通过Type::isIntegerTy()、Type::isFloatTy()、Type::isPointerTy()等类型判别方法构造一个选择分支,将数据指针与对应的格式化文本按序映射,实现格式化文本内容动态适应不同数据大小和数据类型。

(3)对于任务2

我们选择使用写文件的方式将格式化后的可读文本进行持久化存储,这里主要设计的实现内容为文件操作相关函数的注入。主要分为两部分,分别为函数声明注入和函数调用注入。

在LLVM pass编程中,注入一个对C语言标准库函数声明所使用的核心函数为Module::getOrInsertFunction()方法,需要向该方法中传入所声明函数的函数类型。这里的函数类型指的是函数的签名信息,包含函数的参数类型以及返回值类型,我们通过Function::get()方法进行构造即可。我们在该模块中注入了对fopen()、fprintf()和fclose()函数的声明,以便于之后文件操作的调用注入。

格式化输出模块的函数调用注入,是将调用日志文件操作函数的指令注入到源LLVM IR比特码文件中。和调用信息收集模块注入getpid()函数的操作同理,我们首先对每一个注入点声明对应的注入Builder,其中fopen()函数的调用注入点为main函数的起始位置,fclose()函数的调用注入点为main函数的return指令之前,fprintf()函数的调用注入点为信息收集模块中所保存的信息输出注入点。然后通过IRBuilder::CreateCall()方法注入对文件操作函数的调用。与调用getpid()函数不同的是,文件操作函数的注入还需要提供函数调用所传入的参数内容,这些参数按序放入一个ArrayRef类型变量,作为CreateCall()函数的第二个参数传入。所传入的参数内容必须是LLVM IR比特码中已有的值内容,也可以通过Module::getOrInsertGlobal()方法以插入全局变量的方式生成新的参数内容。

5.系统调用监控系统的测试

这一小节我们将对实现系统的功能和系统损耗方面进行验证和测试,并对测试结果作以简要总结。

5.1.监控系统测试的环境配置

表1测试机配置信息

(1)LLVM项目

本文所实现监控系统核心为一个LLVM编译器中的Pass组件,所使用LLVM项目源码版本为11.0,配套前端clang版本为6.0。这里我们不但需要编译之后的LLVM可执行程序,还需要使用LLVM项目中的库文件,因此我们不能从Ubuntu开源软件源中安装LLVM,而需要整个LLVM项目源码。由于LLVM项目版本迭代较多,各版本间差异也较大,因此这里推荐使用git工具从github上的LLVM官方仓库中下载对应版本的源码。

(2)cmake

本文的Pass编写完成后需要将其构建为动态共享库文件,以提供给LLVM中的中间代码优化Pass加载工具使用,来完成对监控逻辑的注入,这里我们使用的构建工具为cmake,版本号为3.20。

cmake可以使用过程化描述文件来指定编译过程中所使用的链接库文件、编译参数等,该描述文件通常命名为CMakeLists.txt。这里我们可以通过Ubuntu开源软件源安装或通过git工具下载开源库中的对应版本源码进行本地构建。

5.2.监控系统的功能测试

(1)监控系统Pass的构建

本Pass菜单下主要有include、lib、build、test四个文件夹。include下存放了Pass所使用的部分头文件,lib下存放了Pass的主体源代码,所有的监控逻辑主体均在其中,build用来存放编译后的输出文件,主要为Pass编译后的动态共享库文件,test菜单下提供了一个用于测试监控效果的c语言测试样例代码。

其中Pass菜单及lib菜单下提供了相应的CMakeLists.txt文件,用于将LLVM项目提供的库文件引入以及指定输出位置等,因此我们可以在主目录下使用cmake工具进行构建。它将自动根据所提供的CMakeLists.txt配置生成编译文件,之后在使用make命令来完成所需动态链接库的编译。最终生成的动态共享库文件位于build/lib/libScout.so。

(2)监控系统实现效果

为了测试监控系统对系统调用函数的监控效果,在这里选取了10种系统调用作为测试样例程序进行测试。

表2系统调用监控测试用例

首先,需要通过clang前端将测试源码编译为比特码IR文件,再使用LLVM中提供的opt工具将本文所提供的监控Pass加载。在opt命令中我们需要指定所注册的Pass名称(这里为scout)和Pass所在动态链接库位置。加载结束后我们会获得Pass处理完成之后的比特码IR文件,这时可以使用lli工具将处理后的比特码IR文件以JIT的形式解释执行,也可以使用llc工具将IR转换为汇编文件并最终生成可执行文件来编译执行。由于测试brk()函数在使用LLVM JIT执行方式时会出现堆栈错误,因此这里我们选择编译执行。

将程序执行后,所有的监控信息将输出到测试样例目录下的monitor.log这里我们可以看到测试样例中所使用到的time、open、read、write、close等系统调用函数均被监控系统捕获并将其运行时信息输出至日志文件monitor.log中。

表3日志文件monitor.log内容

去获取专利,查看全文>

相似文献

  • 专利
  • 中文文献
  • 外文文献
获取专利

客服邮箱:kefu@zhangqiaokeyan.com

京公网安备:11010802029741号 ICP备案号:京ICP备15016152号-6 六维联合信息科技 (北京) 有限公司©版权所有
  • 客服微信

  • 服务号