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

frida

1
2
3
4
5
mkdir my-agent
cd my-agent
frida-craete -t agent
npm install
npm install frida-java-bridge
1
2
3
4
5
import Java from "frida-java-bridge"

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

Python 内编译

1
2
3
4
5
6
7
8
9
...
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

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
<?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,先看一眼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 的工作,看一眼主活动

1
2
3
4
5
6
7
8
9
10
11
12
13
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 直接启动那三个活动

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

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

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
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)
1
objection --name com.example.hellojni start
1
2
3
android hooking list activities
android intent launch_activity com.
example.application.IsThisTheRealOne
1
2
3
4
[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}

重打包

1
2
3
4
5
6
7
8
9
10
11
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++ 侧的真实形态

1
public static native int foo(String str);

通常对应:

1
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 调用,结合地址位数可以算出来是哪个函数。

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
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 字符串。