Lab

Lab1

前言

tips:开 VPN 运行远程的 AVD 会很流畅。

Step 1

Task 1

AndroidManifest 文件中进行配置的广播接收者会随系统的启动而一直处于活跃状态,只要接收到感兴趣的广播就会触发(即使程序未运行)所以我们需要把广播写入到 AndroidManifest 里。

image-20230310141941697

Android 8.0 以上使用 startForegroundService() 方法,以下则使用 startService() 方法来启动服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.smali.secretchallenge;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class SecretBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent serviceIntent = new Intent(context, SecretService.class);
context.startService(serviceIntent);
}
}
}

在获取位置时,需要添加权限声明:

image-20230310142205220

使用 LocationManager 和 LocationListener 获取 GPS,然后通过 Handler 进行循环执行读取 GPS 信息,并且弹窗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return flags;
}
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
handler = new Handler(Looper.getMainLooper());
runnable = new Runnable() {
@Override
public void run() {
Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastLocation != null) {
String message = String.format("getAccuracy:%s\ngetLatitude: %s\ngetLongitude: %s", lastLocation.getAccuracy(), lastLocation.getLatitude(), lastLocation.getLongitude());
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
}
handler.postDelayed(runnable, 3000);
}
};
handler.post(runnable);
return flags;
}

在 Android 4.0 之后需要启动应用,否则开机的时候会收不到开机广播,所以还要手动在终端输入 adb shell am broadcast -a android.intent.action.BOOT_COMPLETED 广播消息。

最终实现效果图:

image-20230310141851781

Task 2

先制作样式,可以直接可视化制作,十分方便。

image-20230310204608575

在触发 click 动作时,产生一个线程去执行弹窗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String value = editText.getText().toString();

new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
showDialog(value);
}
});
}
}).start();
}
});

效果展示:

image-20230310204429962

Task 3

了解一下 Java 反射的知识,使用 Method 和 Filed 进行操作需要访问的类的方法和成员变量即可完成任务:

1
2
3
4
5
6
7
8
Class<?> poRELabClass = Class.forName("com.pore.mylibrary.PoRELab");
Field curStrField = poRELabClass.getDeclaredField("curStr");
Method privateMethod = poRELabClass.getDeclaredMethod("privateMethod", String.class, String.class);
curStrField.setAccessible(true);
privateMethod.setAccessible(true);
PoRELab poreLab = new PoRELab();
String curStrValue = (String) curStrField.get(poreLab);
privateMethod.invoke(poreLab, "hello", curStrValue);

成果展示:

image-20230311115622746

Task 4

使用 Android Studio 进行签名,一路往下填写即可。

image-20230311142435805

Step2

smali2java

学习完 smali 的语法就可以开始进行手工恢复成 Java 代码了,我想法是在 IDEA 里面写java代码,跟着 smali 的顺序写,比如刚开始的时候,

image-20230311213504770

image-20230311213609412

下列的意思是,先将 p0 的值给予 v0,而 p0 是 this.checker(根据标注),所以 v0 现在是 this.checker,然后是 v0 调用 check 方法,并且传入一个参数,因此变为 this.checker.check(str) 再将返回值存入 v0,返回 v0,也就是说返回调用this.checker.check(str) 的返回值,因此可以简化为 return this.checker.check(str)。

image-20230311220911056

搞明白这个,再看上面的那部分代码(前面没看懂),这是个构造函数,负责 CheckBox 的初始化,因此下列代码很清晰的可以变成下面的java代码,所以很显然上面写错了,那边只是声明,并未实例化。

1
2
this.encoder = new Encoder();
this.checker = new Checker();

image-20230311223459762

array-length v3, p0指的是将 cmd 窗口编译 java 文件传入的参数个数传递个 v3。同时这边的判断根据jadx的转换可以学习到,不能直接顺着意思去转换,应该先考虑不满足条件的情况,即先写等于的情况,这样接下来的就都是属于其执行的内容。

image-20230311231221514

后续就没什么了,还是相对容易的,最终 CheckBox.java 代码如下:

image-20230312135200456

像这种的构造函数初始化只是去给前面的域赋值,其实是可以省略的,直接对前面的域赋初值就行,如

1
private String secret = "key";

image-20230312135828201

遇到count方法时,是转换成了这样的代码,但是看起来其实是不对劲的,有些地方不合逻辑,因为这个方法的逻辑应该是统计字符中’1’的个数,然后返回该个数,所以需要我们再给代码优化一下。

image-20230312145949836

优化成这样,就舒服了不少:

image-20230312150323229

这边 jadx 转换的很好,两次的判断结果可以直接与返回值联系起来,因此直接就写成如下代码就行:

1
return count == func(count) && this.checkStr1(str.substring(0, 10));

image-20230312151948196

然后最后再转换 checkStr1 这方法(因为他看起来挺复杂的),先扫一眼 jadx 转换结果是一个 for 循环,所以我的思路就跟之前的有些不一致:是先审视过所有的 smali,然后再写一个大致的 java 代码,然后再一一对应的填充。

image-20230312153700980

对应上方的框架,我写的代码如下,但是已经很能表现出来了,显然 v0 是循环次数,count 是 ‘x’ 的个数,当 ‘x’ 有一个和两个时会将此时的循环次数 v0 赋值出去。

image-20230312153727348

优化一下代码为:

image-20230312154523112

接下的就是跟之前的 return 思想一致,就不展示了。

接下来就是最后一个 smali 文件了,发现貌似没啥好说的,直接展现最终的结果吧

image-20230313161648612

Task1

转换后代码就很容易了,需要满足以下条件:

  • 输入字符串的长度在 12~16
  • 从第 10 开始的字符只能含有一个 ‘1’
  • 0~9 的字符串中要含有两个 ‘x’
  • 两个 ‘x’ 的索引值相差 4
  • 第 0 个字符是 ‘0’
  • 第 9 个字符是 ‘9’
  • 在第 0 和 第一个 ‘x’ 出现的地方要含有 ‘key’

综上可以输入 0keyx567x9100

image-20230312215502615

下面是 转换出的 java 的执行结果。

image-20230313152959852

Task2

image-20230313155257746

image-20230313155244679

image-20230313161330955

我们可以在配置里面添加运行的参数。

image-20230313161344219

image-20230313161304295

Step 3

Unpack Apk

把 apktool 和 需要解包的 apk 放在同一目录下,打开 cmd 运行下列命令即可解包 apk。

1
java -jar apktool_2.4.1.jar d Step3_Task123_lab.apk

image-20230315202038753

因遇到报错,更正为(因为刚开始以为是版本问题,所以下了个 2.6.1 的):

1
java -jar apktool_2.6.1.jar -r d Step3_Task123_lab.apk

Reverse

使用 jeb 或者 jadx 直接打开 apk 即可看到反汇编后的 smali 语法,也可以直接看到由 smali 翻译出来的 java 伪代码。

Repack Apk

把 999999 修改为 1。

image-20230315215510774

image-20230315215549175

重打包为 apk。

image-20230316195127711

Sign Apk

git bash 打开,先生成私钥文件:

1
openssl genrsa -3 -out testkey.pem 2048

再生成 CA 自签证书,有限期为 10000 天

1
openssl req -new -x509 -key testkey.pem -out testkey.x509.pem -days 10000

image-20230316195756115

使用 pkcs8 标准保存私钥文件信息(未加密版)。

1
openssl pkcs8 -in testkey.pem -topk8 -outform DER -out testkey.pk8 -nocrypt

用 apksigner.jar 对 apk 进行签名。

1
java -jar apksigner.jar sign --cert testkey.x509.pem --key testkey.pk8 --in Step3_Task123_lab.apk -out lab_signed.apk

image-20230316200548791

安装 apk 后,可以发现次数已经被修改为 1了,

image-20230316210037098

因此可以很轻易的完成 task1。

image-20230316210102989

Tasks

Task 1 Knock the door

任务一已经在上面完成。然后这边我有点疑惑的点就是说,怎么触发的点击事件,不是要实现一个 setOnClickListener 的监听器吗?经过一番学习,还有一种实现方法,就是在 activity_main.xml 中,对 button 直接指定其会触发的方法。

image-20230317153406710

Task 2 Give me your token

任务二也很简单的,把生成的数据自己跑一遍代码即可。

image-20230317155200488

image-20230317155134457

Task 3 Call to the NPC

只需要在最终返回时添加下列的 smali 代码调用 skdaga ,然后输出其结果。

1
2
3
invoke-static {p1}, Lcom/pore/play4fun/PlayGame;->skdaga(Ljava/lang/String;)Ljava/lang/String;

move-result-object v0

image-20230317203250525

调用运行后会打印出 flag

image-20230317203218916

Task 4 Where password flows to

先确认 com.android.insecurebank.InsecureBankActivity 是最开始执行的 activity。

image-20230317185825970

InsecureBankActivity 只实现了一个跳转去 LoginScreen 的功能,所以接下来去看 LoginScreen 的代码。

image-20230317190555333

确认 password_text 就是我们要追踪的变量。

image-20230317190749346

LoginScreen 实现了三个按钮的功能,一个是记住账号,一个是可以设置登录地址的 ip 和 port,第三个是我们需要关注的 login 功能如下图,后续会执行 restClient 的 doLogin 方法,或者 Statusode 为 -1 时执行 PostLogin,我们先追踪 restClient 的 doLogin。

image-20230317192444458

doLogin 实现了一个 URL 的拼接,然后转而执行 postHttpContent。

image-20230317193036009

最终在 postHttpContent 把账号密码通过 post 的方式传入之前设置好的 ip:port

image-20230317200453615

这边已经到头了,转回去分析之前的另一条分支: Statusode 为 -1 时执行的 PostLogin,会有两个新的按钮,一个是 rawhistory,这个跟 password 无关,我们关注另一个 transfer_button,会先执行如下代码。

image-20230318151331373

最终会调用 restClient.dotransfer,此时传入的参数会比之前正常登陆多出三个:fromAccount、toAccount、amount,应该是实现的一个转账功能。

image-20230318151528525

然后进行跟之前类似的处理:访问的页面变成了 /transfer,然后也是进入的 postHttpContent 进行 post 传参。

image-20230318151654663

看看其他的功能,先看 fill_data,非常简单,会从 mySharedPreferences 中读取账号密码输入到账号和密码的输入框中,不存在则默认为 Null。

image-20230317212104246

然后是 Remember Me 的勾选框,跟 fill_data 的功能是相对应,是一个把账号密码保存在 mySharedPreferences 的操作。但是会多一个将密码加一层 base64 的操作。

image-20230317212325932

其他的部分就没涉及到 password 的操作了。

问题及解决

问题一

显示已经安装 HAXM,但是创建安卓虚拟机时却显示未安装。

image-20230309151055599

image-20230309150838150

解决:

找到 Your SDK path\extras\intel\Hardware_Accelerated_Execution_Manager 目录下的 haxm-7.6.5-setup.exe,双击安装他即可。

image-20230309153804785

成功解决问题。

image-20230309153901156

问题二

不存在 -p 的参数。。也许是版本问题。

image-20230309230350062

删去即可。

1
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED

问题三

安装好 Android Studio 一定要先随便创个项目,然后运行一下,看是否能够运行 apk!!!!!!我踩了这个巨坑,因为第一个 task 做就是后台,我一直以为是代码问题,结果去随便创个项目,发现 apk 无法运行在 ADV 上。

解决:直接删了重下。

问题四

我的 Android Studio 必须点击用管理员运行才行,不然会一直问题报错 adb 无法正常运行。

image-20230310194655038

问题五

运行时报了一个错,显示是索引超出了范围。

image-20230313155656757

去搜了一下,byte 在 java 里的范围是 -128127,不是无符号的,但是这边需要的是无符号的数字,所以能表示 0255 即可。

image-20230313161522660

问题六

重打包时,遇到报错如下,根据是因为 res 资源文件的问题,所以在解包时就加上 -r 参数,选择不解包资源文件即可。

image-20230315224040421

image-20230316194743569

知识补充

  • adb 命令简单使用:
    • adb start-server开启 adb 服务。
    • adb kill-server关闭 adb 服务。
    • adb -P <port> start-server 指定 adb server 的网络端口port(默认为5037)启动服务。
    • adb devices 查看adb 连接设备。
    • adb shell am broadcast [options] <INTENT>
      • -a 指定 action,如 android.intent.action.VIEW
      • -c 指定 category,如 android.intent.category.APP_CONTACTS
      • -n 指定完整 component 名,用于明确指定启动哪个 Activity,如
        com.example.app/.ExampleActivity
  • 匿名重写:一种简洁代码的写法,直接进行重写方法,然后把返回的对象作为参数进行传递。
  • java 反射:指在运行时(runtime)动态地获取一个类的信息并操作它的属性、方法和构造函数等。因此我们也可以借此来访问到 private 的属性和方法。
    • java.lang.reflect.Field 用于获取类的属性。
    • java.lang.reflect.Method 用于获取类的方法。
    • 访问 private 的属性与方法的流程如下:
      1. Class<?> x = Class.forName("class_name"); class_name 是要获取的类的名字(写全名)。
      2. Field filed = x.getDeclaredField("class_filed"); or Method method = x.getDeclaredMethod("class_method"); 实例化想获取的属性或方法。
      3. filed.setAccessible(true); or method.setAccessible(true); 设置允许访问。
      4. class_name y = new class_name(); 实例化一个 class_name 的对象 y。
      5. field.get(y); or method.invoke(y); 最终获取属性或调用方法
      6. 对于方法,如果存在参数,则要在 2 步骤中进行说明, 以及在 5 步骤的调用中传入。

smali

smali :一种介于 apk 和 java 源码之间的一种表示方式。使用 apktool 反编译 apk 后,会在反编译工程目录下生成一个 smali文件夹,一般而言,一个 smali 文件对应着一个类。

在 smali 代码中,声明语句一般都是以 . 开始。

基本信息

1
2
3
4
.class <访问权限> [非权限修饰符] <类名>
.super <父类名>
.source <源文件名>
.implements <接口名称>
  • <>中的内容表示必不可缺的,[]表示的是可选择的。
  • 访问权限修饰符即public, protected, private, default
  • 而非权限修饰符则指的是final, abstract
  • L表示类的完整签名,如.super Ljava/lang/Object; 表明是继承于 java/lang/Object 类。
  • 经过混淆后,.source 可能为空。

类型对应

1
2
3
4
5
6
7
8
9
10
11
12
13
B—byte
C—char
D—double
F—float
I—int
J—long
S—short
V—void
Z—boolean
[—array
L-对象类型
注:如果是int数组表示形式为[I。
再比如数组类型String[][],在smali中表示形式为[[Ljava/lang/String;

寄存器

一个方法所申请的寄存器会分配给函数方法的参数 (parameter) 以及局部变量 (local variable) 。在 smali 中,一般有两种命名规则

  • v 命名法
  • p 命名法

假设方法申请了 m+n 个寄存器,其中局部变量占 m 个寄存器,参数占 n 个寄存器,对于不同的命名规则,其相应的命名如下:

属性 v命名法 p命名法
局部变量 v_0,v_1,...,v_{m-1} v_0,v_1,...,v_{m-1}
函数参数 v_m,v_{m+1},...,v_{m+n} p_0,p_1,...,p_{n-1}

一般来说都是 p 命名法,因为其具有较好的可读性,可以方便地让我们知道寄存器属于哪一种类型。

需要注意的是,在非 static 方法中,p0 表示this,p1 才表示第一个参数。

类变量声明

1
2
3
4
5
6
.field <访问权限> <变量名>:<变量类型>
例如:.field private str1:Ljava/lang/String;
等于 private java.lang.String str1;

局部变量声明:
.local <初始值>, <变量名>:<变量类型>

类方法声明

1
2
3
4
5
6
7
8
.method <访问权限> <方法名>(参数类型)<返回值类型>
[.registers] // 寄存器个数=field个数+local个数
[.prologue] // 指定代码开始位置
[.param] // 指定方法参数
[.line] // 指定代码在源代码中的行数,混淆后可能不存在
[.locals] // 使用的局部变量个数
<代码体>
.end method

运算

java 代码 smali 语法
a += b; add-int/2addr v0, v1
a -= b; sub-int/2addr v0, v1
a *= b; mul-int/2addr v0, v1
a /= b; div-int/2addr v0, v1
a %= b; rem-int/2addr v0, v1
a &= b; and-int/2addr v0, v1
a |= b; or-int/2addr v0, v1
a ^= b; xor-int/2addr v0, v1
a <<= b; shl-int/2addr v0, v1
a >>= b; shr-int/2addr v0, v1
a >>>= b; ushr-int/2addr v0, v1

注意,如果是如 add-int/lit8 vx, vy, lit8 的指令,即是让 vy 加上 lit8,将结果保存到 vx。

赋值

常量赋值:

1
2
3
4
const                   v0, 0x7F030018  	# R.layout.activity_challenge   #从R中取出静态值
const/4 v3, 0x2 # 4也可以换成16或者high16,表示取整数值
const-string v2, "Challenge" # 取字符串
const-class v2, Context # 把类对象取出

变量间赋值:

1
2
3
4
move  			vx, vy   		           # 将vy的值赋值给vx,也可以是move-object等
move-result vx # 将上个方法调用后的结果赋值给vx,也可以是move-result-object
return-object vx # 将vx的对象作为函数返回值
new-instance v0, ChallengePagerAdapter # 实例化一个对象存入v0中

对象赋值:

1
2
3
4
5
6
7
8
9
动态字段操作:
iput-object a,(this),b # 将a的值给b,一般用于b的初始化
iget-object a,(this),b # 将b的值给a,一般用于获取b的地址,接着调用它

静态字段操作:
sput
sget

并且-后面是可以跟其他的类型的如:wide、boolean、byte、char、short

函数操作

1
2
3
4
5
6
7
1.private:invoke-direct
2.public|protected: invoke-virtual
3.static:invoke-static
4.parent: invoke-super
基本调用形式:invoke-xxx {参数}, 类;->函数(参数原型)
第一个参数是类的实例化对象
后续的参数则是调用的函数中的传递参数

判断

都是单词的缩写: eq -> equal,lt -> less than, gt -> grater than, z -> zero

1
2
3
4
5
6
7
8
9
10
11
12
13
if-eq vA, vB, :cond_X   	如果vA等于vB则跳转到:cond_X
if-ne vA, vB, :cond_X 如果vA不等于vB则跳转到:cond_X
if-lt vA, vB, :cond_X 如果vA小于vB则跳转到:cond_X
if-ge vA, vB, :cond_X 如果vA大于等于vB则跳转到:cond_X
if-gt vA, vB, :cond_X 如果vA大于vB则跳转到:cond_X
if-le vA, vB, :cond_X 如果vA小于等于vB则跳转到:cond_X

if-eqz vA, :cond_X 如果vA等于0则跳转到:cond_X
if-nez vA, :cond_X 如果vA不等于0则跳转到:cond_X
if-ltz vA, :cond_X 如果vA小于0则跳转到:cond_X
if-gez vA, :cond_X 如果vA大于等于0则跳转到:cond_X
if-gtz vA, :cond_X 如果vA大于0则跳转到:cond_X
if-lez vA, :cond_X 如果vA小于等于0则跳转到:cond_X

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public void encrypt(String str) {
String ans = "";
for (int i = 0 ; i < str.length(); i++){
ans += str.charAt(i);
}
Log.e("ans:", ans);
}
<=对应smali=>

# public void encrypt(String str) {
.method public encrypt(Ljava/lang/String;)V
.locals 4
.param p1, "str" # Ljava/lang/String;
.prologue

# String ans = "";
const-string v0, ""
.local v0, "ans":Ljava/lang/String;

# for (int i 0 ; i < str.length();i++){
# int i=0 =>v1
const/4 v1, 0x0
.local v1, "i":I
:goto_0 # for_start_place

# str.length()=>v2
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v2

# i<str.length()
if-ge v1, v2, :cond_0

# ans += str.charAt(i);
# str.charAt(i) => v2
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual{v2,v0},Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2

#str.charAt(i) => v3
invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C
move-result v3

# ans += v3 =>v0
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0

# i++
add-int/lit8 v1, v1, 0x1
goto :goto_0

# Log.e("ans:",ans);
:cond_0
const-string v2, "ans:"
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method

switch 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public void encrypt(int flag) {
String ans = null;
switch (flag){
case 0:
ans = "ans is 0";
break;
default:
ans = "noans";
break;
}
Log.v("ans:",ans);
}
<=对应smali=>
#public void encrypt(int flag) {
.method public encrypt(I)V
.locals 2
.param p1, "flag" # I
.prologue

#String ans = null;
const/4 v0, 0x0
.local v0, "ans":Ljava/lang/String;

#switch (flag){
packed-switch p1, :pswitch_data_0 # pswitch_data_0指定case区域的开头及结尾

#default: ans="noans"
const-string v0, "noans"

#Log.v("ans:",ans)
:goto_0
const-string v1, "ans:"
invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
return-void

#case 0: ans="ans is 0"
:pswitch_0 #pswitch_<case的值>
const-string v0, "ans is 0"
goto :goto_0 # break
nop
:pswitch_data_0 #case区域的结束
.packed-switch 0x0 #定义case的情况
:pswitch_0 #case 0
.end packed-switch
.end method

其中case定义情况有两种:

  • ```smali
    从0开始递增
    packed-switch p1, :pswitch_data_0

    :pswitch_data_0
    .packed-switch 0x0
    :pswitch_0
    :pswitch_1 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    - ```smali
    无规则switch
    sparse-switch p1,:sswitch_data_0
    ...
    sswitch_data_0
    .sparse-switch
    0xa -> : sswitch_0
    0xb -> : sswitch_1 # 字符会转化成数组

try-catch 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void encrypt(int flag) {
String ans = null;
try {
ans = "ok!";
} catch (Exception e){
ans = e.toString();
}
Log.d("error",ans);
}
<=对应smali=>
#public void encrypt(int flag) {
.method public encrypt(I)V
.locals 3
.param p1, "flag" # I
.prologue

#String ans = null;
const/4 v0, 0x0
.line 20
.local v0, "ans":Ljava/lang/String;

#try { ans="ok!"; }
:try_start_0 # 第一个try开始,
const-string v0, "ok!"
:try_end_0 # 第一个try结束(主要是可能有多个try)
.catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

#Log.d("error",ans);
:goto_0
const-string v2, "error"
invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void

#catch (Exception e){ans = e.toString();}
:catch_0 #第一个catch
move-exception v1
.local v1, "e":Ljava/lang/Exception;
invoke-virtual {v1}, Ljava/lang/Exception;->toString()Ljava/lang/String;
move-result-object v0
goto :goto_0
.end method

补充

  • array-length vA, vB 获取给定 vB 寄存器中数组的长度并赋给 vA 寄存器,数组长度指的是数组中的元素个数。
  • aget-object vx, vy, vz 获取对象引用数组的对象引用值到 vx 中。该数组由 vy 引用并由 vz 索引。
  • aput-object vx, vy, vz 将 vx 中的对象引用值放入对象引用数组的元素中。元素由 vz 索引,数组对象由 vy 引用。
  • aput vx,vy,vz 将 vx 中的整数值放入整数数组的一个元素中。元素由 vz 索引,数组对象由 vy 引用。
  • new-array vx, vy, type_id 生成一个 type_id 类型和 vy 元素大小的新数组,并将对该数组的引用放入 vx。

参考

https://www.anquanke.com/post/id/85035

https://blog.csdn.net/u012184539/article/details/82720885

https://ctf-wiki.org/android/basic_operating_mechanism/java_layer/smali/smali/

Lab2

Step1

导入 soot jar 包。

image-20230327140348837

编写代码运行即可反编译出jimple文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import soot.*;
import soot.options.Options;
import java.util.Collections;

public class test {
public static void main(String[] args) {
G.reset();
Options.v().set_src_prec(Options.src_prec_class);
Options.v().set_process_dir(Collections.singletonList("C:/Users/守城/Desktop/Step1/Lab_1"));
Options.v().set_whole_program(true);
Options.v().set_allow_phantom_refs(true);
Options.v().set_prepend_classpath(true);
Options.v().set_output_format(Options.output_format_jimple);
Scene.v().loadNecessaryClasses();
// PackManager.v().runPacks();
PackManager.v().writeOutput();
}
}

放在同一目录下的sootOutput文件夹里。

image-20230327140529063

选一部分的jimple如下,在有smali的基础上再看这个是很简单,十分易懂。

image-20230327162355481

将jimple代码还原成java代码如下,感觉没有什么特别的地方。

image-20230327162335904

Step2

可以使用 Scene.v().getApplicationClasses() 和 Scene.v().getClasses() 获取类,但是这两个方法有些区别:

  • Scene.v().getApplicationClasses() 方法返回一个集合,其中包含所有在分析目标应用程序中定义的应用程序类(即不包含Java 库或系统类)
  • Scene.v().getClasses() 方法返回一个包含所有类的集合,包括已加载和未加载的类。这个方法返回的集合可能会很大,并且可能包含许多不相关的类。

配置 soot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
G.reset();
Options.v().set_prepend_classpath(true);
Options.v().set_allow_phantom_refs(true);
Options.v().set_keep_line_number(true);
Options.v().set_src_prec(Options.src_prec_apk);
Options.v().set_process_dir(Collections.singletonList("C:/Users/守城/Desktop/Step1/Lab_2/test(2).apk"));
Options.v().set_android_jars("D:/Android SDK/platforms");
Options.v().set_whole_program(true);
List<String> excludeList = new ArrayList<>();
excludeList.add("java.*");
excludeList.add("javax.*");
Options.v().set_exclude(excludeList);
Options.v().set_output_format(Options.output_format_jimple);
Scene.v().loadNecessaryClasses();

打印输出某一个类中所有的 method 和 field,field 不一定会存在,换了几个类才找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int i = 0;
for (SootClass processedClass : Scene.v().getClasses()){
if (i < 3){
i++;
continue;
}
int j = 0;
for (SootMethod method : processedClass.getMethods()){
System.out.println("Method" + j + ":" + method.getName());
j++;
}
int k = 0;
System.out.println("Field size:" + processedClass.getFields().size());
for (SootField field : processedClass.getFields()){
System.out.println("Field" + k + ":" + field.getName());
k++;
}
break;
}

结果如下:

image-20230331110719799

首先,我们明白 soot 中的语句管理是依靠 Body 实现的,所以需要使用 retrieveActiveBody() 获得到其的 body(方法体) ,然后再获取 body 中的 unit(语句的表示) 进行逐一打印。

1
2
3
4
5
SootMethod processedMethod = processedClass.getMethods().get(30);
System.out.println("Units:");
for (Unit unit : processedMethod.retrieveActiveBody().getUnits()){
System.out.println(unit);
}

结果如下:

image-20230331190236898

输出一个类的父类及其子类,父类有直接提供方法,子类的话没有直接的方法,在一番寻找下,可以使用 Scene.v().getOrMakeFastHierarchy() 得到一个 FastHierarchy 对象,这个对象用来表示类之间的继承关系,所以可以通过该对象查询类是否有子类,如下所示:

1
2
3
4
5
6
7
8
if (processedClass.hasSuperclass()){
System.out.println("Superclass:" + processedClass.getSuperclass().getName());
}
Collection<SootClass> subClasses = Scene.v().getOrMakeFastHierarchy().getSubclassesOf(processedClass);
System.out.println("Subclass size:" + subClasses.size());
for (SootClass subClass : subClasses) {
System.out.println("Subclass:" + subClass.getName());
}

另一种是 ppt 上的,通过自己去遍历 SootClass,逐一比对是否父类为我们所需要分析的类。

1
2
3
4
5
6
HashSet<SootClass> subClasses = new HashSet<>();
for (SootClass sootClass : Scene.v().getClasses()){
if (sootClass.hasSuperclass() && sootClass.getSuperclass().equals(processedClass)){
subClasses.add(sootClass);
}
}

结果如下:

image-20230331191501599

Step3

主要参考 http://www.iro.umontreal.ca/~dufour/cours/ift6315/docs/soot-tutorial.pdf 37-55

配置soot,并获取class的SootClass

1
2
3
4
5
6
7
8
9
G.reset();
Options.v().set_src_prec(Options.src_prec_class);
Options.v().set_process_dir(Collections.singletonList("C:\\Users\\守城\\Desktop\\Step2\\Lab3"));
Options.v().set_whole_program(true);
Options.v().set_allow_phantom_refs(true);
Options.v().set_prepend_classpath(true);
Options.v().set_output_format(Options.output_format_jimple);
Scene.v().loadNecessaryClasses();
SootClass sootClass = Scene.v().getSootClass("lab3");

因为是分析活跃变量,需要使用后向分析,继承 BackwardFlowAnalysis 接口,实现构造函数以及一些抽象方法,这边我使用的是java中的Set,也可以使用soot定义的FlowSet进行集合运算的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static class MyBackwardAnalysis extends BackwardFlowAnalysis<Unit, Set<String>> {
public MyBackwardAnalysis(UnitGraph graph) {
super(graph);
doAnalysis();
}

@Override
protected Set<String> newInitialFlow() {
return new HashSet<>();
}

@Override
protected Set<String> entryInitialFlow() {
return new HashSet<>();
}

@Override
protected void merge(Set<String> in1, Set<String> in2, Set<String> out) {
out.clear();
out.addAll(in1);
out.addAll(in2);
}

@Override
protected void copy(Set<String> source, Set<String> dest) {
dest.clear();
dest.addAll(source);
}

@Override
protected void flowThrough(Set<String> in, Unit unit, Set<String> out) {
out.clear();
out.addAll(in);
for (ValueBox box : unit.getDefBoxes()) {
Value value = box.getValue();
if (value instanceof Local) {
out.remove(((Local) value).getName());
}
}
for (ValueBox box : unit.getUseBoxes()){
Value value = box.getValue();
if (value instanceof Local){
out.add(((Local) value).getName());
}
}
}
}

最终进行结果的输出,因为是要对语句的前后活跃变量进行输出,所以要打印的是 unit。还有就是getFlowBefore得到的输出是按正向分析的FlowBefore。

1
2
3
4
5
6
7
8
9
10
for (SootMethod sootMethod : sootClass.getMethods()) {
System.out.println("Analyzing method: " + sootMethod.getName());
CompleteUnitGraph graph = new CompleteUnitGraph(sootMethod.retrieveActiveBody());
MyBackwardAnalysis myBackwardAnalysis = new MyBackwardAnalysis(graph);
for (Unit unit : graph) {
Set<String> flowBefore = myBackwardAnalysis.getFlowBefore(unit);
Set<String> flowAfter = myBackwardAnalysis.getFlowAfter(unit);
System.out.println("Before: " + flowAfter + " | After: " + flowBefore + " | Statement: " + unit);
}
}

结果如下:

image-20230402152829777

过程内的常量分析

处理常量传播,使用正向分析,大部分代码跟上面的差不多:改变了参数的类型,由Set换成了Map,因为要多存储一个标签,所以需要键值对;以及就是flowThroug的实现有区别,因为这个方法才是主要的处理逻辑。

引入isConstant表示在赋值语句中是否能被认为是常量。先判断了x = y - y这种最特殊的情况,遇到了就直接设置isConstant为true,接着跳出循环,第二个判断是当存在一个量为NAC时,就为NAC,这样分为两大类 x + 1 和 1 + x :如果是 x + 1,则直接设为false,跳出循环;如果是 1 + x,哪怕设置为了true,但是循环还在继续,仍然可以设为false。

接着是第三类情况UNDEF,我想了一下,如果对于常量来说,应该是只有除0这个情况了,但是这个情况,在过程内是不存在的,编译器已经筛掉了显式的除0,所以有可能触发这个情况,只能是执行方法后返回了0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
protected void flowThrough(Map<String, String> in, Unit unit, Map<String, String> out) {
out.clear();
out.putAll(in);
boolean isConstant = false;
for (ValueBox box : unit.getUseBoxes()){
Value value = box.getValue();
// 判断 x = y - y 这种特殊情况
if (unit instanceof SubExpr && ((JSubExpr) unit).getOp1Box().getValue() == ((JSubExpr) unit).getOp2Box().getValue()){
isConstant = true;
break;
}
if (out.containsKey(value.toString()) && out.get(value.toString()).equals("NAC")){
isConstant = false;
break;
}
if (value instanceof Constant){
isConstant = true;
}
}
if (isConstant){
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), "Constant");
}
}else {
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), "NAC");
}
}
}

image-20230403102013849

进一步对一些基础运算包括(加、减、乘、除、取余、与、或、异或、取反、左移、右移等)增加了一些判断。其中加、减、乘进了溢出判断;除和取余判断了除数是否为0;与运算、或运算、异或运算判断了-1和0这两种特殊情况;取反判断了当类型为type和char时会出现的异常计算;左移和右移判断了移位的位数是否为负以及是否过大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
 // 判断加法
if (unit instanceof AddExpr){
ValueBox box1 = ((AddExpr) unit).getOp1Box();
ValueBox box2 = ((AddExpr) unit).getOp2Box();
}
// 判断减法
if (unit instanceof SubExpr){
ValueBox box1 = ((JSubExpr) unit).getOp1Box();
ValueBox box2 = ((JSubExpr) unit).getOp2Box();
overflow(box1, box2);
}
// 判断乘法
if (unit instanceof MulExpr){
ValueBox box1 = ((MulExpr) unit).getOp1Box();
ValueBox box2 = ((MulExpr) unit).getOp2Box();
if (box1 instanceof IntConstant && box2 instanceof IntConstant){
IntConstant num1 = (IntConstant) box1.getValue();
IntConstant num2 = (IntConstant) box2.getValue();
if (num1.value > 0 && num2.value > 0 && num1.value * num2.value < 0 ){
System.out.println("存在溢出");
}
}else if (box1 instanceof FloatConstant && box2 instanceof IntConstant){
FloatConstant num1 = (FloatConstant) box1.getValue();
IntConstant num2 = (IntConstant) box2.getValue();
if(Math.round(num1.value) > 0 && num2.value > 0 && Math.round(num1.value) * num2.value < 0 ){
System.out.println("存在溢出");
}
}else if (box1 instanceof IntConstant && box2 instanceof FloatConstant){
IntConstant num1 = (IntConstant) box1.getValue();
FloatConstant num2 = (FloatConstant) box2.getValue();
if(num1.value > 0 && Math.round(num1.value) > 0 && num1.value * Math.round(num2.value) < 0 ){
System.out.println("存在溢出");
}
}
}
// 判断除法
if (unit instanceof DivExpr){
IntConstant op2 = (IntConstant) ((DivExpr) unit).getOp2();
if (op2.value == 0){
System.out.println("除0异常");
}
}
// 判断取余
if (unit instanceof RemExpr){
IntConstant op2 = (IntConstant) ((RemExpr) unit).getOp2();
if (op2.value == 0){
System.out.println("除0异常");
}
}
// 判断与运算
if (unit instanceof AndExpr){
IntConstant op1 = (IntConstant) ((AndExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((AndExpr) unit).getOp2();
if (op1.value == 0 || op2.value == 0){
System.out.println("存在有操作数为0,直接返回0");
}else if (op1.value == -1){
System.out.println("op1为-1,直接返回op2");
}else if (op2.value == -1){
System.out.println("op2为-1,直接返回op1");
}
}
// 判断或运算
if (unit instanceof OrExpr){
IntConstant op1 = (IntConstant) ((OrExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((OrExpr) unit).getOp2();
if (op1.value == -1 || op2.value == -1){
System.out.println("存在有操作数为-1,直接返回-1");
}else if (op1.value == 0){
System.out.println("op1为0,直接返回op2");
}else if (op2.value == 0){
System.out.println("op2为0,直接返回op1");
}
}
// 判断异或运算
if (unit instanceof XorExpr){
IntConstant op1 = (IntConstant) ((XorExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((JXorExpr) unit).getOp2();
if (op1.value == -1){
System.out.println("op1为-1,对op2做取反");
} else if (op2.value == -1){
System.out.println("op2为-1,对op1做取反");
} else if (op1.value == 0){
System.out.println("op1为0,直接返回op2");
} else if (op2.value == 0){
System.out.println("op2为0,直接返回op1");
}
}
// 判断取反运算
if (unit instanceof NegExpr){
Value value = ((NegExpr) unit).getOp();
if (value instanceof TypeConstants){
System.out.println("type类型取反可能为负");
}else if (value instanceof StyleConstants.CharacterConstants){
System.out.println("char类型取反存在异常");
}
}
// 判断左移
if (unit instanceof ShlExpr){
IntConstant op2 = (IntConstant) ((ShlExpr) unit).getOp2();
if (op2.value > 32){
System.out.println("位操作数过大!");
}else if (op2.value < 0){
System.out.println("位操作数为负!");
}
}
// 判断右移
if (unit instanceof ShrExpr){
IntConstant op2 = (IntConstant) ((ShrExpr) unit).getOp2();
if (op2.value > 32){
System.out.println("位操作数过大!");
}else if (op2.value < 0){
System.out.println("位操作数为负!");
}
}

Step4

首先使用config.setCodeEliminationMode(InfoflowConfiguration.CodeEliminationMode.NoCodeElimination);关闭FlowDroid的自带的死代码消除功能。

死代码消除的过程中,可以从以下三个方面入手:

  1. 控制流分析:可以通过soot的控制流图分析,找出不可达代码块,即无法到达的代码块,进而消除这些代码块。
  2. 活跃变量分析:通过活跃变量分析,可以找出不会影响程序执行结果的变量,进而消除与这些变量相关的代码。
  3. 常量传播:通过常量传播,可以将变量的值替换为常量,从而消除与变量相关的代码。

第一步我先做了活跃变量分析,去除无效赋值代码。在进行活跃变量分析时,我们先要确定需要分析的类,Soot会把其他的类也引入进来,但是我们实际上是只需要分析应用程序的代码的,所以要先把其他无关的类过滤掉。

1
2
3
// 去除无需分析的androidx、R类
Collection<SootClass> toAnalysisClasses = Scene.v().getApplicationClasses();
toAnalysisClasses.removeIf(sootClass -> sootClass.getName().startsWith("androidx") || sootClass.getName().startsWith("com.example.lab_code.R") || sootClass.getName().startsWith("com.example.lab_code.BuildConfig"));

BuildConfig是这些内容所以也不需要分析。

image-20230409164903653

过滤后只剩下这些我们需要分析的类,当然这里的dummyMainClass要记得略过。

image-20230409164807529

活跃变量的后向分析实现代码可以直接使用上次做的,不需要进行更改。主要在于应该怎样判定一条赋值语句是否是无效赋值呢?一番思索后,决定:我们从后向分析的角度出发,逐条的对unit进行判断,获取当前判断的unit中被定义的values(通过getDefBoxs()方法),将其与流向这条unit前的存在的活跃变量作比较,如果该value(只要有一个)是活跃变量,则该语句是有效赋值,否则就是无效赋值。

value instanceof InstanceFieldRef && liveVariable.equals(((InstanceFieldRef) value).getBase().toString())这条语句判断的是init的语句,因为如果是字段的赋值,所获取到的value是属于InstanceFieldRef 的,因此要对这种赋值语句进行对比,这样就不会把字段的赋值语句认为是无效赋值语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 活跃变量分析去除无效赋值
for (SootClass sootClass : toAnalysisClasses){
if (sootClass.getName().equals("dummyMainClass")){
continue;
}
for (SootMethod sootMethod : sootClass.getMethods()) {
CompleteUnitGraph graph = new CompleteUnitGraph(sootMethod.retrieveActiveBody());
LiveAnalysis liveAnalysis = new LiveAnalysis(graph);
for (Unit unit : graph) {
boolean toRemove = false;
Set<String> flowAfter = liveAnalysis.getFlowAfter(unit);
for (ValueBox box : unit.getDefBoxes()) {
Value value = box.getValue();
for (String liveVariable : flowAfter){
if (liveVariable.equals(value.toString())){
toRemove = false;
break;
}else if (value instanceof InstanceFieldRef && liveVariable.equals(((InstanceFieldRef) value).getBase().toString())){
toRemove = false;
break;
}
toRemove = true;
}
}
if (toRemove){
System.out.println("在类: " + sootClass.getName() + " 的方法 " + sootMethod.getName() + " 中移除的语句为: " + unit);
Scene.v().getCallGraph().removeAllEdgesOutOf(unit);
// sootMethod.getActiveBody().getUnits().remove(unit);
}
}
}
}

经过一番修修补补,最终的结果是只需要删除一条语句,如下图所示。但是这个方法呢,jimple是存在的,但是jadx反编译是不存在的,初步怀疑这个方法是从未被使用过的。

image-20230409203704604

第二步,进行控制流分析,但是应该第一步做控制流分析的,先去除不被调用的方法,从而减少后续分析的内容。代码顺序已做调整,这边就不改了。

先使用 soot中CG的方法edgesOutOf或者edgesInto,前者是从被调者出发,后者是从调用者出发。完成移除未被调用的方法还是比较简单的,遍历CG图即可。

然后还有个问题就是,由于java的并发问题,无法在迭代器迭代时对于数据进行修改,所以先保存到Map中,然后再进行删除。使用Set集合,是因为键值对问题,键在Map中无法重复,所以采取了Set保存同一个SootClass中的SootMethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Map<SootClass, Set<SootMethod>> hashMap = new HashMap<>();
for (SootClass sootClass : toAnalysisClasses){
if (sootClass.getName().equals("dummyMainClass")) continue;
Set<SootMethod> sootMethods = new HashSet<>();
for (SootMethod sootMethod : sootClass.getMethods()){
boolean isCalled = false;
Iterator<Edge> iterator = Scene.v().getCallGraph().edgesInto(sootMethod);
if (iterator.hasNext()){
isCalled = true;
}
if (!isCalled){
System.out.println("在类: " + sootClass.getName() + " 的方法: " + sootMethod.getName() + " 从未被调用");
sootMethods.add(sootMethod);
}
}
hashMap.put(sootClass, sootMethods);
}

结果如下:

image-20230409214638550

由于无法直接在迭代时直接删除相应的方法,所以需要在迭代后再进行删除。

1
2
3
4
5
6
7
8
9
//删除未被调用的方法
for (Map.Entry<SootClass, Set<SootMethod>> entry : hashMap.entrySet()){
SootClass sootClass = entry.getKey();
Set<SootMethod> sootMethods = entry.getValue();
for (SootMethod sootMethod : sootMethods){
Scene.v().getSootClass(sootClass.getName()).removeMethod(sootMethod);
System.out.println("删除类: " + sootClass.getName() + " 的方法: " + sootMethod.getName());
}
}

删除后,下面的活跃变量分析就不存在结果了,因为方法被删了。

第三步进行常量传播,针对分支不可达的情况进行消除死代码。

常量传播此次进行了一系列的更改,首先是方法的初始入集,在判断完之后会添加方法的参数也进行常量传播,这边我特意使用了Map,为的是同时记录下参数的信息,选择将参数的前十个字符保存,这字符长这样:@parameter0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected Map<String, Object> entryInitialFlow() {
Map<String, Object> map = new HashMap<>();
List<Value> valueList = ((UnitGraph)graph).getBody().getParameterRefs();
for (Value value : valueList){
String className = ((RefType) value.getType()).getClassName();
if (className.equals("java.lang.Integer") || className.equals("java.lang.String")
|| className.equals("java.lang.Boolean") || className.equals("java.lang.Byte")
|| className.equals("java.lang.Character") || className.equals("java.lang.Double")
|| className.equals("java.lang.Float") || className.equals("java.lang.Long")
|| className.equals("java.lang.Short")){
map.put(value.toString(), value.toString().substring(0, 11));
}
}
return map;
}

merge改动,追求准确,采取并集,同时这边因为难以确定具体的值是多少,所以我选择将其记录为Constant,只进行标记为常量。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void merge(Map<String, Object> in1, Map<String, Object> in2, Map<String, Object> out) {
for (Map.Entry<String, Object> entry1 : in1.entrySet()){
String parameter1 = entry1.getKey();
for (Map.Entry<String, Object> entry2 : in2.entrySet()){
String parameter2 = entry2.getKey();
if (parameter1.equals(parameter2)){
out.put(parameter1, "Constant");
}
}
}
}

最后就是flowThrough了,为了能够直接将常量的值直接保存(如果有的话),仔细的判断了目前能想到的基础语句包块:当个赋值语句、加、减、乘、除、取余、与、或、异或、左移、右移(没有非是因为其是一个boolean的判断,但是soot似乎没有集成这个类,所以无法判断)通过这些详细的运算,从而把常量的值真正的先一步计算出来保存。也多加了两个判断的flag记号,是因为先前的初始入集和merge时添加了两个符号,这两个也需要进行一次的判断,因为他们也属于常量,但是因为参数在内部方法是没有具体值的,所以无法直接使用Constant将其包含。merge则是因为两条分支,无法确定是采取哪条分支的值,需要后面才能确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@Override
protected void flowThrough(Map<String, Object> in, Unit unit, Map<String, Object> out) {
out.clear();
out.putAll(in);
boolean isConstant = false;
boolean isConstantValue = true;
boolean isParameter = false;
String para = null;
for (ValueBox box : unit.getUseBoxes()){
Value value = box.getValue();
// 判断 x = y - y 这种特殊情况
if (unit instanceof SubExpr && ((JSubExpr) unit).getOp1Box().getValue() == ((JSubExpr) unit).getOp2Box().getValue()){
isConstant = true;
break;
}
if (out.containsKey(value.toString()) && ((String)out.get(value.toString())).equals("NAC")){
isConstant = false;
break;
}
if (value instanceof Constant || (out.containsKey(value.toString()) && ((String)out.get(value.toString())).startsWith("@parameter"))){
isConstant = true;
if (out.containsKey(value.toString()) && ((String)out.get(value.toString())).equals("Constant")) isConstantValue = false;
if (out.containsKey(value.toString()) && ((String)out.get(value.toString())).startsWith("@parameter")){
isParameter = true;
para = ((String)out.get(value.toString())).substring(0, 11);
}
}
}
if (isConstant && isConstantValue && !isParameter){
int constantValue = 0;
// 单个赋值语句
if (unit instanceof JAssignStmt){
Value op = ((JAssignStmt)unit).getRightOp();
if (out.containsKey(op.toString())){
constantValue = (Integer)(out.get(op.toString()));
}else if (op instanceof Constant){
constantValue = ((IntConstant)op).value;
}
}
// 判断加法
if (unit instanceof AddExpr){
Value op1 = ((AddExpr) unit).getOp1();
Value op2 = ((AddExpr) unit).getOp2();
constantValue = ((IntConstant)op1).value + ((IntConstant)op2).value;
}
// 判断减法
if (unit instanceof SubExpr){
Value op1 = ((JSubExpr) unit).getOp1();
Value op2 = ((JSubExpr) unit).getOp2();
constantValue = ((IntConstant)op1).value - ((IntConstant)op2).value;
}
// 判断乘法
if (unit instanceof MulExpr){
Value op1 = ((MulExpr) unit).getOp1();
Value op2 = ((MulExpr) unit).getOp2();
constantValue = ((IntConstant)op1).value * ((IntConstant)op2).value;
}
// 判断除法
if (unit instanceof DivExpr){
Value op1 = ((DivExpr) unit).getOp1();
Value op2 = ((DivExpr) unit).getOp2();
constantValue = ((IntConstant)op1).value / ((IntConstant)op2).value;
}
// 判断取余
if (unit instanceof RemExpr){
Value op1 = ((RemExpr) unit).getOp1();
Value op2 = ((RemExpr) unit).getOp2();
constantValue = ((IntConstant)op1).value % ((IntConstant)op2).value;
}
// 判断与运算
if (unit instanceof AndExpr){
IntConstant op1 = (IntConstant) ((AndExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((AndExpr) unit).getOp2();
constantValue = op1.value & op2.value;
}
// 判断或运算
if (unit instanceof OrExpr){
IntConstant op1 = (IntConstant) ((OrExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((OrExpr) unit).getOp2();
constantValue = op1.value | op2.value;
}
// 判断异或运算
if (unit instanceof XorExpr){
IntConstant op1 = (IntConstant) ((XorExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((XorExpr) unit).getOp2();
constantValue = op1.value ^ op2.value;
}
// 判断左移
if (unit instanceof ShlExpr){
IntConstant op1 = (IntConstant) ((ShlExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((ShlExpr) unit).getOp2();
constantValue = op1.value << op2.value;
}
// 判断右移
if (unit instanceof ShrExpr){
IntConstant op1 = (IntConstant) ((ShrExpr) unit).getOp1();
IntConstant op2 = (IntConstant) ((ShrExpr) unit).getOp2();
constantValue = op1.value >> op2.value;
}
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), constantValue);
}
}else if (isConstant && !isConstantValue){
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), "Constant");
}
}else if (isConstant && isParameter){
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), para);
}
}
else {
for (ValueBox box : unit.getDefBoxes()){
Value value = box.getValue();
out.put(value.toString(), "NAC");
}
}
}

然后正式开启常量传播。依然是遍历每跳unit,然后判断其是否为branches,之后再将其细分为IfStmt、SwitchStmt。对于IfStmt因为其情况多种:大于、大等于、小于、小等于、等于,这边为了防止代码的冗长,只在小等于的情况下面写了步骤(代码近乎一致,除了在判断时有些差别)。

只做了相对简单的判断,对于merge合并传递的值并未深入判断,而对于方法的参数传递的常量有一些思考,我的思路是因为先前已经提及到会记录参数的符号,所以对比是参数传递的常量,则会开始遍历调用该方法的语句,然后因为参数符号是有保存参数顺序,所以也可以借此使用字符减去48而获得数字的顺序,进而获得到参数。但是,通过打印发现,获取到的参数也可能是个符号,所以如果真要往后做,还需要再对调用方法也做一次的常量传播(这部分没再做了,因为代码的缘故,再处理的话,嵌套层数太多了,有点丑陋),从而获取到真正的值,再判断是否有存在不可达的分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Iterator<Edge> iterator = Scene.v().getCallGraph().edgesInto(sootMethod);
while (iterator.hasNext()){
Edge edge = iterator.next();
int idx = (((String)flowBefore.get(op1.toString())).charAt(10)) - 48;
Unit callEdge = edge.srcUnit();
InvokeExpr invokeExpr = ((InvokeStmt) callEdge).getInvokeExpr();
Value arg = invokeExpr.getArg(idx);
if (((IntConstant)arg).value <= ((IntConstant)op2).value){
List<Unit> preds = graph.getPredsOf(targetUnit);
for (Unit pred : preds){
System.out.println(pred);
}
}
}

第三种情况,则是判断条件本身就是一个常量的赋值,这样的话,直接判断其是否为常量,然后就按照分好的比较条件(大于、大等于、小于、小等于、等于)进行处理不可达分支语句。

1
2
3
4
5
6
7
8
9
else if (flowBefore.containsKey(op1.toString()) &&
flowBefore.get(op1.toString()) instanceof IntConstant){
if (((IntConstant)flowBefore.get(op1.toString())).value <= ((IntConstant)op2).value){
RemoveUnit(toRemoveUnits, graph, unit);
}else {
toRemoveUnits.add(targetUnit);
RemoveUnit(toRemoveUnits, graph, targetUnit);
}
}

RemoveUnit方法是一个封装好的,通过graph获取到语句的后继语句,直至遇到goto或者return语句才停止,因为根据观察发现,无论是if还是switch语句都是这般,分支语句的结尾要不是goto要不就是return语句,因此将其作为结束的记号。同时也具有一定的共性,故将此封装成了一个方法。

1
2
3
4
5
6
7
8
9
public static void RemoveUnit(List<Unit> toRemoveUnits, CompleteUnitGraph graph, Unit targetUnit) {
Unit succ = graph.getSuccsOf(targetUnit).get(0);
while (true){
toRemoveUnits.add(succ);
if ((succ instanceof JGotoStmt) || (succ instanceof JReturnStmt)
|| (succ instanceof JReturnVoidStmt)) break;
succ = graph.getSuccsOf(succ).get(0);
}
}

switch的语句处理于if语句有一定的相似之处,差别只是在于switch处理分支语句是,有提供方法可以通过key值直接获取到其所对应的分支语句,这点对于JTableSwitchStmt和JLookupSwitchStmt都是一致的,所以不需要分开处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (unit instanceof SwitchStmt){
List<Unit> caseTargets = ((SwitchStmt) unit).getTargets();
Value key = ((SwitchStmt) unit).getKey();
if (flowBefore.containsKey(key.toString()) &&
((String)flowBefore.get(key.toString())).equals("NAC")) continue;
if (flowBefore.containsKey(key.toString()) &&
((String)flowBefore.get(key.toString())).equals("Constant")){
System.out.println("该值来自上面的分支,无法直接判断!");
}else if(flowBefore.containsKey(key.toString()) &&
((String)flowBefore.get(key.toString())).startsWith("@parameter")){
System.out.println("该变量为方法参数,无法在方法内部进行判断!");
}else if(key instanceof IntConstant){
Unit notRemove = ((SwitchStmt) unit).getTarget(((IntConstant)key).value);
for (Unit caseTarget : caseTargets){
if (notRemove.toString().equals(caseTarget.toString())) continue;
RemoveUnit(toRemoveUnits, graph, caseTarget);
}
}
}

下图是示例处理结果,可以顺利的将不可达代码添加到List中。

image-20230412212521314

在最后提及一下,我处理的数值都是直接使用了IntConstant进行处理,是因为如果真的去区分其他属于Constant的量的话,会十分复杂和繁乱,因此直接比较暴力的都转换为了IntConstant了,不过这样会将值损失一部分,例如是浮点数,则直接去除了小数部分,但是大体上是影响不大的。

Jimple代码

以下是该任务主要类的jimple代码,主要是用来更清楚看到代码呈现的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
At Class: com.example.lab_code.MainActivity Analyzing method: <init>
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.MainActivity
Before: [] | After: [r0] | Statement: specialinvoke r0.<androidx.appcompat.app.AppCompatActivity: void <init>()>()
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.MainActivity Analyzing method: class_test
Before: [] | After: [] | Statement: r0 := @this: com.example.lab_code.MainActivity
Before: [r1] | After: [] | Statement: r1 = new com.example.lab_code.Locator
Before: [r1] | After: [r1] | Statement: specialinvoke r1.<com.example.lab_code.Locator: void <init>(double,double)>(2.0, 628.0)
Before: [] | After: [r1] | Statement: virtualinvoke r1.<com.example.lab_code.Locator: void showLocation()>()
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.MainActivity Analyzing method: onCreate
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.MainActivity
Before: [ r1, r0] | After: [r0] | Statement: r1 := @parameter0: android.os.Bundle
Before: [r0] | After: [ r1, r0] | Statement: specialinvoke r0.<androidx.appcompat.app.AppCompatActivity: void onCreate(android.os.Bundle)>( r1)
Before: [r0] | After: [r0] | Statement: virtualinvoke r0.<com.example.lab_code.MainActivity: void setContentView(int)>(2131361820)
Before: [r3, r0] | After: [r0] | Statement: r3 = new com.example.lab_code.User
Before: [r3, r0] | After: [r3, r0] | Statement: specialinvoke r3.<com.example.lab_code.User: void <init>(java.lang.String,java.lang.String)>("FDU", "6008")
Before: [r0] | After: [r3, r0] | Statement: virtualinvoke r3.<com.example.lab_code.User: void showUsername()>()
Before: [ r2, r0] | After: [r0] | Statement: r2 = staticinvoke <java.lang.Integer: java.lang.Integer valueOf(int)>(10)
Before: [] | After: [ r2, r0] | Statement: virtualinvoke r0.<com.example.lab_code.MainActivity: void pathTest(java.lang.Integer)>( r2)
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.MainActivity Analyzing method: pathTest
Before: [] | After: [] | Statement: r0 := @this: com.example.lab_code.MainActivity
Before: [ r1] | After: [] | Statement: r1 := @parameter0: java.lang.Integer
Before: [ r1, i0] | After: [ r1] | Statement: i0 = virtualinvoke $r1.<java.lang.Integer: int intValue()>()
Before: [ r1] | After: [ r1, i0] | Statement: if i0 <= 10 goto r2 = virtualinvoke r1.<java.lang.Integer: java.lang.String toString()>()
Before: [ r2] | After: [ r1] | Statement: r2 = virtualinvoke r1.<java.lang.Integer: java.lang.String toString()>()
Before: [] | After: [ r2] | Statement: staticinvoke <android.util.Log: int d(java.lang.String,java.lang.String)>("Exec1", r2)
Before: [] | After: [] | Statement: return
Before: [ r2] | After: [ r1] | Statement: r2 = virtualinvoke r1.<java.lang.Integer: java.lang.String toString()>()
Before: [] | After: [ r2] | Statement: staticinvoke <android.util.Log: int d(java.lang.String,java.lang.String)>("Exec2", r2)
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.MainActivity Analyzing method: getIntent
Before: [this] | After: [] | Statement: this := @this: com.example.lab_code.MainActivity
Before: [ r0] | After: [this] | Statement: r0 = this.<com.example.lab_code.MainActivity: android.content.Intent ipcIntent>
Before: [] | After: [ r0] | Statement: return r0
At Class: com.example.lab_code.MainActivity Analyzing method: setIntent
Before: [this] | After: [] | Statement: this := @this: com.example.lab_code.MainActivity
Before: [parameter0, this] | After: [this] | Statement: parameter0 := @parameter0: android.content.Intent
Before: [] | After: [parameter0, this] | Statement: this.<com.example.lab_code.MainActivity: android.content.Intent ipcIntent> = parameter0
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.MainActivity Analyzing method: setResult
Before: [this] | After: [] | Statement: this := @this: com.example.lab_code.MainActivity
Before: [this] | After: [this] | Statement: parameter0 := @parameter0: int
Before: [this, parameter1] | After: [this] | Statement: parameter1 := @parameter1: android.content.Intent
Before: [] | After: [this, parameter1] | Statement: this.<com.example.lab_code.MainActivity: android.content.Intent ipcResultIntent> = parameter1
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.Locator Analyzing method: <init>
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.Locator
Before: [ d0, r0] | After: [r0] | Statement: d0 := @parameter0: double
Before: [ d0, d1, r0] | After: [ d0, r0] | Statement: d1 := @parameter1: double
Before: [ d0, d1, r0] | After: [ d0, d1, r0] | Statement: specialinvoke r0.<java.lang.Object: void <init>()>()
Before: [ d1, r0] | After: [ d0, d1, r0] | Statement: r0.<com.example.lab_code.Locator: double latitude> = d0
Before: [] | After: [ d1, r0] | Statement: r0.<com.example.lab_code.Locator: double longtitude> = d1
Before: [] | After: [] | Statement: return

At Class: com.example.lab_code.Locator Analyzing method: showLocation
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.Locator
Before: [ r2, r0] | After: [r0] | Statement: r2 = new java.lang.StringBuilder
Before: [ r2, r0] | After: [ r2, r0] | Statement: specialinvoke $r2.<java.lang.StringBuilder: void <init>()>()
Before: [ d0, r2, r0] | After: [ r2, r0] | Statement: d0 = r0.<com.example.lab_code.Locator: double latitude>
Before: [ r2, r0] | After: [ d0, r2, r0] | Statement: virtualinvoke r2.<java.lang.StringBuilder: java.lang.StringBuilder append(double)>($d0)
Before: [ r2, r0] | After: [ r2, r0] | Statement: virtualinvoke $r2.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(" : ")
Before: [ d0, r2] | After: [ r2, r0] | Statement: d0 = r0.<com.example.lab_code.Locator: double longtitude>
Before: [ r2] | After: [ d0, r2] | Statement: virtualinvoke r2.<java.lang.StringBuilder: java.lang.StringBuilder append(double)>($d0)
Before: [ r1] | After: [ r2] | Statement: r1 = virtualinvoke r2.<java.lang.StringBuilder: java.lang.String toString()>()
Before: [] | After: [ r1] | Statement: staticinvoke <android.util.Log: int d(java.lang.String,java.lang.String)>("Location", r1)
Before: [] | After: [] | Statement: return
At Class: com.example.lab_code.User Analyzing method: <init>
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.User
Before: [ r1, r0] | After: [r0] | Statement: r1 := @parameter0: java.lang.String
Before: [ r1, r2, r0] | After: [ r1, r0] | Statement: r2 := @parameter1: java.lang.String
Before: [ r1, r2, r0] | After: [ r1, r2, r0] | Statement: specialinvoke r0.<java.lang.Object: void <init>()>()
Before: [ r2, r0] | After: [ r1, r2, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String username> = r1
Before: [] | After: [ r2, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String password> = r2
Before: [] | After: [] | Statement: return
At Class: com.example.lab_code.User Analyzing method: changePassword
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.User
Before: [ r1, r0] | After: [r0] | Statement: r1 := @parameter0: java.lang.String
Before: [] | After: [ r1, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String password> = r1
Before: [] | After: [] | Statement: return
At Class: com.example.lab_code.User Analyzing method: showUsername
Before: [r0] | After: [] | Statement: r0 := @this: com.example.lab_code.User
Before: [ r1] | After: [r0] | Statement: r1 = r0.<com.example.lab_code.User: java.lang.String username>
Before: [] | After: [ r1] | Statement: staticinvoke <android.util.Log: int d(java.lang.String,java.lang.String)>("Field Username", r1)
Before: [] | After: [] | Statement: return

参考

FlowDroid官方的死代码消除https://github.com/secure-software-engineering/FlowDroid/blob/develop/soot-infoflow/src/soot/jimple/infoflow/codeOptimization/DeadCodeEliminator.java

问题及解决

问题一

出现一直没有输出,后找到原因是因为配置 Options.v().set_process_dir() 需要提供的是具体到某一个 apk 的路径,跟之前分析class 不同,分析 class 给的路径是要求 class 所在的目录,解决方法如下:

1
Options.v().set_process_dir(Collections.singletonList("C:/Users/守城/Desktop/Step1/Lab_2/test(2).apk"));

问题二

出现没找到 active body 的报错。

image-20230331122201671

因为我刚开始使用的是 getActiveBody(),解决办法是转而使用 retrieveActiveBody(),这两个方法的区别如下:

  • retrieveActiveBody() 会在需要的时候动态地获取并构造方法体。如果方法体已经存在,则直接返回该方法体。如果方法体不存在,则会尝试使用 Soot 中的分析器或者其他方法来构造方法体。
  • getActiveBody() 会假设方法体已经存在并被正确地加载。如果方法体不存在,则返回 null。

问题三

出现如下报错,这个问题是因为没能正确加载到要分析的类,然后我使用循环直接读取了 Scene.v().getClasses(),这里是会加载到其他的标准类的,所以就会出现有些是没有没方法体的。

image-20230401213958772

接着这个问题,没能正确加载要分析的类。需要如下的布置才行,其中要先读取目录生成 Scene,然后再读取我们需要分析的类,这边的类名是单纯的名字,不可以加后缀名,这个方法是可以换成 Scene.v().getSootClass(“lab3”); 这个方法的。

1
2
3
4
5
6
7
8
9
G.reset();
Options.v().set_src_prec(Options.src_prec_class);
Options.v().set_process_dir(Collections.singletonList("C:\\Users\\守城\\Desktop\\Step2\\Lab3"));
Options.v().set_whole_program(true);
Options.v().set_allow_phantom_refs(true);
Options.v().set_prepend_classpath(true);
Options.v().set_output_format(Options.output_format_jimple);
Scene.v().loadNecessaryClasses();
SootClass sootClass = Scene.v().loadClassAndSupport("lab3");

问题四

在Java中,当一个线程正在迭代一个集合,而另一个线程修改了该集合时,就有可能出现该异常。

image-20230409164507872

如下

image-20230409170509007

去掉这句sootMethod.getActiveBody().getUnits().remove(unit);即可。

问题五

查看了一下活跃变量去除的无效赋值语句,发现构造方法的一些赋值也被删除了,这显然不合理。

image-20230409172649157

再认真查考的时候,发现其实是代码逻辑的问题,在判断是否为无效赋值时有问题,修改正确后就不存在问题了。

问题六

这个问题应该是在于我直接使用value,toString(),但是getDefBoxes对于这类调用语句时,返回的不止是r0,还有后面连带着的签名。但是呢,有意思的在于,如果是使用getUseBoxes,返回的就是r0了。所以直接做活跃变量时不存在问题,但是这边想要使用getDefBoxes去进行判断就要出问题了。

image-20230409195704430

所以多加一条语句进行判断。

1
2
3
4
5
if (value instanceof InstanceFieldRef && 
liveVariable.equals(((InstanceFieldRef) value).getBase().toString())){
toRemove = false;
break;
}

问题七

在做常量传播时,从CFG图得到的参数是一种RefType,没办法与IntType等进行 instanceof判断,而此时的value也一样,都属于Ref类型的,所以不管是那种都是返回一个false。

image-20230411205549016

选择通过判断返回的类名进行对比。

1
2
3
4
5
6
7
8
9
10
for (Value value : valueList){
String className = ((RefType) value.getType()).getClassName();
if (className.equals("java.lang.Integer") || className.equals("java.lang.String")
|| className.equals("java.lang.Boolean") || className.equals("java.lang.Byte")
|| className.equals("java.lang.Character") || className.equals("java.lang.Double")
|| className.equals("java.lang.Float") || className.equals("java.lang.Long")
|| className.equals("java.lang.Short")){
flowSet.add(value.toString());
}
}

问题八

如果语句是return,返回值是空的,那么类型是属于JReturnVoidStmt的,而不属于JReturnStmt,通过succ.getClass().getName()而获得。

查看评论