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 的属性与方法的流程如下:
Class<?> x = Class.forName("class_name");
class_name 是要获取的类的名字(写全名)。
Field filed = x.getDeclaredField("class_filed");
or Method method = x.getDeclaredMethod("class_method");
实例化想获取的属性或方法。
filed.setAccessible(true);
or method.setAccessible(true);
设置允许访问。
class_name y = new class_name();
实例化一个 class_name 的对象 y。
field.get(y);
or method.invoke(y);
最终获取属性或调用方法
对于方法,如果存在参数,则要在 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 中,一般有两种命名规则
假设方法申请了 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 const/4 v3, 0x2 const-string v2, "Challenge" const-class v2, Context
变量间赋值:
1 2 3 4 move vx, vy move-result vx return-object vx new-instance v0, ChallengePagerAdapter
对象赋值:
1 2 3 4 5 6 7 8 9 动态字段操作:iput-object a,(this),b iget-object a,(this),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_Xif-ne vA, vB, :cond_X 如果vA不等于vB则跳转到:cond_Xif-lt vA, vB, :cond_X 如果vA小于vB则跳转到:cond_Xif-ge vA, vB, :cond_X 如果vA大于等于vB则跳转到:cond_Xif-gt vA, vB, :cond_X 如果vA大于vB则跳转到:cond_Xif-le vA, vB, :cond_X 如果vA小于等于vB则跳转到:cond_Xif-eqz vA, :cond_X 如果vA等于0则跳转到:cond_Xif-nez vA, :cond_X 如果vA不等于0则跳转到:cond_Xif-ltz vA, :cond_X 如果vA小于0则跳转到:cond_Xif-gez vA, :cond_X 如果vA大于等于0则跳转到:cond_Xif-gtz vA, :cond_X 如果vA大于0则跳转到:cond_Xif-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=>.method public encrypt(Ljava/lang/String; )V .locals 4 .param p1, "str" .prologue const-string v0, "" .local v0, "ans" :Ljava/lang/String; const/4 v1, 0x0.local v1, "i" :I:goto_0 invoke-virtual {p1}, Ljava/lang/String; ->length()Imove-result v2 if-ge v1, v2, :cond_0 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 invoke-virtual {p1, v1}, Ljava/lang/String; ->charAt(I)C move-result v3invoke-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 v0add-int/lit8 v1, v1, 0x1goto :goto_0:cond_0 const-string v2, "ans:" invoke-static {v2, v0}, Landroid/util/Log; ->e(Ljava/lang/String; Ljava/lang/String; )Ireturn-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=>.method public encrypt(I)V .locals 2 .param p1, "flag" .prologue const/4 v0, 0x0 .local v0, "ans" :Ljava/lang/String; packed-switch p1, :pswitch_data_0 const-string v0, "noans" :goto_0 const-string v1, "ans:" invoke-static {v1, v0}, Landroid/util/Log; ->v(Ljava/lang/String; Ljava/lang/String; )I return-void :pswitch_0 const-string v0, "ans is 0" goto :goto_0 nop :pswitch_data_0 .packed -switch 0x0 :pswitch_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=>.method public encrypt(I)V .locals 3 .param p1, "flag" .prologue const/4 v0, 0x0 .line 20 .local v0, "ans" :Ljava/lang/String; :try_start_0 const-string v0, "ok!" :try_end_0 .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0 } :catch_0 :goto_0 const-string v2, "error" invoke-static {v2, v0}, Landroid/util/Log; ->d(Ljava/lang/String; Ljava/lang/String; )I return-void :catch_0 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().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(); 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的自带的死代码消除功能。
死代码消除的过程中,可以从以下三个方面入手:
控制流分析:可以通过soot的控制流图分析,找出不可达代码块,即无法到达的代码块,进而消除这些代码块。
活跃变量分析:通过活跃变量分析,可以找出不会影响程序执行结果的变量,进而消除与这些变量相关的代码。
常量传播:通过常量传播,可以将变量的值替换为常量,从而消除与变量相关的代码。
第一步我先做了活跃变量分析,去除无效赋值代码。在进行活跃变量分析时,我们先要确定需要分析的类,Soot会把其他的类也引入进来,但是我们实际上是只需要分析应用程序的代码的,所以要先把其他无关的类过滤掉。
1 2 3 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); } } } }
经过一番修修补补,最终的结果是只需要删除一条语句,如下图所示。但是这个方法呢,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(); 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()
而获得。