是日,北风萧萧,天寒地冻...
超级牛打开了他的秘籍宝典
“请选择你的功法——”
《渗透测试高级技巧》
《SYN扫描》
《Java反编译》
...
那么,Java反编译,启动!
控制流图(control-flow graph)简称CFG,是计算机科学中的表示法,利用数学中图的表示方式,标示计算机程序执行过程中所经过的所有路径。控制流图中的每个顶点都对应一个基本块,也就是一段没有分支指令。
下面是两段python代码和对应的CFG
if x > 0:
print(1)
else
print(0)
while x > 0:
print(1)
x-=1
通过CFG可以清晰的看到程序的所有执行路径,有助于后续分析。
支配:在控制流图(CFG)中,如果从入口节点到达基本块 N 的所有路径都必须经过基本块 M,则称 M 支配 N,记作 M dom N
DFN序:在深度优先搜索(DFS)生成树中,节点的访问顺序被称为 DFN 序。根据 DFN 序的定义,如果 dfn(A)<dfn(B),则在生成树上,节点 A 是节点 B 的祖先。
后退边:在对程序的 CFG 进行 DFS 时,生成的 DFS 树中,如果存在一条边 A->B,且 B 是 A 的祖先,则称该边为后退边。
回边:如果有一条边 A->B,且 B dom A,则称该边为回边。回边一定是后退边,但后退边不一定是回边。
对于Java代码,执行顺序是自上而下,如果将每一条语句视为执行单元,那也就不存在回边,java语句中可能导致回边的语句有循环语句、break、continue语句。
根据推论:如果程序中不存在从循环外跳到循环内的goto语句,那么这个程序的控制流图就是可规约的。可以得出,Java的控制流图是可规约的,而对于可规约图,回边集合和后退边集合相同。由此可以推理得到:
如果存在一条边 A->B,且dfn(B)<dfn(A),那么这条边就是回边。
由于Java的CFG是可规约的,所以图中的循环也一定是自然循环。所有循环都至少具有一条回边,回边的目标节点就是循环头。所以只要找出所有回边就可以分析出相应的循环语句。
直接支配节点:在控制流图中,节点 A 支配的除自身外最近的节点称为 A 的直接支配节点,通常记作 idom(A)。
通过直接配置节点构建出来的树就是支配树。
上述两个案例的支配树如下:
支配树可以展现出程序的所有代码执行路径和支配关系,清晰看出程序结构,便于后续分析。
反编译过程中首要目标是将字节码解析恢复为等价的源码,对于一些表达式的简化,语法糖的优化可以放到后面处理。那主要任务就是将CFG规约为一条线性的链表形式。
一个class 的构成包括:class的描述、成员、函数。其中的函数的code属性就是函数体编译后的字节码。反编译过程主要任务就是解析函数的code属性。
code属性解析出来后是一个线性的指令列表、每个元素就是指令+操作数,如
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field lock:Ljava/lang/Object;
15: return
JVM是基于栈基实现的,所以在运行时存在一个操作数栈,用于储存临时数据。例如计算a=1+1的字节码:
0: iconst_1 // 将1存到操作数栈
1: iconst_1 // 将1存到操作数栈
2: iadd // 从栈中取出两个integer类型值,进行加法运算后将结果push到栈
3: istore_1 // 从栈中取出值存到局部变量表中
在jvm执行时,会为每个栈帧开辟一个操作数栈和局部变量表。这个变量表是一个线性列表,可以通过索引访问,这个变量表是忽略变量类型的。
如0号槽位可以存引用,也可以存int类型变量。在jvm执行时不存在作用域的概念,可以理解为所有变量在同一作用域。
对变量a赋值等价于存到变量表n号位,对变量a取值等价于从n号位取值。对于生命周期结束的变量,变量表会用新变量直接覆盖。由此可以推出,局部变量表中的变量与存活的变量是一一对应的。
恢复类型
由于字节码不存在声明指令,需要推理变量声明操作,可以通过下面算法找出声明操作集合:
恢复变量名
按照声明顺序将变量名设置为var0、var1......
下面是一段if语句的字节码
79: iload 9
81: ifeq 94
84: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
87: iconst_1
88: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
91: goto 101
94: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
97: iconst_2
98: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
101: return
ifeq是条件跳转语句,goto是跳转语句,对于jvm在运行时直接跳转到相应代码执行。仔细读一下代码可以勉强理解:加载一个变量,如果它的值是true那就执行94号之后的代码,否则执行84号之后的代码,到91号时再跳转到101号,画个图直观看一下
翻译成现代编程语言应该是这样
if var0 {
getstatic
iconst_1
...
}else{
getstatic
iconst_2
invokevirtual
}
return
反编译过程就是将这个思路转换为代码,大概流程是通过代码语言对字节码列表进行约束,如:
-
存在if指令
-
if指令下存在goto指令p1跳转到p2
-
if指令跳转到目标地址为p3,那么p3小于等于p2
符合上述条件的语句就可以翻译为if语句,其中if指令到p1之间为if body,p3到p2之间是else body。
但这种方案抗干扰性比较弱,需要较多的限制条件,如当存在break、continue语句时,还需要特殊处理。
另一种方案是基于图2进行描述:
-
存在if指令
-
if节点有两个后继基本块
-
两个后继基本块具有汇合点
对于下面这种不存在else body的情况,也同样可以描述
第二种描述方法显然描述的更准确,且更抗干扰。所以后面实现都是基于图的。
java字节码的入口就是字节码序列的第一条指令,而出口是return指令,但return指令可以有多个,导致结束节点不统一。所以为了方便分析,可以添加一条虚拟指令end,让所有return指令指向end指令。
本篇作为Java反编译系列的开篇,介绍了一些背景知识和实现思路,明确了后面对于变量、控制流的分析方法。后面会更新针对每种语句的具体分析方法。
END
YAK官方资源
Yak 语言官方教程:
Yak:致力于安全能力融合的语言 | Yak Program Language
Yakit 视频教程:
YakProject的个人空间-YakProject个人主页-哔哩哔哩视频
Github下载地址:
GitHub - yaklang/yakit: Cyber Security ALL-IN-ONE Platform
GitHub - yaklang/yaklang: A programming language exclusively designed for cybersecurity
Yakit官网下载地址:
https://yaklang.com/
Yakit安装文档:
下载安装与更新配置 | Yak Program Language
Yakit使用文档:
Yakit: 集成化单兵安全能力平台 | Yak Program Language
常见问题速查:
FAQ | Yak Program Language