使用ESP32 DIY一个AirTag

好久不见!又到了一年夏季,去年到现在攒了一大堆活没整,今天先整一个简单的,用ESP32作为蓝牙Beacon模拟airtag使用(白嫖)苹果的Find My网络进行查找。

What you need

  • ESP32 开发版
  • 一台Mac(或者黑苹果/黑果虚拟机)
  • 32.768kHz无源晶振(可选)
  • 两颗10pF电容(可选)
  • 5-10兆欧电阻(可选)
  • 电池
  • 烙铁

引子

前段日子每日逛GitHub的时候发现了一个很有意思的开源仓库,这个团队早在2019年就开始逆向工程苹果的Find My网络,站在Seemoo Lab的项目之上,我们免去了逆向工程的麻烦,可以直接使用来自他们的代码工作。

警告
本项目(OpenHaystack以及本人基于其代码构建的软体)均为实验性软件,代码未经测试或不完整。本项目不隶属于Apple Inc.,也不受支持

实践

构建硬件

将购得的硬件按如图方式飞线焊接之后,即可进行下一步操作

硬件设计手册
美妙的飞线

完成了上述硬件操作之后,着手构建软件

安装软件

  1. 在你的Mac上下载安装openhaystack的软件
  2. 打开OpenHayStack 然后安装插件到 ~/Library/Mail/Bundle.
  3. 在终端中输入 sudo spctl --master-disable 来暂时关闭Mac的Gatekeeper服务以运行邮箱插件
  4. 打开苹果自带邮件. 打开 Preferences → General → Manage Plug-Ins… 然后点击 OpenHaystackMail.mailbundle 旁边的复选框以启用
    • 如果没有管理插件按钮,请在终端输入 sudo defaults write "/Library/Preferences/com.apple.mail" EnableBundles 1
  5. 允许访问并重启邮件App
  6. 打开终端输入 sudo spctl --master-enable, 重新打开Gatekeeper服务

在OpenHaystack中点击加号新建一个物品,复制他的Advertisement Key(base64),作者原版的软件没有发挥出ESP32的省电潜能(没有使用到Light Sleep或者Modem Sleep模式),300mAh的电池不足以支撑它发信24H,故此处不使用官方固件(若要使用可以直接点击Deploy选择esp32刷入)

构建固件

准备好一个esp-idf运行环境(或者跟随本文使用docker),克隆本仓库,在Firmware/ESP32/main中更改openhaystack_main.c,下面附上我修改过的文件

#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>

#include "nvs_flash.h"
#include "esp_partition.h"

#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "esp_err.h"
#include "esp_pm.h"
static const char* LOG_TAG = "open_haystack";

/** Callback function for BT events */
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);

/** Random device address */
static esp_bd_addr_t rnd_addr = { 0xFF, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };

/** Advertisement payload */
static uint8_t adv_data[31] = {
	0x1e, /* Length (30) */
	0xff, /* Manufacturer Specific Data (type 0xff) */
	0x4c, 0x00, /* Company ID (Apple) */
	0x12, 0x19, /* Offline Finding type and length */
	0x00, /* State */
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	0x00, /* First two bits */
	0x00, /* Hint (0x00) */
};

/* https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_gap_ble.html#_CPPv420esp_ble_adv_params_t */
static esp_ble_adv_params_t ble_adv_params = {
    // Advertising min interval:
    // Minimum advertising interval for undirected and low duty cycle
    // directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
    // (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
    .adv_int_min        = 0x4000, // 10.24s
    // Advertising max interval:
    // Maximum advertising interval for undirected and low duty cycle
    // directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
    // (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
    .adv_int_max        = 0x4000, // 10.24s,长间隔有助于省电
    // Advertisement type
    .adv_type           = ADV_TYPE_NONCONN_IND,
    // Use the random address
    .own_addr_type      = BLE_ADDR_TYPE_RANDOM,
    // All channels
    .channel_map        = ADV_CHNL_ALL,
    // Allow both scan and connection requests from anyone. 
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;

    switch (event) {
        case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
            esp_ble_gap_start_advertising(&ble_adv_params);
            break;

        case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
            //adv start complete event to indicate adv start successfully or failed
            if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
                ESP_LOGE(LOG_TAG, "advertising start failed: %s", esp_err_to_name(err));
            } else {
                ESP_LOGI(LOG_TAG, "advertising has started.");
            }
            break;

        case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
            if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
                ESP_LOGE(LOG_TAG, "adv stop failed: %s", esp_err_to_name(err));
            }
            else {
                ESP_LOGI(LOG_TAG, "stop adv successfully");
            }
            break;
        default:
            break;
    }
}

int load_key(uint8_t *dst, size_t size) {
    const esp_partition_t *keypart = esp_partition_find_first(0x40, 0x00, "key");
    if (keypart == NULL) {
        ESP_LOGE(LOG_TAG, "Could not find key partition");
        return 1;
    }
    esp_err_t status;
    status = esp_partition_read(keypart, 0, dst, size);
    if (status != ESP_OK) {
        ESP_LOGE(LOG_TAG, "Could not read key from partition: %s", esp_err_to_name(status));
    }
    return status;
}

void set_addr_from_key(esp_bd_addr_t addr, uint8_t *public_key) {
	addr[0] = public_key[0] | 0b11000000;
	addr[1] = public_key[1];
	addr[2] = public_key[2];
	addr[3] = public_key[3];
	addr[4] = public_key[4];
	addr[5] = public_key[5];
}

void set_payload_from_key(uint8_t *payload, uint8_t *public_key) {
    /* copy last 22 bytes */
	memcpy(&payload[7], &public_key[6], 22);
	/* append two bits of public key */
	payload[29] = public_key[0] >> 6;
}

void app_main(void)
{
    esp_pm_config_esp32_t pm_config = {
        .max_freq_mhz = 80, // e.g. 80, 160, 240
        .min_freq_mhz = 40, // e.g. 40
        .light_sleep_enable = true, // enable light sleep
    };//降频,省电
    ESP_ERROR_CHECK( esp_pm_configure(&pm_config) );
    ESP_ERROR_CHECK(nvs_flash_init());
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    esp_bt_controller_enable(ESP_BT_MODE_BLE);

    esp_bluedroid_init();
    esp_bluedroid_enable();

    // Load the public key from the key partition
    static uint8_t public_key[28];
    if (load_key(public_key, sizeof(public_key)) != ESP_OK) {
        ESP_LOGE(LOG_TAG, "Could not read the key, stopping.");
        return;
    }

    set_addr_from_key(rnd_addr, public_key);
    set_payload_from_key(adv_data, public_key);

    ESP_LOGI(LOG_TAG, "using device address: %02x %02x %02x %02x %02x %02x", rnd_addr[0], rnd_addr[1], rnd_addr[2], rnd_addr[3], rnd_addr[4], rnd_addr[5]);

    esp_err_t status;
    //register the scan callback function to the gap module
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(LOG_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

    if ((status = esp_ble_gap_set_rand_addr(rnd_addr)) != ESP_OK) {
        ESP_LOGE(LOG_TAG, "couldn't set random address: %s", esp_err_to_name(status));
        return;
    }
    if ((esp_ble_gap_config_adv_data_raw((uint8_t*)&adv_data, sizeof(adv_data))) != ESP_OK) {
        ESP_LOGE(LOG_TAG, "couldn't configure BLE adv: %s", esp_err_to_name(status));
        return;
    }
    ESP_LOGI(LOG_TAG, "application initialized");
}

同时,我们需要指定esp32 ble睡眠时钟为外置晶振以释放睡眠锁,在ESP32目录输入 docker run --rm -v $PWD:/project -w /project -it espressif/idf 进入esp-idf环境,运行 idf.py menuconfig ,进入Component config

  • (Top) → Component config → Hardware Settings → RTC Clock Config中切换RTC clock source 为External 32kHz crystal
  • (Top) → Component config → Power Management中打开Support for power management
  • (Top) → Component config → Bluetooth → Controller Options → MODEM SLEEP Options中打开Bluetooth modem sleep且切换Bluetooth low power clock为External 32kHz crystal

之后输入idf.py build再使用./flash_esp32.sh -p /dev/yourSerialPort "Base64-encoded advertisement key"刷入固件即可

效果

刷入固件后会在15分钟左右获得回报数据,暂时只能在OpenHaystack应用中查看位置数据,在苹果地图国际版中定位精确,若使用国内版苹果地图会出现WGS-84坐标转换为GCJ02火星坐标的问题,目前没有修复。

结语

离线查找网络无非是一个大坑,基于它全球的用户数量,苹果有能力去踩这个坑,Find My Network无非是一个创举,之后肯定会被其他公司争相模仿(如本文撰写完成时发布的Huawei Tag),用华为的网络对比苹果的网络,苹果的用户分布更广,网络设计更加透明,且当前Huawei Tag仅支持中国大陆境内销售的设备,未来可能还会有Xiaomi Tag等等产品,当然,适合自己的才是最好的(价位,精准度,续航等)

说回Find My Network,网络设计的目的是让用户安全隐私的得到自己的离线设备的位置而无需承担传统GPS定位器的联网成本和续航问题,有居心叵测之人利用这个网络是无法避免的,苹果为此做出了防范,在Apple设备上会有Tag跟踪提示,但是对于安卓设备只能通过第三方软件实现(如同属上组织的AirGuard),如果用户没有此类软件则只能依靠Airtag自己发声意识到自己被跟踪,希望各位读者可以提高自身防范意识,避免隐私泄露。

本文仅做可行性分析实践,依靠此教程行违法乱纪之事作者不承担任何责任!
另付上在中国使用以上工具实施跟踪行为可能触犯的法规

《中华人民共和国治安管理处罚法》

第四十二条  有下列行为之一的,处五日以下拘留或者五百元以下罚款;情节较重的,处五日以上十日以下拘留,可以并处五百元以下罚款: (一)写恐吓信或者以其他方法威胁他人人身安全的; (二)公然侮辱他人或者捏造事实诽谤他人的; (三)捏造事实诬告陷害他人,企图使他人受到刑事追究或者受到治安管理处罚的; (四)对证人及其近亲属进行威胁、侮辱、殴打或者打击报复的; (五)多次发送淫秽、侮辱、恐吓或者其他信息,干扰他人正常生活的; (六)偷窥、偷拍、窃听、散布他人隐私的。

评论

  1. 11月前
    2022-7-24 0:31:03

    晕死,图片18M,回头压一下换源

  2. 寒山
    10月前
    2022-7-31 7:22:01

    我不实施跟踪,可以上地铁贴贴吗

    • 博主
      寒山
      10月前
      2022-7-31 13:24:06

      好怪哦

  3. 季悠然
    9月前
    2022-9-07 18:06:31

    臣附议

  4. 9月前
    2022-9-17 12:49:06

    换CDN之后总算把真实IP修好了XD

  5. FutureAppleWei
    9月前
    2022-9-23 21:37:37

    今天的2890也没有更新呐

    • 博主
      FutureAppleWei
      9月前
      2022-9-23 21:40:27

  6. FutureAppleWei
    8月前
    2022-9-24 19:24:04

    今天的 2890 也没有更新呐

  7. 李先生
    1月前
    2023-4-22 17:13:54

    博主您好,想跟您請教細節,我依照您的方案,貌似都沒有辦法讓esp32發訊號,不過我用原生的檔案就可以使用。不知道能否跟您進一步聯繫。

    • 博主
      李先生
      1周前
      2023-5-27 19:13:06

      有可能是更新了,主要省电的点就是降低发射信号频率和降低CPU频率,在原版代码里面找到实现这个的地方改掉就好了
      (改了也不怎么省电,实测1000mAh电池最多续航七天

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇