
2026/01/17 7:13
「Drawbot:かわいいものをハックしてみよう(2025)」
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Summary:
この記事では、著者が8ビットバーコードを使用した物理カードで256種類の事前定義された図形から1つを選択する単純な描画ロボットを逆エンジニアリングした方法を説明しています。2つのSPI NORフラッシュチップ(1つは音声、もう1つはグラフィック)をダンプすると、ファイル名とCRCチェックサムを含むディレクトリ構造が判明しました。生データは座標ペアであり、SVGパスに解析できます。Pythonスクリプトで16 MBのダンプから254個の利用可能な画像を抽出しました。新しい図形を追加するために、著者はSVGをカードのバウンディングボックスに合わせてリサイズし、必要なバイナリ形式にパックして
flashromで書き戻すスクリプトを書きました。ロボットは複数段階でマルチプレクスされたセンサーを通じてバーコードを読み取り、5つのカテゴリ(食べ物・動物・植物・車両・円)にわたる8ビットインデックスを取得します。今後の計画としては、抽出からアップロードまでのパイプライン全体を自動化し、音声フォーマットを解読し、ロボットの機械構造を再設計することが挙げられます。この作業により、ホビイストは玩具のアートライブラリをカスタマイズできるようになり、低電力組込みグラフィック保存技術への洞察が得られ、同様のデバイスを改良または再利用するメーカーにもインスピレーションを与える可能性があります。
Summary Skeleton
What the text is mainly trying to say (main message)
著者は8ビットバーコード付き物理カードを使用して256種類の事前定義された図形から1つを選択する玩具描画ロボットを逆エンジニアリングし、フラッシュメモリに新しい図形を抽出・変更・アップロードする方法を示しました。
Evidence / reasoning (why this is said)
- ロボットのハードウェアはARM Cortex‑M0 MCUと2つのSPI NORフラッシュチップで構成されており、より小さいチップが音声ファイルを保持し、大きいチップに描画データが保存されています。
- フラッシュダンプからディレクトリ構造(CRC、オフセット、サイズ、フラグ、ファイル名例「001.f1a」)が判明し、生データはSVGパスに解析できる座標ペアを含んでいます。
- Pythonスクリプト(
)で16 MBダンプから254個のSVG図形を抽出し、期待される画像数を確認しました。img_carver.py
Related cases / background (context, past events, surrounding info)
- ロボットはマルチプレクスされたセンサーを2フェーズで駆動してバーコードを読み取り、5つのカテゴリ(食べ物・動物・植物・車両・円)にわたる8ビット値を取得します。
- カスタムスクリプト(
)はSVGをスロットのバウンディングボックスにマッピングし、必要なバイナリ形式にパックしてsvg_fit_to_slot.py
で書き戻します。flashrom
What may happen next (future developments / projections written in the text)
著者は抽出からアップロードまでの全プロセスを自動化し、音声ファイルフォーマットを解読し、ロボットの機械的構造を再設計することを計画しています。
What impacts this could have (users / companies / industry)
ユーザーはロボット用にカスタム図形を作成できるようになり、その創造性を拡張します;ホビイストや研究者は低電力組込みグラフィック保存技術への洞察を得られ、メーカーは同様の玩具デバイスを改善または再利用するインスピレーションを受ける可能性があります。
本文
ターゲット
数か月前、私は楽しくてユニークなハードウェアプロジェクトに取り掛かるべきだと感じました。時々、新しい面白い電子子ども向け玩具が市場に出ているのを確認するのが好きです。調査するときは、潜在的な攻撃対象(通常は同伴モバイルアプリやワイヤレス通信、その他の複雑さを持つもの)を意識しています。
私はあるロボットを見つけました。このロボットは事前に定義された画像セットから描画します。すべてのブランドでカードが100〜150枚ずつ付属し、描画は非常に似通っています。物理的なカードを使っているので攻撃対象は小さく見えました(FCC ID を覗き込んで内部を見ることもできないでしょうし、私はそれ自体がスプライヤーだと考えていました)。それでも興味深いターゲットのように思え、抵抗できませんでした。
選んだ理由
- 箱のデザインが一番良かった。
- ラベルやタイプミスはクワイアリティの兆候だった。
- 100枚のカード付きで、それぞれに8ビット(256通り)の「バーコード」が最小限に記載されている。
- カードは食べ物、動物、植物、車両、円形という5つのカテゴリに分かれていた。
私は「ボブスカクタス」のカードを読み込ませ、実際に描画させました。ロボットが話し、歌い、完璧でした。
解析開始
分解
正直言って、これは私のお気に入りの部分です。
底部には極端に浅く掘られたネジ穴があります。この段階で適切な工具を持っているはずなのですが、実際にはありませんでした。ハードウェア評価では、まだ何が足りないか(忍耐力以外)を常に認識します。自宅ラボで集めた専門ツールや部品でも、必ず購入しなければならないものがあります。この時点で同じ日または翌日に必要なものを簡単に手に入れられましたが、それには忍耐が必要です。3Dプリントで解決できるかもしれませんが、私はそれほど忍耐力がありません。
忍耐不足をパワーツールで補います。穴を少し大きく掘り、ドライバーのビットとチェーンビットを組み合わせました。4つのネジを外すと上半分はほぼ問題なく外れました。
バーコードリーダーが露出
バーコードリーダーが見えるようになったので、正当なカードを取り込み、予期しない入力としてシフトして基本的なファズィングを試みました。カードをセンサーに置くとデバイスは音を鳴らし、その後関連画像を発表します。
ファズィング中、さまざまな画像が報告され、すべてはデッキ内の他のカードと一致していましたが、ロボットが「お風呂に入って!」と言い出しました。これは奇妙で(少し)不快でしたので、デッキを調べましたが、お風呂のイメージを持つカードは見当たりませんでした。カードを固定したままボタンを押すと、ロボットは歌い始め、この画像を描きました。
目標
作業対象を把握した今、次の2つのゴールを設定しました:
-
利用可能なすべての描画を列挙し、特定する。
カードだけでは全貌が分かりません。 -
Bird on It™ を実装する(つまり、これらの描画がどのように表現・保存されるかを解明し、その情報を使って自分で追加できるようにする)。
コンポーネント
次にボード上の部品を特定し、取得/ダンプできるものを確認します。主な3つは以下です:
– ARM Cortex‑M0 MCULKS32MC07x
– 64 MB SPI NORフラッシュuc25IQ64
– 128 MB SPI NORフラッシュuc25IQ128A
私は64 MBフラッシュをSPI経由でダンプできましたが、128 MBフラッシュは取得できず、MCUにSWDで接続することもできませんでした。良いスタートではありませんが、諦めません。
バーコード解析
マルチメータと印刷されたトレース線を使って、バーコードを読み取る光学センサーの配線をマッピングし、どのカードが挿入されているか判定します。最初はセンサーとピンに重複があることに困惑しましたが、入力マルチプレクシングという概念に出会いました。基本的にはボードはVCC 1でセンサー1〜5を電源供給し、その後入力ピンの値を読み取ります。その後VCC 2でセンサー6〜8を電源供給し、同じ入力ピンを再利用して値を読み取ります。
理論を検証するためにロジックアナライザを各入力に接続しました。元の赤いワイヤーコネクタはカラ―コード付きの独自製作に置き換え、ブレッドボードとヘッダーで2行に分けました。このセットアップにより受動解析と積極的信号操作を同時に行うことができました。
VCCラインが定期的に交互に変化する様子を確認し、マルチプレクシング理論の説得力を高めました。Saleae に接続した状態でカードをスキャンすると、期待通りの振る舞いを観察できます。
00000001 のバーコード値に対応するカード(クラブカード)を選びました。右側センサーが HIGH になるはずですが、裏面では逆になっているため左側センサーが HIGH になります。
VCC 1フェーズでセンサー1〜5が電源供給され、ピンが読み取られます。左側(センサー1)が唯一の HIGH (1) です。他は LOW (0)。次に VCC 2 フェーズでセンサー6〜8が電源供給され、再びピンを読み取り、バーコードの最後の3ビットを構成します。最終的な値は
00000001 となります。
VCCトリガーを逆順(VCC 2→VCC 1)で示したものもありますが、処理は何度か繰り返されるため順序は重要ではありません。
バーコードエミュレーション
バーコードが信号に変換される仕組みを理解したので、プログラム可能な入力形式でエミュレートできます。Raspberry Pi を使用しました(3.3 V で動作し、この用途に最適)。
まずセンサー基板の各関連ピンを Pi の GPIO ピンにマッピングします。実際にはボードを電源供給する必要はないので、入力として扱い、パワーサイクルを追跡して「どのセンサーセット」が読み取られているか、そしてそれがバーコードの何ビットに対応するかを知ります。
すべてを配線した後、次のスクリプトでロジックを書きました。10進数値 1〜256 をループし、2進数へ変換し、それを2つのグループに分割して VCC マルチプレクシングフェーズに応じた GPIO ピンに書き込みます。
import gpiod import time chip = gpiod.Chip("gpiochip0") # inputs: vcc1 = chip.get_line(23) vcc2 = chip.get_line(5) vcc1.request(consumer="vcc1", type=gpiod.LINE_REQ_DIR_IN) vcc2.request(consumer="vcc2", type=gpiod.LINE_REQ_DIR_IN) # outputs: outputs = chip.get_lines([2, 3, 4, 17, 27]) outputs.request(consumer="barcode", type=gpiod.LINE_REQ_DIR_OUT) try: for bcode in range(1, 256): bits = [int(b) for b in format(bcode, '08b')] group1 = bits[-5:][::-1] # sensors 1‑5 group2 = bits[:-5][::-1] # sensors 6‑8 last_vcc1 = 0 last_vcc2 = 0 start = time.time() while time.time() - start < 4: # 各バーコードを 4 秒間保持 v1 = vcc1.get_value() v2 = vcc2.get_value() if v1 == 1 and last_vcc1 == 0: # VCC 1 上昇エッジ outputs.set_values(group1) time.sleep(0.02) outputs.set_values([1, 1, 1, 1, 1]) if v2 == 1 and last_vcc2 == 0: # VCC 2 上昇エッジ outputs.set_values(group2 + [1, 1]) time.sleep(0.02) outputs.set_values([1, 1, 1, 1, 1]) last_vcc1 = v1 last_vcc2 = v2 outputs.set_values([1, 1, 1, 1, 1]) time.sleep(2) except KeyboardInterrupt: outputs.set_values([1, 1, 1, 1, 1]) outputs.release() vcc1.release() vcc2.release()
多くの試行錯誤、ロジック解析、配線確認と呪文を繰り返してこのポイントに到達しました。
画像列挙
「lookup」ファイルでスクリプトを修正し、数値とともにテキストも出力できるようにしました。これで奇妙なものや「チェーンブロック」と呼ばれるものを描画できます。
$ python3 all-barcodes.py 218 ([1, 1, 0, 1, 1, 0, 1, 0]) 218: chain block?
これで、カードにない「隠し」画像も見つけることができました。以下は利用可能なすべての画像です。ロボットと一緒に提供されるカードは 1〜100 の画像のみを含み、それ以降は謎めいた隠しボーナスです。
描画解析
自分で画像を描くには、画像がどのように表現・保存されているか、そして上書きできるかを理解する必要があります。最初に開封したとき、小さい方のフラッシュチップだけダンプできたので、そこに画像ファイルがあると仮定して解析しました。HEX 出力はディレクトリエントリーのレイアウトと数値ファイル名を含んでいるようでした。
典型的なディレクトリエントリー(32 バイト)は次の通りです:
┌───────────────┬──────────────┬─────────────┬─────────────┬──────────────────────────┐ │ CRC / checksum│ START offset │ SIZE │ FLAGS │ NAME (ASCII, 16 bytes) │ │ (uint32 LE) │ (uint32 LE) │ (uint32 LE) │ (uint32 LE) │ null‑terminated │ └───────────────┴──────────────┴─────────────┴─────────────┴──────────────────────────┘
例:
001.f1a のディレクトリエントリーは
- CRC:
0xD243AE42 - START offset:
0x004520 - SIZE bytes:
0xFAC - FLAGS / unk:
0x0000FF02 - NAME:
'001.f1a'
これを基に Logic の HLA を使い、SPI フラッシュの読み取り操作を検出し、fast_read から取得したアドレスでディレクトリエントリーと照合し、読み取られているファイル名を表示します。
ロボットが画像 105 を描いた際は
Y105.f1a が読み込まれました。音声ファイルもいくつか読み込んだため、大きなフラッシュチップに描画データが格納されていると判明しました。
フラッシュ抽出
128 MB のフラッシュを BusPirate で取り外し、抽出しました:
$ sudo flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r UC25IQ128_3.bin
内容は構造化されており、おそらく座標ベースの描画データです。LLM で解析した結果、16 MB 読み取りを強制すると254 個の画像が確認できました。
生データ → SVG
Python スクリプト
img_carver.py は各画像スロットを解析し SVG を書き出します:
def write_svg(strokes, out: Path, flip_y=True, stroke_w=2): bb = bbox(strokes) if not bb: return False xmin, ymin, xmax, ymax = bb w, h = xmax - xmin + 1, ymax - ymin + 1 with out.open("w", encoding="utf-8") as f: if flip_y: f.write(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{xmin} {ymin} {w} {h}" ' f'width="{w}" height="{h}" stroke="black" fill="none" stroke-width="{stroke_w}">\n') f.write(f' <g transform="translate(0,{ymin + ymax}) scale(1,-1)">\n') for s in strokes: f.write(' <path d="M{},{} {}"/>\n'.format( s[0][0], s[0][1], " ".join(f"L{x},{y}" for x, y in s[1:]))) f.write(' </g>\n</svg>\n') else: # … (non‑flipped version omitted) return True
実行例:
$ python3 img_carver.py UC25IQ128_forced.bin --no-flip-y --out svgs Exported 254 SVGs to svgs from 279 slots …
SVG → スロット
単一画像を上書きするために
svg_fit_to_slot.py を作成しました。手順は:
- 既存スロットを読み込み、バウンディングボックスを取得。
- SVG を連続ストロークにサンプリング。
- ストロークをターゲットバウンディングボックス内に量子化。
- 0xEA5C バイトペイロード形式に再パック。
$ python3 svg_fit_to_slot.py slot_006_content.bin whisky-outline.svg \ --out slot_006_new_content.bin --step 6.0 --margin 4 [info] target bbox (from slot): x[56..1124], y[176..1005] [done] wrote slot_006_new_content.bin (59996 bytes).
新しいバイナリをフラッシュへ書き戻します:
$ dd if=slot_006_new_content.bin of=UC25IQ128_trunc_mod.bin bs=1 seek=$((0x000493E4)) conv=notrunc $ sudo flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=250k -f -w UC25IQ128_trunc_mod.bin -VV
フラッシュをロボットに再装着すると、新しい描画が正しく表示されます。
今後の作業
- SVG → フラッシュ書き込み → GPIO で描画トリガーするパイプラインを自動化。
- 音声ファイル(カスタム WAV/ADPCM のように見える)を解析・置換。
- より簡単にフラッシュへアクセスできるロボット本体の再設計や新規構築を検討。
このプロジェクトを再度取り掛かる、または別のターゲットを選んで尊敬しつつ「解剖」する場合は、随時更新します。ハッキングを楽しんでください!
— Jessie Chab
リサーチコンサルティングディレクター