├── 1.md ├── 2.md ├── 3.md ├── 4.1.md ├── 4.2.md └── 4.3.md /1.md: -------------------------------------------------------------------------------- 1 | # 安卓逆向系列教程(一)Dalvik 指令集 2 | 3 | > 作者:[飞龙](https://github.com/wizardforcel) 4 | 5 | ## 寄存器 6 | 7 | Dalvik 指令集完全基于寄存器,也就是说,没有栈。 8 | 9 | 所有寄存器都是 32 位,无类型的。也就是说,虽然编译器会为每个局部变量分配一个寄存器,但是理论上一个寄存器中可以存放一个`int`,之后存放一个`String`(的引用),之后再存放一个别的东西。 10 | 11 | 如果要处理 64 位的值,需要连续的两个寄存器,但是代码中仍然只写一个寄存器。这种情况下,你在代码中看到的`vx`实际上是指`vx`和`vx + 1`。 12 | 13 | 寄存器有两种命名方法。`v`命名法简单直接。假设一共分配了 10 个寄存器,那么我们可以用`v0`到`v9`来命名它们。 14 | 15 | ![](http://img.blog.csdn.net/20160731141440713) 16 | 17 | 除此之外,还可以用`p`命名法来命名参数所用的寄存器,参数会占用后面的几个寄存器。假如上面那个方法是共有两个参数的静态方法,那么,我们就可以使用`p0`和`p1`取代`v8`和`v9`。如果是实例方法,那么可以用`p0 ~ p2`取代`v7 ~ v9`,其中`p0`是`this`引用。 18 | 19 | ![](http://img.blog.csdn.net/20160731142057192) 20 | 21 | 但在实际的代码中,一般不会声明所有寄存器的数量,而是直接声明局部变量所用的寄存器(后面会看到)。也就是说局部变量和参数的寄存器是分开声明的。我们无需关心`vx`是不是`py`,只需知道所有寄存器的数量是局部变量与参数数量的和。 22 | 23 | ## 数据类型 24 | 25 | Dalvik 拥有独特的数据类型表示方法,并且和 Java 类型一一对应: 26 | 27 | | Java 类型 | Dalvik 表示 | 28 | | --- | --- | 29 | | `boolean` | Z | 30 | | `byte` | B | 31 | | `short` | S | 32 | | `char` | C | 33 | | `int` | I | 34 | | `long` | J | 35 | | `float` | F | 36 | | `double` | D | 37 | | `void` | V | 38 | | 对象类型 | L | 39 | | 数组类型 | [ | 40 | 41 | 其中对象类型由`L<包名>/<类名>;`(完全限定名称)表示,要注意末尾有个分号,比如`String`表示为`Ljava/lang/String;`。 42 | 43 | 数组类型是`[`加上元素类型,比如`int[]`表示为`[I`。左方括号的个数也就是数组的维数,比如`int[][]`表示为`[[I`。 44 | 45 | ## 类定义 46 | 47 | 一个 smali 文件中存放一个类,文件开头保存类的各种信息。类的定义是这样的。 48 | 49 | ``` 50 | .class <权限修饰符> <非权限修饰符> <完全限定名称> 51 | .super <超类的完全限定名称> 52 | .source <源文件名> 53 | ``` 54 | 55 | 比如这是某个`MainActivity`: 56 | 57 | ``` 58 | .class public Lnet/flygon/myapplication/MainActivity; 59 | .super Landroid/app/Activity; 60 | .source "MainActivity.java" 61 | ``` 62 | 63 | 我们可以看到该类是`public`的,完整名称是`net.flygon.myapplication.MainActivity`,继承了`android.app.Activity`,在源码中是`MainActivity.java`。如果类是`abstract`或者`final`的,会在`public/private/protected`后面表示。 64 | 65 | 类可以实现接口,如果类实现了接口,那么这三条语句下面会出现`.implements <接口的完全限定名称>`。比如通常用于回调的匿名类中会出现`.implements Landroid/view/View$OnClickListener;`。 66 | 67 | 类还可以拥有注解,同样,这三条语句下方出现这样的代码: 68 | 69 | ``` 70 | .annotation <完全限定名称> 71 | 键 = 值 72 | ... 73 | .end annotation 74 | ``` 75 | 76 | 这些语句下面就是类拥有的字段和方法。 77 | 78 | ## 字段定义 79 | 80 | 字段定义如下: 81 | 82 | ``` 83 | .field <权限修饰符> <非权限修饰符> <名称>:<类型> 84 | ``` 85 | 86 | 其中非权限修饰符可以为`final`或者`abstract`。 87 | 88 | 比如我在`MainActivity`中定义一个按钮: 89 | 90 | 91 | ``` 92 | .field private button1:Landroid/widget/Button; 93 | ``` 94 | 95 | ## 方法定义 96 | 97 | 方法定义如下: 98 | 99 | ``` 100 | .method <权限修饰符> <非权限修饰符> <名称>(<参数类型>)<返回值类型> 101 | ... 102 | .end method 103 | ``` 104 | 105 | 要注意如果有多个参数,参数之间是紧密挨着的,没有逗号也没有空格。如果某个方法的参数是`int, int, String`,那么应该表示为`IILjava/lang/String;`。 106 | 107 | ### `.locals` 108 | 109 | 方法里面可以包含很多很多东西,可以说是反编译的重点。首先,方法开头处可能会含有局部变量个数声明和参数声明。`.locals <个数>`可以用于变量个数声明,比如声明了`.locals 10`之后,我们就可以直接使用`v0`到`v9`的寄存器。 110 | 111 | ### `.param` 112 | 113 | 另外,参数虽然也占用寄存器,但是声明是不在一起的。`.param px,"<名称>"`用于声明参数。不知道是不是必需的。 114 | 115 | ### `.prologue` 116 | 117 | 之后`.prologue`的下面是方法中的代码。代码是接下来要讲的东西。 118 | 119 | ### `.line` 120 | 121 | 代码之间可能会出现`.line <行号>`,用来标识 Java 代码中对应的行,不过这个是非强制性的,修改之后对应不上也无所谓。 122 | 123 | ### `.local` 124 | 125 | 还可能出现局部变量声明,`.local vx, "<名称>":<类型>`。这个也是非强制性的,只是为了让你清楚哪些是具名变量,哪些是临时变量。临时变量没有这种声明,照样正常工作。甚至你把它改成不匹配的类型(`int`改成`Object`),也可以正常运行。 126 | 127 | ## 数据定义 128 | 129 | | 指令 | 含义 | 130 | | --- | --- | 131 | | const/4 vx,lit4 | 将 4 位字面值`lit4`(扩展为 32 位)存入`vx` | 132 | | const/16 vx,lit16 | 将 16 位字面值`lit16`(扩展为 32 位)存入`vx` | 133 | | const vx, lit32 | 将 32 位字面值`lit32`存入`vx` | 134 | | const-wide/16 vx, lit16 | 将 16 位字面值`lit16`(扩展为 64 位)存入`vx`及`vx + 1` | 135 | | const-wide/32 vx, lit32 | 将 32 位字面值`lit32`(扩展为 64 位)存入`vx`及`vx + 1` | 136 | | const-wide vx, lit64 | 将 64 位字面值`lit64`存入`vx`及`vx + 1` | 137 | | const/high16 v0, lit16 | 将 16 位字面值`lit16`存入`vx`的高位 | 138 | | const-wide/high16, lit16 | 将 16 位字面值`lit16`存入`vx`和`vx + 1`的高位 | 139 | | const-string vx, string | 将指字符串常量(的引用)`string`存入`vx` | 140 | | const-class vx, class | 将指向类对象(的引用)`class`存入`vx` | 141 | 142 | 这些指令会在我们给变量赋字面值的时候用到。下面我们来看看这些指令如何与 Java 代码对应,以下我定义了所有相关类型的变量。 143 | 144 | ```java 145 | boolean z = true; 146 | z = false; 147 | byte b = 1; 148 | short s = 2; 149 | int i = 3; 150 | long l = 4; 151 | float f = 0.1f; 152 | double d = 0.2; 153 | String str = "test"; 154 | Class c = Object.class; 155 | ``` 156 | 157 | 编译之后的代码可能是这样: 158 | 159 | ``` 160 | const/4 v10, 0x1 161 | const/4 v10, 0x0 162 | const/4 v0, 0x1 163 | const/4 v8, 0x2 164 | const/4 v5, 0x3 165 | const-wide/16 v6, 0x4 166 | const v4, 0x3dcccccd # 0.1f 167 | const-wide v2, 0x3fc999999999999aL # 0.2 168 | const-string v9, "test" 169 | const-class v1, Ljava/lang/Object; 170 | ``` 171 | 172 | 我们可以看到,`boolean`、`byte`、`short`、`int`都是使用`const`系列指令来加载的。我们在这里为其赋了比较小的值,所以它用了`const/4`。如果我们选择一个更大的值,编译器会采用`const/16`或者`const`指令。然后我们可以看到`const-wide/16`用于为`long`赋值,说明`const-wide`系列指令用于处理`long`。 173 | 174 | 接下来,`float`使用`const`指令处理,`double`使用`const-wide`指令处理。以`float`为例,它的`const`语句的字面值是`0x3dcccccd`,比较费解。实际上它是保持二进制数据不变,将其表示为`int`得到的。 175 | 176 | 我们可以用这段 c 代码来验证。 177 | 178 | ```c 179 | int main() { 180 | int i = 0x3dcccccd; 181 | float f = *(float *)&i; 182 | printf("%f", f); 183 | return 0; 184 | } 185 | ``` 186 | 187 | 结果是`0.100000`,的确是我们当初赋值的 0.1。 188 | 189 | 最后,`const-string`用于加载字符串,`const-class`用于加载类对象。虽然文档中写着“字符串的 ID”,但实际的反编译代码中是字符串字面值,比较方便。对于类对象来说,代码中出现的是完全先定名称。 190 | 191 | ## 数据移动 192 | 193 | 数据移动指令就是大名鼎鼎的`move`: 194 | 195 | | 指令 | 含义 | 196 | | --- | --- | 197 | | move vx,vy | `vx = vy` | 198 | | move/from16 vx,vy | `vx = vy` | 199 | | move/16 vx,vy | `vx = vy` | 200 | | move-wide vx,vy | `vx, vx + 1 = vy, vy + 1` | 201 | | move-wide/from16 vx,vy | `vx, vx + 1 = vy, vy + 1` | 202 | | move-wide/16 vx,vy | `vx, vx + 1 = vy, vy + 1` | 203 | | move-object vx,vy | `vx = vy` | 204 | | move-object/from16 vx,vy | `vx = vy` | 205 | | move-object/16 vx,vy | `vx = vy` | 206 | | move-result vx | 将小于等于 32 位的基本类型(`int`等)的返回值赋给`vx` | 207 | | move-result-wide vx | 将`long`和`double`类型的返回值赋给`vx` | 208 | | move-result-object vx | 将对象类型的返回值(的引用)赋给`vx` | 209 | | move-exception vx | 将异常对象(的引用)赋给`vx`,只能在`throw`之后使用 | 210 | 211 | `move`系列指令以及`move-result`用于处理小于等于 32 位的基本类型。`move-wide`系列指令和`move-result-wide`用于处理`long`和`double`类型。`move-object`系列指令和`move-result-object`用于处理对象引用。 212 | 213 | 另外不同后缀(无、`/from16`、`/16`)只影响字节码的位数和寄存器的范围,不影响指令的逻辑。 214 | 215 | ## 数据运算 216 | 217 | ### 二元运算 218 | 219 | 二元运算指令格式为`<运算类型>-<数据类型> vx,vy,vz`。其中算术运算的`type`可以为`int`、`long`、`float`、`double`四种(`short`、`byte`按`int`处理),位运算的只支持`int`、`long`,下同。 220 | 221 | | 指令 | 运算类型 | 含义 | 222 | | --- | --- | --- | 223 | | 算术运算 | | | 224 | | add- vx, vy, vz | 加法 | `vx = vy + vz` | 225 | | sub- vx, vy, vz | 减法 | `vx = vy - vz` | 226 | | mul- vx, vy, vz | 乘法 | `vx = vy * vz` | 227 | | div- vx, vy, vz | 除法 | `vx = vy / vz` | 228 | | rem- vx, vy, vz | 取余 | `vx = vy % vz` | 229 | | 位运算 | | | 230 | | and- vx, vy, vz | 与 | `vx = vy & vz` | 231 | | or- vx, vy, vz | 或 | `vx = vy | vz` | 232 | | xor- vx, vy, vz | 异或 | `vx = vy ^ vz` | 233 | | shl- vx, vy, vz | 左移 | `vx = vy << vz` | 234 | | shr- vx, vy, vz | 算术右移 | `vx = vy >> vz` | 235 | | ushr- vx, vy, vz | 逻辑右移 | `vx = vy >>> vz` | 236 | 237 | 我们可以查看如下代码: 238 | 239 | ```java 240 | int a = 5, 241 | b = 2, 242 | c = a + b, 243 | d = a - b, 244 | e = a * b, 245 | f = a / b, 246 | g = a % b, 247 | h = a & b, 248 | i = a | b, 249 | j = a ^ b, 250 | k = a << b, 251 | l = a >> b, 252 | m = a >>> b; 253 | ``` 254 | 255 | 编译后的代码可能为: 256 | 257 | ``` 258 | const/4 v0, 0x5 259 | const/4 v1, 0x2 260 | add-int v2, v0, v1 261 | sub-int v3, v0, v1 262 | mul-int v4, v0, v1 263 | div-int v5, v0, v1 264 | rem-int v6, v0, v1 265 | and-int v7, v0, v1 266 | or-int v8, v0, v1 267 | xor-int v9, v0, v1 268 | shl-int v10, v0, v1 269 | shr-int v11, v0, v1 270 | ushr-int v12, v0, v1 271 | ``` 272 | 273 | 这里有个特例,当操作数类型是`int`,并且第二个操作数是字面值的时候,有一组特化的指令: 274 | 275 | | 指令 | 运算类型 | 含义 | 276 | | --- | --- | --- | 277 | | 算术运算 | | | 278 | | add-int/ vx, vy, | 加法 | `vx = vy + ` | 279 | | sub-int/ vx, vy, | 减法 | `vx = vy - ` | 280 | | mul-int/ vx, vy, | 乘法 | `vx = vy * ` | 281 | | div-int/ vx, vy, | 除法 | `vx = vy / ` | 282 | | rem-int/ vx, vy, | 取余 | `vx = vy % ` | 283 | | 位运算 | | | 284 | | and-int/ vx, vy, | 与 | `vx = vy & ` | 285 | | or-int/ vx, vy, | 或 | `vx = vy | ` | 286 | | xor-int/ vx, vy, | 异或 | `vx = vy ^ ` | 287 | | shl-int/ vx, vy, | 左移 | `vx = vy << ` | 288 | | shr-int/ vx, vy, | 算术右移 | `vx = vy >> ` | 289 | | ushr-int/ vx, vy, | 逻辑右移 | `vx = vy >>> ` | 290 | 291 | 其中``可以为`lit8`或`lit16`,即 8 位或 16 位的整数字面值。比如`int a = 0; a += 2;`可能编译为`const/4 v0, 0`和`add-int/lit8 v0, v0, 0x2`。 292 | 293 | ### 二元运算赋值 294 | 295 | 二元运算赋值指令格式为`<运算类型>-<数据类型>/2 vx,vy,vz`。 296 | 297 | | 指令 | 运算类型 | 含义 | 298 | | --- | --- | --- | 299 | | 算术运算 | | | 300 | | add-/2addr vx, vy | 加法赋值 | `vx += vy` | 301 | | sub-/2addr vx, vy | 减法赋值 | `vx -= vy` | 302 | | mul-/2addr vx, vy | 乘法赋值 | `vx *= vy` | 303 | | div-/2addr vx, vy | 除法赋值 | `vx /= vy` | 304 | | rem-/2addr vx, vy | 取余赋值 | `vx %= vy` | 305 | | 位运算 | | | 306 | | and-/2addr vx, vy | 与赋值 | `vx &= vy` | 307 | | or-/2addr vx, vy | 或赋值 | `vx |= vy` | 308 | | xor-/2addr vx, vy | 异或赋值 | `vx ^= vy` | 309 | | shl-/2addr vx, vy | 左移赋值 | `vx <<= vy` | 310 | | shr-/2addr vx, vy | 算术右移赋值 | `vx >>= vy` | 311 | | ushr-/2addr vx, vy | 逻辑右移赋值 | `vx >>>= vy` | 312 | 313 | 我们可以查看这段代码: 314 | 315 | ```java 316 | int a = 5, 317 | b = 2; 318 | a += b; 319 | a -= b; 320 | a *= b; 321 | a /= b; 322 | a %= b; 323 | a &= b; 324 | a |= b; 325 | a ^= b; 326 | a <<= b; 327 | a >>= b; 328 | a >>>= b; 329 | ``` 330 | 331 | 可能会编译成: 332 | 333 | ``` 334 | const/4 v0, 0x5 335 | const/4 v1, 0x2 336 | add-int/2addr v0, v1 337 | sub-int/2addr v0, v1 338 | mul-int/2addr v0, v1 339 | div-int/2addr v0, v1 340 | rem-int/2addr v0, v1 341 | and-int/2addr v0, v1 342 | or-int/2addr v0, v1 343 | xor-int/2addr v0, v1 344 | shl-int/2addr v0, v1 345 | shr-int/2addr v0, v1 346 | ushr-int/2addr v0, v1 347 | ``` 348 | 349 | ### 一元运算 350 | 351 | | 指令 | 运算类型 | 含义 | 352 | | --- | --- | --- | 353 | | 算术运算 | | | 354 | | neg- vx, vy | 取负 | `vx = -vy` | 355 | | 位运算 | | | 356 | | not- vx, vy | 取补 | `vx = ~vy` | 357 | 358 | 简单来说,如果代码为`int a = 5, b = -a, c = ~a`,并且变量依次分配给`v0, v1, v2`的话,我们会得到`const/4 v0, 0x5`、`neg-int v1, v0`和`not-int v2, v0`。 359 | 360 | ## 跳转 361 | 362 | ### 无条件 363 | 364 | Java 里面没有`goto`,但是 Smali 里面有,一般来说和`if`以及`for`配合的可能性很大,还有一个作用就是用于代码混淆。 365 | 366 | | 指令 | 类型 | 367 | | --- | --- | 368 | | goto target | 8 位无条件跳 | 369 | | goto/16 target | 16 位无条件跳 | 370 | | goto/32 target | 32 位无条件跳 | 371 | 372 | `target`在 Smali 中是标签,以冒号开头,使用方式是这样: 373 | 374 | ``` 375 | goto :label 376 | 377 | # 一些语句 378 | 379 | :label 380 | ``` 381 | 382 | 这三个指令在使用形式上都一样,就是位数越大的语句支持的距离也越长。 383 | 384 | ### 条件跳转 385 | 386 | `if`系列指令可用于`int`(以及`short`、`char`、`byte`、`boolean`甚至是对象引用): 387 | 388 | | 指令 | 含义 | 389 | | --- | --- | 390 | | if-eq vx,vy,target | `vx == vy`则跳到 target | 391 | | if-ne vx,vy,target | `vx != vy`则跳到 target | 392 | | if-lt vx,vy,target | `vx < vy`则跳到 target | 393 | | if-ge vx,vy,target | `vx >= vy`则跳到 target | 394 | | if-gt vx,vy,target | `vx > vy`则跳到 target | 395 | | if-le vx,vy,target | `vx <= vy`则跳到 target | 396 | | if-eqz vx,target | `vx == 0`则跳到 target | 397 | | if-nez vx,target | `vx != 0`则跳到 target | 398 | | if-ltz vx,target | `vx < 0`则跳到 target | 399 | | if-gez vx,target | `vx >= 0`则跳到 target | 400 | | if-gtz vx,target | `vx > 0`则跳到 target | 401 | | if-lez vx,target | `vx <= 0`则跳到 target | 402 | 403 | 看一下这段代码: 404 | 405 | ```java 406 | int a = 10 407 | if(a > 0) 408 | a = 1; 409 | else 410 | a = 0; 411 | ``` 412 | 413 | 可能的编译结果是: 414 | 415 | ``` 416 | const/4 v0, 0xa 417 | if-lez v0, :cond_0 # if 块开始 418 | const/4 v0, 0x1 419 | goto :cond_1 # if 块结束 420 | :cond_0 # else 块开始 421 | const/4 v0, 0x0 422 | :cond_1 # else 块结束 423 | ``` 424 | 425 | 我们会看到用于比较逻辑是反着的,Java 里是大于,Smali 中就变成了小于等于,这个要注意。也有一些情况下,逻辑不是反着的,但是`if`块和`else`块会对调。还有,标签不一定是一样的,后面的数字会变,但是多数情况下都是两个标签,一个相对跳一个绝对跳。 426 | 427 | 如果只有`if`: 428 | 429 | ```java 430 | int a = 10; 431 | if(a > 0) 432 | a = 1; 433 | ``` 434 | 435 | 相对来说就简单一些,只需要在条件不满足时跳过`if`块即可: 436 | 437 | ``` 438 | const/4 v0, 0xa 439 | if-lez v0, :cond_0 # if 块开始 440 | const/4 v0, 0x1 441 | :cond_0 # if 块结束 442 | ``` 443 | 444 | ### 比较 445 | 446 | 对于`long`、`float`和`double`又该如何比较呢?Dalvik 提供了下面这些指令: 447 | 448 | | 指令 | 含义 | 449 | | --- | --- | 450 | | cmpl-float vx, vy, vz | `vx = -sgn(vy - vz)` | 451 | | cmpg-float vx, vy, vz | `vx = sgn(vy - vz)` | 452 | | cmp-float vx, vy, vz | `cmpg-float`的别名 | 453 | | cmpl-double vx, vy, vz | `vx = -sgn(vy - vz)` | 454 | | cmpg-double vx, vy, vz | `vx = sgn(vy - vz)` | 455 | | cmp-double vx, vy, vz | `cmpg-double`的别名 | 456 | | cmp-long vx, vy, vz | `vx = sgn(vy - vz)` | 457 | 458 | 其中`sgn(x)`是符号函数,定义为:`x > 0`时值为 1,`x = 0`时值为 0,`x < 0`时值为 -1。 459 | 460 | 我们把之前例子中的`int`改为`float`: 461 | 462 | ```java 463 | float a = 10; 464 | if(a > 0) 465 | a = 1; 466 | else 467 | a = 0; 468 | ``` 469 | 470 | 我们会得到: 471 | 472 | ``` 473 | const v0, 0x41200000 # float 10 474 | const v1, 0x0 475 | cmp-float v2, v0, v1 476 | if-lez v2, :cond_0 # if 块开始 477 | const v0, 0x3f800000 # float 1 478 | goto :goto_0 # if 块结束 479 | :cond_0 # else 块开始 480 | const/4 v0, 0x0 481 | :goto_0 # else 块结束 482 | ``` 483 | 484 | 由于`cmpg`更类似平时使用的比较器,用起来更加顺手,但是`cmpl`也需要了解。 485 | 486 | ### `switch` 487 | 488 | Dalvik 共支持两种`switch`,密集和稀疏。先来看密集`switch`,密集的意思是`case`的序号是挨着的: 489 | 490 | ```java 491 | int a = 10; 492 | switch (a){ 493 | case 0: 494 | a = 1; 495 | break; 496 | case 1: 497 | a = 5; 498 | break; 499 | case 2: 500 | a = 10; 501 | break; 502 | case 3: 503 | a = 20; 504 | break; 505 | } 506 | ``` 507 | 508 | 编译为: 509 | 510 | ``` 511 | const/16 v0, 0xa 512 | 513 | packed-switch v0, :pswitch_data_0 # switch 开始 514 | 515 | :pswitch_0 # case 0 516 | const/4 v0, 0x1 517 | goto :goto_0 518 | 519 | :pswitch_1 # case 1 520 | const/4 v0, 0x5 521 | goto :goto_0 522 | 523 | :pswitch_2 # case 2 524 | const/16 v0, 0xa 525 | goto :goto_0 526 | 527 | :pswitch_3 # case 3 528 | const/16 v0, 0x14 529 | goto :goto_0 530 | 531 | :goto_0 # switch 结束 532 | return-void 533 | 534 | :pswitch_data_0 # 跳转表开始 535 | .packed-switch 0x0 # 从 0 开始 536 | :pswitch_0 537 | :pswitch_1 538 | :pswitch_2 539 | :pswitch_3 540 | .end packed-switch # 跳转表结束 541 | ``` 542 | 543 | 然后是稀疏`switch`: 544 | 545 | ```java 546 | int a = 10; 547 | switch (a){ 548 | case 0: 549 | a = 1; 550 | break; 551 | case 10: 552 | a = 5; 553 | break; 554 | case 20: 555 | a = 10; 556 | break; 557 | case 30: 558 | a = 20; 559 | break; 560 | } 561 | ``` 562 | 563 | 编译为: 564 | 565 | ``` 566 | const/16 v0, 0xa 567 | 568 | sparse-switch v0, :sswitch_data_0 # switch 开始 569 | 570 | :sswitch_0 # case 0 571 | const/4 v0, 0x1 572 | goto :goto_0 573 | 574 | :sswitch_1 # case 10 575 | const/4 v0, 0x5 576 | 577 | goto :goto_0 578 | 579 | :sswitch_2 # case 20 580 | const/16 v0, 0xa 581 | goto :goto_0 582 | 583 | :sswitch_3 # case 15 584 | const/16 v0, 0x14 585 | goto :goto_0 586 | 587 | :goto_0 # switch 结束 588 | return-void 589 | 590 | .line 55 591 | :sswitch_data_0 # 跳转表开始 592 | .sparse-switch 593 | 0x0 -> :sswitch_0 594 | 0xa -> :sswitch_1 595 | 0x14 -> :sswitch_2 596 | 0x1e -> :sswitch_3 597 | .end sparse-switch # 跳转表结束 598 | ``` 599 | 600 | ## 数组操作 601 | 602 | 数组拥有一套特化的指令。 603 | 604 | ### 创建 605 | 606 | | 指令 | 含义 | 607 | | --- | --- | 608 | | new-array vx,vy,type | 创建类型为`type`,大小为`vy`的数组赋给`vx` | 609 | | filled-new-array {params},type_id | 从`params`创建数组,结果使用`move-result`获取 | 610 | | filled-new-array-range {vx..vy},type_id | 从`vx`与`vy`之间(包含)的所有寄存器创建数组,结果使用`move-result`获取 | 611 | 612 | 对于第一条指令,如果我们这样写: 613 | 614 | ```java 615 | int[] arr = new int[10]; 616 | ``` 617 | 618 | 就可以使用该指令编译: 619 | 620 | ``` 621 | const/4 v1, 0xa 622 | new-array v0, v1, I 623 | ``` 624 | 625 | 但如果我们直接使用数组字面值给一个数组赋值: 626 | 627 | ``` 628 | int[] arr = {1, 2, 3, 4, 5}; 629 | // 或者 630 | arr = new int[]{1, 2, 3, 4, 5}; 631 | ``` 632 | 633 | 可以使用第二条指令编写如下: 634 | 635 | ``` 636 | const/4 v1, 0x1 637 | const/4 v2, 0x2 638 | const/4 v3, 0x3 639 | const/4 v4, 0x4 640 | const/4 v5, 0x5 641 | filled-new-array {v1, v2, v3, v4, v5}, I 642 | move-result v0 643 | ``` 644 | 645 | 我们这里的寄存器是连续的,实际上不一定是这样,如果寄存器是连续的,还可以改写为第三条指令: 646 | 647 | ``` 648 | const/4 v1, 0x1 649 | const/4 v2, 0x2 650 | const/4 v3, 0x3 651 | const/4 v4, 0x4 652 | const/4 v5, 0x5 653 | filled-new-array-range {v1..v5}, I 654 | move-result v0 655 | ``` 656 | 657 | ### 元素操作 658 | 659 | `aget`系列指令用于读取数组元素,效果为`vx = vy[vz]`: 660 | 661 | ``` 662 | aget vx,vy,vz 663 | aget-wide vx,vy,vz 664 | aget-object vx,vy,vz 665 | aget-boolean vx,vy,vz 666 | aget-byte vx,vy,vz 667 | aget-char vx,vy,vz 668 | aget-short vx,vy,vz 669 | ``` 670 | 671 | 有两个指令需要说明,`aget`用于获取`int`和`float`,`aget-wide`用于获取`long`和`double`。 672 | 673 | 同样,`aput`系列指令用于写入数组元素,效果为`vy[vz] = vx`: 674 | 675 | ``` 676 | aget vx,vy,vz 677 | aget-wide vx,vy,vz 678 | aget-object vx,vy,vz 679 | aget-boolean vx,vy,vz 680 | aget-byte vx,vy,vz 681 | aget-char vx,vy,vz 682 | aget-short vx,vy,vz 683 | ``` 684 | 685 | 如果我们编写以下代码: 686 | 687 | ``` 688 | int[] arr = new int[2]; 689 | int b = arr[0]; 690 | arr[1] = b; 691 | ``` 692 | 693 | 可能会编译成: 694 | 695 | ``` 696 | const/4 v0, 0x2 697 | new-array v1, v0, I 698 | const/4 v0, 0x0 699 | aget-int v2, v1, v0 700 | const/4 v0, 0x1 701 | aput-int v2, v1, v0 702 | ``` 703 | 704 | ## 对象操作 705 | 706 | ### 对象创建 707 | 708 | | 指令 | 含义 | 709 | | --- | --- | 710 | | new-instance vx, type | 创建`type`的新实例,并赋给`vx` | 711 | 712 | `new-instance`用于创建实例,但之后还需要调用构造器``,比如: 713 | 714 | ``` 715 | Object obj = new Object(); 716 | ``` 717 | 718 | 会编译成: 719 | 720 | ``` 721 | new-instance v0, Ljava/lang/Object; 722 | invoke-direct-empty {v0}, Ljava/lang/Object;->()V 723 | ``` 724 | 725 | 方法调用后面再讲。 726 | 727 | ### 字段操作 728 | 729 | `sget`系列指令用于获取静态字段,效果为`vx = class.field`: 730 | 731 | ``` 732 | sget vx, type->field:field_type 733 | sget-wide vx, type->field:field_type 734 | sget-object vx, type->field:field_type 735 | sget-boolean vx, type->field:field_type 736 | sget-byte vx, type->field:field_type 737 | sget-char vx, type->field:field_type 738 | sget-short vx, type->field:field_type 739 | ``` 740 | 741 | `sput`系列指令用于设置静态字段,效果为`class.field = vx`: 742 | 743 | ``` 744 | sput vx, type->field:field_type 745 | sput-wide vx, type->field:field_type 746 | sput-object vx, type->field:field_type 747 | sput-boolean vx, type->field:field_type 748 | sput-byte vx, type->field:field_type 749 | sput-char vx, type->field:field_type 750 | sput-short vx, type->field:field_type 751 | ``` 752 | 753 | 我们在这里创建一个类: 754 | 755 | ``` 756 | public class Test 757 | { 758 | private static int staticField; 759 | 760 | public static int getStaticField() { 761 | return staticField; 762 | } 763 | 764 | public static void setStaticField(int staticField) { 765 | Test.staticField = staticField; 766 | } 767 | } 768 | ``` 769 | 770 | 编译之后,我们可以在`getStaticField`中找到: 771 | 772 | ``` 773 | sget v0, Lnet/flygon/myapplication/Test;->staticField:I 774 | return v0 775 | ``` 776 | 777 | 在`setStaticField`中可以找到: 778 | 779 | ``` 780 | sput p0, Lnet/flygon/myapplication/Test;->staticField:I 781 | return-void 782 | ``` 783 | 784 | `iget`系列指令用于获取实例字段,效果为`vx = vy.field`: 785 | 786 | ``` 787 | iget vx, vy, type->field:field_type 788 | iget-wide vx, vy, type->field:field_type 789 | iget-object vx, vy, type->field:field_type 790 | iget-boolean vx, vy, type->field:field_type 791 | iget-byte vx, vy, type->field:field_type 792 | iget-char vx, vy, type->field:field_type 793 | iget-short vx, vy, type->field:field_type 794 | ``` 795 | 796 | `iput`系列指令用于设置实例字段,效果为`vy.field = vx`: 797 | 798 | ``` 799 | iput vx, vy, type->field:field_type 800 | iput-wide vx, vy, type->field:field_type 801 | iput-object vx, vy, type->field:field_type 802 | iput-boolean vx, vy, type->field:field_type 803 | iput-byte vx, vy, type->field:field_type 804 | iput-char vx, vy, type->field:field_type 805 | iput-short vx, vy, type->field:field_type 806 | ``` 807 | 808 | 我们将之前的类修改一下: 809 | 810 | ``` 811 | public class Test 812 | { 813 | private int instanceField; 814 | 815 | public int getInstanceField() { 816 | return instanceField; 817 | } 818 | 819 | public void setInstanceField(int instanceField) { 820 | this.instanceField = instanceField; 821 | } 822 | } 823 | ``` 824 | 825 | 反编译之后,我们可以在`getInstanceField`中找到:· 826 | 827 | ``` 828 | iget v0, p0, Lnet/flygon/myapplication/Test;->instanceField:I 829 | return v0 830 | ``` 831 | 832 | 在`setInstanceField`中可以找到: 833 | 834 | ``` 835 | iset p1, p0, Lnet/flygon/myapplication/Test;->instanceField:I 836 | return-void 837 | ``` 838 | 839 | 在实例方法中,`this`引用永远是`p0`。第一个参数从`p1`开始。 840 | 841 | ### 方法调用 842 | 843 | 有五类方法调用指令: 844 | 845 | | 指令 | 含义 | 846 | | --- | --- | 847 | | invoke-static | 调用静态方法 | 848 | | invoke-direct | 调用直接方法 | 849 | | invoke-direct-empty | 无参的`invoke-direct` | 850 | | invoke-virtual | 调用虚方法 | 851 | | invoke-super | 调用超类的虚方法 | 852 | | invoke-interface | 调用接口方法 | 853 | 854 | 这些指令的格式均为: 855 | 856 | ``` 857 | invoke-* {params}, type->method(params_type)return_type 858 | ``` 859 | 860 | 如果需要传递`this`引用,将其放置在`param`的第一个位置。 861 | 862 | 那么这些指令有什么不同呢?首先要分辨两个概念,虚方法和直接方法(JVM 里面叫特殊方法)。其实 Java 是没有虚方法这个概念的,但是 DVM 里面有,直接方法是指类的(`type`为某个类)所有实例构造器和`private`实例方法。反之`protected`或者`public`方法都叫做虚方法。 863 | 864 | `invoke-static`比较好分辨,当且仅当调用静态方法时,才会使用它。 865 | 866 | `invoke-direct`(在 JVM 中叫做`invokespecial`)用于调用直接方法,`invoke-virtual`用于调用虚方法。除了一种情况,显式使用`super`调用超类的虚方法时,使用`invoke-super`(直接方法仍然使用`invoke-direct`)。 867 | 868 | 就比如说,每个`Activity`的`onCreate`中要调用`super.onCreate`,该方法属于虚方法,于是我们会看到: 869 | 870 | ``` 871 | invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V 872 | ``` 873 | 874 | 但是呢,每个`Activity`构造器里面要调用`super`的无参构造器,它属于直接方法,那么我们会看到: 875 | 876 | ``` 877 | invoke-direct {p0}, Landroid/app/Activity;->()V 878 | ``` 879 | 880 | `invoke-interface`用于调用接口方法,接口方法就是接口的方法,`type`一定为某个接口,而不是类。换句话说,类中实现的方法仍然是虚方法。比如我们在某个对象上调用`Map.get`,属于接口方法,但是调用`HashMap.get`,属于虚方法。这个指令一般在向上转型为接口类型的时候出现。 881 | 882 | 此外,五类指令中每一个都有对应的`invoke-*-range`指令,格式为: 883 | 884 | ``` 885 | invoke-*-range {vx..vy},type->method(params_type)return_type 886 | ``` 887 | 888 | 如果参数所在的寄存器的连续的,可以替换为这条指令。 889 | 890 | ### 对象转换 891 | 892 | 对象转换有自己的一套检测方式,DVM 使用以下指令来实现: 893 | 894 | | 指令 | 含义 | 895 | | --- | --- | 896 | | instance-of vx, vy, type | 检验`vy`的类型是不是`type`,将结果存入`vx` | 897 | | check-cast vx, type | 检验`vx`类型是不是`type`,不是的话会抛出`ClassCastException` | 898 | 899 | `instance-of`指令对应 Java 的`instanceof`运算符。如果我们编写: 900 | 901 | ``` 902 | String s = "test"; 903 | boolean b = s instanceof String; 904 | ``` 905 | 906 | 可能会编译为: 907 | 908 | ``` 909 | const-string v0, "test" 910 | instance-of v1, v0, Ljava/lang/String; 911 | ``` 912 | 913 | `check-cast`用于对象类型强制转换的情况,如果我们编写: 914 | 915 | ``` 916 | String s = "test"; 917 | Object o = (Object)s; 918 | ``` 919 | 920 | 那么就会: 921 | 922 | ``` 923 | const-string v0, "test" 924 | check-cast v0, Ljava/lang/Object; 925 | move-object v1, v0 926 | ``` 927 | 928 | ## 返回 929 | 930 | ``` 931 | return-void 932 | return vx 933 | return-wide vx 934 | return-object vx 935 | ``` 936 | 937 | 如果函数无返回值,那么使用`return-void`,注意在 Java 中,无返回值函数结尾处的`return`可以省,而 Smali 不可以。 938 | 939 | 如果函数需要返回对象,使用`return-object`;需要返回`long`或者`double`,使用`return-wide`;除此之外所有情况都使用`return`。 940 | 941 | 942 | ## 异常指令 943 | 944 | 异常指令实际上只有一条,但是代码结构相当复杂。 945 | 946 | | 指令 | 含义 | 947 | | --- | --- | 948 | | throw vx | 抛出`vx`(所指向的对象) | 949 | 950 | 我们需要看看 Smali 如何处理异常。 951 | 952 | ## try-catch 953 | 954 | 不失一般性,我们构造以下语句: 955 | 956 | ```java 957 | int a = 10; 958 | try { 959 | callSomeMethod(); 960 | } catch (Exception e) { 961 | a = 0; 962 | } 963 | callAnotherMethod(); 964 | ``` 965 | 966 | 可能会编译成这样,这些语句每个都不一样,可以按照特征来定位: 967 | 968 | ``` 969 | const/16 v0, 0xa 970 | 971 | :try_start_0 # try 块开始 972 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callSomeMethod()V 973 | :try_end_0 # try 块结束 974 | 975 | .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 976 | 977 | :goto_0 978 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callAnotherMethod()V 979 | return-void 980 | 981 | :catch_0 # catch 块开始 982 | move-exception v1 983 | const/4 v0, 0x0 984 | goto :goto_0 # catch 块结束 985 | ``` 986 | 987 | 我们可以看到,`:try_start_0`和`:try_end_0`之间的语句如果存在异常,则会向下寻找`.catch`(或者`.catch-all`)语句,符合条件时跳到标签的位置,这里是`:catch_0`,结束之后会有个`goto`跳回去。 988 | 989 | ## try-finally 990 | 991 | ```java 992 | int a = 10; 993 | try { 994 | callSomeMethod(); 995 | } finally { 996 | a = 0; 997 | } 998 | callAnotherMethod(); 999 | ``` 1000 | 1001 | 编译之后是这样: 1002 | 1003 | ``` 1004 | const/16 v0, 0xa 1005 | 1006 | :try_start_0 # try 块开始 1007 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callSomeMethod()V 1008 | :try_end_0 # try 块结束 1009 | 1010 | .catchall {:try_start_0 .. :try_end_0} :catchall_0 1011 | 1012 | const/4 v0, 0x0 # 复制一份到外面 1013 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callAnotherMethod()V 1014 | return-void 1015 | 1016 | :catchall_0 # finally 块开始 1017 | move-exception v1 1018 | const/4 v0, 0x0 1019 | throw v1 # finally 块结束 1020 | ``` 1021 | 1022 | 我们可以看到,编译器把`finally`编译成了重新抛出的`.catch-all`,这在逻辑上也是说得通的。但是,`finally`中的逻辑在无异常情况下也会执行,所以需要复制一份到`finally`块的后面。 1023 | 1024 | 1025 | ### try-catch-finally 1026 | 1027 | 下面看看如果把这两个叠加起来会怎么样。 1028 | 1029 | ```java 1030 | int a = 10; 1031 | try { 1032 | callSomeMethod(); 1033 | } catch (Exception e) { 1034 | a = 1; 1035 | } 1036 | finally { 1037 | a = 0; 1038 | } 1039 | callAnotherMethod(); 1040 | ``` 1041 | 1042 | ``` 1043 | const/16 v0, 0xa 1044 | 1045 | :try_start_0 # try 块开始 1046 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callSomeMethod()V 1047 | :try_end_0 # try 块结束 1048 | 1049 | .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0 1050 | .catchall {:try_start_0 .. :try_end_0} :catchall_0 1051 | 1052 | const/4 v0, 0x0 # 复制一份到外面 1053 | 1054 | :goto_0 1055 | invoke-direct {p0}, Lnet/flygon/myapplication/SubActivity;->callAnotherMethod()V 1056 | return-void 1057 | 1058 | :catch_0 # catch 块开始 1059 | move-exception v1 1060 | const/4 v0, 0x1 1061 | const/4 v0, 0x0 # 复制一份到 catch 块里面 1062 | goto :goto_0 # catch 块结束 1063 | 1064 | :catchall_0 # finally 块开始 1065 | move-exception v2 1066 | const/4 v0, 0x0 1067 | throw v2 # finally 块结束 1068 | ``` 1069 | 1070 | 我们可以看到,其中同时含有`.catch`块和`.catchall`块。有一些不同之处在于,`finally`块中的语句异常发生时也要执行,并且如果把`finally`编译成`.catchall`,那么和`.catch`就是互斥的,所以要复制一份到`catch`块里面。特别是`finally`块中的语句一多,就容易乱。 1071 | 1072 | ## 锁 1073 | 1074 | | 指令 | 含义 | 1075 | | --- | --- | 1076 | | monitor-enter vx | 获得`vx`所引用的对象的锁 | 1077 | | monitor-exit vx | 释放`vx`所引用的对象的锁 | 1078 | 1079 | 对应 Java 的`synchronized`语句。而`synchronized`一般是被`try-finally`包起来的。 1080 | 1081 | 如果你编写: 1082 | 1083 | ``` 1084 | int a = 1; 1085 | synchronized(this) { 1086 | a = 2; 1087 | } 1088 | ``` 1089 | 1090 | 就相当于 1091 | 1092 | ``` 1093 | int a = 1; 1094 | // monitor-enter this 1095 | try { 1096 | a = 2; 1097 | } finally { 1098 | // monitor-exit this 1099 | } 1100 | ``` 1101 | 1102 | 此外 Java 中没有与这两条指令相对应的方法,所以这两条指令一定成对出现。 1103 | 1104 | ## 数据转换 1105 | 1106 | ### 整数与浮点以及浮点与浮点 1107 | 1108 | ``` 1109 | int-to-float vx, vy 1110 | int-to-double vx, vy 1111 | long-to-float vx, vy 1112 | long-to-double vx, vy 1113 | float-to-int vx, vy 1114 | float-to-long vx,vy 1115 | float-to-double vx, vy 1116 | double-to-int vx, vy 1117 | double-to-long vx, vy 1118 | double-to-float vx, vy 1119 | ``` 1120 | 1121 | 因为它们的表示方式不同,所以要保持表示的值不变,重新计算二进制位。如果不转换的话,就相当于二进制位不变,而表示的值改变,结果毫无意义。比如前面的`0.1f`如果不转换为直接使用,就会表示`0x3dcccccd`。 1122 | 1123 | ### 整数之间的向上转换 1124 | 1125 | 这种转换方式相当直接,`int`向`long`转换,`long`的第一个寄存器完全复制,第二个寄存器以`int`的最高位填充。除此之外没有其它的指令了,因为比`int`小的整数其实都是 32 位表示的,只是有效范围是 8 位或 16 位罢了(见数据定义)。 1126 | 1127 | ``` 1128 | int-to-long vx,vy 1129 | ``` 1130 | 1131 | ### 整数之间的向下转换 1132 | 1133 | 其规则是数据位截断,符号位保留。每个整数的最高位都是符号位,其余是数据位。以`int`转`short`为例,`int`的低 15 位复制给`short`,然后`int`的最高位(符号位)复制给`short`的最高位。其它同理。如果不转换而直接使用的话,会直接截断低 16 位,符号可能不能保留。 1134 | 1135 | ``` 1136 | long-to-int vx,vy 1137 | int-to-byte vx,vy 1138 | int-to-char vx,vy 1139 | int-to-short vx,vy 1140 | ``` 1141 | 1142 | ## NOP 1143 | 1144 | `nop`指令表示无操作。在一些场合下,不能修改二进制代码的字节数和偏移,需要用`nop`来填充,但是安卓逆向中几乎用不到。 1145 | 1146 | ## 参考 1147 | 1148 | + [Bytecode for the Dalvik VM](http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html) 1149 | + [Dalvik字节码含义查询表](http://blog.csdn.net/jiayanhui2877/article/details/41008985) 1150 | + [DVM 指令集图解](https://github.com/corkami/pics/blob/master/binary/DVM.png) 1151 | -------------------------------------------------------------------------------- /2.md: -------------------------------------------------------------------------------- 1 | # 安卓逆向系列教程(二)APK 和 DEX 2 | 3 | > 作者:[飞龙](https://github.com/wizardforcel) 4 | 5 | ## APK 6 | 7 | APK 是 Android 软件包的分发格式,它本身是个 Zip 压缩包。APK 根目录下可能出现的目录和文件有: 8 | 9 | | 名称 | 用途 | 10 | | --- | --- | 11 | | `META-INF` | 存放元数据 | 12 | | `AndroidManifest.xml` | 编译后的全局配置文件 | 13 | | `assets` | 存放资源文件,不会编译 | 14 | | `classes.dex` | 编译并打包后的源代码 | 15 | | `lib` | 存放二进制共享库,含有`armeabi-*`、`mips`、`x86`等文件夹,对应具体的平台 | 16 | | `res` | 存放资源文件 | 17 | | `resources.arsc` | 编译并打包后的`res/values`中的文件 | 18 | 19 | ### `res` 20 | 21 | res 中可能出现的目录如下: 22 | 23 | | 名称 | 用途 | 24 | | --- | --- | 25 | | `anim` | 存放编译后的动画 XML 文件(``) | 26 | | `color` | 存放编译后的选择器 XML 文件(``) | 27 | | `drawable-*` | 存放图片,`*`为不同分辨率,图片按照不同分辨率归类。其中带`.9`的图片为可拉伸的图片。 | 28 | | `layout` | 存放编译后的布局 XML 文件(``) | 29 | | `menu` | 存放编译后的菜单 XML 文件(``) | 30 | | `mipmap-*` | 存放使用 mipmap 技术加速的图片,一般用来存放应用图标,其它同`drawable-*` | 31 | | `raw` | 存放资源文件,不会编译,比如音乐、视频、纯文本等 | 32 | | `xml` | 存放编译后的自定义 XML 文件 | 33 | 34 | ### `resources.arsc` 35 | 36 | 在 APK 中是找不到`res/values`这个目录的,因为它里面的文件编译后打包成了`resources.arsc`。为了理解它,我们先看一看原始的`res/values`。 37 | 38 | `res/values`中保存资源 XML 文件,根节点为``。一般可能会出现以下几种文件: 39 | 40 | | 名称 | 用途 | 41 | | --- | --- | 42 | | `arrays.xml` | 存放整数数组和字符串数组,使用``或``定义,元素使用``定义 | 43 | | `bools.xml` | 存放布尔值,使用``定义 | 44 | | `colors.xml` | 存放颜色,使用``定义 | 45 | | `dimens.xml` | 存放尺寸,使用``定义 | 46 | | `drawables.xml` | 存放颜色,使用``定义 | 47 | | `ids.xml` | 存放 ID,使用``定义 | 48 | | `integers.xml` | 存放整数,使用``定义 | 49 | | `strings.xml` | 存放字符串,使用``定义 | 50 | | `styles.xml` | 存放颜色,使用`