ゆとりーなの日記

日記的な事を書いて行くと思はれる

ページビュー10000越え記念

中何回が自分でまわしたものなのか気になるところです。

ページビュー10000越え特別企画「自分で作った多重起動を自分で潰す」

何という不毛な特集。私が解析にハマっているせいです。何をやるかはタイトルの通りです。自分で多重起動防止のプログラムを書いて、自分で逆アセンブルしてその処理を潰します。

先ずは対象となるアプリケーションを作ろうか

多重起動防止を潰すテスト用のプログラムソースはこれです。

#include <stdexcept>
#include <windows.h>

class MultipleCheck {
public:
  MultipleCheck() : mutex_(CreateMutex(NULL, FALSE, "多重起動防止オブジェクト")), error_(0) {
    if (!mutex_) {
	  throw std::runtime_error("多重起動防止判定オブジェクトの生成に失敗しました");
	}
    error_ = GetLastError();
  }
  ~MultipleCheck() {
	CloseHandle(mutex_);
  }
  bool isMultiple() const {
	return error_ == ERROR_ALREADY_EXISTS;
  }
private:
  HANDLE mutex_;
  DWORD  error_;
};

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR cmdLine, int cmdShow) {
  MultipleCheck check;
  if (check.isMultiple()) {
	MessageBox(NULL, "多重起動は出来ません", "error", MB_OK);
	return 0;
  }
  WNDCLASSEX wc;
  ZeroMemory(&wc, sizeof(WNDCLASSEX));
  wc.cbSize        = sizeof(WNDCLASSEX);
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = WndProc;
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));
  wc.lpszClassName = "BASE";
  if (!RegisterClassEx(&wc)) return 0;		
  HWND hWnd = CreateWindow("BASE", "ウィンドウ", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst, NULL);
  if (!hWnd) return 0;
  ShowWindow(hWnd, cmdShow);
  MSG msg;
  do {
	if (!(~GetMessage(&msg, NULL, 0, 0))) break;
	  DispatchMessage(&msg);
  } while (msg.message != WM_QUIT);
  return static_cast<int>(msg.wParam);
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
  if (msg == WM_DESTROY) {
	PostQuitMessage(0); 
	return 0;
  }
  return DefWindowProc(hWnd, msg, wp, lp);
}

コードについて軽く説明を。ウィンドウ生成とプロシージャ、メッセージループ等はいつか書いた超短いウィンドウ生成コードの流用です。
特筆すべきはMultipleCheckですかね。このクラスが多重起動をチェックします。コンストラクタでMutexを生成します。NULLが返ってきたらやばそうなので例外で落とします。多重起動判定はここでは行いません。多重起動判定は専用のメソッドを用意します。多重起動していたらtrueを返すisMultiple()を用意します。ここを例外で落とさずエラーコードを返す方式にしたのは、この返り値を使ってどんな処理を行うかは各アプリケーションが判断すべきなので、例外を投げて受けないと落とす方式は良ろしくないと考えたからです。多重起動の判定自身はCreateMutexでMutexを生成するとき、同じ名前のMutexが既に存在するとGetLastErrorがERROR_ALREADY_EXISTSを返すことを利用する一般的な物です。クラスにしたのはRAIIを利用したリソース管理をしたかったからです。因みにisMultiple()内でGetLastErrorを呼び出しても、期待した結果が得られないので、CreateMutex直後に呼び出してエラーコードを保存しておく必要があります。

実際に逆汗する

逆汗して多重起動関連の処理を行っている所を探します。exeの名前はboot.exeとしました。逆汗にはOllyDbgを使います。まずOllyDbgからboot.exeを実行、検索で、全ての定数からERROR_ALREADY_EXISTSの値B7を検索します。ヒットした周辺の逆アセンブルを表示するとこんな感じでした。

002E1267  |> FF15 38202E00  CALL DWORD PTR DS:[<&KERNEL32.GetLastErr>; [GetLastError
002E126D  |. 3D B7000000    CMP EAX,0B7
002E1272    ^0F85 02FFFFFF  JNZ boot.002E117A
002E1278  |. 6A 00          PUSH 0                                   ; /Style = MB_OK|MB_APPLMODAL
002E127A  |. 68 CC212E00    PUSH OFFSET boot.??_C@_05KKCIMGE@error?$>; |Title = "error"
002E127F  |. 68 D4212E00    PUSH OFFSET boot.??_C@_0BF@PDCHOLH@?$JB?>; |Text = "多重起動は出来ません"
002E1284  |. 6A 00          PUSH 0                                   ; |hOwner = NULL
002E1286  |. FF15 FC202E00  CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; \MessageBoxA
002E128C  |> 53             PUSH EBX                                 ; /hObject
002E128D  |. FF15 08202E00  CALL DWORD PTR DS:[<&KERNEL32.CloseHandl>; \CloseHandle
002E1293  |. 5F             POP EDI
002E1294  |. 5E             POP ESI
002E1295  |. 33C0           XOR EAX,EAX
002E1297  |. 5B             POP EBX
002E1298  |. 8BE5           MOV ESP,EBP
002E129A  |. 5D             POP EBP
002E129B  \. C2 1000        RETN 10

第2行目はCMP EAX,0B7ですので、EAXレジスタの中身と0B7、すなわちERROR_ALREADY_EXISTSと値が等しいかどうかを調べています。EAXの中身は第1行目からGetLastErrorの値が入っている雰囲気です。次に第3行、JNZ boot.002E117Aは上の判定で調べた値同士が違えばアドレス002E117Aに飛ぶということを表しています。
4行目以降は文字列の中身やMessegeBoxAといったコメントを見るからに多重起動に引っかかった処理の香りがぷんぷんします。まあ第2
3行目がC++コードっぽく書くと、

if (GetLastError() != ERROR_ALREADY_EXISTS) {
  アドレス002E117Aに飛ぶ;
}
// 第4行目以降の処理

になります。詰まり、Mutexが既に存在しなければアドレス002E117Aに飛ぶという処理が行われていると予想できます。ということは、上の条件式に関係なくアドレス002E117Aに飛べば多重起動防止に引っかからないであろうことが容易に予想できます。無条件ジャンプはアセンブラだとJMPなので、第3行目を逆アセ修正でJMP boot.002E117Aに書き換えます。元の命令よりも短いコードなので、余った部分をNOPで埋めるにチェックを入れておかないと不幸なことが起きそうです。この状態で、プログラムを再生すると、多重起動防止に引っかからずウィンドウが生成されることが確認できます。
アセンブラ命令がバリバリ出てきましたが、良く分からん場合はアセンブラ命令表等を調べると幸せになれると思います。
アセンブラを見た感想としては、クラスの中身が変な形で入り組んでるなという印象を受けました。なるほどインライン展開がかいま見えたような気がします。