Java层逆向分析方法和技巧

127 阅读8分钟

本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如有错漏,欢迎留言指正

Java层逆向分析方法和技巧

一、smali汇编

1. Dalvik字节码

  • java字节码、Dalvik字节码、机器码之间的关系?
    • 在Android上,Java代码首先经过编译器编译成Java字节码,然后再经过dx工具转换成Dalvik字节码,并打包到apk文件中(存储在DEX(Dalvik Executable)文件中),Dalvik字节码最终转换为机器码Android设备上运行。(因为大多数Android设备都是使用ARM处理器。因此,Android系统对应用程序的代码进行编译时,生成的是与ARM处理器兼容的机器码。)
    • 编译器编译Java代码成Java字节码是为了将Java代码转换成机器无关的中间代码,使得编译后的代码可以在任何支持JVM的系统上运行,不受机器的体系结构的限制。
    • Dalvik字节码是专门为Android系统优化的字节码,它比Java字节码更加高效。Dalvik字节码在一定程度上是Java字节码的一个子集
    • Java字节码和Dalvik字节码是两种不同的字节码格式,它们的区别主要体现在文件格式(java字节码文件扩展名为.class,Dalvik字节码文件扩展名为.dex。),存储方式(Java字节码在Java虚拟机中一次性加载,并在内存中存储;Dalvik字节码是以dalvik指令为单位存储的,仅加载需要的部分。),运行方式等方面(Java字节码直接在Java虚拟机中执行,Dalvik字节码在Dalvik虚拟机中执行,但需要经过dalvik指令集解释执行(Dalvik字节码->arm机器码)。)
  • Dalvik字节码和smali的关系?
    • Smali是一种对Dalvik字节码进行编码的人类可读的汇编语言,是对Dalvik指令的高级抽象。
    • Dalvik字节码 是以二进制形式存在于 .dex 文件中,而 smali 是以文本形式存在于磁盘文件中,是将 Dalvik字节码 反编译回文本文件的结果。
  • 为什么有了ART虚拟机,为什么还需要学Dalvik字节码?
    • Dalvik虚拟机是在启动应用程序时,将Dalvik字节码动态编译成机器码来实现运行的。所以对于每次打开一个应用程序,是需要进行编译的。
    • 在Android 5.0以后的版本,ART已经取代了Dalvik作为Android的默认虚拟机。
    • ART虚拟机机是在安装应用程序时,将Dalvik字节码预先编译成机器码来实现运行的。因此不再需要在每次启动应用程序时进行编译,提高运行速度。
    • 可以看到只是编译的时机不同而已。无论是Dalvik虚拟机还是ART虚拟机,dex文件是没有变的,即还是Dalvik字节码,反汇编得到Smali汇编,可以通过Smali汇编语言重写和修改应用程序的Dalvik字节码,以实现各种定制化效果和优化性能。

2. Smali汇编

寄存器命名方法:V与P

  • JAVA虚拟机使用了栈架构,Java字节码被执行时通过一个操作栈来进行解释,每个方法在运行时都有一个私有的操作栈。
  • Dalvik (ART)虚拟机基于寄存器架构,不是使用操作栈来执行字节码,而是使用寄存器,速度快,节省代码。
    • V命名法:所有变量(局部,参数都用V0,1,...命名)
    • P命名法:局部变量用V,参数用P,容易判断局部变量和参数

类型描述符

  • C 表示字符(char),用来存储 8 位字符,用一个32位的Dalvik寄存器来存储。
  • l 表示布尔型(boolean),用来存储 8 位布尔型,用一个32位的Dalvik寄存器来存储。
  • S 表示短整型(short),用来存储 16 位短整型,用一个32位的Dalvik寄存器来存储。
  • I 表示整型(int),用来存储 32 位有符号整数。用一个32位的Dalvik寄存器来存储。
  • J 表示长整型(long),用来存储 64 位有符号整数。用两个相邻的32位Dalvik寄存器来存储。
  • F 表示单精度浮点类型(float),用来存储 32 位单精度浮点数。用一个32位的Dalvik寄存器来存储。
  • D 表示双精度浮点类型(double),用来存储 64 位双精度浮点数。用两个相邻的32位Dalvik寄存器来存储。
  • v 表示无返回值的void类型
  • [ 表示数组类型,后面跟着一个 Smali 类型描述符,代表这个数组存储的数据类型。
    • [I表示int []
    • [II表示int[][],每多一维就加一个方括号,最多可以设置255维。
  • L +对象的全限定名表示对象类型(L``Package/Name/ObjectName;注意后面分号结束)
    • 比如String,其完整名称是java.lang.String,那么其全限定名就是java/lang/String;,即java.lang.String的”.”用”/”代替,并在末尾添加分号”;”做结束符.
    • 成员:Lcom/MyClass;``->``name:Ljava/lang/String;(`对象类型;->成员名:成员类型)``
    • 方法:LPackage/Name/objectName;``->``myFunc(III)Z(对象类型;->函数名:(参数类型)返回值)III表示3int参数
  • 函数:fun(Z [I [II Ljava/lang/String; J [Ljava/lang/Object;)Ljava/lang/String;(string fun(boolean, int[],int[][],String,long,Object[]))
    • 直接方法:static、构造、包含静态语句块(比如静态数组初始化)
    • 虚方法

指令

  • 数据定义指令
  • 数据操作指令
  • 实例操作指令
  • 数组操作指令
  • 比较指令
  • 跳转指令
  • 字段操作指令
  • 数据转换指令
  • 算术指令
  • 空指令锁指令
  • 异常指令
  • 方法调用指令
  • 返回指令

3. smali文件详解

# 文件头描述。<>中的内容表示必不可缺的,[]表示的是可选择的.
# 访问权限修饰符即所谓的public,protected,private即default.而非权限修饰符则指的是final,abstract.
#.class <访问权限修饰符> [非权限修饰符] <类名>  
#.super <父类名> 
#.source <源文件名称>
.class public Lcom/example/helloworld/HelloWorld; # .cLass 表示当前类,public 当前这个类的修饰符,类表示方式=L+包名+类名;
.super Ljava/lang/Object;  # .super 表示父类

# 在文件头之后便是文件的正文,即类的主体部分,包括类实现的接口描述,注解描述,字段描述和方法描述四部分.
# 接口描述
# .implements <接口名称>
# 注解描述
# .annotation [注解的属性] <注解类名> 
#     [注解字段=值] 
#     ... 
# .end
# 普通字段
# .field <访问权限修饰符> [非权限修饰符] <字段名>:<字段类型>
# 静态字段
# .field <访问权限> static [修饰词] <字段名>:<字段类型>
# 直接方法/虚方法
# .method <访问权限修饰符> [非访问权限修饰符] <方法原型> 
#    <.locals> 
#    [.parameter] 
#    [.prologue] 
#    [.line] 
#    <代码逻辑> 
# .end
.method public static main([Ljava/lang/String;)V  # 方法以 `.method`开始,以 `.end method` 结束
    .registers 4  # 声明总共需要使用4个寄存器.在Smali中,如果需要存储变量,必须先声明足够数量的寄存器。
	.parameter 1  # 参数使用寄存器个数是1  v开头的局部寄存器 p开头是参数寄存器
	.locals 3     # 局部使用寄存器个数是3  v开头的局部寄存器 p开头是参数寄存器.它用于声明非参数的寄存器个数(包含在registers声明的个数当中)
    .prologue     # 方法代码逻辑开始位置
    nop           # 空指令
 
	# 数据定义指令
	# const[/4、/16、/hight16] v1 xxx  # 将常量xxx赋值给v1寄存器,`/`后数字指的是指令操作数的长度,不写则默认是32位
	# const-wide[/16、/32、/hight16] v1 xxx	# 将双字型常量xxx赋值给v1寄存器,`/`后数字指的是指令操作数的长度,不写则默认是32位
	# const-string[/jumbo] v1 “aaa”	 # 将字符串常量”aaa”赋给v1寄存器,过长时需要加上/jumbo(字符串长度超过 65535 个字节时)
	# const-class v1 La/b/TargetClass # 将Class常量a.b.TargetClass赋值给v1,等价于a.b.TargetClass.class
	const/16 v0, 0x8 # 将16bit的常量0x8加载到32bit寄存器v0低16bit中,Smali中的寄存器长度是32位,没有16位的。这里的16"指的是指令操作数的长度,而不是寄存器的长度。
	const/4 v1, 0x5 
	const/4 v2, 0x3
	# 数据操作指令
	move v1, v2
	# 数组操作指令
	new-array v0, v0, [I # 创建一个v0长度的int数组,数组首地址存储在寄存器 v0 中
	array-length v1, v0 # 把数组的长度存储在寄存器 v1 中
	# 实例操作指令
	new-instance v1, Ljava/lang/StringBuilder; # new StringBuilder类的实例,地址存放在v1中
	# 方法调用指令 直接方法是针对特定对象的方法调用,它直接在目标对象上调用方法,并返回结果。
	invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V # 构造方法
	# 跳转指令
	if-nez v0, :cond_0
	goto :goto_0
	:cond_0
	# 数据转换指令
	int-to-float v2, v2
	# 数据运算指令
	add-float v2, v2, v2
	# 比较指令
	cmpl-float v0, v2, v2 # 比较v2的值与v2的值,并将结果存储在v0中。
	# 字段操作指令 
	sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; # v0 = System.out
	const-string v1, "Hello World" # 将字符串常量"Hello World"赋给v1寄存器
	# 方法调用指令 虚方法是一种动态调用方法的方式,它在运行时决定调用哪个方法。  v0.println(v1)
	invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V  # v0.println(v1)    即System.out.println(v1)
	# 返回指令
	:goto_0
    return-void
.end method

还原成java代码

更多内容请移步公众号:坚毅猿

mp.weixin.qq.com/s?__biz=MzU…