FireEye社が毎年開催しているReverse Engineeringの技術を問われるCTFです。
実際のマルウェアから発想を得た問題を出題する傾向があり、PEファイルの解析が多いようです。
また、通常のCTFとは違い、簡単な問題から順番に出題されるようになっており、問題を解かなければ次の問題が出題されない形式となっています。
Windowsアプリケーションの実行ファイル一式が与えられます。
X.exe
とX.dll
の表層解析を行うとX.dll
が.NETのruntime上で動作するアプリケーションだということがわかります。
X.dll
をiLSpyで中間言語から復元します。
monogame1
名前空間以下にゲームロジックのようなものが実装されていることがわかります。
このうち、Game1
クラス内に何かしらの値が42
になった際にフラグが表示される処理がありました。
フラグ文字列が取得できたので問題自体は解けたのですが、一応exeを起動してみると以下のようになりました。
7セグを42
に設定して錠マークをクリックするとフラグが表示されます。
ItsOnFire.apk
というアンドロイドアプリケーションが渡されます。
jadx
でソースコードを復元してみると、アプリケーションIDcom.secure.itsonfire
にインベーダーゲーム?が実装されているようです。
しばらくソースコードを確認しているとResources/res/raw
に画像ファイルが配置されていることがわかりました。
ただし、これらの画像ファイルはpngとしてInvalidであり、ビューアなどでは開けませんでした。
ファイルのエントロピーを確認してみるとどちらも暗号化されている可能性があることがわかります。
よってソースコード内にこれらを復号する処理がある可能性があることを念頭に置いて調査を継続しました。
またしばらく調査を続けるとソースコード内にcom.secure.itsonfire.MessageWorker.onMessageReceived
を起点とする復号の処理が存在することがわかりました。
復号処理に至るまでの関数呼び出しは以下のようになっています。
また4の処理で以下の処理が呼ばれていることがわかりました。
順を追って調査していきます。
まずonMessageReceived
によってc.c.a
が呼ばれます。
処理は以下のようになっており、onMessageReceived
から渡ってきたparam
の値によってb.b.f(bVar.f)
に渡すi3
の値を分岐させています。
i3
の値はR.raw.ps
かR.raw.iv
のどちらか、即ち暗号化されたファイルのどちらかになります。
@Nullable
public final PendingIntent a(@NotNull Context context, @NotNull String param) {
String string;
int i2;
b bVar;
int i3;
Intrinsics.checkNotNullParameter(context, "context");
Intrinsics.checkNotNullParameter(param, "param");
Intent intent = new Intent();
if (!Intrinsics.areEqual(param, context.getString(R.string.m1))) {
if (Intrinsics.areEqual(param, context.getString(R.string.t1))) {
bVar = b.f360a;
i3 = R.raw.ps;
} else if (Intrinsics.areEqual(param, context.getString(R.string.w1))) {
bVar = b.f360a;
i3 = R.raw.iv;
} else if (Intrinsics.areEqual(param, context.getString(R.string.t2))) {
intent.setAction(context.getString(R.string.av));
i2 = R.string.t3;
} else if (!Intrinsics.areEqual(param, context.getString(R.string.f1))) {
if (Intrinsics.areEqual(param, context.getString(R.string.s1)) || Intrinsics.areEqual(param, context.getString(R.string.s2))) {
intent.setAction(context.getString(R.string.av));
string = context.getString(R.string.s3);
intent.setData(Uri.parse(string));
}
return PendingIntent.getActivity(context, 100, intent, 201326592);
} else {
intent.setAction(context.getString(R.string.av));
i2 = R.string.f3;
}
return PendingIntent.getActivity(context, 100, bVar.f(context, i3), 201326592);
}
intent.setAction(context.getString(R.string.ad));
i2 = R.string.m2;
string = context.getString(i2);
intent.setData(Uri.parse(string));
return PendingIntent.getActivity(context, 100, intent, 201326592);
}
b.b.f
ではb.b.c
が呼ばれます。
最後のほうにFilesKt.writeBytes
があることからファイルの書き込みをしているっぽいことがわかります。
そこに至るまでの処理はe(ファイル読み込み)
→ b.b.d
→ new SecretKeySpec(鍵生成)
→ b.b.b
となっています。
private final File c(int i2, Context context) {
Resources resources = context.getResources();
Intrinsics.checkNotNullExpressionValue(resources, "context.resources");
byte[] e2 = e(resources, i2);
String d2 = d(context);
Charset charset = Charsets.UTF_8;
byte[] bytes = d2.getBytes(charset);
Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, context.getString(R.string.ag));
String string = context.getString(R.string.alg);
Intrinsics.checkNotNullExpressionValue(string, "context.getString(R.string.alg)");
String string2 = context.getString(R.string.iv);
Intrinsics.checkNotNullExpressionValue(string2, "context.getString(\n … R.string.iv)");
byte[] bytes2 = string2.getBytes(charset);
Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)");
byte[] b2 = b(string, e2, secretKeySpec, new IvParameterSpec(bytes2));
File file = new File(context.getCacheDir(), context.getString(R.string.playerdata));
FilesKt.writeBytes(file, b2);
return file;
}
b.b.e
は単純にファイルを読み込む処理でしたので解析はスキップします。
次にb.b.d
の処理を確認します。
private final String d(Context context) {
String string = context.getString(R.string.c2);
Intrinsics.checkNotNullExpressionValue(string, "context.getString(R.string.c2)");
String string2 = context.getString(R.string.w1);
Intrinsics.checkNotNullExpressionValue(string2, "context.getString(R.string.w1)");
StringBuilder sb = new StringBuilder();
sb.append(string.subSequence(4, 10));
sb.append(string2.subSequence(2, 5));
String sb2 = sb.toString();
Intrinsics.checkNotNullExpressionValue(sb2, "StringBuilder().apply(builderAction).toString()");
byte[] bytes = sb2.getBytes(Charsets.UTF_8);
Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
long a2 = a(bytes);
StringBuilder sb3 = new StringBuilder();
sb3.append(a2);
sb3.append(a2);
String sb4 = sb3.toString();
Intrinsics.checkNotNullExpressionValue(sb4, "StringBuilder().apply(builderAction).toString()");
return StringsKt.slice(sb4, new IntRange(0, 15));
}
ここではR.string.c2
とR.string.w1
を元に文字列が生成されていることがわかります。
この二つはres/values/strings.xml
にて以下のように定義されています。
<string name="c2">https://flare-on.com/evilc2server/report_token/report_token.php?token=</string>
<string name="w1">wednesday</string>
この文字列を元にb.b.d
の処理をPythonで再現すると以下のようになります。
import binascii
c2 = b"https://flare-on.com/evilc2server/report_token/report_token.php?token="
w1 = b"wednesday"
key = (str(binascii.crc32(c2[4:10]+w1[2:5])).encode("utf-8")*2)[0:16]
また、b.b.a
が呼ばれている箇所がありますが、b.b.a
は単純なCRC32計算のみでした。
b.b.d
で生成した文字列はb.b.c
内でnew secretKeySpec
の第一引数として渡されます。
第二引数のR.string.ag
はres/values/strings.xml`にて以下のように定義されています。
<string name="ag">AES</string>
その後、R.string.alg
、R.string.iv
が参照されます。
これらはres/values/strings.xml`にて以下のように定義されています。
<string name="alg">AES/CBC/PKCS5Padding</string>
<string name="iv">abcdefghijklmnop</string>
そして、b.b.b
に対して以下が渡されます。
byte[] b2 = b(string, e2, secretKeySpec, new IvParameterSpec(bytes2));
b.b.b
はパラメータを使用して復号を行う処理です。
(cipher.init
の第一引数2
はDECRYPT_MODE
を表します。)
private final byte[] b(String str, byte[] bArr, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec) {
Cipher cipher = Cipher.getInstance(str);
cipher.init(2, secretKeySpec, ivParameterSpec);
byte[] doFinal = cipher.doFinal(bArr);
Intrinsics.checkNotNullExpressionValue(doFinal, "cipher.doFinal(input)");
return doFinal;
}
ここまでで復号に必要な情報が揃っているのでスクリプトを書きます。
from Crypto.Cipher import AES
import binascii
c2 = b"https://flare-on.com/evilc2server/report_token/report_token.php?token="
w1 = b"wednesday"
iv = b"abcdefghijklmnop"
key = (str(binascii.crc32(c2[4:10]+w1[2:5])).encode("utf-8")*2)[0:16]
aes = AES.new(key, AES.MODE_CBC, iv)
iv_png = open("C:\\Users\\rikoteki\\Desktop\\Repository\\flare-on\\ItsOnFire\\app\\src\\main\\res\\raw\\iv.png", "rb").read()
ps_png = open("C:\\Users\\rikoteki\\Desktop\\Repository\\flare-on\\ItsOnFire\\app\\src\\main\\res\\raw\\ps.png", "rb").read()
open("./dec_iv.png", "wb").write(aes.decrypt(iv_png))
open("./dec_ps.png", "wb").write(aes.decrypt(ps_png))
このスクリプトを実行すると画像が復号され、iv.png
にフラグが描画されていました。