利用Soot对APK插桩实践
一、使用soot插装apk应用程序
1、编写apk
首先编写一个需要被插装代码的apk,为了方便起见,编写的apk非常简单,只有一个Activity,未插装前代码如下:
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Toast toast=Toast.makeText(MainActivity.this,"toast message", Toast.LENGTH_LONG);
//toast.show();
//Log.i("test", "log infor");
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(Menu.NONE, Menu.FIRST + 1, 0, "设置");
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
/**
* 在onCreateOptionsMenu执行后,菜单被显示前调用;如果菜单已经被创建,则在菜单显示前被调用。 同样的,
* 返回true则显示该menu,false 则不显示; (可以通过此方法动态的改变菜单的状态,比如加载不同的菜单等) TODO
* Auto-generated method stub
*/
return super.onPrepareOptionsMenu(menu);
}
@Override
public void onOptionsMenuClosed(Menu menu) {
/**
* 每次菜单被关闭时调用. (菜单被关闭有三种情形,menu按钮被再次点击、back按钮被点击或者用户选择了某一个菜单项) TODO
* Auto-generated method stub
*/
super.onOptionsMenuClosed(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) //得到被点击的item的itemId
{
case Menu.FIRST + 1: //对应的ID就是在add方法中所设定的Id
break;
case Menu.FIRST + 2:
break;
}
return true;
}
}
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id=" @+id/menu_setting " android:title="设置"></item>
</menu>
apk运行在虚拟机中的样子如图所示:
2、导入jar包
file->Project Structure
点击SDK,点击加号,选中soot.jar
之后就可以使用soot包中的内容了
3、编写test.java插桩的脚本:
我们插装的目的是在函数onCreate函数里的setContentView()函数之后插入log消息,即插入Log.i("test","log infor!")语句。
我们新建一个Java项目,导入soot-trunk.jar包(nightly-build版本),编写代码如下
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import soot.Body;
import soot.BodyTransformer;
import soot.G;
import soot.Pack;
import soot.PackManager;
import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.Transform;
import soot.Unit;
import soot.jimple.InvokeStmt;
import soot.jimple.Jimple;
import soot.jimple.StaticInvokeExpr;
import soot.jimple.Stmt;
import soot.jimple.StringConstant;
import soot.options.Options;
public class test {
public final static String androidJar="/Users/wzx/Downloads/android-platforms-master";
public final static String APK="/Users/wzx/Desktop/first.apk";
public static void initsoot(String[] args){
//G.reset();
Options.v().set_allow_phantom_refs(true);//设置允许伪类(Phantom class),指的是soot为那些在其classpath找不到的类建立的模型
Options.v().set_prepend_classpath(true);//prepend the VM's classpath to Soot's own classpath
Options.v().set_output_format(Options.output_format_dex);//设置soot的输出格式
Options.v().set_android_jars(androidJar);//设置android jar包路径
Options.v().set_src_prec(Options.src_prec_apk);
Options.v().set_process_dir(Collections.singletonList(APK));
Options.v().set_force_overwrite(true);
//Options.v().set_soot_classpath("");
Scene.v().loadNecessaryClasses();
//call soot.Main
//soot.Main.main(args);
}
public static void main(String[] args) {
initsoot(args);
PackManager.v().getPack("jtp").add(
new Transform("jtp.MyTransform", new MyTransform()));//添加自己的BodyTransformer
PackManager.v().runPacks();
PackManager.v().writeOutput();
}
// custom bodyTransformer
static class MyTransform extends BodyTransformer {
@Override
protected void internalTransform(Body arg0, String arg1,
Map<String, String> arg2) {
Iterator<Unit> unitsIterator = arg0.getUnits().snapshotIterator();//获取Body里所有的units,一个Body对应Java里一个方法的方法体,Unit代表里面的语句
while (unitsIterator.hasNext()) {
Stmt stmt = (Stmt) unitsIterator.next();//将Unit强制转换为Stmt,Stmt为Jimple里每条语句的表示
if (stmt.containsInvokeExpr()) {//如果是一条调用语句
String declaringClass = stmt.getInvokeExpr().getMethod().getDeclaringClass().getName();//获取这个方法所属的类
String methodName = stmt.getInvokeExpr().getMethod().getName();//获取这个方法的名称
if (methodName.equals("onCreate")) {
List<Unit> toastUnits = makeToast(arg0, "toast infor!");
arg0.getUnits().insertAfter(toastUnits, stmt);//在这条语句之后插入Toast消息
break;
}
}
}
}
private List<Unit> makeToast(Body body, String toast) {
List<Unit> unitsList = new ArrayList<Unit>();
//插入语句Log.i("test",toast);
SootClass logClass = Scene.v().getSootClass("android.util.Log");//获取android.util.Log类
SootMethod sootMethod = logClass.getMethod("int i(java.lang.String,java.lang.String)");
StaticInvokeExpr staticInvokeExpr = Jimple.v().newStaticInvokeExpr(sootMethod.makeRef(), StringConstant.v("test"), StringConstant.v(toast));
InvokeStmt invokeStmt = Jimple.v().newInvokeStmt(staticInvokeExpr);
unitsList.add(invokeStmt);
return unitsList;
}
}
}
运行之后,其会在sootOutput目录下生成一个同名字的apk,本例中first.apk,记得给这个apk签名(也可以不签名)
我们用 dex2jar 反编译这个apk
产生了jar包文件
用jd-gui 查看它代码究竟有没有插入成功,如下:
可见已经插入成功了。
4、签名
首先生成 a.keystore
(base) ➜ Desktop keytool -genkey -alias a.keystore -keyalg RSA -validity 20000 -keystore a.keystore
输入密钥库口令:
再次输入新口令:
您的名字与姓氏是什么?
[Unknown]: w
您的组织单位名称是什么?
[Unknown]: w
您的组织名称是什么?
[Unknown]: w
您所在的城市或区域名称是什么?
[Unknown]: w
您所在的省/市/自治区名称是什么?
[Unknown]: w
该单位的双字母国家/地区代码是什么?
[Unknown]: w
CN=w, OU=w, O=w, L=w, ST=w, C=w是否正确?
[否]: y
输入 <a.keystore> 的密钥口令
(如果和密钥库口令相同, 按回车):
再次输入新口令:
Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore a.keystore -destkeystore a.keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。
接着对未签名的包进行签名。
jarsigner -verbose -keystore a.keystore -signedjar first_output.apk first.apk a.keystore
此时就可以安装在android虚拟机中了。