ぼくのかんがえたcrackme [日記]
シェアウェアには、シリアルコードを入力すると使用制限が解除されるものが良くあります。
また、シリアルコードを入力しないとインストールすらできない製品も多くあります。
そして、
プログラムを解析(リバースエンジニアリング)して、シリアルコードを見つけ出したり、そもそもシリアルコードを入力しなくても良い様にプログラムを改変したりすることも、行われていたりします。
一方、解析できなくする(解析できなくすることは実質不可能)、解析を難しくする方法が考案されています。
そこで、自分も(解析を難しくする方法を)考えてみました。
注1)解析関係(特に最近のもの)は素人同然なので、「そんなの基本じゃん」と思われること必至です。
注2)アイデアだけで、ここにはcrackmeのプログラムはありません。
前提
- シリアルコードを入力しないと、一部の機能が制限されるタイプを対象
- コンパイルして出来上がった実行ファイルに対して、何らかの操作(例えば「パッカー」で圧縮とか)は、行いたくない
- アセンブリ言語(インラインアセンブラとか)も極力使いたくない
まず、全体の構成から。
// 正しいシリアルを入力済みか(グローバル変数) bool serial_ok_ = false; // シリアルコードを入力・判定する。 void regist() { std::string serial = シリアルコードの入力(); if (シリアルコードが正しいか判定) { // シリアルコードが正しい serial_ok_ = true; } else { // シリアルコードが間違い メッセージ表示とか再入力とか } } // シリアルを入力していないと実行できない制限機能 void limited_function() { if (!serial_ok_) return; 何らかの処理 }
こんな構造になっていると、何らかの方法で、serial_ok_ にtrueを設定することが出来てしまえば、機能制限が解除されてしまいます。
そこで、次の様なコードにします。
// 入力したシリアルコード(グローバル変数) std::string serial_; // シリアルコードを入力・判定する。 void regist() { serial_ = シリアルコードの入力(); if (シリアルコードが間違っているか) { // シリアルコードが間違い メッセージ表示とか再入力とか } } // シリアルを入力していないと実行できない制限機能 void limited_function() { if (シリアルコードが間違っているか) { // シリアルコードが間違い return; } 何らかの処理 }
多少無駄な処理になりますが、serial_ には、正しいシリアルコードを代入しなければならないので、bool型の変数よりは改変に強くなります。
(まぁ、このままだと、まだ色々と問題がありますが、それは後で)
次に、入力したシリアルコードがあっているかどうかの部分ですが、大体、こんな感じになっています。
if (serial_ == 正しいシリアルコード) { // 正しい } else { // 間違っている }
でも、この方法だと、実行ファイル(exeファイル)の中に正しいシリアルコードがそのまま格納されていますので、(ファイル中の文字列っぽいところを取り出すツール等を使って)意外とアッサリと見破られてしまいます。
そこで、比較するデータが見つかっても元のシリアルコードが分からなくするために、一方向関数(ハッシュ関数)を使うことにします。
if (md5(serial_) == 正しいシリアルコードのMD5) { // 正しい } else { // 間違っている } // 関数 md5() は、指定したデータのMD5を計算する関数
「md5(serial) == md5(正しいシリアルコード)
」ではないことに注意。
ちゃんと事前に計算しておいた「正しいシリアルコードのMD5」をソースコード上に記述します。
これなら、(MD5の安全性が保障されている間は)正しいシリアルコードを見つけられる心配はありません。
ところが、シリアルコードのチェックをしているところを丸ごと削除、ソースコードで言えば、こんな感じに変更されてしまうとアウトです。
(実行ファイルを直接変更するのであってソースコードを変更するわけではありません)
//if (md5(serial_) == 正しいシリアルコードのMD5) { // 正しい //} else { // // 間違っている //}
使用が制限されている機能のそれぞれにチェック処理があるようにしてあるので、その全てを無効化しなければならず面倒ですが、ただ面倒なだけです。
そこで、もう一手間かけることにしましょう。
void limited_function_body() { 何らかの処理 } void limited_function() { if (シリアルコードが間違っているか) { // シリアルコードが間違い return; } reinterpret_cast<void(*)()>(reinterpret_cast<int>(&limited_function_body) ^ 正しいシリアルコードのCRC32 ^ crc32(serial_))(); } // 関数 crc32() は、指定したデータのCRC32を計算する関数
関数本体のアドレスを分からない様にして、正しいアドレスを求めるには、正しいシリアルコードが必要になるという仕組みです。
「関数 limited_function_body()
のアドレス」は固定値。
「正しいシリアルコードのCRC32」も固定値。
なので、コンパイラの最適化処理によって、
「reinterpret_cast<int>(&limited_function_body) ^ 正しいシリアルコードのCRC32
」はコンパイル時に計算されて、実行ファイルには正しいアドレスが分からなくなって出力される………ハズです。
……
……
……
本当に、そうなのかな?
試しに逆アセンブルしてみました。
0040101E |. 8BC8 MOV ECX,EAX ; crc32()の戻り値 00401020 |. 81F1 00104000 XOR ECX,401000 ; limited_function_bodyのアドレス 00401026 |. 81F1 61626364 XOR ECX,64636261 ; 正しいシリアルのCRC32 0040102E |. FFD1 CALL ECX
あれ?
正しい関数のアドレスが普通に見えてる……。
……
……
……
あー。
「関数 limited_function_body()
のアドレス」が確定するのはリンク時なので、コンパイル時の最適化では計算してくれないんだ。
仕方が無いので、バイナリエディタを使って、直接いじりました。(前提に反しますが…)
00401020 |. 81F1 64237261 XOR ECX,64237261 00401026 |. 81F1 00000000 XOR ECX,0
さぁ、コレでどうだ。
シリアルコードのCRC32が分からなければ、関数のアドレスは絶対に分かりません。
(32bitしかありませんが、平文=「関数のアドレス」、鍵=「シリアルコードのCRC32」のバーナム暗号?)
シリアルコードが分からなければ、そのCRCも分かりません。
シリアルコードについて分かっていることは、そのMD5のみ。
理論上は完璧です。
バイナリエディタで編集するところが面倒ですが、コードに特徴があるので、自動化ツールも作れそうです。
ソースコードの関数呼び出しのところも、書くのが面倒ですが、C++なら、こんな手もあります。
引数の数とかが変わっていても、安全に処理できます。
template<typename T> T* FUNC(T& f, int master, int pass) { return reinterpret_cast<T*>((reinterpret_cast<int>(&f) ^ master) ^ pass); } FUNC(limited_function_body, 正しいシリアルコードのCRC32, crc32(serial_))();
これの制限を解除するには、どこからも呼び出されていない関数っぽい箇所を手がかりに解析を進めることになるので、それを何とかごまかしたらいいのかな?
あまりやりすぎると逆に手がかりを与えることになりかねないですが……。
今日の一冊 | |
|
コメント 0