标签 Grpc 下的文章
作者: 纯情
时间: 2026-01-19
分类: 开源
评论
Intro Tai-e作为一个优秀的静态分析框架,内置了指针分析、污点分析等等实现。为增强其作为一个底座框架的可扩展性,其提供了插件系统,通过插件系统可以控制在静态分析过程中的各个阶段的数据处理,更进一步的进行定制化分析的实现。如下图为Tai-e官方提供的有关于插件系统的原理图:
本文中提及的有关于微服务应用的静态分析框架MScan同样是基于Tai-e进行实现的,针对微服务应用中使用的一些特殊的API进行服务间的高速通信过程,传统的静态分析方式不能够原生支持该类服务间通信的污点流的传播,但是这里采用了上面介绍了插件系统的方式,为服务间的通信过程进行建模,定制化的支持该过程的数据流分析,例如是Grpc、Dubbo或者Feign等通信方式。 具体的分析因篇幅太长分为了上下两篇,上篇主要集中于理论层面的代码分析,剖析基于Tai-e框架的改造细节,明晰从source点提取到扩展的污点分析引擎工作原理的全流程。而下篇主要集中于实战层面的内容,在剖析微服务应用各服务间的通信建模方式,也即如何构建一个SDG(Service Dependence Graph),同时贴近实战批量拉取github\gitee高star项目进行自动化 clone-complie-scan 全流程。 DistancePruning 该类的实现对应着论文中提及到的基于距离引导的上下文选择策略,但是感觉具体对其的实现还是和论文中的描述存在出入,后面具体分析其实现
在 options.yml 中若对 advance 进行配置,将会使用特定的上下文选择器,这里的动态上下文选择策略的实现和核心逻辑在 DistancePruning#run ,核心是三个原则 1 对于一个方法,其能够调用到某一个sink点方法且能够被某一个source点方法调用到(不局限于单次调用,只要在调用图上能找到一个调用链即可),对于这样的方法,将其 csMap 的值设为 MAX ,也即是这样的方法采用最大的上下文进行分析 2 对于仅仅能够形成调用链到sink点方法,但不能够某个source点方法调用的方法,这样的方法,将其 csMap 的值设为固定的 2 ,在分析时采用 2-call 的方法进行上下文的选择 3 而对于上述两种情况都不满足的情况,则直接将其上下文选择为 MIN ,采用最小的上下文 总的来说,虽然与论文中提出的基于一个方法到达最近的 source-sink 链的距离进行上下文的选择有所出路,但是这里的上下文选择方法也是基于一个 context-insensitivity 的分析结果,所以对于可能的 source-to-sink 调用链长度进行最大上下文的选择也一种有效的避免假阳性的方法
与此同时,注意到在 Pruning 类也存在有两种上下文选择的思路 1 csMapByTaintNum 方法,基于一种成本控制的思路进行上下文的选择,首先通过流式处理,从指针分析结果( pta )的调用图中获取所有可到达的方法( reachableMethods ),对于每个方法,计算其参数中属于“污点”( Taint )的数量。然后过滤掉污点参数数为0的方法,并将剩余方法按污点参数数从高到低排序。确保了那些 更可能涉及敏感数据流 的方法会被优先考虑 总的来说,上下文的大小是由一个动态的分析成本预算控制的。它优先处理污点参数多的方法,但同时严格限制方法的分析成本总和不超过上限(硬件条件)。这种设计巧妙地在分析广度(覆盖更多方法)和深度(分析复杂方法)之间取得了平衡,防止资源消耗无限增长
○ 对于每个方法,只有当累计成本 count 小于阈值(1e5)时,才会将其加入 csMap 并标记为 "5" ,同时计数器 count5 增1 ○ 如果方法非抽象,则计算其分析成本: 变量数 * (调用者数量)^4 ,并将此成本累加到 count 上 ○ 一旦 count 的值达到或超过 1e5 ,循环便会停止,后续方法不再被加入 csMap 2 csMapByTaintFlow 方法,这个方法猜测是想基于通过上下文不敏感的静态分析结果得到的 TaintFlow 进行上下文的动态选择,但是感觉后面可能烂尾了,没有实现完
SDG (Service Dependence Graph) OpenFeignPlugin 该插件核心是用来建立通过 Feign 方式进行跨服务调用的调用边,用于构建 SDG (Service Dependence Graph) 对于该插件同样是实现了标准的 Plugin 接口,其实现了 onStart 方法以及 onNewCSMethod 方法用于在程序分析前进行处理以及在遭遇新的方法时进行处理 对于 onstart 主要是在静态分析前对 FeignClient 进行处理,获取所有的feign类型的路由以及实现类,保存在 mappingEdges 中
而对于 onNewCSMethod 实现了一个访问者模式,遍历遇到的所有新方法的所有 Stmt ,如何遇到函数调用的 Stmt 则会考虑其是否是一个 invokeInterface 类型的调用,也即是是否调用的是实现的接口的方法,这里是用来处理 Feign 这种方式进行跨服务通信的机制,根据 feignClient 类的类签名从 mappingEdges 获取所有的实现方法,并通过 addCallEdge 为这个调用过程建立一个调用边
GrpcPlugin 这个插件所起的作用和 OpenFeignPlugin 类似,均是用来处理微服务中的各个 service 间的调用关系 前者是用来处理 Feign 这种调用方式,这里的插件是用来处理通过 Grpc 这种方式进行调用的方式 对于 onStart 方法,其主要是用于构建 invoke-callee 的映射,也即是调用关系,Grpc服务端以及客户端stub的实现分别是实现了 io.grpc.BindableService 或者 io.grpc.stub.AbstractStub 1 通过获取所有自己实现的 io.grpc.BindableService 类,将其有参类方法存储在 serviceMethod ,作为对位提供的 grpc 方法 2 筛选所有 Grpc 客户端的实现方法,通过审查所有的 invoke 函数调用,若被调用的函数所在类属于 io.grpc.stub.AbstractStub 实现,则认为其是一个客户端 stub ,获取这个远程调用方法的第一个参数变量,构建了一个 var-invoke 的映射,同时如果该方法能够在 grpc 服务端实现的可调用方法中找到的话,会构建一个从客户端调用点到被具体调用的方法的一个映射 invoke2calleeMap
而 onNewCSMethod 同样是在基于访问者模式构建一个跨服务调用的关系 1 对于所有跨服务调用点,在 PFG (Pointer Flow Graph) 上构建一个被调用方法参数传递的边,同时构建一个调用边 2 处理在微服务中采用 guice 这种轻量级的依赖注入组件,通过寻找其实现类的方式直接通过 addPointsTo 建立联系
RestTemplatePlugin 该插件用来处理使用 RestTemplate 进行各服务间通信的调用关系 1 最开始通过筛选 exchange 函数的调用点,构建 var2InvokeMap 用来映射 exchange 的传参以及调用点 2 在指针集发生变化时,通过 var2InvokeMap 中var所对应的指针集去获取想要请求的URI是什么,并保存在 targetString 中
3 遍历上面收集的 targetString ,与 GatewaySourcePlugin 插件中识别到的 endpoint 的路由做比对,如果存在匹配成功的情况,将会构建一个从 exchange 函数调用点到对应路由提供者方法的一个调用边,并通过 addPFGEdge 将传入的参数进行跨服务传递
DubboPlugin dubbo作为一个RPC服务开发框架,同样提供一种在微服务架构中进行不同服务通信的方式,这里的 DubboPlugin 也即是对其进行支持,构建 dubbo 场景下的服务依赖图 在静态分析前基于注解进行 dubbo 服务端的识别
在指针分析过程中实时筛选所有的函数调用过程,如果存在调用了 dubbo 服务的函数,则建立此调用点到dobbo服务中定义的目标函数的调用边
KafkaPlugin 该插件用来处理在微服务框架中采用 kafka 进行服务间通信的方式 首先在进行静态分析之前, onStart 方法中,从 ApplicationClass 中获取被 KafkaListener 注解的消费者方法,并以 topic-method 的映射保存在 kafkaListeners 中。同时从获取到生产者方法保存在 kafkaSendMethods 中
其次是在 onNewStmt 事件触发时,判断是否是调用的生产者方法,若是的话,构建生产者方法的第一个参数,也就是 topic 和方法调用的一个映射
最后则是在指针集发生变化是触发的 onNewPointsToSet 事件中,判断是否 topic 对应的指向出现变化,遍历获取其指代的所有 topic 后在 kafkaListeners 寻找是否存在有消费该 topic 的消费者方法,若存在,将会通过 addPFGEdge 构建一个从生产者方法生产的消息内容到消费者方法消费的消息内容的指向边,以及通过 addCallEdge 构建一个从生产者方法到消费者方法的调用边
RabbitMQPlugin 该插件和kafka处理的对象都是消息队列的跨服务通信的依赖构建,且都是采用消息队列的方式,实现逻辑也类似 1 将消费者的监听队列以及处理时间方法映射保存在 rabbitmqListeners 中,以及将生产者的消息发送方法保存在 rabbitmqSendMethods 中
2 构建消息发送函数调用同 exchange 和 route key 的映射关系,同时构建消息处理函数调用同 queue, exchange, route key 的映射关系
3 类似的,最后就是根据 route key 以及 exchange 去匹配对应的消费者方法,同时构建从发送者方法所发送消息到消费者方法所消费消息的 pointer edge ,以及构建在消息发送点到消息处理点的 call edge
Full progress 对于tai-e的整个流程大致可以分为以下的过程 1 进行静态分析前的准备工作,包括有指定 appClassPath 以及 ClassPath 而对于这里的 Mscan ,包括有以下几点: ● 将配置文件中的 Config.classpathKeywords 添加到 classpathKeywords 中 ● 将前面 Jar parser 中提取到到的 ${targetPath}/BOOT-INF/classes 中的类添加到 appClassPath 中 ● 将 Jar parser 提取到的 ${targetPath}/BOOT-INF/lib 中的jar包添加到 classPath 中
2 通过 options 中的配置去生成对应的 plan 文件
3 调用 Soot 对所有的类进行解析,包括有 BOOT-INF/classes 以及 BOOT-INF/lib 中的类,核心是使用了 SootWorldBuilder#build 方法进行处理
4 执行前面生成的 analysis plan ,对于 pta ,则使用对应的配置调用 PointerAnalysis#analyze 进行分析 a 首先是构建一个 Heap abstraction ,用来将动态时无限的对象抽象为有限,通常选用为 Allocation-Site 这一抽象方式 b 其次则是构建 ContextSelector ,优先使用 advanced 中的配置,若没有配置 advanced ,则根据 makePlainSelector 去正常获取上下文选择器,支持有以下 context selector variant
■ ci : context-insensitive analysis ■ k-obj/call/type c 在构建了 heap abstraction 以及 context selector 后调用 runAnalysis 进入指针分析逻辑
d 在核心的指针分析逻辑中,其主要是根据 heapModel 以及 selector 构建一个 Solver 对象,通过其中的 solve 方法进行分析 值得注意的是,tai-e设计中存在有一个扩展性极强的插件系统,详情可见 https://tai-e.pascal-lab.net/docs/current/reference/en/index-single.html#analysis-plugin-system
e 对于 solve 方法,其实现了指针分析算法
f 其中算法的伪代码中的添加入口点以及 addReachable 由 DafaultSolver#initialize 方法实现,其首先对一些全局变量进行了初始化,核心是通过插件系统的 onStart 方法调用去实现,依靠插件系统可以实现在整个程序分析的生命周期中的各个环节的实时计算,这里通过 onStart 方法调用,一方面对装载的各个插件进行初始化,另一方面对算法中的 addEntry 以及 addReachable 进行实现
g 而对于 solve 方法的第二部分,也即是 analyze 则对应于伪代码中的work list的处理过程,核心是对于 work list 中的各个元素,首先判断其指针集是否存在变化,若存在变化则处理对应的store以及load操作
1 对于指针分析的分析结果其通过构建一个 PointerAnalysisResultImpl 对象,存储了调用图,指针流图,指针集等丰富的信息,且最终的分析结果根据 analysis-id 的对应关系保存在了 World 中
Real world 上述内容主要是对静态分析框架的整个框架的原理以及代码实现进行了阐述,下面基于上面的静态分析框架为基座,构建了一个clone-complie-scan全流程的自动化漏洞检测闭环 clone 首先是clone环节,对于目标项目的选择,我们采用github以及gitee平台提供的筛选的功能对高star的Java项目进行初步筛选,后续得益于LLM的理解能力,通过LLM对初筛的项目文档进行理解对项目进行分类,具体可以从两个角度进行分类 1 使用maven进行项目编译还是gradle进行项目编译:通过识别项目的编译方式以便于下一步的自动化编译过程 2 项目所具备的特征:例如是一个微服务项目或者电商项目,通过这样的方法对业务进行分类 同时,在收集的过程中,也不单单局限于仅对微服务相关项目进行收集,可对全部的基于Java开发的项目进行收集进行批量检测
如上图所示,则是收集的一些Java项目的样例,通过yml文件的方式将待检测项目进行归类 之后分别提取每一个项目的URL,通过调用系统命令 git clone 的方法将项目克隆到本地
compile 而对于编译阶段,核心是对上一阶段克隆的项目进行编译处理,能够将项目打包成一个一个完成的jar包,以便于收集这些项目包使用静态分析工具进行漏洞检测任务。 通过前面项目收集过程中标注的该项目所采用的项目是基于Maven还是Gradle进行开发的,我们选择不同的系统命令进行Java项目的编译
经过我的全过程的测试,值得注意的是,在进行项目编译的过程中不仅仅需要动态的选择不同的编译命令进行Java项目的编译,在编译过程中其核心会使用 JAVA_HOME 这一环境变量所指向的JDK版本环境参与项目的编译过程,千人千面,不同的Java项目所能够支持的最低JDK版本不同,这里需要进行尝试性编译,也即是动态的调整JDK版本,按照从高到低的JDK版本对项目进行自动化编译,能够明显的降低仅采用同一种JDK版本进行编译而导致的编译失败几率。 在编译成功后会在对应目录中生成打包的Jar包,Maven项目默认的编译目录为 target ,而Gradle项目默认的编译目录为 build
scan 上一阶段仅仅是对克隆的项目进行了编译、打包Jar任务,对于多模块开发的Java项目,其生成的Jar包散落在各个文件夹下的 target 目录中,以便于静态工具进行扫描,我们首先需要将编译成功的Jars包进行收集整理到一起
通过上述代码可以根据规则提取生成的jar包
而对于核心的扫描任务,我们首先对Mscan进行改造,使得将其打包后可以动态的修改options.yml文件以便于指定待检测项目以及检测过程中产生文件的保存位置
通过以上代码能够对所有编译成功的项目执行静态分析任务 其检测结果保存在每一个项目名文件夹下的 microservice-taint-flows.txt 文件中
对于不存在Taint通路的项目其内容为空,在大量项目中筛选存在有通路的可以使用以下脚本输出可能存在漏洞的项目
对于最终的检测结果也算是有所收获
Conclusion 上文对Mscan针对微服务应用这一特定应用进行了建模,针对微服务应用中的各个服务间通过OpenFeign、Grpc、Kafka以及RabbitMQ等框架进行通信的方式构建了一个服务依赖图,用于表征数据流的传递路径,进一步的进行污点传播进行外部可控的Web漏洞检测。通过对类似于OpenFeign等框架的通信机制的分析,使用Tai-e插件系统提供的生命周期API构建调用边,对于一些其他未使用这类框架进行服务间通信的微服务应用可以采用类似的方式扩展的构建调用边以便于支持其漏洞检测任务。同时也对静态分析框架在完整流程的重要阶段过程进行了阐述,也即是Soot程序分析,以及指针分析算法的实现。最后也是基于静态分析框架为核心构建了一个 clone-compile-scan 全流程的workflow。