对Java进行SSA化数据流追踪初体验

本文探讨了如何将Java的面向对象代码从AST形式转化为SSA形式,以增强静态代码分析的效率和可逆向追踪能力,重点涉及类、对象蓝图、数据流分析的应用和实例解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

@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
class Act{
    public static void Sing(){
    }
    public static void Dance(){
    }
}

将其转换为SSA形式,则变成两个函数,函数名则由类名加上方法名:

Java
function:
func Act_Sing(){
}
function:
func Act_Dance(){
}

1.3引用变量

像上述例子中,直接将方法转化为函数还是不够完善的。该方式不支持引用变量,比如以下例子中,MyClass方法能通过引用变量this来获取x的值:

Java
public class A {
    private int a =1 ;
    public void setA(int a) {
         this.a = a;
    }
}

为了解决引用变量问题,我们会在成员方法转化为函数的时候,默认给函数一个this参数.上面代码将会被处理成以下形式:

Java
class:
        class A{
                member:
                        "a": 1
                method:
                        A_setA
        }
function:
func A_setA(this, this.a,a){
         this.a=a
}

1.4类的实例化

当我们解决完方法与引用变量的时候,我们就可以自然而然解决类的实例化问题。对类里面数据进行初始化有多种方式,但是最常用的是使用构造函数。而构造函数又可以处理成使用引用变量的方法,并在类实例化的时候调用这个方法,给对象蓝图里面的数据进行初始化。

比如以下这个例子:

Java
public class A {
    private int a =1 ;
      Public A (int a) {
         this.a = a;
    }
}

该例的代码会被处理成以下形式:

Java
class:
        class A{
                member:
                        "a": 1
                method:
                        A_setA
        }
function:
func A_A(this, this.a,a){
         this.a=a
}

可以看到构造函数会被当作函数A_A,该函数就会在类实例化的时候进行调用。

1.5枚举类型

Java中的枚举(Enum)类型是一种特殊的数据类型,用于存储固定的常量集。不过枚举类型里面的内容,并非普通的常量值,因此我们没办法简单使用一张表存储全局变量。为了解决枚举类型的问题,我们必须先明确枚举类型的本质是什么。

以下是一个枚举类型的例子:

Java
public enum EnumDemo {
    A(1),B(2),C(3);
    int num;
    EnumDemo(int num){
        this.num = num;
    }
}

当对该类进行编译后,jvm会将该枚举类型处理成类似下面代码的形式(为了方便说明,下面代码进行了删减):

Java
public final class EnumDemo extends Enum{
    ...
    private Fruit(String s, int i, int j)
    {
        super(s, i);
        num = j;
    }
    public static final EnumDemo A;
    public static final EnumDemo B;
    public static final EnumDemo C;
    static {
        A=new EnumDemo("A",0,1);
        B=new EnumDemo("B",1,2);
        C=new EnumDemo("C",2,3);
    }
}

我们可以看到,编译器其实是这样处理枚举类型的:

  • 将枚举类型转化为一个继承Enum类的类,并且使用final修饰。
  • 每个枚举实例创建一个类对象,并且使用public static final修饰。
  • 生成一个静态代码块,用于初始化类对象。

这样子,我们思路就清晰了。处理一个枚举类型,只有创建一个对象蓝图,并且在对象蓝图中将每个枚举实例当作静态变量变量去进行维护,并使用static对每个枚举实例进行初始化就可以了。

2.数据流分析初尝试

以上遇到将Java转化为SSA格式过程中的问题只是一部分,事实上,还有许多有趣的问题碍于篇幅没办法展开。

接下来,我们初步看一下SSA格式在数据流分析中强大的功能吧!

下面是一段可能存在命令执行漏洞的代码:

Java
package org.example;

public class Main {
   public static void main(String[] args) {
             String dir=request.getParameter("dir");
       if(dir.length < 1 ){
          dir = "tmp";
       }else {
          dir = "tmp" + dir;
       }
       Runtime runtime=Runtime.getRuntime();
       Process process=runtime.exec(dir);
       int result=process.waitFor();
   }
}

2.1正向数据流追踪

从上图可以看出,变量dir数据从request.getParameter获取,并通过一个if语句,最终变Runtime.getRuntime.exec方法的参数。我们可以发现以SSA形式进行正向数据流分析是非常清晰的。

将以上代码进行正向分析(注意,以下代码是能够真实在yak runner运行的):

Go
code := `
    package org.example;

public class Main {
   public static void main(String[] args) {
             String dir=request.getParameter("dir");
       if(dir.length < 1 ){
          dir = "tmp";
       }else {
          dir = "tmp" + dir;
       }
       Runtime runtime=Runtime.getRuntime();
       Process process=runtime.exec(dir);
       int result=process.waitFor();
     
   }
}`
prog:= ssa.Parse(code,
    ssa.withLanguage(ssa.Java))~ // 将在Yaklang v1.3.1-sp8及以后版本支持
prog.Ref("request").Ref("getParameter").GetBottomUses().ShowWithSource()//获取=request.getParameter方法获取的内容的底层应用

分析结果如下:

Java
Values: 1
        0: [Call  ] Undefined-runtime.exec(valid)(phi(dir)["tmp",add("tmp", Undefined-request.getParameter(valid)("dir"))])        13:26 - 13:35: exec(dir)

我们可以发现,dir数据最终作为runtime.exec方法的参数执行了。

2.2逆向数据流追踪

但是很多情况下,我们去进行代码审计,一般是从最底层的危险函数开始,然后一层层向上追踪。然而这种逆向追踪的技术对AST分析实在太不友好了。而SSA分析则能够很好的进行逆向数据追踪。

Go
code := `
    package org.example;

public class Main {
   public static void main(String[] args) {
             String dir=request.getParameter("dir");
       if(dir.length < 1 ){
          dir = "tmp";
       }else {
          dir = "tmp" + dir;
       }
       Runtime runtime=Runtime.getRuntime();
       Process process=runtime.exec(dir);
       int result=process.waitFor();
     
   }
}`
prog:= ssa.Parse(code,
    ssa.withLanguage(ssa.Java))~ // 将在Yaklang v1.3.1-sp8及以后版本支持
runtime := prog.Ref("Runtime").Ref("getRuntime")[0].GetCalledBy() //获取Runtime.getRuntime方法被调用的位置,得到runtime(代码第12行)
exec := runtime.Ref("exec")[0]//获取runtime.exec方法调用的位置,即process(代码第13行)
args := exec.GetCalledBy()[0].GetCallArgs()//获取runtime.exec的参数,即dir
for _, arg := range args {
    arg.GetTopDefs().ShowWithSource()//获取dir的顶层定义
}

分析得到以下结果:

Java
Values: 5
        0: [ConstInst] "tmp"        8:9 - 8:14: "tmp"
        1: [ConstInst] "tmp"        10:9 - 10:14: "tmp"
        2: [Undefined] Undefined-request        6:17 - 6:24: request
        3: [Undefined] Undefined-request.getParameter(valid)        6:25 - 6:44: getParameter("dir")
        4: [ConstInst] "dir"        6:38 - 6:43: "dir"

可以发现,逆向分析很清楚的呈现了exec执行参数的数据最终来自getParameter("dir")。

3.总结

SSA形式的代码分析所程序出来的高效性、清晰性以及可逆向追踪的能力,是以AST为基础的代码分析所不能及的。目前,我们逐步完善对有类语言如Java转化为SSA形式的过程,也期待着其未来能够有更好的表现!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值