# 【蓝牙】CVE-2019-2209 未检测PIN码长度导致越界读造成信息泄露

补丁

* <https://android.googlesource.com/platform/packages/apps/Bluetooth/+/5a426f6ed5620a39388aa78ed1d01c465f47958c>

| CVE           | 参考编号        | 类型 | 严重程度 | 已更新的 AOSP 版本 |
| ------------- | ----------- | -- | ---- | ------------ |
| CVE-2019-2209 | A-139287605 | ID | 高    | 8.0、8.1、9、10 |

补丁描述使用PIN码前先进行长度检查，所以我们猜测PIN码长度是一个关键的地方

```c++
AdapterService: Check the PIN code length before using

The length is assigned by the framework. We should be better to check
again before using, and dropped any unexcepted input.

Bug: 139287605
Test: PoC, atest -t BluetoothInstrumentationTests:com.android.bluetooth.btservice
Change-Id: Ie2dd01e0b192e7ed1fe4b464618ddfa415dbf15c
(cherry picked from commit d6c84aa34962333448e0ed8e4ddbc9de8b73c5ac)
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index 508eacf..53cf723 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -2233,6 +2233,12 @@
             return false;
         }
 
+        if (pinCode.length != len) {
+            android.util.EventLog.writeEvent(0x534e4554, "139287605", -1,
+                    "PIN code length mismatch");
+            return false;
+        }
+
         StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
                 obfuscateAddress(device), 0, device.getType(),
                 BluetoothDevice.BOND_BONDING,
@@ -2249,6 +2255,12 @@
             return false;
         }
 
+        if (passkey.length != len) {
+            android.util.EventLog.writeEvent(0x534e4554, "139287605", -1,
+                    "Passkey length mismatch");
+            return false;
+        }
+
         StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
                 obfuscateAddress(device), 0, device.getType(),
                 BluetoothDevice.BOND_BONDING,
```

Android官方文档

* <https://developer.android.google.cn/reference/android/bluetooth/BluetoothDevice.html#setPin(byte\\[>])

一般来说我们进行蓝牙连接，需要先输入一个PIN码，但是现在有的设备为了配对方便，会进行自动配对，自动配对使用的是反射调用`setPin()`

```c++
try {
    Method setPinMethod = btClass.getDeclaredMethod("setPin", new Class[] {byte[].class});
    Boolean returnValue = (Boolean) setPinMethod.invoke(btDevice, new Object[] {pinCode.getBytes()});
} catch (Exception e) {
    e.printStackTrace();
}
```

跟入源码，调用系统函数`IBluetooth.setPin()`

```c++
/**
 * Set the pin during pairing when the pairing method is {@link #PAIRING_VARIANT_PIN}
 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}.
 *
 * @return true pin has been set false for error
 */
public boolean setPin(byte[] pin) {
    final IBluetooth service = sService;
    if (service == null) {
        Log.e(TAG, "BT not enabled. Cannot set Remote Device pin");
        return false;
    }
    try {
        return service.setPin(this, true, pin.length, pin); // <-- 1
    } catch (RemoteException e) {
        Log.e(TAG, "", e);
    }
    return false;
}
```

找到系统蓝牙组件源码，这里面先进行调用者的检查，然后调用`AdapterService.setPin()`

```c++
@Override
public boolean setPin(BluetoothDevice device, boolean accept, int len, byte[] pinCode) {
    if (!Utils.checkCaller()) {
        Log.w(TAG, "setPin() - Not allowed for non-active user");
        return false;
    }

    AdapterService service = getService();
    if (service == null) {
        return false;
    }
    return service.setPin(device, accept, len, pinCode); // <-- 2
}
```

到达补丁函数，我并没有看出来这里的`pinCode.length`和`len`能不相等，因为`len`是由参数传入，回溯回去可以看到是`pin.length`，并且没有其它地方能够改变这个值，所以在什么情况下这两个值能不相等呢？

```c++
boolean setPin(BluetoothDevice device, boolean accept, int len, byte[] pinCode) {
    enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
    DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
    // Only allow setting a pin in bonding state, or bonded state in case of security upgrade.
    if (deviceProp == null || (deviceProp.getBondState() != BluetoothDevice.BOND_BONDING
            && deviceProp.getBondState() != BluetoothDevice.BOND_BONDED)) {
        return false;
    }

+   if (pinCode.length != len) { // <-- 3
+        android.util.EventLog.writeEvent(0x534e4554, "139287605", -1,
+                "PIN code length mismatch");
+        return false;
+   }

    StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
            obfuscateAddress(device), 0, device.getType(),
            BluetoothDevice.BOND_BONDING,
            BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_PIN_REPLIED,
            accept ? 0 : BluetoothDevice.UNBOND_REASON_AUTH_REJECTED);
    byte[] addr = Utils.getBytesFromAddress(device.getAddress());
    return pinReplyNative(addr, accept, len, pinCode);
}
```

这里的`AdapterServiceBinder`定义如下，它使用的是AIDL，定义为`IBluetooth`接口，也就是说，这个Service里的方法可以通过Binder进行调用，所以这里的四个参数我们可控，就可以造成`pinCode.length`与`len`不相等

```c++
/**
 * Handlers for incoming service calls
 */
private AdapterServiceBinder mBinder;

@Override
public IBinder onBind(Intent intent) {
    debugLog("onBind()");
    return mBinder;
}
    
/**
 * The Binder implementation must be declared to be a static class, with
 * the AdapterService instance passed in the constructor. Furthermore,
 * when the AdapterService shuts down, the reference to the AdapterService
 * must be explicitly removed.
 *
 * Otherwise, a memory leak can occur from repeated starting/stopping the
 * service...Please refer to android.os.Binder for further details on
 * why an inner instance class should be avoided.
 *
 */
private static class AdapterServiceBinder extends IBluetooth.Stub {
    private AdapterService mService;

    AdapterServiceBinder(AdapterService svc) {
        mService = svc;
    }

    ...
    
    @Override
    public boolean setPin(BluetoothDevice device, boolean accept, int len, byte[] pinCode) {
        if (!Utils.checkCaller()) {
            Log.w(TAG, "setPin() - Not allowed for non-active user");
            return false;
        }

        AdapterService service = getService();
        if (service == null) {
            return false;
        }
        return service.setPin(device, accept, len, pinCode);
    }
    
    ...
```

我们继续往下看代码，通过定义可以看到`pinReplyNative()`是Native函数

```c++
private native boolean pinReplyNative(byte[] address, boolean accept, int len, byte[] pin);
```

`pinReplyNative()`的Native实现如下，细节注释都写在代码里了

```c++
static jboolean pinReplyNative(JNIEnv* env, jobject obj, jbyteArray address,
                               jboolean accept, jint len, jbyteArray pinArray) {
    ALOGV("%s", __func__);

    if (!sBluetoothInterface) return JNI_FALSE;

    // 获取设备地址的字节数组对象
    jbyte* addr = env->GetByteArrayElements(address, NULL);
    if (addr == NULL) {
        jniThrowIOException(env, EINVAL);
        return JNI_FALSE;
    }

    // 获取PIN码的字节数组对象
    jbyte* pinPtr = NULL;
    if (accept) {
        pinPtr = env->GetByteArrayElements(pinArray, NULL);
        if (pinPtr == NULL) {
            jniThrowIOException(env, EINVAL);
            env->ReleaseByteArrayElements(address, addr, 0);
            return JNI_FALSE;
        }
    }

    // 调用pin_reply()
    int ret = sBluetoothInterface->pin_reply((RawAddress*)addr, accept, len,
                                           (bt_pin_code_t*)pinPtr);
    env->ReleaseByteArrayElements(address, addr, 0);
    env->ReleaseByteArrayElements(pinArray, pinPtr, 0);

    return (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}
```

继续调用函数，此处将第四个参数`pin_code`转为`bt_pin_code_t*`类型，这里影响不大，做了个检查继续调用`btif_dm_pin_reply()`

```c++
/** Bluetooth PinKey Code */
typedef struct {
    uint8_t pin[16];
} __attribute__((packed))bt_pin_code_t;

static int pin_reply(const bt_bdaddr_t* bd_addr, uint8_t accept,
                        uint8_t pin_len, bt_pin_code_t* pin_code) {
    /* sanity check */
    if (interface_ready() == false) return BT_STATUS_NOT_READY;

    return btif_dm_pin_reply(bd_addr, accept, pin_len, pin_code);
}
```

先判断`pin_len`的长度是否大于`PIN_CODE_LEN`，通过宏定义可以看到`PIN_CODE_LEN`的长度为`16`，同时我们也可以推出`len`的值我们不可以改变，也就是说，补丁进行对比的两个值，有问题的是`pinCode`

```c++
/*******************************************************************************
 *
 * Function         btif_dm_pin_reply
 *
 * Description      BT legacy pairing - PIN code reply
 *
 * Returns          bt_status_t
 *
 ******************************************************************************/

bt_status_t btif_dm_pin_reply(const bt_bdaddr_t* bd_addr, uint8_t accept,
                                uint8_t pin_len, bt_pin_code_t* pin_code) {
    BTIF_TRACE_EVENT("%s: accept=%d", __func__, accept);
    // #define PIN_CODE_LEN 16
    if (pin_code == NULL || pin_len > PIN_CODE_LEN) return BT_STATUS_FAIL;
    if (pairing_cb.is_le_only) {
        int i;
        uint32_t passkey = 0;
        int multi[] = {100000, 10000, 1000, 100, 10, 1};
        BD_ADDR remote_bd_addr;
        bdcpy(remote_bd_addr, bd_addr->address);
        for (i = 0; i < 6; i++) {
            passkey += (multi[i] * (pin_code->pin[i] - '0'));
        }
        BTIF_TRACE_DEBUG("btif_dm_pin_reply: passkey: %d", passkey);
        BTA_DmBlePasskeyReply(remote_bd_addr, accept, passkey);
        
    } else {
        BTA_DmPinReply((uint8_t*)bd_addr->address, accept, pin_len, pin_code->pin);
        if (accept) pairing_cb.pin_code_len = pin_len;
    }
    return BT_STATUS_SUCCESS;
}
```

在CVE官网有一段关于该漏洞的描述

```
In BTA_DmPinReply of bta_dm_api.cc, there is a possible out of bounds read due to an incorrect bounds check. This could lead to local information disclosure with User execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-8.0 Android-8.1 Android-9 Android-10Android ID: A-139287605
```

所以我们跟入`BTA_DmPinReply()`，定义了一个`tBTA_DM_API_PIN_REPLY*`类型指针，`accept`一开始传进来就是`true`，调用`memcpy()`拷贝数据，`p_msg->p_pin`的长度是`PIN_CODE_LEN`，也就是`16`，上面已经看过源码了，如果`pin_len`为4，但是`p_pin`为16，所以就会多拷贝

```c++
typedef struct {
    BT_HDR hdr;
    BD_ADDR bd_addr;
    bool accept;
    uint8_t pin_len;
    uint8_t p_pin[PIN_CODE_LEN];
} tBTA_DM_API_PIN_REPLY;

/*******************************************************************************
 *
 * Function         BTA_DmPinReply
 *
 * Description      This function provides a pincode for a remote device when
 *                  one is requested by DM through BTA_DM_PIN_REQ_EVT
 *
 *
 * Returns          void
 *
 ******************************************************************************/
void BTA_DmPinReply(BD_ADDR bd_addr, bool accept, uint8_t pin_len,
                        uint8_t* p_pin)
{
    tBTA_DM_API_PIN_REPLY* p_msg =
        (tBTA_DM_API_PIN_REPLY*)osi_malloc(sizeof(tBTA_DM_API_PIN_REPLY));

    p_msg->hdr.event = BTA_DM_API_PIN_REPLY_EVT;
    bdcpy(p_msg->bd_addr, bd_addr);
    p_msg->accept = accept;
    if (accept) {
        p_msg->pin_len = pin_len;
        memcpy(p_msg->p_pin, p_pin, pin_len);
    }

    bta_sys_sendmsg(p_msg);
}
```

我们来使用代码模拟一下进行调试

```c++
#include <jni.h>
#include <string>
#include <stdlib.h>
#include <android/log.h>

#define LOG_TAG "HELLO_JNI"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG ,__VA_ARGS__)

#define PIN_CODE_LEN 16

typedef struct {
    bool accept;
    uint8_t pin_len;
    uint8_t p_pin[PIN_CODE_LEN];
} tBTA_DM_API_PIN_REPLY;

void* osi_malloc(size_t size) {
//    size_t real_size = allocation_tracker_resize_for_canary(size);
    void* ptr = malloc(size);
//    CHECK(ptr);
    return ptr;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_wnagzihxa1n_bluetooth_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";

    tBTA_DM_API_PIN_REPLY* p_msg =(tBTA_DM_API_PIN_REPLY*) osi_malloc(sizeof(tBTA_DM_API_PIN_REPLY));

//    uint8_t p_pin[16] = {0x01, 0x02, 0x03, 0x04};
    uint8_t p_pin[16];
    p_pin[0] = 0x01;
    p_pin[1] = 0x02;
    p_pin[2] = 0x03;
    p_pin[3] = 0x4;

    memcpy(p_msg->p_pin, p_pin, PIN_CODE_LEN);

    for (int i = 0; i < 16; i++) {
        LOGE("666 TEST index = %d p_msg->p_pin[%d] = 0x%x\n", i, i, p_msg->p_pin[i]);
    }
    return env->NewStringUTF(hello.c_str());
}
```

输出数据如下，可以看到打出了未初始化的数据

```shell
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 0 p_msg->p_pin[0] = 0x1
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 1 p_msg->p_pin[1] = 0x2
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 2 p_msg->p_pin[2] = 0x3
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 3 p_msg->p_pin[3] = 0x4
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 4 p_msg->p_pin[4] = 0x4d
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 5 p_msg->p_pin[5] = 0x8e
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 6 p_msg->p_pin[6] = 0x93
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 7 p_msg->p_pin[7] = 0xb2
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 8 p_msg->p_pin[8] = 0x1
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 9 p_msg->p_pin[9] = 0x0
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 10 p_msg->p_pin[10] = 0x0
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 11 p_msg->p_pin[11] = 0x0
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 12 p_msg->p_pin[12] = 0xeb
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 13 p_msg->p_pin[13] = 0x63
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 14 p_msg->p_pin[14] = 0x6c
02-08 17:26:33.040 21778-21778/com.wnagzihxa1n.bluetooth E/CVE-2019-2209: index = 15 p_msg->p_pin[15] = 0xb2
```

```java
/** @hide */
public boolean setPasskey(int passkey) {
    //TODO(BT)
    /*
    try {
        return sService.setPasskey(this, true, 4, passkey);
    } catch (RemoteException e) {Log.e(TAG, "", e);}*/
    return false;
}
```

```java
@Override
public boolean setPasskey(BluetoothDevice device, boolean accept, int len, byte[] passkey) {
    if (!Utils.checkCaller()) {
        Log.w(TAG, "setPasskey() - Not allowed for non-active user");
        return false;
    }

    AdapterService service = getService();
    if (service == null) {
        return false;
    }
    return service.setPasskey(device, accept, len, passkey);
}
```

```java
boolean setPasskey(BluetoothDevice device, boolean accept, int len, byte[] passkey) {
    enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
    DeviceProperties deviceProp = mRemoteDevices.getDeviceProperties(device);
    if (deviceProp == null || deviceProp.getBondState() != BluetoothDevice.BOND_BONDING) {
        return false;
    }
    
+   if (passkey.length != len) {
+       android.util.EventLog.writeEvent(0x534e4554, "139287605", -1,
+               "Passkey length mismatch");
+       return false;
+   }

    StatsLog.write(StatsLog.BLUETOOTH_BOND_STATE_CHANGED,
            obfuscateAddress(device), 0, device.getType(),
            BluetoothDevice.BOND_BONDING,
            BluetoothProtoEnums.BOND_SUB_STATE_LOCAL_SSP_REPLIED,
            accept ? 0 : BluetoothDevice.UNBOND_REASON_AUTH_REJECTED);
    byte[] addr = Utils.getBytesFromAddress(device.getAddress());
    return sspReplyNative(addr, AbstractionLayer.BT_SSP_VARIANT_PASSKEY_ENTRY, accept,
            Utils.byteArrayToInt(passkey));
}
```

## Reference

* <https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-2209>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wnagzihxa1n.gitbook.io/happy-android-security/system_security/cve20192209-wei-jian-ce-pin-ma-chang-du-dao-zhi-yue-jie-du-zao-cheng-xin-xi-xie-lou.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
