Mac, iPhone不要!ESP32でAirtag互換 OpenHaystackを使ってみる
追記
リバースエンジニアリングがうまくいったらしく、追跡にMacOS自体不要になりました。Hackintoshを使うまでもありません。
https://github.com/biemster/FindMy
Readmeを参考にしつつ以下のようにすると位置情報を取得できました。
足りないライブラリは適宜入れてください。(Cryptoモジュールがないときはpycryptodomeを入れると動いた)
$ git clone https://github.com/biemster/FindMy
$ cd FindMy
$ docker run -d --restart always --name anisette-v3 -p 6969:6969 --volume anisette-v3_data:/home/Alcoholic/.config/anisette-v3/lib/ dadoum/anisette-v3-server
$ sqlite3 reports.db 'CREATE TABLE reports (id_short TEXT, timestamp INTEGER, datePublished INTEGER, payload TEXT, id TEXT, statusCode INTEGER, PRIMARY KEY(id_short,timestamp))'
$ python3 generate_keys.py
$ (任意のデバイスにFWデプロイする)
$ python3 request_reports.py
初回はApple ID, パス, 2FAキーが求められます。認証でおかしくなったらauth.jsonを消してもう一回やると直りました。
概要
OpenHaystack
AppleのFind myネットワークにただ乗りすることで、世界中から任意のデータを送受信することができます。
https://gigazine.net/news/20210513-find-my-network-data-transmission
OpenHaystackはこのFind Myネットワークにただ乗りして、本物のAirtagのようにBluetoothで任意の機器を追跡できるようにするプロジェクトです。
ただし、iPhoneの純正アプリにOpenHaystackで作ったデバイスを登録して追跡することはできません。
https://github.com/seemoo-lab/openhaystack
ESP32やnRF51(BBC micro:bit)、もしくはラズパイみたいなLinuxで動くファームウェアが公開されています。
今回はESP32-WROOM-32Dを使いました。
Macがないと使えない
Find myネットワークにただ乗りするのは良いとして、世界中のAppleデバイスからAppleのサーバーに送られた自分のデバイスの位置情報データを引っ張ってくるにはMacOSがどうしても必要らしい。
実機は持ってないので仮想環境上のHackintoshでやりましたが、OpenHaystack上のマップがブラックアウトして何も見えなくなるバグが発生して使えませんでした。
同じように純正のMapsアプリもブラックアウトしていたので、おそらくGPU周りの仮想化の問題でしょう。
そこで、OpenHaystackの代わりにコマンドラインからその位置情報を引っ張り出せるFindMy(名前そのまま?!)を使ってマップを使わずに位置を取得します。こちらは成功しました。
Findmy
https://github.com/biemster/FindMy
環境
追跡する対象: ESP32-WROOM-32D
Ubuntu 22.04 5.15.0-53-generic
また Docker-OSX を利用した macOS Monterey 12.6.2
FindMy monterey branch 9369284856bbe64a918d7d8abfdbd3214433b513
手順
ダウンロード
まず最初にicloudにログインする必要があるみたいです。
また、Xcodeかコマンドラインツールを入れておいてください。
使用したmacOSはMontereyなので、FindmyのMontereyブランチを使いました。
% git clone -b monterey https://github.com/biemster/FindMy
% cd FindMy
必要なライブラリ
% pip3 install -U pip
% pip3 install cryptography pyobjc requests
キーを生成
ESP32から周囲のAppleデバイスに送りつけるAdvertisement keyと、データが保存されているAppleのサーバーからデータを持ってくるために必要なPrivate keyのペアを生成します。
なぜかpython3だとエラーが出た
% python generate_keys.py
出来上がった.keysファイルを覗いてみます
% cat xxxxxxx.keys
Private key: xxxx
Advertisement key: xxxx
Hashed adv key: xxxx
この中のAdvertisement keyを使います。
ESP32にファームウェアを焼く
ここをファームウェアと手順が置いてあるので参考にします。
https://github.com/seemoo-lab/openhaystack/tree/main/Firmware/ESP32
Ubuntuで行いました。
まず、公式ドキュメントを参考にESP-IDFをインストールしてください。
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/
ファームウェアを持ってきます。
$ git clone https://github.com/seemoo-lab/openhaystack
$ cd openhaystack/Firmware/ESP32
ビルドして、ESP32デバイスを指定して焼きます。ttyUSB0みたいな名前です。
$ idf.py build
$ ./flash_esp32.sh -p /dev/ttyUSBXXX さっきのAdvertisementkey
追跡してみる
macに戻って、少し待ってからFindMyのrequest_reports.pyを叩きます。こっちはpython3じゃないと動きませんでした。
Keychain passwordはmacのパスワードです。
% python3 request_reports.py
Keychain password:
200 OK
1 reports received.
1 reports used.
{'lat': XXX.XXXXXXX, 'lon': XXX.XXXXXXX, 'conf': 73, 'status': 0, 'timestamp': 1672904182, 'isodatetime': '2023-01-04T23:36:22', 'key': 'XXXXXXX', 'goog': 'https://maps.google.com/maps?q=XXX.XXXXXXX,XXX.XXXXXXX'}
緯度と経度が取得できました。google mapのリンクも出てるのでそのままアクセスすればマップでどの位置にあるかわかります。
結構精度高くてびっくりしました。住宅街で周りにそんなapple製品ないと思うのですが誤差10mくらいでした。
本物のAirtagと遜色ないのではないでしょうか。

ちなみに、ESP32-DevKitCは余計なものが無駄に電気を食うので、バッテリー駆動には厳しいです。頑張れば省電力化できるらしいですが。
https://kohacraft.com/archives/202104231107.html
そもそもこの用途ではESP32では過剰スペックなので、ボタン電池駆動とかはなかなか厳しいものがありそう。
もっと省電力で動くやつを作る
ST17H66B
FindMyレポジトリにはLenze_ST17H66とTelink_TLSR825X用のファームウェアが置いてありました。
こいつらはアリエクで売られているGPSトラッカー(大嘘)に内蔵されているチップらしいです。購入してみました。

分解すると、ST17H66Bが搭載されていました。
ファームウェアを焼く
https://github.com/biemster/FindMy/issues/14
https://github.com/biemster/FindMy/tree/main/Lenze_ST17H66
この手順通りにやってみます。
USBシリアル変換(CH340)を使ってファームウェアを焼きます。
外部から3.3Vを用意してこないと電流が足りず失敗するらしい。乾電池2本でやったらいけました(3Vでいいのかな?)
基板のP10をTX, P9をRX, GNDをGNDに繋いで(まだ3.3Vは繋がない)、コマンドを実行
$ python3 flash_st17h66.py <your base64 adv key>
少し待ってから電源を繋げます。ブザーからカリカリ音?がし始めて、大体1分ちょっとぐらいで焼けました。
sent checksum
Response is: b’#OK>>:’ で終了していたら多分成功。
このファームウェアではブザー音が鳴らなくなるっぽいです。最初壊したかと思いました。

無事に追跡できることを確認😍
ただし感度が少し悪いですね。基板むき出し状態だとそれなりですが、ケースに戻すと悪くなります;;
2024年10月時点では、この手のトラッカーのチップは大体ST17H66Tにマイナーチェンジされたらしく、そいつらはOTPチップなのでもうファームウェアを焼けないらしいです。残念
ちなみに、純正Airtagとは違い、ストーカー防止の機能はなく近くのiPhoneに通知が行くこともないですが、AirGuardなるアプリを使えばこのトラッカーを怪しいAppleトラッキングデバイスとして検出することができました。

2番目が今作ったトラッカーで、他の3つは365円の正規?中華トラッカーです。
ちなみに、全て同じ場所に置いてこれなのでやはり感度が悪い
位置情報をマップにプロットする
FindMyのrequest_reports.pyで得た位置情報をマップにプロット(HTMLに出力)するプログラムをGPT-4oに書いてもらいました。パスは適宜変えてください。
移動経路のアニメーションと、ピンをクリックすると確度と日時が見られるようにしてます。また、最後のピンは赤色で表示します。
import subprocess
import json
import folium
from folium.plugins import AntPath
# request_reports.pyを実行して、結果を取得する
result = subprocess.run(['python3', 'request_reports.py'], capture_output=True, text=True)
# 出力をそのまま取得
output_lines = result.stdout.splitlines()
# 位置情報データをパースするためのリスト
locations = []
# 緯度と経度の情報をパースする
for line in output_lines:
try:
if line.startswith("{") and line.endswith("}"):
line = line.replace("'", '"')
location_data = json.loads(line)
lat = location_data['lat']
lon = location_data['lon']
conf = location_data['conf']
goog = location_data['goog']
time = location_data['isodatetime']
locations.append({'lat': lat, 'lon': lon, 'conf': conf, 'goog': goog, 'time': time})
except json.JSONDecodeError as e:
print(f"JSONDecodeError: {e}")
print(f"問題の行: {line}")
continue
# データがあるか確認
if locations:
# 地図の中心を最後の位置に設定
last_loc = locations[-1]
m = folium.Map(location=[last_loc['lat'], last_loc['lon']], zoom_start=15)
coordinates = []
# 最後のピン以外をプロット
for loc in locations[:-1]:
popup_info = f"<a href='{loc['goog']}' target='_blank'>Google Maps Link</a><br>"
popup_info += f"Time: {loc['time']}<br>"
popup_info += f"Confidence: {loc['conf']}%"
folium.Marker(
location=[loc['lat'], loc['lon']],
popup=popup_info,
tooltip=f"Confidence: {loc['conf']}%"
).add_to(m)
coordinates.append([loc['lat'], loc['lon']])
# 最後のピンを赤色で表示
popup_info = f"<a href='{last_loc['goog']}' target='_blank'>Google Maps Link</a><br>"
popup_info += f"Time: {last_loc['time']}<br>"
popup_info += f"Confidence: {last_loc['conf']}%"
folium.Marker(
location=[last_loc['lat'], last_loc['lon']],
popup=popup_info,
tooltip=f"Confidence: {last_loc['conf']}%",
icon=folium.Icon(color='red')
).add_to(m)
coordinates.append([last_loc['lat'], last_loc['lon']])
# 移動経路(線)を追加
folium.PolyLine(locations=coordinates, color='blue', weight=5, opacity=0.7).add_to(m)
# アニメーション付きの移動経路(オプション)
AntPath(coordinates, color='green').add_to(m)
# マップを/var/www/html/に保存
m.save('/var/www/html/points.html')
print("マップが保存されました: '/var/www/html/points.html'")
else:
print("有効な位置情報が見つかりませんでした。")
Webサーバーを立てて/var/www/htmlに保存するようにしました。
crontabで10分おきぐらいにこれが走るようにします。
プロットした位置情報を確認する前に、手動でCGIで叩いて実行するようにしました。頻繁にリクエストしてBANされても嫌だし、リアルタイムで見えたほうがいいので。
参考に、request_reports.pyのパラメーターは次のようになっていました。(request_reports.pyから抜粋)
特定のキーを指定したり、直近n時間のレポートを表示できそうですね。
parser.add_argument('-H', '--hours', help='only show reports not older than these hours', type=int, default=24)
parser.add_argument('-p', '--prefix', help='only use keyfiles starting with this prefix', default='')
parser.add_argument('-r', '--regen', help='regenerate search-party-token', action='store_true')
parser.add_argument('-t', '--trusteddevice', help='use trusted device for 2FA instead of SMS', action='store_true')
おまけ
365円でFind my アプリに登録可能な完全Airtag互換トラッカーが買えるみたいなので、iPhoneユーザーならOpenHaystackよりこっちのほうが安上がりですね
最近のコメント