安卓 day2 怎么隔了这么多天?

frida#

frida 17 之后的 java 桥

mkdir my-agent
cd my-agent
frida-craete -t agent
npm install
npm install frida-java-bridge
import Java from "frida-java-bridge"

Java.perform(() => {
	...
});

Python 内编译

...
PROJECT_ROOT = os.path.abspath("./my-agent")
ENTRY = "agent.ts"

compiler = frida.Compiler()
compiler.on("diagnostics", on_diagnostics)

bundle = compiler.build(ENTRY, project_root=PROJECT_ROOT)
...

手动编译(略)

攻防世界 ill-intentions hook#

先看 manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    package="com.example.hellojni"
    platformBuildVersionCode="22"
    platformBuildVersionName="5.1.1-1819727">
    <uses-sdk
        android:minSdkVersion="22"
        android:targetSdkVersion="23"/>
    <permission
        android:name="ctf.permission._MSG"
        android:protectionLevel="signature"
        android:description="@string/android.permission._msg"/>
    <permission
        android:name="ctf.permission._SEND"
        android:description="@string/android.permission._msg"/>
    <application
        android:label="CTF Application"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:label="Main Activity"
            android:name="com.example.application.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:label="Activity: Is This The Real One"
            android:name="com.example.application.IsThisTheRealOne"/>
        <activity
            android:label="This Is The Real One"
            android:name="com.example.application.ThisIsTheRealOne"/>
        <activity
            android:label="Definitely Not This One"
            android:name="com.example.application.DefinitelyNotThisOne"/>
        <receiver
            android:name="com.example.application.Send_to_Activity"
            android:exported="true"/>
    </application>
</manifest>

关注到主活动外有三个活动,都是解密过程,分别调用了一个不同的 native 函数,算法都不是很复杂可以直接分析,这里考虑 hook,但先按下不表。

还有一个 receiver,先看一眼

public class Send_to_Activity extends BroadcastReceiver {
    @Override // android.content.BroadcastReceiver
    public void onReceive(Context context, Intent intent) {
        String msgText = intent.getStringExtra("msg");
        if (msgText.equalsIgnoreCase("ThisIsTheRealOne")) {
            Intent outIntent = new Intent(context, (Class<?>) ThisIsTheRealOne.class);
            context.startActivity(outIntent);
        } else if (msgText.equalsIgnoreCase("IsThisTheRealOne")) {
            Intent outIntent2 = new Intent(context, (Class<?>) IsThisTheRealOne.class);
            context.startActivity(outIntent2);
        } else if (msgText.equalsIgnoreCase("DefinitelyNotThisOne")) {
            Intent outIntent3 = new Intent(context, (Class<?>) DefinitelyNotThisOne.class);
            context.startActivity(outIntent3);
        } else {
            Toast.makeText(context, "Which Activity do you wish to interact with?", 1).show();
        }
    }
}

做的就是 dispatcher 的工作,看一眼主活动

public class MainActivity extends Activity {
    @Override // android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView tv = new TextView(getApplicationContext());
        tv.setText("Select the activity you wish to interact with.To-Do: Add buttons to select activity, for now use Send_to_Activity");
        setContentView(tv);
        IntentFilter filter = new IntentFilter();
        filter.addAction("com.ctf.INCOMING_INTENT");
        BroadcastReceiver receiver = new Send_to_Activity();
        registerReceiver(receiver, filter, Manifest.permission._MSG, null);
    }
}

发现这个 receiver 的 filter 是动态注册的,权限为 ctf.permission._MSG,manifest 里面可用看到是 signature 级别(只有同签名的app才有该权限),不考虑重打包基本没办法利用。

尝试 adb 直接启动那三个活动

adb shell am start -n com.example.hellojni/com.example.application.IsThisTheRealOne

报错,活动的 exported 标签不为真,外部启动不了。了解到工具 objection 可以把 agent 注入到进程内部,在 app 的进程里构造 intent 可以绕过这个限制。那么可以把那三个 native 函数钩住,再用 objection 启动活动。

function main() {
    Java.perform(function() {
        var DefinitelyNotThisOneHandler = Java.use('com.example.application.DefinitelyNotThisOne');
        DefinitelyNotThisOneHandler.definitelyNotThis.implementation = function(arg0, arg1) {
            console.log("[*] Entered DefinitelyNotThisOneHandler(), arg0: " + arg0 + ", arg1: " + arg1);
            var ret = this.definitelyNotThis(arg0, arg1);
            console.log("[*] Finished DefinitelyNotThisOneHandler(), return value: " + ret);
            return ret;
        }

        var ThisIsTheRealOneHandler = Java.use("com.example.application.ThisIsTheRealOne");
        ThisIsTheRealOneHandler.orThat.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(arg0, arg1, arg2) {
            console.log('[*] Entered ThisIsTheRealOneHandler(), arg0: ' + arg0 + ', arg1: ' + arg1 + ', arg2: ' + arg2);
            var ret = this.orThat(arg0, arg1, arg2);
            console.log('[*] Finished ThisIsTheRealOneHandler(), return value: ' + ret);
            return ret;
        }

        var IsThisTheRealOneHandler = Java.use("com.example.application.IsThisTheRealOne");
        IsThisTheRealOneHandler.perhapsThis.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(arg0, arg1, arg2) {
            console.log('[*] Entered IsThisTheRealOneHandler(), arg0: ' + arg0 + ', arg1: ' + arg1 + ', arg2: ' + arg2);
            var ret = this.perhapsThis(arg0, arg1, arg2);
            console.log('[*] Finished IsThisTheRealOneHandler(), return value: ' + ret);
            return ret;
        }
    })
}

setImmediate(main)
objection --name com.example.hellojni start
android hooking list activities
android intent launch_activity com.
example.application.IsThisTheRealOne
[MI 6::com.example.hellojni ]-> [*] Entered IsThisTheRealOneHandler(), arg0: TRytfrgooq|F{i-JovFBungFk\VlphgQbwvj~HuDgaeTzuSt.@Lex^~, arg1: ZGFkNGIwYzIWYjEzMTUWNjVjNTVlNjZhOGJkNhYtODIyOGEaMTMWNmQaOTVjZjkhMzRjYmUzZGE?
, arg2: MzQxZTZmZjAxMmIiMWUzNjUxMmRiYjIxNDUwYTUxMWItZGQzNWUtMzkyOWYyMmQeYjZmMzEaNDQ?

[*] Finished IsThisTheRealOneHandler(), return value: Congratulation!YouFoundTheRightActivityHereYouGo-CTF{IDontHaveABadjokeSorry}

重打包#

apktool d -f raw.apk -o out_dir
apktool b -f out_dir -o unsigned.apk

# 对齐优化
zipalign -p -f 4 unsigned.apk aligned.apk

# 生成签名文件
keytool -genkeypair -alias myapp -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 10000 -keystore my-release.keystore -storetype PKCS12 -v

# 签名
apksigner sign --ks my-release.keystore --out signed.apk aligned.apk

JNI 函数在 C/C++ 侧的真实形态#

public static native int foo(String str);

通常对应:

JNIEXPORT jint JNICALL Java_com_example_test_JNI_foo(JNIEnv* env, jclass clazz, jstring str);

JNIEnv* env 是 JNI 环境指针,线程相关,里面是一堆函数表,对应 *JNINativeInterface

jclass clazz 是类对象(如果不是 static,则是 jobject thiz

jstring str Java 传进来的 String

JNI 环境指针函数表中的函数通常通过 *env + offset 调用,结合地址位数可以算出来是哪个函数。

int __fastcall Java_com_example_test_ctf03_JNI_getResult(int a1, int a2, int a3)
{
  int v3; // r4
  const char *v4; // r8
  char *v5; // r6
  char *v6; // r4
  char *v7; // r5
  int i; // r0
  int j; // r0

  v3 = 0;
  str = (*(*a1 + 676))(a1, a3, 0);
  if ( strlen(v4) == 15 )
  {
    v5 = malloc(1u);
    v6 = malloc(1u);
    v7 = malloc(1u);
    Init(v5, v6, v7, v4, 15);
    if ( !First(v5) )
      return 0;
    for ( i = 0; i != 4; ++i )
      v6[i] ^= v5[i];
    if ( !strcmp(v6, a5) )
    {
      for ( j = 0; j != 4; ++j )
        v7[j] ^= v6[j];
      return strcmp(v7, "AFBo}") == 0;
    }
    else
    {
      return 0;
    }
  }
  return v3;
}

676/4=169,索引到 GetStringUTFChars()(查文档或者对应版本的 jni.h,也可以用参数和用途推断),这个函数把 java 的 String 转成 C 字符串。