問題
ksnctf.sweetduet.info
APKファイルが用意されている。
解法
APKファイルなのでとりあえずエミュレータで動かそう…と思ったが、エラーが出て動かず。(そりゃそうか)
ということでまずはファイルの中身を見ていく。APKはZIP形式なので、ZIPを解凍すると以下のようなファイルが入っていた。
各ファイルを見ていくと、resディレクトリの中に怪しい画像ファイルが存在している。拡張子はpngだが、中身は画像ではなかった。
ほかのファイルに対してもstringsコマンドとかを使ってみたが、それだけではフラグを取れそうにはなかった。そのため、ここからはAPKファイルをデコンパイル(リバースエンジニアリング)していく。
APKファイルのデコンパイルについてはこちらの記事を参考にした。
qiita.com
まずはdex2jarをダウンロードする。
その後、dex2jarを使用してclass.dex
をjarファイルに変換する。
そのjarファイルをunzipすると、以下のように3つのclassファイルを確認できる。
次に、JAD(Java Decompiler)をダウンロードし、先ほど確認したファイルのうち JewelActivity.class
をデコンパイルする。
これでAPKファイルのデコンパイルが完了した。
コードの解析編
生成されたJewelActivity.jad
の中身は以下のようになっている。(コメント箇所は追記している)
package info.sweetduet.ksnctf.jewel;
import android.app.Activity;
import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class JewelActivity extends Activity
{
public JewelActivity()
{
}
public void onCreate(Bundle bundle)
{
Object obj;
super.onCreate(bundle);
setContentView(0x7f030000);
obj = ((TelephonyManager)getSystemService("phone")).getDeviceId();
try
{
bundle = MessageDigest.getInstance("SHA-256");
bundle.update(((String) (obj)).getBytes("ASCII"));
bundle = (new BigInteger(bundle.digest())).toString(16);
if(!((String) (obj)).substring(0, 8).equals("99999991"))
{
(new android.app.AlertDialog.Builder(this)).setMessage("Your device is not supported").setCancelable(false).setPositiveButton("OK", new b(this)).show();
return;
}
}
catch(Bundle bundle)
{
Toast.makeText(this, bundle.toString(), 1).show();
return;
}
if(!bundle.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5"))
{
(new android.app.AlertDialog.Builder(this)).setMessage("You are not a valid user").setCancelable(false).setPositiveButton("OK", new a(this)).show();
return;
}
Object obj1 = getResources().openRawResource(0x7f040000);
bundle = new byte[((InputStream) (obj1)).available()];
((InputStream) (obj1)).read(bundle);
obj = new SecretKeySpec((new StringBuilder("!")).append(((String) (obj))).toString().getBytes("ASCII"), "AES");
obj1 = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, ((java.security.Key) (obj)), ((java.security.spec.AlgorithmParameterSpec) (obj1)));
bundle = cipher.doFinal(bundle);
obj = new ImageView(this);
((ImageView) (obj)).setImageBitmap(BitmapFactory.decodeByteArray(bundle, 0, bundle.length));
setContentView(((android.view.View) (obj)));
return;
}
}
コードの処理は大まかに前半パートと後半パートに分かれており、前半パートではスマホのIMEI(端末識別番号)のチェックを行い、後半パートではそのIMEIを使って暗号処理を行い、画像を作成している。最終的に作成される画像にフラグが含まれていると思われる。
まずは前半パートから見ていく。
前半パートのコードを見ていくと、IMEIが特定の値である場合だけ後半パートの処理に進めるようになっている。
IMEIのヒントとして、先頭8文字が99999991
であること、IMEIをsha256でハッシュ化した際の値が356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5
であることが分かっている。また、IMEIは15桁の数字であることが決められている。
これらのヒントから正しいIMEIを導き出すことが可能になっている。
例として以下のようなプログラムを作成した。
import hashlib
head = "99999991"
for i in range(9999999):
imei = head + str(i)
hash = hashlib.sha256(imei.encode()).hexdigest()
if hash == "356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5":
print("IMEI:" + imei)
break
上記プログラムを実行すると、IMEIが999999913371337
であることが判明した。
次に後半パートを見ていく。
後半パートではAES-CBC暗号(PKCS#5パディング)による暗号化処理を行っている。
暗号化するデータは最初に見つけた怪しい画像(jewel_c.png)になる。直感的にこのファイルを暗号化するんだろうなということが分かると思うが、ちゃんとリソースID(0x7f040000
)を確認したい場合はAPKファイルをAndroid Studio等で開くと確認できる。
そして秘密鍵はIMEIの先頭に!
を付けた!999999913371337
で、IV(初期化ベクトル)はkLwC29iMc4nRMuE5
になる。
この設定で暗号化処理を行い、その結果をファイルとして出力すると画像ファイルを得られる。
後半パートの処理もPythonでやろうと思ったが、PKCS#5パディングを使った簡単な暗号化方法が分からなかった…。そのため、結局以下のようなJavaプログラムを書いた。(デコンパイルされたコードとほとんど同じ内容)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class hack {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream("jewel_c.png");
byte bundle[] = new byte[fis.available()];
fis.read(bundle);
fis.close();
Object obj = "999999913371337";
obj = new SecretKeySpec((new StringBuilder("!")).append(((String) (obj))).toString().getBytes("ASCII"), "AES");
Object obj1 = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, ((java.security.Key) (obj)), ((java.security.spec.AlgorithmParameterSpec) (obj1)));
bundle = cipher.doFinal(bundle);
FileOutputStream fos = new FileOutputStream("flag.png");
fos.write(bundle);
fos.close();
}
}
その結果、以下のような画像を得られた。
ということで、この画像のコメントにフラグが記載されていた。
所感
CTFでリバエン問題を解いたのはたぶん初めてで、APKファイルの解析も初めての経験だった。
特にAPKファイルの解析は何かしらで役立てそう。APKのマルウェアを解析してみたいなぁー