アイジア

CTF, 情報セキュリティの学んだことメモ

ksnctf 15 Jewel

ksnctfの15問目を解いていきます。Javaと暗号の問題です。

ksnctf.sweetduet.info

アプリのインストールと実行

androidでapkファイルをダウンロードして実行すると「Your device is not supported」と表示されてアプリが終了します。でも機種の問題ではなさそう。

apkファイルの解析

androidファイルを解析するのは比較的簡単で逆コンパイラを使えばJavaのコードにまで復元してくれます。詳しいやり方は以下から↓

Android apk の解析 - Qiita
dex2jarは.apkを.jarに、jadは.classを.javaに変換します。kali linuxには2つともデフォルトでインストールされているのでそれを利用します。

>dex2jar Jewel.apk
>unzip Jewel_dex2jar.jar -d ./Classes
>jad -p JewelActivity.class > JewelActivity.java

ここからはソースコードを読んで解析していきます。アセンブリ読むより100倍マシですね。

// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3) 

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;

// Referenced classes of package info.sweetduet.ksnctf.jewel:
//            b, a

public class JewelActivity extends Activity
{

    public JewelActivity()
    {
    }

    public void onCreate(Bundle bundle)
    {
        String s;
        super.onCreate(bundle);
        setContentView(0x7f030000);
        s = ((TelephonyManager)getSystemService("phone")).getDeviceId();
        String s1;
        try
        {
            MessageDigest messagedigest = MessageDigest.getInstance("SHA-256");
            messagedigest.update(s.getBytes("ASCII"));
            s1 = (new BigInteger(messagedigest.digest())).toString(16);
            if(!s.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(Exception exception)
        {
            Toast.makeText(this, exception.toString(), 1).show();
            return;
        }
        if(!s1.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5"))
        {
            (new android.app.AlertDialog.Builder(this)).setMessage("You are not a valid user").setCancelable(false).setPositiveButton("OK", new a(this)).show();
            return;
        }
        InputStream inputstream = getResources().openRawResource(0x7f040000);
        byte abyte0[] = new byte[inputstream.available()];
        inputstream.read(abyte0);
        SecretKeySpec secretkeyspec = new SecretKeySpec((new StringBuilder("!")).append(s).toString().getBytes("ASCII"), "AES");
        IvParameterSpec ivparameterspec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(2, secretkeyspec, ivparameterspec);
        byte abyte1[] = cipher.doFinal(abyte0);
        ImageView imageview = new ImageView(this);
        imageview.setImageBitmap(BitmapFactory.decodeByteArray(abyte1, 0, abyte1.length));
        setContentView(imageview);
        return;
    }
}


前半部分(~58行目、 InputStream inputstream = getResources().openRawResource(0x7f040000);以前)はアンドロイド端末のデバイスID(IMEI)前半が「99999991」で、ID全体をSHA256でハッシュ化した結果が「356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5」でなければブログラムを終了するというもの。
SHA256は不可逆関数で逆算できませんが、IMEIは15桁の数字なので、総当たりで求めます。

#!/usr/bin/env python
import hashlib
deviceid = 999999910000000
for x in range(0, 10000000):
    text = str((deviceid+x))
    hash=hashlib.sha256(text).hexdigest()
    if hash == "356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5":
        print text
        break

プログラムからデバイスIDは「999999913371337」であることが分かりました。後半はこのデバイスIDを使って暗号化処理を行っています。

InputStream inputstream = getResources().openRawResource(0x7f040000);
byte abyte0[] = new byte[inputstream.available()];
inputstream.read(abyte0);

リソースID(0x7f040000)から何らかのストリームを取得して内容をabyte0に格納しています。リソースIDとはアプリに置かれたリソースファイルを参照するために割り振られる数字のことです。リソースIDが何を指すのかを調べるには.apkの中にある.arscファイルを解析します。

unzip Jewel.apk
aapt dump --values resources Jewel.apk resource.arsc > v.txt

v.txtにリソースIDの詳細が出力されます。

resource 0x7f040000 info.sweetduet.ksnctf.jewel:raw/jewel_c: t=0x03 d=0x00000004 (s=0x0008 r=0x00)
          (string16) "res/raw/jewel_c.png"

ID0x7f04000はjewel_c.pngを指しているようです。jewel_c.pngは/res/raw内にあります。jewel_c.pngはそのままでは画像として表示できないので、次の処理で復号処理を行っていると思われます。

SecretKeySpec secretkeyspec = new SecretKeySpec((new StringBuilder("!")).append(s).toString().getBytes("ASCII"), "AES");
IvParameterSpec ivparameterspec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());

AES暗号の暗号鍵を作成し、初期化ベクトルを指定しています。初期ベクトルは「kLwC29iMc4nRMuE5」暗号鍵は「!」と「デバイスID(999999913371337)」を組み合わせたものです。

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, secretkeyspec, ivparameterspec); 
byte abyte1[] = cipher.doFinal(abyte0);

AES暗号のCBCモード、PKCS5でjewel_c.pngの復号を実行しています。

jewel_c.pngの復号

カニズムが分かったのでコードをかいてjewel_c.pngを復号します。コードは適当です。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Main {

    public static void main(String[] args) throws NoSuchAlgorithmException, IOException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        //jewel_c.pngの読み込み
        FileInputStream fs = new FileInputStream("jewel_c.png");
        byte abyte0[] = new byte[fs.available()];
        fs.read(abyte0);

        //jewel_c.pngの復号
        String s = "999999913371337";
        SecretKeySpec secretkeyspec = new SecretKeySpec((new StringBuilder("!")).append(s).toString().getBytes("ASCII"), "AES");
        IvParameterSpec ivparameterspec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(2, secretkeyspec, ivparameterspec);
        byte abyte1[] = cipher.doFinal(abyte0);

        //復号したデータの書き込み
        FileOutputStream fileOutStm = new FileOutputStream("jewel.png");
        fileOutStm.write(abyte1);
    }
}

↑で出力したファイルを開くと、
f:id:favoritte15:20181121162140p:plain
ちゃんと復元されていました。