フリーソフトの不都合を踏んだのでGDBとGhidraで解析してみた

# フリーソフトの不都合を踏んだ

今回不都合を踏んだのは、指定したファイルの内容を音声に変換して再生するというこちらのフリーソフトです。

[MSX CSAVE エミュレータの詳細情報 : Vector ソフトを探す！](https://www.vector.co.jp/soft/win95/util/se470812.html)

「とりあえず試してみるか…」

```
Microsoft Windows [Version 10.0.19042.1348]
(c) Microsoft Corporation. All rights reserved.

C:\MyApps\msxcsav1>msxcsave.exe ReadMe.txt
MSXCSAVE Copyright 2008 by DEKO
```

＼ピーーーーーーーーブッ／ (7秒くらい)

```
C:\MyApps\msxcsav1>
```

「あれ…？やけに短くないか…？」

```
Microsoft Windows [Version 10.0.19042.1348]
(c) Microsoft Corporation. All rights reserved.

C:\MyApps\msxcsav1>msxcsave.exe ReadMe.txt
MSXCSAVE Copyright 2008 by DEKO
```

＼ピーーーーーーーーブッ／ (7秒くらい)

```
C:\MyApps\msxcsav1>
```

「`ReadMe.txt`は約2KB、`msxcsave.exe`は約56KBのファイル。
サイズがこんなに違うのに出力が同じ感じなのはおかしいのでは…？」

```
C:\MyApps\msxcsav1>echo %ERRORLEVEL%
-1073741819

C:\MyApps\msxcsav1>
```

[2進数、8進数、10進数、16進数相互変換ツール](https://hogehoge.tk/tool/number.html) で変換すると、`-1073741819` は16進数で `C0000005`となりました。
これはアクセス違反を表す値のようです。
[Access Violation C0000005 | Microsoft Docs](https://docs.microsoft.com/ja-jp/shows/Inside/C0000005)

`msxcsave.exe` に互換モードを設定しても、動作の改善はみられませんでした。
そこで、どこで落ちているのか解析してみようと考えました。

# 解析していいかどうかを調べる

世の中に出回っているソフトウェアの中には、解析(逆アセンブルなど)が禁止されているものがあります。
違反したら使用許可を自動的に即取り消し、全て破棄しなければいけない、という規程のソフトウェアもあり、恐ろしいです。

今回のソフトウェアについては、`ReadMe.txt`や配布サイトを調べた結果、
幸い解析を禁止するような記述は見当たりませんでした。

# Ghidraで逆コンパイルしてみる

[Ghidra](https://ghidra-sre.org/) を用いると、実行可能ファイルを逆コンパイルし、プログラムの内容をC言語風に表示できます。
そこで、これを用いて`msxcsave.exe`の処理内容を見てみることにしました。

具体的には、以下の手順で逆コンパイルができます。
(以下の手順でしか逆コンパイルができないというわけではないです)
(今回は Ghidra Version 10.0.4 を用いました。バージョンの違いによってUIが異なる可能性が考えられます)

1. Ghidraを起動する。
   2. 配布されているzipファイルを展開し、`ghidraRun.bat`を実行する。
2. プロジェクトを作成する。
   1. Ghidraのプロジェクトを保存する用の適当なディレクトリを用意する。
   2. 「File」メニューから「New Project...」を選択する。
   3. 「Non-Shared Project」を選択し、「Next」を押す。
   4. 「Project Directory」に用意したディレクトリのパスを入れる。
   5. 「Project Name」に適当なプロジェクト名を入れる。(例えば `a` とか)
   6. 「Finish」を押す。
3. 解析対象のファイルを読み込む。
   1. 「File」メニューから「Import File...」を選択する。
   2. 解析対象のファイルを選択し、「Select File To Import」を押す。
   3. 「Import (ファイルパス)」ダイアログが開くので、「OK」を押す。
   4. 「Import Results Summary」ダイアログが開くので、「OK」を押す。
4. 解析を実行する。
   1. 「Tool Chest」にある龍のアイコン (CodeBrowser) をクリックする。
   2. CodeBrowserの「File」メニューから「Open...」を選択する。
   3. 読み込んだファイルを選択肢、「OK」を押す。
   4. analyzeするか聞かれるので、「Yes」を押す。
   5. 「Analysis Options」ダイアログが開くので、「Analyse」をクリックする。
   6. CodeBrowserの右下に解析の進行状況が表示されるので、表示が消える(解析が終わる)まで待つ。
   7. ツールバーにあるフロッピーディスクのアイコン(Save '(ファイル名)')をクリックし、解析結果を保存する。
5. 解析結果を見る。
   1. 「Symbol Tree」内の「Functions」を展開する。
   2. 処理内容を見たい関数を選択する。

`entry`関数を見ると、

```c
UVar3 = FUN_00401a80(DAT_0040d300,DAT_0040d304);
```

という行があり、`FUN_00401a80` が `main` 関数に相当しそうであることがわかります。
このソースコード表示内の `FUN_00401a80` をダブルクリックすると、この関数の表示に移ることができます。

…が、これを見ただけではどこが原因でアクセス違反になっているのかよくわかりません。
そこで、GDBを用いてアクセス違反になっている場所を探すことにしました。

# GDBでアクセス違反になった場所を特定する

GDBは、[MinGW](https://sourceforge.net/projects/mingw/)などに含まれるデバッガです。
ステップ実行や、レジスタやメモリの内容の書き換えなどもできますが、
今回はアクセス違反を起こした場所を調べるのに使います。
早速調べてみましょう。

```
C:\MyApps\msxcsav1>gdb msxcsave.exe
GNU gdb (GDB) 7.6.1
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\MyApps\msxcsav1\msxcsave.exe...(no debugging symbols found)...done.
(gdb) r ReadMe.txt
Starting program: C:\MyApps\msxcsav1/msxcsave.exe ReadMe.txt
[New Thread 7640.0x2468]
[New Thread 7640.0x510]
[New Thread 7640.0x22e4]
[New Thread 7640.0x23d4]
MSXCSAVE Copyright 2008 by DEKO
[New Thread 7640.0x2b90]
[New Thread 7640.0x2efc]

Program received signal SIGSEGV, Segmentation fault.
0x00401db5 in ?? ()
(gdb) x/i $eip
=> 0x401db5:    mov    (%edi,%esi,1),%dl
(gdb) p/x $edi
$2 = 0x0
(gdb) p/x $esi
$3 = 0x0
(gdb)
```

`r` コマンドで、解析対象のプログラムを実行できます。
`r` の後に続いて引数を指定することで、実行時のコマンドライン引数を指定できます。

実行すると、アドレス `0x00401db5` でアクセス違反を起こしたようです。
`x` コマンドを用いると、メモリの内容を見ることができます。
スラッシュに続けて出力する数や形式を指定することができ、今回は`x/i`で内容を逆アセンブルして1命令出力しています。
続いて出力するメモリのアドレスを指定します。`$eip`は、EIPレジスタ(プログラムカウンタ)が指している場所を表します。
すなわち、今回はアクセス違反を起こした時に実行しようとした命令を出力させています。
`mov    (%edi,%esi,1),%dl`命令、すなわちEDIレジスタの値にESIレジスタの値を足した値をメモリのアドレスとし、
その場所から1バイト読み込んでDLレジスタに格納する命令でアクセス違反が起きたようです。

`p`コマンドを用いると、変数やレジスタの値を見ることができます。
`/x`は、16進数で出力することを指定しています。
今回は、読み込むアドレスの計算で用いられたEDIレジスタとESIレジスタの値を出力し、
どちらも0であることがわかりました。

したがって、アクセス違反はアドレス `0x00401db5` でナルポインタをデリファレンスしたために発生したようです。
アクセス違反を起こした場所がわかったので、そこをGhidraで見てみましょう。

# Ghidraでアクセス違反を起こした場所の処理をチェックする

GhidraのCodeBrowserに戻り、「Navigation」メニューから「Go To...」を選択します。
アドレスを聞かれるので、`0x00401db5` を入力して「OK」を押します。
すると、`0x00401db5` 付近の処理内容が表示されます。

![Ghidraで0x00401db5付近の処理内容を表示する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/6a2ff26a-8e73-1541-2318-bdc9f3716a16.png)

`0x00401db5` の直前の処理内容は、以下のようになっています。

```
// _strrchr(ECX, 0x5c) を呼び出す (ECXが指す文字列から円マークを探す)

00401da6 6a 5c           PUSH       0x5c
00401da8 51              PUSH       ECX
00401da9 e8 62 09        CALL       _strrchr
         00 00

// _strrchr 関数に渡した引数を片付ける

00401dae 83 c4 08        ADD        ESP,0x8

// _strrchr 関数の返り値をEDIに入れる

00401db1 8b f8           MOV        EDI,EAX

// ESIに0を入れる

00401db3 33 f6           XOR        ESI,ESI

// メモリのアドレス EDI+ESI から1バイト読み込み、DLに入れる

                     LAB_00401db5
00401db5 8a 14 37        MOV        DL,byte ptr [EDI + ESI*0x1]
```

したがって、アクセス違反が起こる原因は[`strrchr`関数](http://www9.plala.or.jp/sgwr-t/lib/strrchr.html)の返り値が`NULL`かどうかを確認せず、
いきなりデリファレンスしてしまっていることであるとわかりました。
`strrchr`関数は、指定された文字列から指定の文字を検索し、
見つかった場合はその中で一番後ろにある文字を指すポインタ、見つからなかった場合は`NULL`を返す関数です。

ところで、`_strrchr`に渡している `ECX` の中身は何でしょうか？
GDBで確認してみましょう。

# GDBでstrrchr関数の引数を確認する

GDBで`_strrchr`を呼び出す前にECXの値をスタックにコピーしている場所
`0x401da8` にブレークポイントをセットし、ECXの内容を確認してみます。
ブレークポイントとは、そこを実行しようとする直前で実行を一旦止めるポイントのことであり、
`b`コマンドで追加することができます。
`b`コマンドで(シンボルではなく)アドレスを直接指定するには、アドレスの前に`*`を付けます。
一旦止めた実行は、`c`コマンドで再開できます。

`x`コマンド(メモリの内容を見る)の形式に`s`を用いると、そこから始まるナル終端の文字列を出力できます。

```
C:\MyApps\msxcsav1>gdb msxcsave.exe
GNU gdb (GDB) 7.6.1
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\MyApps\msxcsav1\msxcsave.exe...(no debugging symbols found)...done.
(gdb) b *0x401da8
Breakpoint 1 at 0x401da8
(gdb) r ReadMe.txt
Starting program: C:\MyApps\msxcsav1/msxcsave.exe ReadMe.txt
[New Thread 9260.0x3824]
[New Thread 9260.0x34a4]
[New Thread 9260.0x1a08]
[New Thread 9260.0x234c]
MSXCSAVE Copyright 2008 by DEKO
[New Thread 9260.0x2e4c]
[New Thread 9260.0x1884]

Breakpoint 1, 0x00401da8 in ?? ()
(gdb) x/i $eip
=> 0x401da8:    push   %ecx
(gdb) p/x $ecx
$1 = 0x1061574
(gdb) x/s $ecx
0x1061574:      "ReadMe.txt"
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00401db5 in ?? ()
(gdb)
```

結果、ECX (`strrchr`関数の第1引数) の内容は `"ReadMe.txt"` であることがわかりました。
渡したコマンドライン引数の内容が入っていると推測できます。
`strrchr`関数は、指定したファイルパスからファイル名を抽出するために使われているようですね。

# 不都合を回避する

踏んだ不都合の内容がわかったようなので、これを回避してこのフリーソフトを利用します。
指定するコマンドライン引数に`\ `が入っていないと`strrchr`関数が`NULL`を返してアクセス違反になるようなので、
`\ `を含むファイルパスを指定します。

```
C:\MyApps\msxcsav1>msxcsave.exe .\ReadMe.txt
MSXCSAVE Copyright 2008 by DEKO
```

＼ピーーーーーーーーブッ。ピーーーー％▲＆□＄×○♭△※☆ブーーーーピッ／

```
C:\MyApps\msxcsav1>echo %ERRORLEVEL%
0

C:\MyApps\msxcsav1>
```

この音声からデータを取り出すツールは手元にないので内容が正しいかはわかりませんが、
どうやらアクセス違反を回避し、ファイルの内容を音声に変換させることができたようです。

# まとめ

* 今回踏んだ不都合は、ファイルパスからファイル名を取り出すために用いた`strrchr`関数の返り値を  
  `NULL`かどうかのチェックをせずに利用したため、  
  `\ `を含まないファイルパス(ファイル名)を渡されるとアクセス違反を起こす、というものでした。
  * 関数の返り値は利用する前にチェックしよう！
  * ファイルパスの操作は(可能であれば)それ用のAPIを利用しよう！
* Ghidraを用いると、実行可能ファイルの処理内容をC言語風に表示できます。
* GDBを用いると、プログラムの実行中の状態を調べることができます。
  * ブレークポイント：指定の場所を実行しようとしたら一旦停止する
  * 変数やレジスタの値の調査
  * メモリの値の調査
  * アクセス違反を起こした場所の表示
  * など (レジスタやメモリの書き換え、メモリからのデータの検索など、今回は使わなかった機能も多数)
* 解析を行う前に、解析が禁止されていないか確認しよう！