この記事はGMOインターネットグループ Advent Calendar 2025 6日目の記事です。Python を使って光学ドライブのトレイを動かすことを通じて、ローレイヤの世界に触れてみましょう。
はじめに
GMOサイバーセキュリティ byイエラエの三村です。普段は主に組込機器の脆弱性診断業務や教育などを行っています。
この記事では、セキュリティ診断などでも利用する PyUSB を用いて、光学ドライブのトレイを動かすことを通じて、USBのローレベルの通信と SCSI の通信を軽くご紹介出来たらと考えています。
USB と PyUSB
何かの機器をパソコンに繋ぐときやスマホを充電するとき、さまざまなタイミングでよく使われる身近な端子に USB (Universal Serial Bus) があります。
USB は、パソコンと周辺機器を接続するための一般的なインタフェースのひとつです。USB を用いてマウスやキーボード、オーディオ機器やディスプレイまで様々な機器を接続することができます。
機器と同じように USBの通信では、USBのプロトコルに沿った通信の上で、それぞれの機器の種類にあわせた様々な通信が行われています。その中身には USB-Ethernet アダプタであれば Ethernet、USBメモリであれば SCSI というように、他のメディアで長く使われてきたプロトコルも使って通信が行われます。
そして PyUSB は Python を使ってお手軽に USB 機器とおしゃべりが出来るライブラリです。USB機器と原始的なおしゃべりをすることができ、機器に送るバイナリデータを Python で作って機器に送信したり受信することができます。開発テストやセキュリティ試験など様々な場所でも利用されるライブラリです。
つまり、PyUSB を使って USB のプロトコルとその上に乗る別のプロトコルに合わせたデータを作って送信すれば、USBを通して別のプロトコルを利用する機器とおしゃべりすることも出来るのです。
USBってとても万能ですよね。
トレイを出したい
USBの上で別のプロトコルが流れる例として分かりやすいものとして、USB の「マスストレージ」の通信があります。マスストレージにはハードディスクやSSD、そしてこれから話をする光学ドライブなどがあります。
光学ドライブは使われる機会が少なくなった機器ではありますが、他のマスストレージと異なり、「取り出し」操作を行うと写真のようにディスクを載せるトレイが大きく飛び出してくるという特徴があります。
機器を制御して大きく機器が動けば喜びもひとしお、ということで今回はこれを制御していきます。
USB の通信のざっくり理解
今回の内容の理解に必要なUSBの知識は下記の3つです。
ベンダーID / プロダクトIDエンドポイント転送モード(バルク転送)
なお以降の説明は必要最小限の内容に留めています。ご了承ください。
ベンダーID / プロダクトID
機器を個体識別するために利用出来る、機器に予め設定されている ID になります。今回は送信する機器を指定するために必要になります。
Windows であればデバイスマネージャーのハードウェアIDで、Linux であれば lsusb コマンドで確認出来ます。
(Windows でのデバイスマネージャーの画面例。 VID がベンダーID, PID がプロダクトID になります。この例ではベンダーIDが0x0408, プロダクトID が 0x5265 になります)
エンドポイント
USBはデバイス側の「エンドポイント」とよばれるバッファに対して、読み書きを行うことでデータのやり取りを行います。
エンドポイントは番号を付けて複数用意することができ、また各エンドポイントは単方向で用いられます。インターネット通信におけるポート番号のようなものと考えても良いでしょう。
今回の光学ドライブでは、ホストがデータを書き込む用のエンドポイント (Bulk Out) と、ホストがデータを読み取る用のエンドポイント (Bulk In) の2つが用意されています。例として光学ドライブに対して何らかのコマンドを送る場合は次のような流れで行われます。
(ホスト)光学ドライブのエンドポイント (Bulk Out) に対してデータを書き込む。(光学ドライブ)自身のエンドポイント (Bulk Out) に書き込まれたデータを読み取り、処理をする。(光学ドライブ)結果を自身のエンドポイント (Bulk In) に書き込む。(ホスト)光学ドライブのエンドポイント (Bulk In) を読みに行き結果を得る。
転送モード(バルク転送)
USBは様々な転送モードを持っています。USBで利用可能な転送モードは下記の4つです。
コントロール転送バルク転送インタラプト転送アイソクロナス転送
今回はこのうち「バルク転送」というある程度まとまった量のデータを転送するためのモードを使って通信を行います。各モードの詳細は今回の記事では割愛します。気になる方は調べてみてください。
PyUSB でおしゃべりをする
今回は PyUSB を用いて下記のような通信を行います。
SCSI の仕様にそって、「START/STOP UNIT」コマンドを用いてディスクを取り出す (Eject) 指示が入ったデータを作成するUSB の Mass Storage Class の仕様にそって、 Command Block Wrapper のデータを作成する。このとき CBWCB パラメータに先述の1で作成したデータを格納するUSBデバイスの bulk-out に先述の2で作成したデータを書き込む機器側でデータが処理され、トレイが排出される
それでは実際にやってみましょう。
1. PyUSB をインストールする
pip コマンドで簡単にインストールが可能です。
pip install pyusb
また、RedHat / Fedora では "python3-pyusb", Debian / Ubuntu では "python3-usb" というパッケージでも提供されています。
Windows のみ / Zadig を用いてドライバをインストールする
Windows では、そのままでは PyUSB で通信をすることが出来ません。Zadig (https://zadig.akeo.ie/) というソフトを用いてドライバをインストールする必要があります。
1. Options から "List All Devices" にチェックを入れる
2. 操作したい機器 (今回は "USB-SATA Bridge") を選択し、Driver が "libusbK" になっていることを確認して、"Replace Driver" を押下
処理が完了すると、Windows 側からの光学ドライブの認識が外れて、PyUSB から自由に制御出来る状態になります。
2. 仕様書を読む
光学ドライブに送るデータを作成するために下記に示す2つの仕様(SCSI と USB の Mass Storage Class [Bulk Only Transport] )の理解が必要になります。
SCSI / START STOP UNIT コマンド
Disc の取り出し操作は "Start Stop Unit" コマンドを利用して行います。下記に示す構造に沿ってデータを作成し、後述する "Command Block Wrapper" を用いて光学ドライブに送付すればOKです。
START STOP UNIT コマンド (筆者一部加筆・修正)(横方向はビット、縦方向はバイトです)
765432100OPERATION CODE (1Bh)1ReservedIMMED2Reserved3Reserved4ReservedLOEJSTART5Reserved
各パラメータの説明は下記のとおりとなります
OPERATION CODE : START STOP UNIT コマンドを示す 0x1B を指定します。IMMED : 即時実行するかを指定します。今回は不要のため「0」を指定します。LOEJ : LOad / EJect のフラグです。取り出しを行うため「1」を指定します。START : 今回のケースでは 「0」を指定します。
なお上記の表で Reservedとなっている部分は 0 を指定してください。また4バイト目については LOEJ のみを 1 とすればよいため 0x02 を指定すれば問題ありません。
(Reference : SCSI Commands Reference Manual,p231, https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf )
USB / Command Block Wrapper (CBW)
今回の例では USB として光学ドライブに命令を送信するために Mass Storage Class の Bulk-Only Transport の機能を利用します。下記に示す構造に沿ってデータを作成して、光学ドライブに送信すればOKです。
データの仕様は下記のとおりです。
Command Block Wrapper (筆者一部加筆)(横方向はビット、縦方向はバイトです)
765432100~3dCBWSignature4~7dCBWTag8~11dCBWDataTransferLength12bmCBWFlags13ReservedbCBWLUN14ReservedbCBWCBLength15~30CBWCB
各パラメータの説明は下記のとおりとなります
dCBWSignature : 送信したデータが Command Block Wrapper であることを示す値。リトルエンディアンで 0x43425355 の固定値となります。dCBWTag : 送信したコマンドを識別するためのタグになります。実行結果を取得した際にどのコマンドに対する結果なのかをこの値で判断します。今回はどのような値を入れても構いません。dCBWDataTransferLength : データの送受信を行う場合に予想されるデータのサイズを設定します。今回はディスクの取り出しのみであるため「0」を設定します。bmCBWFlags : dCBWDataTransferLength に紐付いたデータの送信方向を指定します。今回は「0」を設定します。bCBWLUN : Logical Unit Number (LUN) の指定が必要な場合に指定します。今回は「0」を指定します。bCBWCBLength : 送信される SCSI のコマンド列が何バイトかを指定します。今回は "START STOP UNIT"コマンドを送るのでそのデータ長である「6」を指定します。CBWCB : SCSI のコマンドのデータを設定します。
(Reference : Universal Serial Bus Mass Storage Class, Bulk-Only Transport, Revision 1.0 https://www.usb.org/sites/default/files/usbmassbulk_10.pdf)
3. 送る
先述までの仕様にそって、Python でデータを作成・送付するスクリプト例を下記に示します。Vendor ID や USB の Endpoint については、お使いの機器にあわせてください。
import usb
import usb.core
import struct
# 操作を行いたいデバイスにあわせて設定を変更する。
# ※今回サンプルとして用いた機器の Vendor ID は 0x67B、プロダクトID は 0x2773 でした。
dev = usb.core.find(idVendor=0x67B,idProduct=0x2773)
dev.set_configuration()
# ---
# ※今回のサンプルの機器では dev.get_active_configuration() 実行時に
# 下記に示すように [Bulk IN], [Bulk Out] の順番で情報が得られたため
# [0] を ep_in, [1] を ep_out として取り扱っています。
#
# 出力例)
# print(dev.get_active_configuration())
#
# ENDPOINT 0x84: Bulk IN ===============================
# bLength : 0x7 (7 bytes)
# (省略)
# ENDPOINT 0x1: Bulk OUT ===============================
# bLength : 0x7 (7 bytes)
# (省略)
#
# 誤作動がないよう、試す場合は下記コマンドを実行して
# どのような順番で主力されるかを念のためご確認ください。
#
# print(dev.get_active_configuration())
# ---
# Endpoint の情報を求める
ep_in = dev.get_active_configuration()[(0,0)][0]
ep_out = dev.get_active_configuration()[(0,0)][1]
# SCSI (Command Block) のデータを作る
scsi_data = struct.pack(">B",0x1B) # Operation Code (1Bh = Start stop unit)
scsi_data += struct.pack(">BBB",0,0,0) # Reserved x3
scsi_data += struct.pack(">B",2) # LoEJ = 1, Start = 0
scsi_data += struct.pack(">B",0) # Reserved
# USB Mass Storage Class / Command Block Wrapper のデータを作る
usb_data = b"USBC" # dCBWSignature
usb_data += struct.pack("<L", 0x11111111) # dCBWTag
usb_data += struct.pack("<L", 0) # dCBWDataTransferLength
usb_data += struct.pack("<B", 0) # bmCBWFlags
usb_data += struct.pack("<B", 0) # bCBWLUN
usb_data += struct.pack("<B", len(scsi_data)) # bCBWCBLength
usb_data += scsi_data # CBWCB (SCSI DATA)
# Padding を挿入 (CBWCB のブロックサイズにあわせて SCSI のデータが全体で 16bytes になるように)
usb_data += b"\x00" * (16-len(scsi_data))
ep_out.write(usb_data)
ep_in.read(size_or_buffer=32,timeout=10000)
これを実行すると、ドライブからいい音がしてトレイが飛び出してきます。
まとめ
この記事ではUSB とその上で通信される SCSI について、自分でパケットを作成して通信する例を紹介しました。
USB メモリや外付けハードディスクなどもこのような SCSI コマンドで通信されています。仕様書を見て送信データを書き換えて、ストレージ内のデータの読み出しにチャレンジしてみるのもよいかもしれません。
今後もこのような形で、ローレイヤーの世界を紹介して、みなさまの視野とワクワク感を増やすことをよりご紹介出来たらと思います。最後までお読みいただきありがとうございました!