注释
1 |
类声明
1 |
方法声明
1 |
字段声明
1 |
字段取值赋值
1 |
方法调用
1 |
方法取值
1 |
乱码三千 – 码出一个新世界
Smali
语言以及逆向反编译有兴趣的同学有任意一门编程语言的同学均可学习, 有Java或者安卓基础最佳
安卓逆向必备基础知识 学完后快速掌握smali
语法
23课时 帮你快速入门Smali语言
从0到1快速入门 基础语法开始 循序渐进
内容清晰流畅 不废话不拖沓
配套工具 快速上手 一边学习一边实战
第一部分: Smali语言和Java语言的对比分析
第二部分: 类和各种方法以及属性的定义
第三部分: 寄存器的声明和定义
第四部分: 手写Smali代码打印输出
移动端讲师、作家、开发者、独立音乐人
《smali语言从门到精通》《安卓进阶之逆向安卓反编译从0到1》《硬件进阶之三大架构汇编语言入门》系列视频作者 九年移动开发经验 旨在将复杂的事情简单化
欢迎大家来学习 一块进步~
本文为作者原创 转载时请注明出处 谢谢
对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。
声明于方法内部(必须)
1 | .method public getName()V |
在一个方法(method)中有两中方式指定有多少个可用的寄存器。指令.registers指令指定了在这个方法中有多少个可用的寄存器,
指令.locals指明了在这个方法中非参(non-parameter)寄存器的数量。然而寄存器的总数也包括保存方法参数的寄存器。
例如,你写了一个非静态方法LMyObject;->callMe(II)V
。这个方法有2个int参数,但在这两个整型参数前面还有一个隐藏的参数LMyObject
;也就是当前对象的引用,所以这个方法总共有3个参数。
假如在一个方法中包含了五个寄存器(V0-V4),如下:
1 | .method public callMe(II)V |
那么只需用.register指令指定5个,或者使用.locals指令指定2个(2个local寄存器+3个参数寄存器)。如下:
1 | .method public callMe(II)V |
该方法被调用的时候,调用方法的对象(即this引用)会保存在V2中,第一个参数在V3中,第二个参数在v4中。
那么参数少了对象引用,除此之外和非静态原理相同,registers为4 locals依然是2
上面的例子中我们使用的是v命名法,也就是在本地寄存器后面依次添加参数寄存器,
但是这种命名方式存在一种问题:假如我后期想要修改方法体的内容,涉及到增加或者删除寄存器,由于v命名法需要排序的局限性,那么会造成大量代码的改动,有没有一种办法让我们只改动registers或者locals的值就可以了呢, 答案是:有的
除v命名法之外,还有一种命名法叫做p命名法
p命名法只能给方法参数命名,不能给本地变量命名
假如有一个非静态方法如下:
1 | .method public print(Ljava/lang/String;Ljava/lang/String;I)V |
以下是p命名法参数对应表:
p0 | this |
---|---|
p1 | 第一个参数Ljava/lang/String; |
p2 | 第二个参数Ljava/lang/String; |
p3 | 第三个参数I |
如前面提到的,long和double类型都是64位,需要2个寄存器。当你引用参数的时候一定要记住,例如:你有一个非静态方法
1 | LMyObject;->MyMethod(IJZ)V |
方法的参数为int、long、bool。所以这个方法的所有参数需要5个寄存器。
p0 | this |
---|---|
p1 | I |
p2, p3 | J |
p4 | Z |
另外当你调用方法后,你必须在寄存器列表,调用指令中指明,两个寄存器保存了double-wide宽度的参数。
注意:在默认的baksmali中,参数寄存器将使用P命名方式,如果出于某种原因你要禁用P命名方式,而要强制使用V命名方式,应当使用-p/–no-parameter-registers选项。
v0 | the first local register | |
---|---|---|
v1 | the second local register | |
v2 | p0 | the first parameter register |
v3 | p1 | the second parameter register |
v4 | p2 | the third parameter register |
1 | 字符串类型数据 |
1 | 第一种 const开头 占用一个容器(寄存器) 32位/容器 |
1 | const-string v0 , "hello"# 定义字符串 将字符串hello赋值给v0 |
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站
smali | java |
---|---|
B | byte |
S | short |
I | int |
J | long |
F | float |
D | double |
C | char |
Z | boolean |
V | void |
[ | 数组 |
L+全类名路径用/分割 | object |
在smali语言中注释使用#
表示
1 | # 我是注释 |
1 | .class +权限修饰符 +类名; |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
凡是L开头全包名路径结尾都需要加分号
1 | .field 权限修饰符+静态修饰符 +变量名:变量全类名路径; |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
补充:
1 | 基本数据类型示例: |
1 | .field 权限修饰符+静态修饰符 +final+变量名:变量全类名路径;=常量值 |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | .method 权限修饰符+静态修饰符 +方法名(参数类型)返回值类型 |
比如以下java代码:
1 |
|
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .method public getName(Ljava/lang/String;)Ljava/lang/String; |
1 | return-void |
smali方法返回关键字 | java |
---|---|
return | byte |
return | short |
return | int |
return-wide | long |
return | float |
return-wide | double |
return | char |
return | boolean |
return-void | void |
return-object | 数组 |
return-object | object |
1 | .method 权限修饰符 +constructor <init>(参数类型)返回值类型 |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | .method static +constructor <clinit>()V |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | invoke-virtual //用于非私有实例方法的调用 |
1 | invoke-virtual {参数}, 方法所属类名;->方法名(参数类型)返回值类型; |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | invoke-direct {参数}, 方法所属类名;->方法名(参数类型)返回值类型; |
私有方法调用:
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
构造方法调用:
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | invoke-static {参数}, 方法所属类名;->方法名(参数类型)返回值类型; |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
1 | invoke-super |
比如以下java代码
1 | @Override |
用smali代码表示为
1 | .method protected onCreate(Landroid/os/Bundle;)V |
1 | invoke-interface {参数}, 方法所属类名;->方法名(参数类型)返回值类型; |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
对象的创建分多步进行:
1 | # 声明实例 |
const/4 v0, 0x4
new-array v0, v0, [I
fill-array-data v0, :array_a
:array_a
.array-data 4 # 表示占用四个字节
0x0
0x1
0x2
0x3
.end array-data
1 | 字符串类型数据 |
1 | 第一种 const开头 占用一个容器(寄存器) 32位/容器 |
1 | const-string v0 , "hello"# 定义字符串 将字符串hello赋值给v0 |
1 | 1000 → -8; |
算法:正数的符号位是0,负数的符号位是1。正数的反码、补码与原码一样。负数的反码是让符号位不变,数据位按位取反;补码是将反码加1。
分多步进行 关键代码:
1 | sput-object # s代指static |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
分多步进行 关键代码:
1 | iput-object # i代表instance |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
关键代码
1 | sget-object # s代指static |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
关键代码:
1 | iget-object # i代表instance |
比如以下java代码:
1 | public class Test |
用smali代码表示为:
1 | .class public LTest;#声明类 (必须) |
值定义
1 | const/4 v0, 0x1 # 实例变量值内容定义 值皆为十六进制 |
取值:
1 | iget #实例变量int型取值 |
赋值
1 | iput #实例变量int型赋值 |
下表以实例变量举例:
smali取值赋值和值定义关键字 | java |
---|---|
iget-byte iput-byte const/4 |
byte |
iget-short iput-short const/4 |
short |
iget iput const/4 |
int |
iget-wide iput-wide const-wide/16 |
long |
iget- iput const/high16 |
float |
iget-wide- iput-wide const/high16 |
double |
iget-char- iput-char const/16 |
char |
iget-boolean- iput-boolean const/4 |
boolean |
#### 如果是基本数据类型,那么按照如下表处理: |
smali取值赋值和值定义关键字 | java |
---|---|
iget-object- iput-object new-array v0, v0, [数据类型签名 fill-array-data v0, :array_c |
数组 |
iget-object- iput-object 以下两步为类对象定义 new-instance v0, 全包名类路径; invoke-direct #调用构造方法 |
类和接口 |
iget-object- iput-object sget-object |
枚举 |
iget-object- iput-object const-string |
String |
以上表结果示例java代码如下,可自行试验:
1 | public class Test |
$$
$$
1 | public class Test |
1 | "if-eq vA, vB, :cond_**" 如果vA等于vB则跳转到:cond_** #equal |
比如以下java代码
1 | public class Test { |
对应的smali代码为:
1 | .method public static main([Ljava/lang/String;)V |
如果将int改成long, 结果又不一样,这里使用到了比较运算符cmp(comparable)
1 | .method public static main([Ljava/lang/String;)V |
表示与java源文件代码的映射关系,比如:
1 | .line 3 # 代表以下代码还原成java代码在源文件第三行 |
删除该关键字不影响程序执行,该关键字在反编译时能很好地帮助我们阅读smali代码,以该关键字当作代码块的分割线,方便快速阅读执行内容
条件分支,配合if使用
表示程序的开始 可省略
goto跳转分支,配合goto关键字使用
显示局部变量别名信息,作用等同.line
1 | move-result-object v0 # 调用方法后结果储存在v0中 |
注意这个和上面local的区别多加了一个s
标明了你在这个函数中最少要用到的本地寄存器的个数 也即是指明了在这个方法中非参(non-parameter)寄存器的数量
locals和registers具体区别参见:点击跳转
在Smali中,如果需要存储变量,必须先声明足够数量的寄存器,1个寄存器可以存储32位长度的类型,比如Int,而两个寄存器可以存储64位长度类型的数据,比如Long或Double
声明可使用的寄存器数量的方式为:.registers N
,N代表需要的寄存器的总个数
示例:
1 | .method private test(I)V |
那么,如何确定需要使用的寄存器的个数?
由于非static方法,需要占用一个寄存器以保存this指针,那么这类方法的寄存器个数,最低就为1,如果还需要处理传入的参数,则需要再次叠加,此时还需要考虑Double和Float这种需要占用两个寄存器的参数类型,举例来看:
如果一个Java方法声明如下:
1 | myMethod(int p1, float p2, boolean p3)1 |
那么对应的Smali则为:
1 | method LMyObject;->myMethod(IJZ)V1 |
此时,寄存器的对应情况如下:
寄存器名称 | 对应的引用 |
---|---|
p0 | this |
p1 | int型的p1参数 |
p2, p3 | float型的p2参数 |
p4 | boolean型的p3参数 |
那么最少需要的寄存器个数则为:5
如果方法体内含有常量、变量等定义,则需要根据情况增加寄存器个数,数量只要满足需求,保证需要获取的值不被后面的赋值冲掉即可,方法有:存入类中的字段中(存入后,寄存器可被重新赋值),或者长期占用一个寄存器
寄存器数量只能多不能少
如果需要使用Smali编写程序,还需要掌握常用的Dalvik虚拟机指令,其合集称为Dalvik指令集。这些指令有点类似x86汇编的指令,但指令更多,使用也非常简单方便。最详尽的介绍,可以参考Android官方的Dalvik相关文档:
https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions
一般的指令格式为:[op]-[type](可选)/[位宽,默认4位] [目标寄存器],[源寄存器](可选)
,比如:move v1,v2
,move-wide/from16 v1,v2
这里也列举一些常用的指令,并结合Smali进行说明:
此类操作常用于赋值
指令 | 说明 |
---|---|
move v1,v2 | 将v2中的值移入到v1寄存器中(4位,支持int型) |
move/from16 v1,v2 | 将16位的v2寄存器中的值移入到8位的v1寄存器中 |
move/16 v1,v2 | 将16位的v2寄存器中的值移入到16位的v1寄存器中 |
move-wide v1,v2 | 将寄存器对(一组,用于支持双字型)v2中的值移入到v1寄存器对中(4位,猜测支持float、double型) |
move-wide/from16 v1,v2 | 将16位的v2寄存器对(一组)中的值移入到8位的v1寄存器中 |
move-wide/16 v1,v2 | 将16位的v2寄存器对(一组)中的值移入到16位的v1寄存器中 |
move-object v1,v2 | 将v2中的对象指针移入到v1寄存器中 |
move-object/from16 v1,v2 | 将16位的v2寄存器中的对象指针移入到v1(8位)寄存器中 |
move-object/16 v1,v2 | 将16位的v2寄存器中的对象指针移入到v1(16位)寄存器中 |
move-result v1 | 将这个指令的上一条指令计算结果,移入到v1寄存器中(需要配合invoke-static、invoke-virtual等指令使用) |
move-result-object v1 | 将上条计算结果的对象指针移入v1寄存器 |
move-result-wide v1 | 将上条计算结果(双字)的对象指针移入v1寄存器 |
move-exception v1 | 将异常移入v1寄存器,用于捕获try-catch语句中的异常 |
用于返回值,对应Java中的return语句
指令 | 说明 |
---|---|
return-void | 返回void,即直接返回 |
return v1 | 返回v1寄存器中的值 |
return-object v1 | 返回v1寄存器中的对象指针 |
return-wide v1 | 返回双字型结果给v1寄存器 |
android studio自带的.class转smali就非常好用,如图:
1 | SLog.smali[24,4] Invalid register: v-1. Must be between v0 and v15, inclusive. |
寄存器命名从v0-v15 一共15个
1 | SLog.smali[17,0] A .registers or .locals directive must be present for a non-abstract/non-final method |
.registers或者.locals必须存在, 除非是抽象方法或者final方法
1 | java.lang.VerifyError: Rejecting class com.pangshu.SLog because it failed compile-time verification (declaration of 'com.pangshu.SLog' appears in /sdcard/ex.dex) |
这种错误一般很难定位,因为没有提示具体原因或者具体的行数,有可能是静态方法调用你写成了虚方法的调用,或者是构造函数调用没有加尖括号, 甚至是寄存器数量过少 等等
为什么方法中包括参数在内需要3个寄存器,但是在定义的时候只写了两个却也不报错呢?
如:
1 | .method public static print(Ljava/lang/String;)V |
答案是:系统会更具最大寄存器的位置进行判断,从v0到vN,数量必须大于N,
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站
进制转换属于计算机基础,虽然是基础,但是想要熟练计算,需要花点时间
1 | 0101001001001101 //转成十进制值是2^0+2^2+2^3+2^6+2^9+2^12+2^14=21069 |
从低到高每四位进行分割,也就是:
1 | 0101 0010 0100 1101 //结果 524D |
从低到高每三位进行分割,也就是:
1 | 0 101 001 001 001 101 |
不足三位的补0, 也就是:
1 | 000 101 001 001 001 101 结果051115 |
二进制每四位(1111)最大值是15, 按照四位分割转成十六进制
二进制每三位(111)最大值是7,按照三位分割转八进制
那么延伸:
二进制每两位(11)最大值是3,按照两位分割转成四进制
二进制每五位(11111)最大值是31,按照五位分割转成三十二进制
二进制每六位(111111)最大值是63,按照六位分割转成六十四进制
二进制每七位(1111111)最大值是127,按照七位分割转成一百二十八进制
……..
十六进制和二进制一一对应关系表:
十六进制 | 二进制 |
---|---|
1 | 0001 |
2 | 0010 |
3 | 0011 |
4 | 0100 |
5 | 0101 |
6 | 0110 |
7 | 0111 |
8 | 1000 |
9 | 1001 |
A | 1010 |
B | 1011 |
C | 1100 |
D | 1101 |
E | 1110 |
F | 1111 |
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站
该压缩包为《Android进阶之逆向安全反编译视频教程-胖薯出品》的配套工具包
该压缩包包含以下工具:
本文为作者原创 转载时请注明出处 谢谢
mov ax, ffffh
应该写成 mov ax, 0ffffh
否则编译报错## BX存在的意义
1. asm编译器无法识别中括号,mov ax,[0]
编译时会默认去除中括号.使用bx替代可以解决这个问题,如果在debug模式下使用-a命令输入中括号则没有问题,可以正常识别
1 | ;错误写法:编译器无法识别[0],会直接取值为0 而不是偏移地址0所对应的内容 |
问题: 那如果非要以带中括号的方式进行编写可以吗? 答案是:需要带上段地址ds,如下:
1 | assumme cs:code |
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站
在搭建好了8086汇编的开发环境后,接下来介绍8086的debug模式。执行debug.exe以进入debug调试模式,在dos中通过输入命令的方式进行交互
## -R命令
R命令的作用是查看和修改debug模式下CPU中寄存器的值
## -D命令
D命令的作用是查看内存中的内容
上面为(段地址:偏移地址)查看方式。D命令默认会显示寻址地址开始的后128个内存单元的内容,以16进制的方式显示(每个内存单元8位,一行最多16个内存单元),而最右边会将内存单元中的二进制数据以ascll码的形式翻译展示
但有时,我们只想聚焦于某一部分内存地址的内容,而默认展示的内存视图不是很方便。
D命令提供了另外一种访问内存的方式(段地址:偏移起始地址 偏移终止地址),其能够展示(段地址:偏移起始地址 至 段地址:偏移终止地址)的内存信息,范围两端均为闭区间
E命令的作用是改变内存中的内容。
和对CPU中寄存器的查看,修改不同,对内存进行查看和修改较为复杂,为此debug设计了两个不同的命令分别进行控制(E命令修改内存、D命令查看内存)。
通过(E 起始地址 数据1 数据2 数据3…)命令可以修改内存中以起始地址开始,顺序的N个内存单元的值(N为实际参数传递的数量)
也可以和R命令修改CPU中寄存器值类似的,通过提示来修改特定内存单元的值。00.12 00代表内存单元在修改前的值,12是我们手动输入的、需要修改的新值
U命令的作用是将内存中的二进制数据转换为汇编指令展示(反汇编)
D命令能够将内存中的数据以16进制或ascll码的形式展现出来,但有时我们需要观察的是内存中的机器指令时,D命令的视图过于抽象,不利于理解。debug提供了U命令来解决这个问题。
对于前面我们在1000:0处输入的机器指令,使用 U 1000:0 命令(u 内存地址)可以将内存中的数据以汇编语言指令的方式进行展示
A命令能够以汇编指令的形式向内存中写入内容
对于内存操作,D命令可以查看内存中的内容,但如果想查看的是程序指令,显然U命令更加方便;E命令可以向内存中写入数据,但对于程序指令的写入,直接操作二进制机器码的方式过于硬核。为此,debug提供了A命令,我们可以通过A命令以汇编指令的形式向内存中写入内容。
通过A命令将(mov ax,0001,mov bx,0002,add ax,bx)三条指令写入内存1000:0处:
通过A命令进行指令的写入,和E命令达到的效果一样,但使用起来却更加便捷。A命令能够自动识别所输入汇编指令的长度,正确的在内存中写入程序指令。
debug提供了D、E两种命令用于对内存进行通用的操作(纯二进制、十六进制数据的读、写)。
对于程序指令,debug提供了U、A两种命令以更人性化的方式来读写内存中的指令内容
T命令的作用是进行单步机器指令的调试
G命令的作用是进行Debug程序断点调试
1 | -g 代码地址(cs+ip)地址 |
p命令的作用是断点跳过执行 ,可用于循环调试
本文为作者原创 转载时请注明出处 谢谢
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站