ChogeLog

主にセキュリティ関係の記録やWrite-up。たまーに日記も。

#15 Jewel (write-up)

問題

ksnctf.sweetduet.info

APKファイルが用意されている。

解法

APKファイルのデコンパイル

APKファイルなのでとりあえずエミュレータで動かそう…と思ったが、エラーが出て動かず。(そりゃそうか)
ということでまずはファイルの中身を見ていく。APKはZIP形式なので、ZIPを解凍すると以下のようなファイルが入っていた。

各ファイルを見ていくと、resディレクトリの中に怪しい画像ファイルが存在している。拡張子はpngだが、中身は画像ではなかった。

ほかのファイルに対してもstringsコマンドとかを使ってみたが、それだけではフラグを取れそうにはなかった。そのため、ここからはAPKファイルをデコンパイルリバースエンジニアリング)していく。
APKファイルのデコンパイルについてはこちらの記事を参考にした。

qiita.com

まずはdex2jarをダウンロードする。
その後、dex2jarを使用してclass.dexをjarファイルに変換する。

そのjarファイルをunzipすると、以下のように3つのclassファイルを確認できる。

次に、JADJava Decompiler)をダウンロードし、先ほど確認したファイルのうち JewelActivity.classデコンパイルする。

これでAPKファイルのデコンパイルが完了した。

コードの解析編

生成されたJewelActivity.jadの中身は以下のようになっている。(コメント箇所は追記している)

// 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)
    {
        //前半パート
        Object obj;
        super.onCreate(bundle);
        setContentView(0x7f030000);
        obj = ((TelephonyManager)getSystemService("phone")).getDeviceId(); //デバイスIDを取得
        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")) //デバイスIDの先頭8文字
            {
                (new android.app.AlertDialog.Builder(this)).setMessage("Your device is not supported").setCancelable(false).setPositiveButton("OK", new b(this)).show();
                return;
            }
        }
        // Misplaced declaration of an exception variable
        catch(Bundle bundle)
        {
            Toast.makeText(this, bundle.toString(), 1).show();
            return;
        }
        if(!bundle.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5")) //デバイスIDのSHA-256の結果
        {
            (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()); //IVの作成
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //AES暗号、CBCブロック、PKCS#5パディング
        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");
        
        // IV
        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のマルウェアを解析してみたいなぁー