4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Chipwhisperer CW305に実装したAES回路にCPAを行う公式チュートリアルをなぞる

Last updated at Posted at 2020-10-22

Attacking AES on FPGA (CW305 / Artix7 XC7A100T)

(2022/05/07 ChipWhisperer5.6.0 に対応するよう更新)

FPGAに実装したAESに対してCPAする.

注意

現時点でLatestとしてタグ付けされているv5.5.2の場合,WindowsでCW-LiteとCW305を同時にUSBで繋ぐとcw.scope()した際にCW-Liteにつながらない問題が発生する.
原因は不明だが,回避できそうな方法のメモを最後に記載している.

v5.6.0では修正(というよりUSBライブラリの変更に関連してコードが一通りリファクタリング?)されているため,Pre-releaseではあるがv5.6.0を使用するか,この問題が発生する前のバージョン(おそらくv5.3.1かそれよりも古いもの)を使用するのがよい(と思われるが,Pre-Releaseなので他のバグが発生する可能性がある).

2020年11月以降にリリースされたファームウェアでは,ChipwhispererはWindows上でPlug-and-Playに対応しているためドライバのインストールは不要である.
これ以前のものを使用している場合,ドライバを手動で適用する必要がある.(参考リンク)

動作確認済み環境

  • Windows10 Home (21H2/19044.1645)
  • ChipWhisperer-Lite (Mouser)
  • CW305 Artix7 FPGA Target (XC7A100T-2FTG256) (Mouser)
  • ChipWhisperer 5.6.0

なお,Windowsのサポートはあまり熱心にされていないのでLinux上でGitHubから最新のものをcloneしてきて動かすほうが楽である.

インストールと接続

  1. ChipWhispererのGitHubからインストーラをダウンロード
    今回はChipWhisperer5.6.0,Windowsの場合はChipwhisperer.v5.6.0.Setup.64-bit.exeをダウンロードしてインストール.

  2. 公式の指示通りにCW305とChipWhisperer-Liteを接続する.

  3. PCにChipWhisperer-LiteとCW305の両方をUSBケーブルで接続する.

動かす

ChipWhispererを起動すると,ブラウザ上でJupyter Notebookが開かれる.
適当に作業用フォルダを設定し,空のNotebookを作ったら以下のプログラムを順にNotebookのセル上にコピペして動作させる.

必要なライブラリのインポート

import chipwhisperer as cw
import os

Chipwhisperer-Liteの設定

scope = cw.scope()

scope.gain.db = 25
scope.adc.samples = 129
scope.adc.offset = 0
scope.adc.basic_mode = "rising_edge"
scope.clock.clkgen_freq = 7370000
scope.clock.adc_src = "extclk_x4"
scope.trigger.triggers = "tio4"
scope.io.tio1 = "serial_rx"
scope.io.tio2 = "serial_tx"
scope.io.hs2 = "disabled"

CW305の設定

bitstreamの書き込み

  • force=Falseだと,FPGAに回路がロードされていない時のみ書き込みが行われる
  • force=Trueだと,問答無用で書き込みが行われる

生成済みbitstreamファイルが以下に提供されている.

Artix7 XC7A100Tの場合

chipwhisperer/hardware/victims/cw305_artixtarget/fpga/vivado_examples/aes128_verilog/aes128_verilog.runs/impl_100t/cw305_top.bit

自分でソースからbitstreamを生成する場合: 公式Tutorial (少し古い), Qiita記事 (公式より新しいが,これも古い)

bitstream = os.path.join(
    '..', '..', 
    'hardware', 'victims', 'cw305_artixtarget', 'fpga', 
    'vivado_examples', 'aes128_verilog', 'aes128_verilog.runs', 'impl_100t',
    'cw305_top.bit'
    )

target = cw.target(scope, cw.targets.CW305, bsfile=bitstream, force=True)

プロジェクトファイルの作成

projectオブジェクトのメンバ

  • project.location: str
  • project.waves: Iterable (M samples x N traces)
  • project.textins: Iterable (128 bytes x N traces)
  • project.textouts: Iterable (128 bytes x N traces)
  • project.keys: (128 bytes x N traces)
  • project.get_filename: str
  • project.trace_manager: trace_manager object
  • project.save
  • project.export
project_file = os.path.join('.', 'projects', 'AES')
project = cw.create_project(project_file, overwrite=True)

ジャンパピンの設定

  • J16 = 0
    オンボードPLLからFPGAにクロックを供給

  • K16 = 1
    FPGAのclockをChipWhispererのHS-Inチャネルに接続

target.vccint_set(1.0) # Set VCC-INT to 1V
# we only need PLL1:
target.pll.pll_enable_set(True) # Enable PLL chip
target.pll.pll_outenable_set(False, 0) # Dsable unused PLL0
target.pll.pll_outenable_set(True, 1) # Enable PLL
target.pll.pll_outenable_set(False, 2) # Disable unused PLL2

# run at 10 MHz:
target.pll.pll_outfreq_set(10E6, 1)

# optional, but reduces power trace noise
# 1ms is plenty of idling time
target.clkusbautooff = True
target.clksleeptime = 1 # 1ms typically good for sleep

ADCのロックに失敗する場合があるが,何回か試してるうちに成功する. (公式情報)

Liteだとほぼ失敗しないが,Proはよく失敗する

# ensure ADC is locked:
scope.clock.reset_adc()
assert (scope.clock.adc_locked), "ADC failed to lock"

トレースの取得

from tqdm import tnrange
import numpy as np
import time
from Crypto.Cipher import AES

ktp = cw.ktp.Basic()
ktp.setInitialKey('CA FE BA BE DE AD BE EF FE EB DA ED EB AB EF AC') # Secret key

traces = []
textin = []
keys = []
N = 10000  # Number of traces

# initialize cipher to verify DUT result:
key, text = ktp.next()
cipher = AES.new(bytes(key), AES.MODE_ECB)

for i in tnrange(N, desc='Capturing traces'):
    # run aux stuff that should come before trace here

    key, text = ktp.next()  # manual creation of a key, text pair can be substituted here
    textin.append(text)
    keys.append(key)
    
    ret = cw.capture_trace(scope, target, text, key)
    if not ret:
        print("Failed capture")
        continue
        
    # HWでの暗号化の結果が合ってるか確認する
    assert ( list(ret.textout) == list( cipher.encrypt(bytes(text)) ) ), \
        "Incorrect encryption result!\nGot {}\nExp {}\n".format(ret.textout, list(text))
    #trace += scope.getLastTrace()
        
    traces.append(ret.wave)
    project.traces.append(ret)

Plot

from bokeh.plotting import figure, show
from bokeh.io import output_notebook

output_notebook()
p = figure(plot_width=800)

xrange = range(len(traces[0]))
p.line(xrange, traces[0], line_color="red")
show(p)

2.PNG

project.save()
scope.dis()
target.dis()

Attack

  • cwa.leakage_models.last_round_state_diff

    9->10ラウンド目のSbox_inレジスタのHamming Distance

import chipwhisperer as cw
import chipwhisperer.analyzer as cwa
project_file = os.path.join('.', 'projects', 'AES')
project = cw.open_project(project_file)
attack = cwa.cpa(project, cwa.leakage_models.last_round_state_diff)
cb = cwa.get_jupyter_callback(attack)

結果の見方

  • 設定した秘密鍵の各バイトが列に対応

  • PGE (Partial Guessing Entropy)は正解鍵のランクに対応 (攻撃成功時に0)

  • 0~4の行は候補鍵の相関値Top-5を示しており,赤色(正解鍵)がRank0になればそのバイトは攻撃成功となる

attack_results = attack.run(cb)

キャプチャ.PNG

保存する

import pickle
pickle_file = project_file + ".results.pickle"
pickle.dump(attack_results, open(pickle_file, "wb"))

答え合わせ

from chipwhisperer.analyzer.attacks.models.aes.key_schedule import key_schedule_rounds
recv_lastroundkey = [kguess[0][0] for kguess in attack_results.find_maximums()]
recv_key = key_schedule_rounds(recv_lastroundkey, 10, 0)
for subkey in recv_key:
    print(hex(subkey))
0xca
0xfe
0xba
0xbe
0xde
0xad
0xbe
0xef
0xfe
0xeb
0xda
0xed
0xeb
0xab
0xef
0xac
key = list(key)
assert (key == recv_key), "Failed to recover encryption key\nGot:      {}\nExpected: {}".format(recv_key, key)

以下,チュートリアルにはない追加部分

自分でCPAしてみる

# 波形のロード
project_file = os.path.join('.', 'projects', 'AES')
project = cw.open_project(project_file)

9->10 round目のsbox_inのHDベースのCPA

関数定義

from chipwhisperer.analyzer import aes_funcs
from chipwhisperer.analyzer.attacks.models.base import getHW

# HDの計算
# v: previous value of sbox_in register
# k: key candidate
# c: cipher
def hd_model( v, k, c ):
    return getHW( aes_funcs.inv_sbox(v ^ k) ^ c  )

# レジスタのHDの計算
# 順方向: Round9-output -> sbox -> shift_row -> add_round_key
# 求めたいのは HD(Round9-output, cipher)
def calc_hd(cipher, target_byte):
    
    v = cipher[target_byte]
    c = cipher[ np.where(np.array(aes_funcs.inv_shiftrows(list(range(16)))) == target_byte)[0][0] ] # inv_shift_row(cipher)
    
    hd = []
    for k in range(256):
        hd.append([np.array(hd_model(v, k, c), dtype=np.float32)])
    
    return np.concatenate(hd, axis=0)

# ピアソンの相関係数の計算
# x, yの相関を計算する
# yが2次元の場合,axis=1に沿って相関を計算する
def pearsonr(x, y):
    '''
                sum( ( x-x.mean ) * ( y-y.mean ) )
    r = --------------------------------------------------
            root( sum( (x-x.mean)^2 * (y-y.mean)^2 ) )

    '''
    
    x = np.asarray(x, dtype=np.float64)
    y = np.asarray(y, dtype=np.float64)

    # xとyの次元数がともに1のとき
    #普通に相関を計算する
    if x.ndim == 1 and y.ndim == 1:
        mx = x.mean()
        my = y.mean()
        xm = x-mx
        ym = y-my

        r1 = np.sum( xm * ym )
        r2 = np.sqrt( np.sum(xm**2)
                            * np.sum(ym**2) )

        ret = r1/r2
        if np.isnan(ret):
            return 0
        else:
            return ret

    # yの次元数が2の時
    # yの2次元目に沿って相関を計算する
    if x.ndim == 1 and y.ndim == 2:
        assert(x.shape[0] == y.shape[1]), "size of axis 1 is must be same size of x, x:{} != y:{}".format(x.shape[0], y.shape[1])

        xm = x-x.mean()
        ym = y-y.mean(axis=1).reshape(-1,1)

        r1 = np.sum( xm.reshape(1,-1) * ym, axis=1 )
        r2 = np.sqrt(
                np.sum(xm*xm).reshape(-1,1)
                * np.sum(ym*ym, axis=1)
             )

        ret = (r1/r2).reshape(-1)
        ret[np.isnan(ret)] = 0
        return ret

攻撃

target_byte = 0 # 攻撃対象のバイト鍵
correct_key = project.keys[target_byte] # 正解鍵
outs = project.textouts # Cipher
waves = project.waves # 電力波形

各候補鍵におけるレジスタのHDを計算する

hds = []
for o in outs:
    hds.append([calc_hd(o, target_byte)])
hds = np.concatenate(hds, axis=0)

レジスタのHDと電力波形の相関を計算する

ret = []
for i in range(256):
    ret.append([pearsonr(hds[:, i], np.array(waves).T)])
ret = np.concatenate(ret, axis=0)

結果のプロット

import matplotlib.pyplot as plt
from chipwhisperer.analyzer.attacks.models.aes.key_schedule import key_schedule_rounds

# Mean waveform
fig1 = plt.figure()
ax1 = fig1.add_subplot(111)
ax1.plot(np.mean(waves, axis=0))
ax1.set_title('Mean waveform')

# CPA result
fig2 = plt.figure()
ax2 = fig2.add_subplot(111)
ax2.plot(ret.T, 'gray')
ax2.plot(ret[ aes_funcs.key_schedule_rounds(correct_key, 0, 10)[target_byte], :], 'red')
ax2.set_title('target_byte: {}'.format(target_byte))

plt.show()

output_45_0.png

output_45_1.png

10ラウンド目の計算を行っているタイミングで相関が正解鍵の相関が最大になっていることがわかる.

おわり.

メモ:v5.5.2で発生する接続エラーのバグ探しの途中経過

原因となるコードはだいたいここ(chipwhisperer/hardware/naeusb/naeusb.py)

NAEUSB_Backendクラスの定義(L252-)内,有効なデバイス一覧を取得する関数get_possible_devices(L422-)において,以下のようなバグが観測された.

  • L448 dev = list(usb.core.find(**my_kwargs))にて,変数devに接続可能なデバイスオブジェクトのList(CW-LiteとCW305の2台ぶん)が代入される.

  • 変数devの要素数が2以上なので,以下のループ(L460-468)が実行される.

    for d in dev:
        try:
            d.serial_number
            devlist.append(d)
        except ValueError as e:
            if "langid" in str(e):
                naeusb_logger.info('A device raised the "no langid" error, it is being skipped')
            else:
                raise

ここで,正常な場合でもd.serial_numberの実行時にValueError例外が発生する.
また,発生する例外にlangidが含まれないためそのまま無名の例外が発生し,そのままに接続エラーとなる.

なぜか,d.serial_numberをtryの外で一度呼んでやるとCW-Liteとの接続に成功する(が,その後CW305が繋がるかはテストしていない).

    for d in dev:
	d.serial_number
        try:
            # d.serial_number  #  ちなみに,tryの外で一度呼ぶと,tryの中で呼んでも例外は発生しないよう.    
            devlist.append(d)
        except ValueError as e:
            if "langid" in str(e):
                naeusb_logger.info('A device raised the "no langid" error, it is being skipped')
            else:
                raise

あとnaeusb.pyを5.6.0のものにそのまま置き換えても動いた.
5.6.0の場合,こんなにややこしいコードにはなっていないのでそもそもこのようなバグには遭遇しそうにない.多分.

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?