@intSheep师傅
0.前言
前段时间,我们介绍了以SSA形式进行静态代码行为分析方法,这种方法具备高效率、逆向追踪能力以及更高的可读性,这些特性是以AST形式为基础的代码分析所不能比的。因此,许多读者也对这种分析形式在Java静态代码审计中的应用表现充满期待,因为Java系统漏洞检测一直是安全从业人员热衷研究的领域。近期,我们尝试将Java语言从AST形式转化为SSA形式,并取得了一定成果。在本篇文章中,我们将分享在处理OOP(面向对象)语言时,AST转化为SSA的思路,并初步展示SSA形式的Java静态代码分析在实践中的表现力。
1.从无类语言到有类语言
在往期推文中,我们分享过SSA在JavaScript的实践,SSA形式给JavaScript的分析带来了新的可能性,对于爬虫可以说是一次重要革新。但是JavaScript本质上还是无类语言,其成功的经验无法照搬的Java上。
Java是很典型的面向对象编程范式语言,在Java代码世界中,随处可见都是“class”,这就意味着想要对Java进行SSA的转化就绕不开类这个概念。
其实早在1967年,第一个OOP语言Simula 67就出现了。不过,由于当时硬件性能低下,而OOP又过于先进,所以很长时间内,只有少部分研究机构在使用OOP。知道上世纪90年代,互联网热潮的出现,Java的出现让OOP逐渐让人们熟悉起来。
OOP的出现解决了以往结构化编程所不能解决的两大问题:全局变量问题和可重用性差问题。结构化语言,比如说C语言,子程序中传递的是临时变量,如果想要持久化保存信息就得放在子程序之外,保存全局变量。但是当需要修改全局变量的话,为了查清楚影响范围,就必须调查所有逻辑。同样的,结构化语言中的子程序虽然可重用,但是对于不断增大应用程序规模来说还是微不足道。
OOP之所以能解决这两大问题的在于其结构为“去除程序的冗余、进行整理的结构”。这就好比一个满是东西的房间,可以使用各种收纳架进行整理。类结构将紧密关联的子程序(函数)和全局变量汇总在一起,创建大粒度的软件构件。通过该结构,我们能够对之前分散的子程序和变量加以整理。
顺着这个思路,我们在将代码转化为SSA的时候,也能够拥有一个"收纳箱"对各种形式的数据进行聚合、整理。
1.1对象蓝图
对象蓝图就是我们对类各种数据的“收纳箱”,它是一种用于存储类信息的数据结构。它能够存储类名称、成员、方法等信息。
有了对象蓝图,就能够很简便的维护类与各类里面元素之间关系,同时也更加方便将Java的AST形式转化为SSA形式。
1.2是类不是类,这是个问题
有了对象蓝图,我们可以使用更符合我们思维的方式去将代码转化为SSA的形式。为什么说是更符合我们思维的方式呢?要知道,OOP编程范式是为了让开发者开发与维护软件更加容易,但是其实它与结构化语言编译后的结构是一样的。所以理论上,如果我们不考虑语法底层含义,不考虑我们直观思维感受,我们是能将OOP当作无类化语言进行处理的。
因此我们在处理的过程中,不会局限在"类"的思维框架下,而是综合考虑哪种方式处理起来更合理。比如说类的方法本质上和函数并无多大区别,方法其实也被称作做成员函数。因此我们处理类中的方法的时候可以摆脱限制,兼容原有的API,直接将方法当作函数进行处理。
不过值得注意的是,方法需要通过类被调用,因此我们转化后的函数也需要和类有一定联系。
比如下面一段Java代码,该代码为一个Main类包含两个方法:
Java |
将其转换为SSA形式,则变成两个函数,函数名则由类名加上方法名:
Java |
1.3引用变量
像上述例子中,直接将方法转化为函数还是不够完善的。该方式不支持引用变量,比如以下例子中,MyClass方法能通过引用变量this来获取x的值:
Java |
为了解决引用变量问题,我们会在成员方法转化为函数的时候,默认给函数一个this参数.上面代码将会被处理成以下形式:
Java |
1.4类的实例化
当我们解决完方法与引用变量的时候,我们就可以自然而然解决类的实例化问题。对类里面数据进行初始化有多种方式,但是最常用的是使用构造函数。而构造函数又可以处理成使用引用变量的方法,并在类实例化的时候调用这个方法,给对象蓝图里面的数据进行初始化。
比如以下这个例子:
Java |
该例的代码会被处理成以下形式:
Java |
可以看到构造函数会被当作函数A_A,该函数就会在类实例化的时候进行调用。
1.5枚举类型
Java中的枚举(Enum)类型是一种特殊的数据类型,用于存储固定的常量集。不过枚举类型里面的内容,并非普通的常量值,因此我们没办法简单使用一张表存储全局变量。为了解决枚举类型的问题,我们必须先明确枚举类型的本质是什么。
以下是一个枚举类型的例子:
Java |
当对该类进行编译后,jvm会将该枚举类型处理成类似下面代码的形式(为了方便说明,下面代码进行了删减):
Java |
我们可以看到,编译器其实是这样处理枚举类型的:
- 将枚举类型转化为一个继承Enum类的类,并且使用final修饰。
- 每个枚举实例创建一个类对象,并且使用public static final修饰。
- 生成一个静态代码块,用于初始化类对象。
这样子,我们思路就清晰了。处理一个枚举类型,只有创建一个对象蓝图,并且在对象蓝图中将每个枚举实例当作静态变量变量去进行维护,并使用static对每个枚举实例进行初始化就可以了。
2.数据流分析初尝试
以上遇到将Java转化为SSA格式过程中的问题只是一部分,事实上,还有许多有趣的问题碍于篇幅没办法展开。
接下来,我们初步看一下SSA格式在数据流分析中强大的功能吧!
下面是一段可能存在命令执行漏洞的代码:
Java |
2.1正向数据流追踪
从上图可以看出,变量dir数据从request.getParameter获取,并通过一个if语句,最终变Runtime.getRuntime.exec方法的参数。我们可以发现以SSA形式进行正向数据流分析是非常清晰的。
将以上代码进行正向分析(注意,以下代码是能够真实在yak runner运行的):
Go |
分析结果如下:
Java |
我们可以发现,dir数据最终作为runtime.exec方法的参数执行了。
2.2逆向数据流追踪
但是很多情况下,我们去进行代码审计,一般是从最底层的危险函数开始,然后一层层向上追踪。然而这种逆向追踪的技术对AST分析实在太不友好了。而SSA分析则能够很好的进行逆向数据追踪。
Go |
分析得到以下结果:
Java |
可以发现,逆向分析很清楚的呈现了exec执行参数的数据最终来自getParameter("dir")。
3.总结
SSA形式的代码分析所程序出来的高效性、清晰性以及可逆向追踪的能力,是以AST为基础的代码分析所不能及的。目前,我们逐步完善对有类语言如Java转化为SSA形式的过程,也期待着其未来能够有更好的表现!