HackintoshでOpenHaystackを使ってみる ESP32でAirtag互換
追記
365円でFind my アプリに登録可能な完全Airtag互換トラッカーが買えるみたいなので、iPhoneユーザーならOpenHaystackよりこっちのほうが安上がりですね
追記2
リバースエンジニアリングがうまくいったらしく、追跡にMacOSが不要になったようです🥳 もちろんiPhoneも不要
https://github.com/biemster/FindMy
Readmeを参考にしつつ以下のようにすると位置情報を取得できました。
足りないライブラリは適宜入れてください。(Cryptoモジュールがないときはpycryptodomeを入れると動いた)
1 2 3 4 5 6 7 | $ 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ネットワークを使ってAirtagのようにBluetoothで任意の機器を追跡できるようにするプロジェクトです。
https://github.com/seemoo-lab/openhaystack
理論上はBluetoothを搭載したデバイスなら何でも追跡できますが、追跡用のカスタムファームウェアが公開されてるESP32やnRF51(BBC micro:bit)、もしくはラズパイみたいなLinuxが簡単です。
今回はESP32-WROOM-32Dを使いました。
また、OpenHaystackはMacからでないと追跡ができないという厄介な点があります。
実機は持ってないので仮想環境上のHackintoshでやりましたが、OpenHaystack上のマップがブラックアウトして何も見えなくなるバグが発生して使えませんでした。
同じように純正のMapsアプリもブラックアウトしていたので、おそらくGPU周りの仮想化の問題でしょう。
そこで、OpenHaystackの代わりにコマンドラインからその位置情報を引っ張り出せるFindMy(名前そのまま?!)を使ってマップを使わずに位置を取得します。こちらは成功しました。
https://github.com/biemster/FindMy
ちなみに、OpenHaystackのREADMEにはOpenHaystack Mobileなるものが存在すると書かれていますが今は使えないようです。
また、今のところ本物のFind myアプリに登録することもできないみたいです。
https://github.com/seemoo-lab/openhaystack/issues/39
環境
追跡する対象: 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ブランチを使いました。
1 2 | % git clone -b monterey https://github.com/biemster/FindMy % cd FindMy |
必要なライブラリ
1 2 | % pip3 install -U pip % pip3 install cryptography pyobjc requests |
キーを作成します。なぜかpython3だとエラーが出た
1 | % python generate_keys.py |
出来上がった.keysファイルを覗いてみます
1 2 3 4 | % cat xxxxxxx.keys Private key: xxxx Advertisement key: xxxx Hashed adv key: xxxx |
この中のAdvertisement keyを使います。
ESP32-WROOM-32Dのファームウェア焼き
ここをファームウェアと手順が置いてあるので参考にします。
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/
ファームウェアを持ってきます。
1 2 | $ git clone https://github.com/seemoo-lab/openhaystack $ cd openhaystack/Firmware/ESP32 |
ビルドして、ESP32デバイスを指定して焼きます。ttyUSB0みたいな名前です。
1 2 | $ idf.py build $ ./flash_esp32.sh -p /dev/ttyUSBXXX さっきのAdvertisementkey |
追跡してみる
macに戻って、少し待ってからFindMyのrequest_reports.pyを叩きます。こっちはpython3じゃないと動きませんでした。
Keychain passwordはmacのパスワードです。
1 2 3 4 5 6 | % 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では過剰スペックなので、ボタン電池駆動とかはなかなか厳しいものがありそう。
追記3 もう一個作った
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は繋がない)、コマンドを実行
1 | $ 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円の正規?中華トラッカーです。
ちなみに、全て同じ場所に置いてこれなのでやはり感度が悪い
追記4
FindMyのrequest_reports.pyで得た位置情報をマップにプロット(HTMLに出力)するプログラムをGPT-4oに書いてもらいました。パスは適宜変えてください。
移動経路のアニメーションと、ピンをクリックすると確度と日時が見られるようにしてます。また、最後のピンは赤色で表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | 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時間のレポートを表示できそうですね。
1 2 3 4 | 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') |
最近のコメント