逆向利器:Xposed
玩了8年的安卓,终于学会了一些软件破解,写了自己的第一个Xposed模块。中午看见老师讲课的手机里有Magisk,就问老师是不是玩酷安,结果发现居然都是16年入坑酷安的,直接变成酷友😎说起来挺有意思的,也不知道为什么就对安卓这么感兴趣,初中换recovery刷rom,高中开始接触Magisk和Xposed,大学自学了安卓开发,现在又开始学安卓逆向和Xposed开发,可能以后也会继续玩下去吧,只是在这个系统越发封闭的趋势下不知道还能玩多久就是了,希望小米跟一加撑住(
signed.apk 逆向
环境配置
程序脱壳
加固分析
打开 mt 管理器,点击 sign.apk 查看相关信息,可看到程序是用梆梆加固进行了加壳。可以看到里面的代码是完全不可分析的状态。
脱壳
将编译好的 ApkSehlling 安装在手机上并使用它对 signed.apk 进行脱壳,得到如下文件:
我们打开最大的文件进行查看,找到其中的 MainActivity,即可获得 flag:
jstz.apk 逆向
静态分析
MainActivity
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
package an.droid.j; import android.os.Bundle; import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { long df_all; long t; long t1; long t2; int zygote = 0x50e28da8; public native String j(int i); public native int p(int i); protected void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(0x7f09001c); Button button = (Button) findViewById(0x7f070022); Button button2 = (Button) findViewById(0x7f070023); TextView textView = (TextView) findViewById(0x7f07008f); Handler handler = new Handler(); MainActivity$1 r3 = new MainActivity$1(this, textView, handler); button.setOnClickListener( new MainActivity$2(this, handler, r3) ); button2.setOnClickListener( new MainActivity$3(this, handler, r3, textView) ); } static { System.loadLibrary("j"); } }
主要功能如下:
- 设置按钮监听
- 实例化其余 Activity
- 加载 libj.so
- 声明 libj.so 中的两个 native 方法
在安卓的架构里,分为 java 层和 native 层。java 层负责大部分代码逻辑的处理,native 层通过 C/C++ 提供更加快速的处理并让 ART 虚拟机支持更多扩展。从 java 层对 native 层进行调用的时候需要用到 JNI 调用,也就是这里声明的
native
关键字。MainActivity$1
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
package an.droid.j; import android.os.Handler; import android.widget.TextView; class MainActivity$1 implements Runnable { final MainActivity this$0; final Handler val$hd; final TextView val$tv1; MainActivity$1(MainActivity mainActivity, TextView textView, Handler handler) { this.this$0 = mainActivity; this.val$tv1 = textView; this.val$hd = handler; } @Override public void run() { this.this$0.t2 = System.currentTimeMillis(); long j = this.this$0.t2 - this.this$0.t1; MainActivity mainActivity = this.this$0; mainActivity.df_all = mainActivity.t2 - this.this$0.t; if (j > 100) { this.this$0.t1 += 100; MainActivity mainActivity2 = this.this$0; mainActivity2.zygote = mainActivity2.p(mainActivity2.zygote); this.val$tv1.setText(String.format( "Time:%.1f", Double.valueOf( Math.floor(this.this$0.df_all / 100) / 10.0d))); } this.val$hd.postDelayed(this, 30L); } }
主要功能如下:
- 每隔 100 ms 进行调用 libj.so 中的
p()
方法,对zygote
进行迭代 - 迭代后将其显示在 TextView 上
- 每隔 100 ms 进行调用 libj.so 中的
MainActivity$2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
package an.droid.j; import android.os.Handler; import android.view.View; class MainActivity$2 implements View.OnClickListener { final MainActivity this$0; final Handler val$hd; final Runnable val$rn; MainActivity$2(MainActivity mainActivity, Handler handler, Runnable runnable) { this.this$0 = mainActivity; this.val$hd = handler; this.val$rn = runnable; } @Override public void onClick(View view) { this.this$0.t = System.currentTimeMillis(); MainActivity mainActivity = this.this$0; mainActivity.t1 = mainActivity.t; this.val$hd.postDelayed(this.val$rn, 0L); } }
主要功能如下:
- 重写
onClick()
, 当 start 按钮被按下时计算与开始时间的差值
- 重写
MainActivity$3
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
package an.droid.j; import android.os.Handler; import android.view.View; import android.widget.TextView; class MainActivity$3 implements View.OnClickListener { final MainActivity this$0; final Handler val$hd; final Runnable val$rn; final TextView val$tv1; MainActivity$3(MainActivity mainActivity, Handler handler, Runnable runnable, TextView textView) { this.this$0 = mainActivity; this.val$hd = handler; this.val$rn = runnable; this.val$tv1 = textView; } @Override public void onClick(View view) { this.val$hd.removeCallbacks(this.val$rn); if (this.this$0.df_all / 100 != 99999) { if (this.this$0.df_all / 100 < 9999) { this.val$tv1.setText("too soon"); return; } else { this.val$tv1.setText("too late"); return; } } TextView textView = this.val$tv1; StringBuilder sb = new StringBuilder(); sb.append("flag{"); MainActivity mainActivity = this.this$0; sb.append(mainActivity.j(mainActivity.zygote)); sb.append("}"); textView.setText(sb.toString()); } }
主要功能如下:
- 判断累计时间是否在
9999900
到9999999
之间 - 符合条件则调用 libj.so 中的
j()
方法,传入zygote
更新获取 flag - 不符合则输出相应提示
- 判断累计时间是否在
分析
根据上述函数的静态分析,可以知道我们应该让程序正好停在
9999900
毫秒,并且要获得这个时刻的zygote
值。手动停止显然是不可能的,一是时间太长,二是不可能做到这么精确。因此需要对程序进行 hook。
Java 层 Hook
课堂上老师讲解的方法是通过 frida 爆破时间,获得需要的 zygote
,再用 unidbg 模拟出来一个安卓环境,在模拟器中从 libj.so 中 dump 出 flag。但是这个方法需要用到不止一个工具,配环境很麻烦,而且只要用到模拟器,效率一定不高。正巧我从初中就开始接触 Xposed 框架,去年我在可信计算课上也完整讲了一遍 Xposed 的原理,何不通过这个机会学习一下如何开发一个 Xposed 模块呢?
以下的步骤将逐步构建一个 Xposed 模块,用一个更加优雅的方式在手机上实现实时对程序进行 hook。
创建项目
在 Android Studio 中新建一个不含任何 Activity 的空项目,在 settings.gradle 中引入 Xposed 依赖库:
1 2 3 4 5 6 7 8
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url 'https://api.xposed.info/' } // 添加这一行即可 } }
进入 build.gradle (Module :app) 添加如下依赖:
1 2 3
dependencies { compileOnly 'de.robv.android.xposed:api:82' }
由于 Xposed 模块不包含 Activity,因此不含界面,也就无需主题配置,移除相关配置:
- 移除 src/res/values/themes.xml (night) 中的所有
<item>
项 - 移除 AndroidManifest.xml 中的
android:theme="xxx"
- 移除 src/res/values/themes.xml (night) 中的所有
声明 Xposed 模块
新建 src/res/values/arrays.xml,在其中配置模块作用域:
1 2 3 4 5 6
<resources> <string-array name="xposedscope" > <!-- 模块作用域应用的包名 --> <item>an.droid.j</item> </string-array> </resources>
修改 AndroidManifest.xml,加入模块声明以让框架识别出应用类别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<application OtherSettings="xxx" > <meta-data android:name="xposedmodule" android:value="true"/> <!-- 模块的简介(在框架中显示) --> <meta-data android:name="xposeddescription" android:value="一个简单的Xposed框架" /> <!-- 模块最低支持的Api版本 一般填54即可 --> <meta-data android:name="xposedminversion" android:value="54"/> <!-- 模块作用域 --> <meta-data android:name="xposedscope" android:resource="@array/xposedscope"/> </application>
最后修改启动配置,勾选
Always install with package manager
并且将Launch Options
改成Nothing
。模块编写
新建 src/main/assets/xposed_init,在其中填写入口类名,我的是
com.aaroncomo.simplexposed.MainHook
。根据上述填写的入口类名,创建相应 java 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
package com.aaroncomo.simplexposed; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage; public class MainHook implements IXposedHookLoadPackage { // 具体的hook逻辑 private void hookJ(XC_LoadPackage.LoadPackageParam lpparam) {} @Override public void handleLoadPackage( XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { // 过滤应用 if (lpparam.packageName.equals("me.kyuubiran.xposedapp")) { hookJ(lpparam); } } }
- 实现
IXposedHookLoadPackage
接口 - 重写父类方法
handleLoadPackage()
在其中找到我们的目标包名并调用hook()
来完成相关代码逻辑
OK,编译并推送到手机上,打开 LSPosed,可以看到模块界面已经出现了刚刚写的模块 SimpleXposed!第一个 Xposed 模块初步完成。选择我们的 jstz.apk 为作用域:
- 实现
hook 逻辑编写
通过上一节的分析,我们需要获取
zygote
并将时间设为9999900
。首先对
MainActivity
中的protected void onCreate(Bundle bundle)
进行 hook,如果成功的话,只要我们打开应用就会弹出一个窗口通知模块已经挂载。这里因为我们 hook 的是MainActivity
,它本身就是一个上下文所以可以直接强转为Context
,作为Toast.makeText()
的参数。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// Hook主函数,弹出提示 XposedHelpers.findAndHookMethod( "an.droid.j.MainActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); Toast.makeText( (Activity) param.thisObject, "LSPosed: 模块加载成功!", Toast.LENGTH_SHORT ).show(); } } );
编译一下,推送到手机,打开 jstz.apk,模块成功挂载:
接下来,对
MainActivity$3
中的onClick()
进行 hook,劫持结束按钮的行为。每次点击时我们之间调用 libj.so 中的p()
函数对zygote
进行迭代,最后修改累计时间val$tv1
绕过 if 函数的判断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
// Hook结束按钮,直接输出flag XposedHelpers.findAndHookMethod( "an.droid.j.MainActivity$3", lpparam.classLoader, "onClick", View.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); XposedBridge.log("Before onClick"); // 获取 MainActivity 实例 Object mainActivity$3 = param.thisObject; Object mainActivity = XposedHelpers.getObjectField(mainActivity$3, "this$0"); // 修改 df_all 字段的值,使条件总是为 true XposedHelpers.setIntField(mainActivity, "df_all", 9999900); // 计算df_all=99999时,zygote的值 zygote = XposedHelpers.getIntField(mainActivity, "zygote"); for (int i = 0; i < 99999; i++) { zygote = (int) XposedHelpers.callMethod( mainActivity, "p", zygote ); } } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); Object mainActivity$3 = param.thisObject; Object mainActivity = XposedHelpers.getObjectField(mainActivity$3, "this$0"); TextView tv1 = (TextView) XposedHelpers.getObjectField(mainActivity$3, "val$tv1"); // 修改 TextView 内容 tv1.append("\nLSPosed: Hook成功!"); tv1.append("\nzygote: " + zygote); } } );
编译运行,直接点击 END 按钮:
在不到 1s 后就可以看到显示出了
zygote
的值,这与老师用 frida 的输出一致但是效率是 frida 的近 50 倍。但是让人奇怪的是 flag,它里面是一句话,告诉我们这不是真的 flag,它还藏在其他地方。
Native 层 Hook
又回去看了一眼代码,发现 java 层已经没有任何有用的信息了,结合 MainActivity
中一直在调用 libj.so,猜测真正的 flag 应该在 native 层里。
静态分析
使用 Binary Ninja 对 libj.so 进行反编译,能够看到如下三个函数:
j()
和p()
都在应用中调用了。首先看一下j()
的逻辑。找到了我之前输出的假 flag:
FlagLostHelpMeGetItBack
,大概看了一下逻辑,没有其他的输出类代码或者 flag 构建的了。接着换到
p()
中,截取了其中一部分,看到这一堆 if 嵌套就知道静态分析已经没戏了切到控制流看一眼:
好家伙,说是电路板我都信,这个应该是经过了代码混淆的效果。不过我们已经知道了它的功能是根据当前的
zygote
状态计算下一个状态,不分析也没什么关系。这两个函数都跟 flag 没啥关系,继续对最后一个
init()
函数进行分析。最关键的代码是这里:高亮处在进行移位等操作,很快就能联想到解密操作。另外程序其实并没有调用这个函数,所以猜测这个就是真正输出 flag 的函数。
hook 逻辑编写
其实代码并不难写,就一句话:
1 2 3 4 5 6
// 获取真正flag String flag = (String) XposedHelpers.callMethod( mainActivity, "init", zygote );
在
MainActivity$3
的onClick()
执行的最后把zygote
送进init()
函数,看看返回的是不是个 flag。以防万一,还是加个异常处理:1 2 3 4 5 6 7 8 9
try { String flag = (String) XposedHelpers.callMethod( mainActivity, "init", zygote ); } catch (Exception e) { XposedBridge.log(e.getMessage()); }
编译运行,啥也没出来:
看一下后台日志:
抛出一个
java.lang.NoSuchMethodError
提示找不到init()
方法,因此调用失败了。想想也是,一个 native 方法如果没有被声明肯定是调用不到的,毕竟 JNI 只有在明确声明的情况下才会被激活。解决调用问题
- 最初的想法很简单,直接在模块里加载我放在 /sdcard 下的 lib.so 库,然后调用
init()
。尝试之后提示找不到init()
方法 - 第二次尝试把 libj.so 编译进模块里作为依赖,依旧是找不到
init()
方法 - 想起反编译的时候,
init()
全名叫Java_an_droid_j_MainActivity_init()
这个名字里是有包名的,所以猜测找不到函数体的原因就是因为我是在模块里调用的。这次开始尝试直接在原始软件里声明这个 native 方法。
- 最初的想法很简单,直接在模块里加载我放在 /sdcard 下的 lib.so 库,然后调用
声明 native 方法
最初尝试了通过 Xposed 向目标程序注入声明,但是发现没法对一个类注入函数声明(应该是有办法的,但是因为第一次写 Xposed 所以没找到),那么改个思路,反正有源码了,直接从 smali 代码入手,通过中间层代码进行注入。
可以看到 java 的语句
public native int p(int);
对应的 smali 代码是上图的形式,模仿这个形式手写一个public native String init(int);
的声明:添加完之后反编译到 java,可以看到已经成功加上了一条 native 方法声明。将应用程序重新打包并签名,覆盖之前到应用。
重启新的程序,Xposed 模块会自动挂载。点击 END 按钮,终于输出了真正的 flag:
getflag.apk 逆向
静态分析
应用分析
打开应用程序,可以看到这是个登陆界面,推测登陆成功即可输出 flag:
源码分析
代码太长了,这里就只说关键的地方了。
首先是
MainActivity.onCreate()
。这个通过 AES 算法对某个字符串进行了解密操作,并且将其存储在str
变量中。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 class MainActivity extends AppCompatActivity { private String str; public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(0x7f0b001c); TextView textView = (TextView) findViewById(0x7f08019f); TextView textView2 = (TextView) findViewById(0x7f08011c); Button materialButton = (Button) findViewById(0x7f0800cf); byte[] bArr = { 11, 22, 13, 23, 121, 32, 15, 27, 44, 35, 43, 32, 13, 24, 13, 111 }; try { this.str = decrypt( "AES/CBC/PKCS5Padding", "HyKsaPpqT4l436tHiSEXtIlLgVV4GE7mGc" + "2WoI0KlP2YhKFco7OPcJYtS58BFwDq", new SecretKeySpec( new byte[]{ 12, 32, 13, 14, 23, 108, 31, 108, 44, 121, 42, 121, 42, 113, 41, 124 }, 0, 16, "AES" ), new IvParameterSpec( new byte[]{ 12, 32, 13, 14, 23, 108, 31, 108, 44, 121, 42, 121, 42, 113, 41, 124 } ) ); } catch (Exception e) { e.printStackTrace(); } materialButton.setOnClickListener( new MainActivity$1(this, textView2, bArr, textView) ); } ... }
然后就是
MainActivity$1.onClick()
。这个函数定义了在登陆界面的用户名和密码提交时对密码与用户名进行匹配,如果成功则通过 Toast 弹出一个数值进行显示,推测就是 flag。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
class MainActivity$1 implements View.OnClickListener { ... @Override public void onClick(View view) { String securePassword = MainActivity.access$000( this.this$0, this.val$textView2.getText().toString(), this.val$bArr ); if (!this.val$textView.getText().toString().equals("hillstone") || !securePassword.equals( "4368354b57abefdb3da930aa1f7db42c" + "3a1d318401d7474c25f5a14bbaf8fb34" )) { Toast.makeText( (Context) this.this$0, (CharSequence) "Login Failure", 0 ).show(); System.out.println(securePassword); } else { MainActivity mainActivity = this.this$0; Toast.makeText( (Context) mainActivity, (CharSequence) MainActivity.access$100(mainActivity), 0 ).show(); } } }
Hook
这道题解法很多,老师讲的是用 frida 去对登陆界面进行动态改动。但其实如果用 Xposed 的话会变得极其简单,因为它能在代码的任何位置进行劫持。既然这样就没有必要去动登陆逻辑了,因为 flag 在 onCreate()
就算出来了,因此我们直接在程序启动的时候 hook 就大功告成了,后面根本就不用管。
新增目标作用域
在 src/res/values/arrays.xml 中加入新的作用域
1
<item>com.ayy.hsapk1</item>
重写接口
基于上一题的代码,加入对新的包名的劫持:
1 2 3 4 5 6 7 8 9
@Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (lpparam.packageName.equals("com.ayy.hsapk1")) hookGetFlag(lpparam); if (lpparam.packageName.equals("an.droid.j")) hookJ(lpparam); }
hook 逻辑编写
对
onCreate()
进行劫持,在执行前调用Toast.makeText()
弹出挂载提示,在执行后获取已经解密的 flag,用一个 Toast 弹出。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
private void hookGetFlag(XC_LoadPackage.LoadPackageParam lpparam) { XposedHelpers.findAndHookMethod( "com.ayy.hsapk1.MainActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); Toast.makeText( (Activity) param.thisObject, "LSPosed: 模块加载成功!", Toast.LENGTH_SHORT).show(); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); String str = (String) XposedHelpers.getObjectField( param.thisObject, "str" ); Toast.makeText( (Activity) param.thisObject, str, Toast.LENGTH_SHORT ).show(); } } ); }
测试
编译运行,打开 getflag.apk,首先弹出了挂载提示:
等这条提示消失后,紧接着弹出了第二条提示,输出了 flag:
这样我们就绕过了登陆界面的 hook,用最简单的方式得到了 flag。
1
2
3
4
5
6
7
8
9
10
11
12
int vuln() {
int var_10 = 10, var_4 = 0, var_C = 0, var_8;
while (var_C < var_10) {
var_4++;
if (var_4 > 5)
var_8 = 44;
var_C++;
}
return var_8;
}