少し前まで、M16C(Mitsubishi / Renesas)からファームウェアを削除する必要がありました。 IDA v6.1.xxxがこのファミリーのコントローラーを「保持」しないことが判明したことに驚いた。 ただし、SDKは利用可能です。つまり、怖くないということではありません。状況を修正します。 実践が示しているように、モジュールを書くことの複雑さ以外には何もありません(ロケット科学、お茶ではありません)。
免責事項
私はIDA proのスペシャリストではなく、そのためのモジュールを作成しています。 タスクはファームウェアを分析することであったため、モジュールは急いで(ひざまずく)書かれ、コードの一部はSDKから引き出されましたが、動作方法(およびこれが必要かどうか)を理解していませんでした。 残りのコードは整理され、「創造的に」再考されました。
テストも書く時間もありませんでした。 作業の正確性は、Renesas IDEの逆アセンブルされたファームウェアのリストで確認されました。 したがって、エラーが発生する可能性があります(プロセス中にエラーに遭遇することはありませんでしたが、確かにあります)。 誰かがテストを書いたり、モジュールを変更したいという願望を持っているなら、私はうれしいです。
それはそうかもしれないが、モジュールは非常に機能していることが判明し、タスクを完了することができた。 この記事では、これらすべてに対する私の考えを概説します。 したがって、この作業は自己責任で行ってください。
ソースとプラグインアセンブリ
ソースは
こちらです。
私は特にStudio(MSVC ++)を尊重していないため、アセンブリにMinGWを使用し、ツールチェーンを積み重ねました。
ここで取ることができ
ます 。 このツールチェーンは自給自足です-コンパイラとアセンブリ用のツールが含まれています。
組み立ての準備は次のとおりです。 IDA_Plugins.7zをどこかで解凍し、GitHubからリポジトリを複製し、m16c_xxディレクトリをIDA_Pluginsディレクトリのルートにコピーしてから、build.cmdを実行します。
はじめに
実際には通常のdllである各プロセッサモジュール(わずかな違いはありますが、DOSヘッダーは若干変更されています)は、タイプprocessor_tのLPHという名前の構造体をエクスポートする必要があります。 主な機能とモジュール構造へのポインタを保存します。
この構造のすべてのさまざまなフィールドのうち、主に次の関数ポインターに関心があります。
processor_t LPH = { … ana,
彼らの助けを借りて、基本的に、すべての作業は完了です。
ana()関数は、新しい命令を分析するときに毎回(分析するものがある場合)、そのシグネチャを呼び出します。
int idaapi ana(void);
この関数のタスクは、現在の命令アドレスポインターからバイトを選択することにより、命令のデコードを試みます。うまくいかない場合は、命令全体とそのオペランドがデコードされるまで後続のバイトを選択し続けます。 次に、グローバル変数cmdのフィールドに入力し、命令の長さをバイト単位で返します。
emu()関数は、命令、その署名をエミュレートすることを目的としています。
int idaapi emu(void);
この機能の目的は次のとおりです。
- (k)データとコードの両方のこの命令から相互参照を作成します。
- スタック変数の作成(残念ながら、これがどのように機能するのかまだわかりません)
- そこに何か他のもの。
out()関数は、アセンブラー命令のテキスト表現、その署名を作成して表示します。
void idaapi out(void);
outop()関数は、アセンブラー命令のオペランド、その署名のテキスト表現を作成して表示します。
bool idaapi outop(op_t &x);
命令分析
ソースデータの分析は、実装する必要がある関数
ana()によって実行されます。 ファームウェアのバイトを順番に読み取るタスクは、命令、そのオペランド、および命令の長さを決定します。
命令とそのパラメーターを認識した後、
insn_t型のグローバル変数cmdのフィールドに入力し
ます 。
class insn_t { public: ea_t cs;
説明からわかるように、命令には最大6つのオペランドを含めることができます(この場合、「背後」にあります-この場合、操作には最大3つのオペランドが含まれます)。
これまでのところ、RISCコントローラー専用のモジュールを作成しました。すべてが非常にシンプルです。 命令のCPCフィールド(オペレーションコード)にマスクを配置し、
switchステートメントの分岐で追加条件
を確認します。実際、すべて(Microchip PICコントローラーのコードはほぼこの方法で分析されます。例としてSDKを見ることができます)。 ここで、RISCの利点は明らかです。コマンドのセットが減り、長さが等しくなります。 残念ながら、M16Cについては300を超える一意のコマンド(すべては視点に依存します-一意のCPCを考慮しました)、さらに可変長コマンド(CISC)をカウントしました。 したがって、
switchステートメントは、キャッチしにくいエラーの扱いやすさと弱さのために、私たちには適していません。
ここで、少し余談をする必要があります。 マシンコード(およびアセンブラ自体)は実際には
通常の言語 (
文法 )であるため、本質的にはプロセッサであるステートマシン(FSM)を使用して問題なく理解する必要があります。
マシンを手動で実行したいという希望はありませんでしたが、C、C ++、C#、Objective-C、D、Java、OCaml、Go、またはRubyのコードで、特別なFSM記述言語からの有限状態マシンのコンパイラである
Ragelには肯定的な経験がありました。 外部依存関係は作成されず、選択されたプログラミング言語の自給自足のソースのみが作成されます。
とりわけ、
Ragelは入力データをその場で解析できるという点で興味深いです。 つまり コマンドを含むことが保証されている大きなバッファーを分析のためにパーサーに形成して渡す必要はありませんが、呼び出し間の状態を維持しながら、最大1バイトの少量のデータに制限することができます。 私たちにぴったりです!
結果は、プロセッサ命令を解析するための一種の
DSLです。
コマンドを解析するためのDSL
スイッチに対するこの
DSLの利点は、主にその直線性です。 これがどのように機能するかを理解したり、動作を変更したりするために、演算子の分岐をスキップする必要はありません。 特定のコマンドのCPCとオペランドのすべての処理は、1箇所に集中しています。 例:
#
または、オプションとして、オペランドを指定したコマンド:
#
それは非常に明確で便利なように思えます。
enum opcodes列挙値の1つ(
ins.hppファイル)は
cmd.itypeに
配置されます 。これは、命令のテキスト表現、オペランドの数を示し、命令とオペランドの相互作用をさらに説明します。 また、オペランドフィールドが入力されます。
命令実行エミュレーション
命令自体、オペランドの数、およびオペランドへの影響は、
instruc_t命令[] 配列 (
ins.cpp )で説明されています。 基本的に、レコード形式はシンプルで直感的です。
instruc_t instructions[ ] = { ... { "ADC.B", CF_USE1|CF_CHG2 }, { "ADC.W", CF_USE1|CF_CHG2 }, { "ADC.B", CF_USE1|CF_CHG2 }, { "ADC.W", CF_USE1|CF_CHG2 }, { "ADCF.B", CF_CHG1 }, { "ADCF.W", CF_CHG1 }, { "ADD.B:G", CF_USE1|CF_CHG2 }, { "ADD.W:G", CF_USE1|CF_CHG2 }, { "ADD.B:Q", CF_USE1|CF_CHG2 }, ... };
命令「
ADC.B 」には2つのオペランドがあり、最初のオペランドのみが使用され、2番目のオペランドは命令の実行中に変更されることが
わかります。 これは論理的です
。ADCは
キャリー付きのADditionであり、操作は次のようになります。
[ Syntax ] ADC.size src,dest ^--- B, W [ Operation ] dest <- src + dest + C
次に、
emu()関数での命令自体の実行が
エミュレートされます。
int emu( ) { unsigned long feature = cmd.get_canon_feature( ); if( feature & CF_USE1 ) TouchArg( cmd.Op1, 1 ); if( feature & CF_USE2 ) TouchArg( cmd.Op2, 1 ); if( feature & CF_CHG1 ) TouchArg( cmd.Op1, 0 ); if( feature & CF_CHG2 ) TouchArg( cmd.Op2, 0 ); if( !( feature & CF_STOP ) ) ua_add_cref( 0, cmd.ea + cmd.size, fl_F); return 1; }
ご覧のとおり、引数を使用して、
TouchArg()関数でそれを消化可能な形式に変換します。 この関数は次のようになります。
void TouchArg( op_t &x, int isload ) { switch ( x.type ) { case o_near: { cref_t ftype = fl_JN; ea_t ea = toEA(cmd.cs, x.addr); if ( InstrIsSet(cmd.itype, CF_CALL) ) { if ( !func_does_return(ea) ) flow = false; ftype = fl_CN; } ua_add_cref(x.offb, ea, ftype); } break; case o_imm: if ( !isload ) break; op_num(cmd.ea, xn); if ( isOff(uFlag, xn) ) ua_add_off_drefs2(x, dr_O, OOF_SIGNED); break; case o_displ: if(x.dtyp == dt_byte) op_dec(cmd.ea, xn); break; case o_mem: { ea_t ea = toEA( dataSeg( ),x.addr ); ua_dodata2( x.offb, ea, x.dtyp ); if ( !isload ) doVar( ea ); ua_add_dref( x.offb, ea, isload ? dr_R : dr_W ); } break; default: break; } }
オペランドのタイプに応じて、
op_t構造体のフィールドに
適宜入力します(オペランドを「デコード」します)。
命令のテキスト表現を出力します
out()関数がこのアクションを担当します。 次のようになります。
void out() { char str[MAXSTR];
最小限の書式設定で、命令のテキスト表現と、もしあればオペランドを印刷します。
オペランドの出力テキスト表現
ここでコードはより興味深いですが、すべてが非常に簡単です:
bool idaapi outop(op_t &x) { ea_t ea; switch (x.type) { case o_void: return 0; case o_imm: OutValue(x, OOF_NUMBER | OOF_SIGNED | OOFW_IMM); break; case o_displ: {
オペランドのタイプに応じて、画面への出力を適切に配置します。
特定された欠陥と問題
現時点では、1つの重大な誤解があります。 未定義データに文字列(ASCII-Z文字列)を作成しようとすると、ゼロバイトがまだ検出されていない場合でも、4バイトの倍数のアドレスまでのみ文字列が作成されます。 ゼロバイトが先に発生した場合、行はそれで終了します。 アレイでは、同じ問題。
最も不愉快なのは、この状況でどこを掘ればよいかさえわからないということです。 誰か教えてもらえますか?
おわりに
したがって、IDA proのプラグインを作成することはそれほど難しくありません。 ターゲットアセンブラの多数のコマンドを除き、すべてが単純で、かなり面倒です。 CPCおよびオペランドを解析するための
DSLの導入により、開発が大幅に簡素化および高速化されます。