最強の.NETアプリケーション保護を考えた
こんにちは。
色々とQuiitaでプロジェクトをすすめていましたが、例のウイルスによってなかなか更新するタイミングが見つけられませんでした(T_T)
そんななかC#で書かれた.NETアプリケーションの最強の保護を考えてみました。
そもそも、なぜ.NETアプリケーションを保護する必要があるかと言いますと..
簡単に言えばエンジニアでなくとも、簡単にコードの閲覧や改竄ができてしまうからです。何も技術的な知識が無くとも、です。
なぜ保護が必要?
Dnspyという便利なソフトウェアがあり、.NETアプリケーションexecutableを放り込むだけで容易にソースコードの閲覧や改竄が可能です。
もちろん、この他にも様々な方法で同じようなことを行うことが出来ます。
例として以下のようなコードを書いてみました。
staticvoidMain(string[]args){varこの変数は絶対秘密っ="HImitsu";varresult=newStringBuilder();for(varx=0;x<この変数は絶対秘密っ.Length;x++){switch(この変数は絶対秘密っ[x]){case'H':result.Append("A");break;case'I':result.Append("B");break;case'm':result.Append("C");break;case'i':result.Append("D");break;case't':result.Append("E");break;case's':result.Append("F");break;case'u':result.Append("G");break;}}Console.WriteLine(result);Console.ReadKey();}
これをdnspyに放り込むだけで、
このようにリバースエンジニアリングが容易であることから、セキュリティリスクは避けられません。
もちろん適材適所という言葉もございますが、再頒布する前提の業務アプリケーション等は特にセキュリティには気を配るべきであって、絶対に生のままリリースするといったことがあってはならないと思っています。えっ、何で・・・?
なぜリバースエンジニアリングが簡単?
それでは本題にうつる前に、なぜ.NETアプリケーションがこんなにも容易にリバースエンジニアリングできてしまうのかを考えてみましょう。
まず、.NET FrameworkはCLR (Common Language Runtime)という共通言語ランタイムによって実行されます。
そしてそのCLRはJavaの仮想環境のような振る舞いをすると考えていただいて問題ありません。(ちょっと違いますが)
ということは、そうです。Javaも簡単にリバースエンジニアリングすることができます。
.NET Frameworkで作成されたアプリケーションをコンパイルすると、MSIL(Microsoft Intermediate Language)という中間コードに変換されます。
そして、その中間コードがJIT (Just In Time) Compilerによって実行時にネイティヴコードに変換されるのです。なるほど、コードはexecutableに中間言語としてバイナリ化されているのじゃな!
へへっ。甘いね。俺には「難読化」という高度な手段があるから安心だぜ。
難読化のメリットとデメリット
こういったセキュリティリスクを避ける為に使われるのがコードの「難読化」という手段で、一番ポピュラーな保護方法です。
しかし、その「難読化」は本当に安全と言えますか?
答えは「Absolutely NO!」です。
インターネット上には様々な「Unpacker(アンパッカー)」と呼ばれる難読化解除ツールが存在し、これもまたエンジニアやプログラミングの知識をもった人間でなくとも、簡単に難読化を解除してしまうものが沢山あります。
ここで書くつもりはありませんが、僕がパッと思い浮かぶものだけで10個はありますね。そのくらい、難読化という保護の手段にさえもリスクが付きまとってきます。
さらに...相手が知識・経験と技術をもったハッカーだとしたら?更に危険です。
もはや「難読化」という手段はハッカーを一定時間足止めする"時間稼ぎ"でしかないでしょう。かーっ!じゃあどうしたらいいんだよ!界王拳20倍!
効果的な保護
それではここから実際にどのように効果的に.NETアプリケーションを保護するか、という本題に移ります。
簡単に言えばこの方法は「.NETをC++でホストする」です。そんなことできちゃうの!?
できちゃいます。
正確にはC++にてCLRをホスティングします。おお、神よ・・・コピペの準備は整いましたぞよ・・・
ネイティヴC++アプリケーションでホストしてみよう
先ず、保護したい.NETアプリケーションのバイナリをゲットしましょう。
ツールは何でも良いですが、僕の場合は簡単にコード化した状態で出力してくれる「HxD」という有名ツールを使用しました。
該当.NETアプリケーションを放り込んで、バイナリを全て選択した状態でFile
▶Copy as
▶C
とすると以下のようなバイナリが出力されます。
日本語設定だとちょっと違うかも知れません。
これを適当にヘッダーを作成した中に放り込んでおきます。
ここではfile.h
としました。
unsignedcharrawData[5120]={0x4D,0x5A,0x90,0x00,0x03,0x00,0x00,0x00,0x04,0x00,0x00,0x00,...}
次に...メインソースです。
インクルード・インポートは以下のようになります。mscorlib.tlb
は%SystemRoot%\Microsoft.NET\Framework
の適当バージョン内で見つかります。
また、インポート属性としてraw_interfaces_only
を指定する必要があります。raw_interfaces_only
に関しては、僕もよく分かっていません。
rawインターフェースのみ定義する。
例えばReadyStateというプロパティーに対し、get_ReadyState()という関数だけ定義される。(通常のCOMインターフェースなんだそうだ)
この属性を指定しない場合は、GetReadyState()という関数も定義される。(特殊なラッパーなんだそうだ)
引用: http://www.ne.jp/asahi/hishidama/home/tech/vcpp/import.html
#include "stdafx.h"
#include <windows.h>
#include <metahost.h>
#include "file.h"
#pragma comment(lib, "mscoree.lib")
#import "mscorlib.tlb" raw_interfaces_only
いよいよメインコードです。
何をしているかについては、適時コード上のコメントアウトをご参照下さい。
僕のコメントアウトは全て環境移行時の文字化けを避ける為貧相な英語となっていますm(_)m
※コードの動作を理解するにはCOM(Component Object Model)の理解が必要です!
int__stdcallwWinMain(HINSTANCEhInstance,HINSTANCEhPrevInstance,LPTSTRlpCmdLine,intnCmdShow){ICLRMetaHost*pMetaHost=NULL;ICLRMetaHostPolicy*pMetaHostPolicy=NULL;ICLRDebugging*pCLRDebugging=NULL;/* Create an instance of CLR_METAHOST */if(FAILED(CLRCreateInstance(CLSID_CLRMetaHost,IID_ICLRMetaHost,reinterpret_cast<LPVOID*>(&pMetaHost)))){MessageBox(NULL,TEXT("Couldn't create an instance of CLR_METAHOST."),TEXT("Error"),MB_OK);return1;}/* Create an instance of CLR_METAHOST_POLICY */if(FAILED(CLRCreateInstance(CLSID_CLRMetaHostPolicy,IID_ICLRMetaHostPolicy,reinterpret_cast<LPVOID*>(&pMetaHostPolicy)))){MessageBox(NULL,TEXT("Couldn't create an instance of CLR_METAHOST_POLICY."),TEXT("Error"),MB_OK);return1;}/* Create an instance of CLR_DEBUGGING */if(FAILED(CLRCreateInstance(CLSID_CLRDebugging,IID_ICLRDebugging,reinterpret_cast<LPVOID*>(&pCLRDebugging)))){MessageBox(NULL,TEXT("Couldn't create an instance of CLR_DEBUGGING."),TEXT("Error"),MB_OK);return1;}/*
* Get the runtime information with the version
* Found at: "%SystemRoot%\Microsoft.NET\Framework"
*/ICLRRuntimeInfo*pRuntimeInfo=NULL;if(FAILED(pMetaHost->GetRuntime(L"v4.0.30319",IID_ICLRRuntimeInfo,reinterpret_cast<void**>(&pRuntimeInfo)))){MessageBox(NULL,TEXT("Failed to get the runtime."),TEXT("Error"),MB_OK);return1;}/* Check if the runtime is loadable */BOOLisLoadable;if(FAILED(pRuntimeInfo->IsLoadable(&isLoadable))||!isLoadable){MessageBox(NULL,TEXT("The runtime is not loadable."),TEXT("Error"),MB_OK);return1;}/* Get an interface pointer of the runtime host */ICorRuntimeHost*pRuntimeHost=NULL;if(FAILED(pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost,IID_ICorRuntimeHost,reinterpret_cast<void**>(&pRuntimeHost)))){MessageBox(NULL,TEXT("Failed to get the interface of CLR."),TEXT("Error"),MB_OK);return1;}/* Start the runtime host */if(FAILED(pRuntimeHost->Start())){MessageBox(NULL,TEXT("Failed to start the runtime host."),TEXT("Error"),MB_OK);return1;}/* Get a pointer of the AppDomain thunk */IUnknownPtrpAppDomainThunk=NULL;if(FAILED(pRuntimeHost->GetDefaultDomain(&pAppDomainThunk))){MessageBox(NULL,TEXT("Failed to get the default domain of runtime host."),TEXT("Error"),MB_OK);return1;}/* Get a pointer of the AppDomain interface */mscorlib::_AppDomainPtrpDefaultAppDomain=NULL;if(FAILED(pAppDomainThunk->QueryInterface(__uuidof(mscorlib::_AppDomain),reinterpret_cast<void**>(&pDefaultAppDomain)))){MessageBox(NULL,TEXT("QueryInterface Failed."),TEXT("Error"),MB_OK);return1;}/* Create a safe array */SAFEARRAYBOUNDarray[1];array[0].cElements=sizeof(rawData);array[0].lLbound=0;SAFEARRAY*pSafeArray=SafeArrayCreate(VT_UI1,1,array);if(!pSafeArray){MessageBox(NULL,TEXT("Failed to create a safe array."),TEXT("Error"),MB_OK);return1;}/* Lock the array for safe access */void*pPvData=NULL;if(FAILED(SafeArrayAccessData(pSafeArray,&pPvData))){MessageBox(NULL,TEXT("Failed to access to the safe array."),TEXT("Error"),MB_OK);return1;}/* Deploy our rawData into the memory. */memcpy(pPvData,rawData,sizeof(rawData));/* Unlock the array for safe access */if(FAILED(SafeArrayUnaccessData(pSafeArray))){MessageBox(NULL,TEXT("Failed to destroy the access to the safe array."),TEXT("Error"),MB_OK);return1;}/*
* Load the assembly
* C#: System.AppDomain.CurrentDomain.Load(byte[] rawAssembly)
*/mscorlib::_AssemblyPtrpAssembly=NULL;if(FAILED(pDefaultAppDomain->Load_3(pSafeArray,&pAssembly))){MessageBox(NULL,TEXT("Failed to load the assemblies."),TEXT("Error"),MB_OK);return1;}/*
* Get the entry point of the assembly
* C#: System.Reflection.Assembly GetEntryAssembly()
*/mscorlib::_MethodInfoPtrpMethodInfo=NULL;if(FAILED(pAssembly->get_EntryPoint(&pMethodInfo))){MessageBox(NULL,TEXT("Failed to get the entry point of the assembly."),TEXT("Error"),MB_OK);return1;}/*
* Invoke a method of the entry point
* C#: static void Main(string[] args)
*/VARIANTretVal,obj;obj.vt=VT_NULL;SAFEARRAY*pParameters=SafeArrayCreateVector(VT_VARIANT,0,0);if(FAILED(pMethodInfo->Invoke_3(obj,pParameters,&retVal))){MessageBox(NULL,TEXT("Failed to invoke the method."),TEXT("Error"),MB_OK);return1;}return0;}
これにて、ホストするC++側の準備は完了です!
実行すると、問題無く.NET Framework 4.7.2で動作するアプリケーションが起動しました。
次にやるべきこと
C・C++等の低レベル言語のリバースエンジニアリングは.NETやJava等と違い、遥かに困難となります。が!
ネイティヴC++アプリケーション側をリバースエンジニアリングしてみると、やはりまだまだセキュリティは甘そうです。
もちろん.NETアプリケーションのバイナリも丸見えで簡単に抜けますから、これでは全く意味が無いですね。
これではハッカーからの攻撃に対して十分な保護能力があるかというと、全くありません。ヨユーww
幾つかのスパイスを加えることでリバースエンジニアリングを更に困難にしましょう!
スパイス | 概要 | ハッキング難易度 |
---|---|---|
VMProtect | リソース保護やユーザモードからカーネルモードまでのデバッガ検出、仮想環境検出やソフトウェア保護に関する様々な機能を提供します。ホスト元であるC++アプリケーションexecutableを保護することで、効果覿面です! ※一応.NETアセンブリにも対応していますが、お世辞にも良い保護とは全く言えませんので絶対にお勧めしません。 | ★★★★★ |
各バイナリをString化 | コンパイルに時間が掛かりすぎました。無理です。手間はかかるようになりますが、どの道リバースエンジニアリングで流出してしまうので、この方法はダメです。 | ★☆☆☆☆ |
各バイナリをString化+XorString(Xor暗号) | 上述した通りコンパイルに凄く凄く時間が掛かりますが、XorString暗号化によりバイナリを強力に保護することができます。 もはやそこらのハッカーには手が出せないでしょう。 Xor暗号に関してはこちらで解説していらっしゃる方がおられましたので丸投げいたします。 | ★★★★☆ |
おまじない | 手のひらと手のひらを合わせておまじないをしましょう。 | ☆☆☆☆☆ |
ちなみにVMProtectにて難読化・仮想化とメモリやリソース等の各種保護をフルでおこなった状態がこちらです。
正直、これを突破するのは相当至難の業です。
専門家でもってしても一握りのみが手を出せる代物となってしまいました。何だよコレ!.NETじゃねーのかよ!やってれんわ!
最後に
以上「僕が考えた最強の.NETアプリケーションの(リバースエンジニアリングからの)保護」でした。
正直、難読化レベル云々よりこういった根本的なところから保護をすることが一番強力だということですね。
最後までご覧いただき、ありがとうございました。
記事内の情報に誤りがございましたら、コメント欄にてお気軽にご指摘いただけますと幸いでございます。