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())) {new  Intent(context, SecretService.class);
在获取位置时,需要添加权限声明:
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)  if  (ActivityCompat.checkSelfPermission(this , Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTEDthis , Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {return  flags;0 , 0 , this );new  Handler(Looper.getMainLooper());new  Runnable() {@Override public  void  run ()  if  (lastLocation != null ) {"getAccuracy:%s\ngetLatitude: %s\ngetLongitude: %s" , lastLocation.getAccuracy(), lastLocation.getLatitude(), lastLocation.getLongitude());3000 );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)  new  Thread(new  Runnable() {@Override public  void  run ()  new  Runnable() {@Override public  void  run ()  
效果展示:
image-20230310204429962 
Task 3 了解一下 Java 反射的知识,使用 Method 和 Filed 进行操作需要访问的类的方法和成员变量即可完成任务:
1 2 3 4 5 6 7 8 Class<?> poRELabClass = Class.forName("com.pore.mylibrary.PoRELab" );"curStr" );"privateMethod" , String.class, String.class);true );true );new  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,如  
 
 
 
匿名重写:一种简洁代码的写法,直接进行重写方法,然后把返回的对象作为参数进行传递。 
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—byteLjava/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  <访问权限> <变量名>:<变量类型> private  str1:Ljava/lang/String;  private  java.lang.String str1;.local  <初始值>, <变量名>:<变量类型>
类方法声明 1 2 3 4 5 6 7 8 .method  <访问权限> <方法名>(参数类型)<返回值类型>.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 
函数操作 1 2 3 4 5 6 7 1.private:invoke-direct invoke-virtual  invoke-super 
判断 都是单词的缩写: 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) {"" ;"ans:" , ans);.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>()VLjava/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) {"ans is 0" ;"noans" ;"ans:" ,ans);.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:pswitch_0
:pswitch_1 
1 2 3 4 5 6 7 8 9  sparse-switch  p1,:sswitch_data_0  . ..  .sparse -switch :  sswitch_0 :  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) {"ok!" ;"error" ,ans);.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)  "C:/Users/守城/Desktop/Step1/Lab_1" ));true );true );true );
放在同一目录下的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();true );true );true );"C:/Users/守城/Desktop/Step1/Lab_2/test(2).apk" ));"D:/Android SDK/platforms" );true );new  ArrayList<>();"java.*" );"javax.*" );
打印输出某一个类中所有的 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 ){continue ;int  j = 0 ;for  (SootMethod method : processedClass.getMethods()){"Method"  + j + ":"  + method.getName());int  k = 0 ;"Field size:"  + processedClass.getFields().size());for  (SootField field : processedClass.getFields()){"Field"  + k + ":"  + field.getName());break ;
结果如下:
image-20230331110719799 
首先,我们明白 soot 中的语句管理是依靠 Body 实现的,所以需要使用 retrieveActiveBody() 获得到其的 body(方法体) ,然后再获取 body 中的 unit(语句的表示) 进行逐一打印。
1 2 3 4 5 SootMethod processedMethod = processedClass.getMethods().get(30 );"Units:" );for  (Unit unit : processedMethod.retrieveActiveBody().getUnits()){
结果如下:
image-20230331190236898 
输出一个类的父类及其子类,父类有直接提供方法,子类的话没有直接的方法,在一番寻找下,可以使用 Scene.v().getOrMakeFastHierarchy() 得到一个 FastHierarchy 对象,这个对象用来表示类之间的继承关系,所以可以通过该对象查询类是否有子类,如下所示:
1 2 3 4 5 6 7 8 if  (processedClass.hasSuperclass()){"Superclass:"  + processedClass.getSuperclass().getName());"Subclass size:"  + subClasses.size());for  (SootClass subClass : subClasses) {"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)){
 结果如下:
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();"C:\\Users\\守城\\Desktop\\Step2\\Lab3" ));true );true );true );"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);@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)  @Override protected  void  copy (Set<String> source, Set<String> dest)  @Override protected  void  flowThrough (Set<String> in, Unit unit, Set<String> out)  for  (ValueBox box : unit.getDefBoxes()) {if  (value instanceof  Local) {for  (ValueBox box : unit.getUseBoxes()){if  (value instanceof  Local){
最终进行结果的输出,因为是要对语句的前后活跃变量进行输出,所以要打印的是 unit。还有就是getFlowBefore得到的输出是按正向分析的FlowBefore。
1 2 3 4 5 6 7 8 9 10 for  (SootMethod sootMethod : sootClass.getMethods()) {"Analyzing method: "  + sootMethod.getName());new  CompleteUnitGraph(sootMethod.retrieveActiveBody());new  MyBackwardAnalysis(graph);for  (Unit unit : graph) {"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)  boolean  isConstant = false ;for  (ValueBox box : unit.getUseBoxes()){if  (unit instanceof  SubExpr && ((JSubExpr) unit).getOp1Box().getValue() == ((JSubExpr) unit).getOp2Box().getValue()){true ;break ;if  (out.containsKey(value.toString()) && out.get(value.toString()).equals("NAC" )){false ;break ;if  (value instanceof  Constant){true ;if  (isConstant){for  (ValueBox box : unit.getDefBoxes()){"Constant" );else  {for  (ValueBox box : unit.getDefBoxes()){"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){if  (unit instanceof  SubExpr){if  (unit instanceof  MulExpr){if  (box1 instanceof  IntConstant && box2 instanceof  IntConstant){if  (num1.value > 0  && num2.value > 0  && num1.value * num2.value < 0  ){"存在溢出" );else  if  (box1 instanceof  FloatConstant && box2 instanceof  IntConstant){if (Math.round(num1.value) > 0   && num2.value > 0  && Math.round(num1.value) * num2.value < 0  ){"存在溢出" );else  if  (box1 instanceof  IntConstant && box2 instanceof  FloatConstant){if (num1.value > 0   && Math.round(num1.value) > 0  && num1.value * Math.round(num2.value) < 0  ){"存在溢出" );if  (unit instanceof  DivExpr){if  (op2.value == 0 ){"除0异常" );if  (unit instanceof  RemExpr){if  (op2.value == 0 ){"除0异常" );if  (unit instanceof  AndExpr){if  (op1.value == 0  || op2.value == 0 ){"存在有操作数为0,直接返回0" );else  if  (op1.value == -1 ){"op1为-1,直接返回op2" );else  if  (op2.value == -1 ){"op2为-1,直接返回op1" );if  (unit instanceof  OrExpr){if  (op1.value == -1  || op2.value == -1 ){"存在有操作数为-1,直接返回-1" );else  if  (op1.value == 0 ){"op1为0,直接返回op2" );else  if  (op2.value == 0 ){"op2为0,直接返回op1" );if  (unit instanceof  XorExpr){if  (op1.value == -1 ){"op1为-1,对op2做取反" );else  if  (op2.value == -1 ){"op2为-1,对op1做取反" );else  if  (op1.value == 0 ){"op1为0,直接返回op2" );else  if  (op2.value == 0 ){"op2为0,直接返回op1" );if  (unit instanceof  NegExpr){if  (value instanceof  TypeConstants){"type类型取反可能为负" );else  if  (value instanceof  StyleConstants.CharacterConstants){"char类型取反存在异常" );if  (unit instanceof  ShlExpr){if  (op2.value > 32 ){"位操作数过大!" );else  if  (op2.value < 0 ){"位操作数为负!" );if  (unit instanceof  ShrExpr){if  (op2.value > 32 ){"位操作数过大!" );else  if  (op2.value < 0 ){"位操作数为负!" );
Step4 首先使用config.setCodeEliminationMode(InfoflowConfiguration.CodeEliminationMode.NoCodeElimination);关闭FlowDroid的自带的死代码消除功能。
死代码消除的过程中,可以从以下三个方面入手:
控制流分析:可以通过soot的控制流图分析,找出不可达代码块,即无法到达的代码块,进而消除这些代码块。 
活跃变量分析:通过活跃变量分析,可以找出不会影响程序执行结果的变量,进而消除与这些变量相关的代码。 
常量传播:通过常量传播,可以将变量的值替换为常量,从而消除与变量相关的代码。 
 
第一步我先做了活跃变量分析,去除无效赋值代码。在进行活跃变量分析时,我们先要确定需要分析的类,Soot会把其他的类也引入进来,但是我们实际上是只需要分析应用程序的代码的,所以要先把其他无关的类过滤掉。
1 2 3 "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()) {new  CompleteUnitGraph(sootMethod.retrieveActiveBody());new  LiveAnalysis(graph);for  (Unit unit : graph) {boolean  toRemove = false ;for  (ValueBox box : unit.getDefBoxes()) {for  (String liveVariable : flowAfter){if  (liveVariable.equals(value.toString())){false ;break ;else  if  (value instanceof  InstanceFieldRef && liveVariable.equals(((InstanceFieldRef) value).getBase().toString())){false ;break ;true ;if  (toRemove){"在类: "  + sootClass.getName() + " 的方法 "  + sootMethod.getName() + " 中移除的语句为: "  + 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 ;new  HashSet<>();for  (SootMethod sootMethod : sootClass.getMethods()){boolean  isCalled = false ;if  (iterator.hasNext()){true ;if  (!isCalled){"在类: "  + sootClass.getName() + " 的方法: "  + sootMethod.getName() + " 从未被调用" );
结果如下:
image-20230409214638550 
由于无法直接在迭代时直接删除相应的方法,所以需要在迭代后再进行删除。
1 2 3 4 5 6 7 8 9 for  (Map.Entry<SootClass, Set<SootMethod>> entry : hashMap.entrySet()){for  (SootMethod sootMethod : sootMethods){"删除类: "  + 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 ()  new  HashMap<>();for  (Value value : valueList){if  (className.equals("java.lang.Integer" ) || className.equals("java.lang.String" )"java.lang.Boolean" ) || className.equals("java.lang.Byte" )"java.lang.Character" ) || className.equals("java.lang.Double" )"java.lang.Float" ) || className.equals("java.lang.Long" )"java.lang.Short" )){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()){for  (Map.Entry<String, Object> entry2 : in2.entrySet()){if  (parameter1.equals(parameter2)){"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)  boolean  isConstant = false ;boolean  isConstantValue = true ;boolean  isParameter = false ;null ;for  (ValueBox box : unit.getUseBoxes()){if  (unit instanceof  SubExpr && ((JSubExpr) unit).getOp1Box().getValue() == ((JSubExpr) unit).getOp2Box().getValue()){true ;break ;if  (out.containsKey(value.toString()) && ((String)out.get(value.toString())).equals("NAC" )){false ;break ;if  (value instanceof  Constant || (out.containsKey(value.toString()) && ((String)out.get(value.toString())).startsWith("@parameter" ))){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" )){true ;0 , 11 );if  (isConstant && isConstantValue && !isParameter){int  constantValue = 0 ;if  (unit instanceof  JAssignStmt){if  (out.containsKey(op.toString())){else  if  (op instanceof  Constant){if  (unit instanceof  AddExpr){if  (unit instanceof  SubExpr){if  (unit instanceof  MulExpr){if  (unit instanceof  DivExpr){if  (unit instanceof  RemExpr){if  (unit instanceof  AndExpr){if  (unit instanceof  OrExpr){if  (unit instanceof  XorExpr){if  (unit instanceof  ShlExpr){if  (unit instanceof  ShrExpr){for  (ValueBox box : unit.getDefBoxes()){else  if  (isConstant && !isConstantValue){for  (ValueBox box : unit.getDefBoxes()){"Constant" );else  if  (isConstant && isParameter){for  (ValueBox box : unit.getDefBoxes()){else  {for  (ValueBox box : unit.getDefBoxes()){"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()){int  idx = (((String)flowBefore.get(op1.toString())).charAt(10 )) - 48 ;if  (((IntConstant)arg).value <= ((IntConstant)op2).value){for  (Unit pred : preds){
第三种情况,则是判断条件本身就是一个常量的赋值,这样的话,直接判断其是否为常量,然后就按照分好的比较条件(大于、大等于、小于、小等于、等于)进行处理不可达分支语句。
1 2 3 4 5 6 7 8 9 else  if  (flowBefore.containsKey(op1.toString()) &&instanceof  IntConstant){if  (((IntConstant)flowBefore.get(op1.toString())).value <= ((IntConstant)op2).value){else  {
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)  0 );while  (true ){if  ((succ instanceof  JGotoStmt) || (succ instanceof  JReturnStmt)instanceof  JReturnVoidStmt)) break ;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){if  (flowBefore.containsKey(key.toString()) &&"NAC" )) continue ;if  (flowBefore.containsKey(key.toString()) &&"Constant" )){"该值来自上面的分支,无法直接判断!" );else  if (flowBefore.containsKey(key.toString()) &&"@parameter" )){"该变量为方法参数,无法在方法内部进行判断!" );else  if (key instanceof  IntConstant){for  (Unit caseTarget : caseTargets){if  (notRemove.toString().equals(caseTarget.toString())) continue ;
下图是示例处理结果,可以顺利的将不可达代码添加到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>@this : com.example.lab_code.MainActivityvoid  <init>()>()return @this : com.example.lab_code.MainActivitynew  com.example.lab_code.Locatorvoid  <init>(double ,double )>(2.0 , 628.0 )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
r1, r0] | After: [r0] | Statement:  
 
  
 r1 := @parameter0 : android.os.Bundle
r1, r0] | Statement: specialinvoke r0.<androidx.appcompat.app.AppCompatActivity: void onCreate(android.os.Bundle)>( 
 
 
  
  
 r1)void  setContentView (int ) >(2131361820 ) Before: [r3, r0] | After: [r0] | Statement: r3  = new  com.example.lab_code.Uservoid  <init>(java.lang.String,java.lang.String)>("FDU" , "6008" )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
r1] | After: [] | Statement:  
 
  
 r1 := @parameter0 : java.lang.Integer
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
r0] | After: [this] | Statement:  
 
  
 r0 = this .<com.example.lab_code.MainActivity: android.content.Intent ipcIntent>
r0] | Statement: return  
 
  
 r0this ] | After: [] | Statement: this  := @this : com.example.lab_code.MainActivitythis ] | After: [this ] | Statement: parameter0 := @parameter0 : android.content.Intentthis ] | Statement: this .<com.example.lab_code.MainActivity: android.content.Intent ipcIntent> = parameter0return this ] | After: [] | Statement: this  := @this : com.example.lab_code.MainActivitythis ] | After: [this ] | Statement: parameter0 := @parameter0 : int this , parameter1] | After: [this ] | Statement: parameter1 := @parameter1 : android.content.Intentthis , parameter1] | Statement: this .<com.example.lab_code.MainActivity: android.content.Intent ipcResultIntent> = parameter1return @this : com.example.lab_code.Locator
d0, r0] | After: [r0] | Statement:  
 
  
 d0 := @parameter0 : double 
d0,  
 
  
 d1, r0] | After: [
d0, r0] | Statement:  
 
  
 d1 := @parameter1 : double 
d0,  
 
  
 d1, r0] | After: [
d0,  
 
  
 d1, r0] | Statement: specialinvoke r0.<java.lang.Object: void  <init>()>()
d1, r0] | After: [ 
 
  
 d0, 
d1, r0] | Statement: r0.<com.example.lab_code.Locator: double latitude> =  
 
 
  
 
  
 
  
 
 d0
d1, r0] | Statement: r0.<com.example.lab_code.Locator: double longtitude> =  
 
 
  
 
  
 
  
 
 d1return @this : com.example.lab_code.Locator
r2, r0] | After: [r0] | Statement:  
 
  
 r2 = new  java.lang.StringBuilder
r2, r0] | After: [ 
 
  
 r2, r0] | Statement: specialinvoke $r2.<java.lang.StringBuilder: void  <init>()>()
d0,  
 
  
 r2, r0] | After: [
r2, r0] | Statement:  
 
  
 d0 = r0.<com.example.lab_code.Locator: double  latitude>
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>
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
r1, r0] | After: [r0] | Statement:  
 
  
 r1 := @parameter0 : java.lang.String
r1,  
 
  
 r2, r0] | After: [
r1, r0] | Statement:  
 
  
 r2 := @parameter1 : java.lang.String
r1,  
 
  
 r2, r0] | After: [
r1,  
 
  
 r2, r0] | Statement: specialinvoke r0.<java.lang.Object: void  <init>()>()
r2, r0] | After: [ 
 
  
 r1, 
r2, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String username> =  
 
 
  
 
  
 
  
 
 r1
r2, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String password> =  
 
 
  
 
  
 
  
 
 r2return @this : com.example.lab_code.User
r1, r0] | After: [r0] | Statement:  
 
  
 r1 := @parameter0 : java.lang.String
r1, r0] | Statement: r0.<com.example.lab_code.User: java.lang.String password> =  
 
 
  
 
  
 
  
 
 r1return @this : com.example.lab_code.User
r1] | After: [r0] | Statement:  
 
  
 r1 = r0.<com.example.lab_code.User: java.lang.String username>
r1] | Statement: staticinvoke <android.util.Log: int d(java.lang.String,java.lang.String)>("Field Username",  
 
  
 r1)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();"C:\\Users\\守城\\Desktop\\Step2\\Lab3" ));true );true );true );"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 && 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){if  (className.equals("java.lang.Integer" ) || className.equals("java.lang.String" )"java.lang.Boolean" ) || className.equals("java.lang.Byte" )"java.lang.Character" ) || className.equals("java.lang.Double" )"java.lang.Float" ) || className.equals("java.lang.Long" )"java.lang.Short" )){
问题八 如果语句是return,返回值是空的,那么类型是属于JReturnVoidStmt的,而不属于JReturnStmt,通过succ.getClass().getName()而获得。