Javaソースコード難読化ツール自作入門 – 第2回

コラム概要

■こんな方におすすめ:

  • 難読化ツールを自作してみたい
  • 手動で行っている難読化を自動化したい

■難易度:★

■ポイント:

  • ソースコード難読化ツールは簡単に作れる。
  • パーサ・難読化コード作成モジュール・ソースコード変更モジュールという観点で見ると、
    ソースコード難読化ツールは理解しやすい。
  • おそらくパーサが最も難しいので、最初は非対応の範囲を広くとる形で作ればよい。

この記事は「Javaソースコード難読化ツール自作入門」シリーズの第1回です。
他の回はこちらからご覧ください。

Javaソースコード難読化ツール自作入門 – 第1回
Javaソースコード難読化ツール自作入門 – 第3回

難読化については、マーケティング担当者コラムにも記事がございます。
難読化をアプリのセキュリティ対策ツールとして検討中の方や
既に対策として実施されている方向けのコラムとなっています。
ご興味のある方は、ぜひご覧ください。

難読化だけで大丈夫?!アプリに施すセキュリティ対策の落とし穴


さて、お待たせしました。
「Javaソースコード難読化ツール自作入門」第2回です。

前回は、難読化と難読化ツール自作について簡単な説明をした後、難読化ツールの構成を説明しました。
また、Javaソースコードから難読化対象を見つけ出す機能について、
各種のスキップを行う所まで説明を行いました。

続きに入る前に、難読化ツールの構成を振り返ってみましょう。
本コラムで紹介している難読化ツールは、以下のモジュールから構成されています。

・パーサ
・難読化コード作成モジュール
・ソースコード変更モジュール

パーサは、ソースコードを解析して、難読化対象の検出などを行います。
難読化コード作成モジュールは、
パーサが検出した難読化対象に対応する関数呼び出しや関数定義を作成します。
ソースコード変更モジュールは、
難読化コード作成モジュールが作成した関数呼び出しで難読化対象を置換するなどして、
ソースコードを変更します。

下記の順序で難読化を行います。

①難読化対象の数値を見つけて、関数呼び出しコードに変更する。
②クラスの終わりを見つけて、関数定義コードを追加する。

①において、各モジュールが果たしている役割は下記です。
・パーサ(1)が、難読化対象の数値を見つける。 (
・難読化コード作成モジュールが、数値に対応する関数呼び出しコードと関数定義を作成する。
・コード変更モジュール(1)が、数値を関数呼び出しコードに置き換える。

②において、各モジュールが果たしている役割は下記です。
・パーサ(2)が、クラスの終わりを見つける。
・コード変更モジュール(2)が、関数定義をまとめて挿入する。

※以降の説明の都合上、パーサとコード変更モジュールに番号を振っています。

前回はパーサ(1)について、その途中までを時系列で説明しました。
難読化ツール自作の肝とも言える部分なので、説明すべきことが多かったのですが、もう残りわずかです。
今回は、パーサ(1)の残りに加えて、その他のモジュールの説明も行います。
前回に引き続き、時系列で説明していきます。




2-8. パーサ(1):整数らしき要素の抽出

セパレータの直後に数字が出現した場合、その次のセパレータまでを抽出します。

数字判定関数(1文字)・要素抽出関数の実装例:

bool bIsNumChr(char c) {
	if (c >= '0' && c <= '9') {
		return true;
	}
	return false;
}

char *pcGetFirstSeparatedElement(char *pcPlaceInSzLine, char *szBuff) {
	if (*pcPlaceInSzLine == '\0' || *pcPlaceInSzLine == '\n') {
		return NULL;
	}
	int iLen = strlen(pcPlaceInSzLine);
	int i, j = 0;
	bool bFoundElement = false;
	for (i = 0; i < iLen; i++) {
		if (!bIsSeparator(pcPlaceInSzLine[i])) {
			bFoundElement = true;
			szBuff[j] = pcPlaceInSzLine[i];
			j++;
		} else if (bFoundElement) {
			szBuff[j] = '\0';
			break;
		}
	}
	if (bFoundElement) {
		return &pcPlaceInSzLine[i];
	} else {
		return NULL;
	}
}



2-9. パーサ(1):整数判定

先頭文字が数字なので、シンボル名ではありません。
「コメントなどのスキップ」によって、文字列リテラルの中などではないことが分かっています。
よって整数もしくは小数なので、以下を除外すればよいでしょう。

・要素が「.」を含む場合(小数)
・要素が「L」または「l」を含む場合(long型※
  ※int型の範囲に収まるlong型の数値(「L」、「l」をつける必要なし)は難読化します。

int型整数判定関数の実装例:

bool bIsIntNum(char *szElement) {
	if (strchr(szElement, '.')) {
		return false;
	}
	if (strchr(szElement, 'L') || strchr(szElement, 'l')) {
		return false;
	}
	return true;
}



2-10. パーサ(1):int型整数ではなかった場合

次のセパレータまでスキップします。

次のセパレータを見つける関数の実装例:

char *pcFindSeparator(char *szStr) {
	char *pcPlaceInSzLine = szStr;
	while (*pcPlaceInSzLine) {
		if (bIsSeparator(*pcPlaceInSzLine)) {
			return pcPlaceInSzLine;
		}
		pcPlaceInSzLine++;
	}
	return NULL;
}

3. その他のモジュール

3-1. 難読化コード作成モジュール:概要

パーサ(1)が抽出した整数をリターンする関数(以下、デコード関数)を作成します。

通常、定数難読化は定数を冗長な計算に置換します。
この場合、コンパイラによる最適化をいかに免れるかが一つの課題になります。
関数呼び出しへの置換は、最適化を免れる一番簡単な方法と言えるでしょう。



3-2. 難読化コード作成モジュール:デコード関数の呼び出し作成

乱数を作成して実引数として渡します。
また、デコード関数内で元々の数値を復元するために使用する補正値( )の計算や、
作成したデコード関数のカウントもしておきます。
※乱数から元々の数値を引いた値

実装例:

int iNumOfDecFunc = 0;
void makeDecFuncCall(int iConst, char *szBuff, int *piRand, int *piCorrectionVal) {
	int iMax = 1000 * 1000;
	if (iConst > iMax) {
		iMax = iConst / 3 * 2;
	}
	*piRand = (int)(rand() * (iMax + 1.0) / (1.0 + RAND_MAX));
	int iRand = *piRand;
	*piCorrectionVal = iRand - iConst;

	sprintf(szBuff, "func%04x(%d)", iNumOfDecFunc, iRand);
}



3-3. 難読化コード作成モジュール:デコード関数の定義作成

引数を足し合わせた結果から補正値を引いた値をリターンする関数を作成します。

実装例:

char szDecFuncDef[10000][10000];

void makeDecFuncDef(int iCorrectionVal) {
	sprintf(szDecFuncDef[iNumOfDecFunc], "	static int func%04x(int iRand) {\n
	return iRand - %d;\n	}\n", iNumOfDecFunc, iCorrectionVal);
	iNumOfDecFunc++;
}

関数定義の例:

static int func000e(int iRand) {
	return iRand1 - 29857;
}



3-4. 難読化コード作成モジュール:解析を妨げる小さな工夫

3-3の「関数定義の例」における29857は補正値です。
これを関数内部に持っておくことによって、実引数から戻り値が推測できないようにしています。
また、同じ数値に対しても毎回異なる関数を作成することによって、
解析結果を使い回せないようにしています。

例:

	int iA = func00e9(710480);	// = 1
	int iB = func00ea(445679);	// = 1



3-5.コード変更モジュール(1)

難読化コード作成モジュールが作成した関数呼び出しで定数を置換します。

コード変更モジュール(1)の実装例:

char *szReplace(char *szOri, char *szSrc, char *szDst) {
	char *szTail = szOri + strlen(szSrc);
	char szTmp[iMaxLettersInLine];

	strcpy(szTmp, szTail);
	strcpy(szOri, szDst);
	strcat(szOri, szTmp);

	szTail = szOri + strlen(szDst);
	return szTail;
}



3-6.パーサ(2)およびコード変更モジュール(2):

難読化コード作成モジュールが作成した関数定義をクラス内に挿入します。

挿入する場所の見つけ方として、おそらく最も簡単なものがあります。
それは、行頭に「}」が来る行を探す方法です。

通常、ソースコードには規則正しくインデントが入っています。
そして、規則正しくインデントが入っているコードで下記のケースはほぼ存在しません。
・クラス(もしくはenum)の終了を示す「}」が行頭に来ないケース(内部クラスを除く)
・クラス(もしくはenum)の終了を示さない「}」が行頭に来るケース
よって、行頭に「}」が来る行を探してその直前に関数定義を挿入すればよい、という事になります。

例外:

class ClassA { int iA = 1; }

実装例:

bool bIsClassEnd(char *szLine) {
	return (szLine == strchr(szLine, '}'));
}

void injectDecFuncDef() {
	for (int i = 0; i < iNumOfDecFunc; i++) {
		printf(szDecFuncDef[i]);
	}
	iNumOfDecFunc = 0;
}

各モジュールについての説明は以上になります。
次回は、これまでに紹介してきた実装例を組み合わせた
定数難読化プログラムの例の紹介と、動作確認を行います。
ご期待ください。