#Jewel
Jewel:宝
https://ksnctf.sweetduet.info/problem/15
問題にはapkファイルが置いてある.apkファイルはandroidアプリのパッケージでただのzipファイルらしい.解凍したら以下のよう構成になっていた.
C:.
│ AndroidManifest.xml
│ classes.dex
│ resources.arsc
│
├─META-INF
│ CERT.RSA
│ CERT.SF
│ MANIFEST.MF
│
└─res
├─drawable-hdpi
│ ic_launcher.png
│
├─drawable-ldpi
│ ic_launcher.png
│
├─drawable-mdpi
│ ic_launcher.png
│
├─layout
│ main.xml
│
└─raw
jewel_c.png
ソースコード(javaファイル)を見たいが,見つからない.
ところで,classes.dexはjavaファイルをAndroid環境で実行できるようにしたファイル.
javaファイルをコンパイルしてclassファイルをつくって,そのclassファイルをdex変換することでdexファイルがつくられる.
つまり,javaファイルを見るためには,dexファイル→classファイル→javaファイルというふうにに逆コンパイルする必要がある.
変換するためのツールには
- dex2jar : dexファイル→classファイル
- jad : classファイル→javaファイル
の2つのツールを使うのが一般的らしい.
(dex2jarは正確には,dexファイルをjarファイルに変換する.jarファイルはclassファイルで構成された,zip形式で圧縮されたファイル.)
こことかこことかここを参考.
この2つを使って逆変換しようとしたけど,なぜかうまくできなかった.
ほかの方法さがしてみると,jadxというapkファイルからjavaファイルに変換してくれるものがありこれを使った.
https://github.com/skylot/jadx
ここを参考.
##ソースコード
jadxを実行して,apkファイルを選択して開くと勝手に逆コンパイルしてくれる.便利.
JewelActivityというjavaファイルのソースコードが得られた.
package info.sweetduet.ksnctf.jewel;
import android.app.Activity;
import android.app.AlertDialog;
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 void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.main);
String deviceId = ((TelephonyManager) getSystemService("phone")).getDeviceId();
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(deviceId.getBytes("ASCII"));
String bigInteger = new BigInteger(instance.digest()).toString(16);
if (!deviceId.substring(0, 8).equals("99999991")) {
new AlertDialog.Builder(this).setMessage("Your device is not supported").setCancelable(false).setPositiveButton("OK", new b(this)).show();
} else if (!bigInteger.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5")) {
new AlertDialog.Builder(this).setMessage("You are not a valid user").setCancelable(false).setPositiveButton("OK", new a(this)).show();
} else {
InputStream openRawResource = getResources().openRawResource(R.raw.jewel_c);
byte[] bArr = new byte[openRawResource.available()];
openRawResource.read(bArr);
SecretKeySpec secretKeySpec = new SecretKeySpec(("!" + deviceId).getBytes("ASCII"), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding");
instance2.init(2, secretKeySpec, ivParameterSpec);
byte[] doFinal = instance2.doFinal(bArr);
ImageView imageView = new ImageView(this);
imageView.setImageBitmap(BitmapFactory.decodeByteArray(doFinal, 0, doFinal.length));
setContentView(imageView);
}
} catch (Exception e) {
Toast.makeText(this, e.toString(), 1).show();
}
}
}
ソースコードを見ていく.
String deviceId = ((TelephonyManager) getSystemService("phone")).getDeviceId();
try {
MessageDigest instance = MessageDigest.getInstance("SHA-256");
instance.update(deviceId.getBytes("ASCII"));
String bigInteger = new BigInteger(instance.digest()).toString(16);
if (!deviceId.substring(0, 8).equals("99999991")) {
new AlertDialog.Builder(this).setMessage("Your device is not supported").setCancelable(false).setPositiveButton("OK", new b(this)).show();
} else if (!bigInteger.equals("356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5")) {
new AlertDialog.Builder(this).setMessage("You are not a valid user").setCancelable(false).setPositiveButton("OK", new a(this)).show();
アプリを起動している端末のデバイスID(IMEI:15桁の数字)の先頭8文字が99999991であり,デバイスIDをASCIIコードに変換したものをSHA-256で暗号化して,さらに16進数に変換したものが,356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5であるときにアプリが正常に動作する.ということが書かれている.特定の端末でないと動作しないアプリだとわかる.
} else {
InputStream openRawResource = getResources().openRawResource(R.raw.jewel_c);
byte[] bArr = new byte[openRawResource.available()];
openRawResource.read(bArr);
/res/rawにあるJewel_c.pngのバイトデータをバイト配列bArrに読み込む.
SecretKeySpec secretKeySpec = new SecretKeySpec(("!" + deviceId).getBytes("ASCII"), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding");
instance2.init(2, secretKeySpec, ivParameterSpec);
byte[] doFinal = instance2.doFinal(bArr);
ImageView imageView = new ImageView(this);
imageView.setImageBitmap(BitmapFactory.decodeByteArray(doFinal, 0, doFinal.length));
setContentView(imageView);
SecretKeySpec:秘密鍵を作るクラス.
IvParameterSpec:初期化ベクトルを作るクラス.
Cipger:実際に暗号・復号化を行うクラス.
- 1行目:デバイスIDの先頭に!をつけた!デバイスIDをASCIIコードに変換したものをAESで暗号化したときの秘密鍵を作成.
- 2行目:kLwC29iMc4nRMuE5を初期化ベクトルに設定.
- 3行目:PKCS5Paddingで,CBC方式AESをつかった復号器を作成.
- 4行目:鍵と初期化ベクトルを使って2(2が復号化モードに対応している.1だと暗号化モード)で復号器を初期化.
- 5行目:画像を復号化
- 6行目:imageViewインスタンス作成
- 7行目:復号化したデータからbitmapオブジェクトを作成して,imageViewにセット
- 8行目:画像を表示
間単に言うと,Jewel_c.pngをAESで復号化している.
この辺を読んでAES暗号の仕組みや初期化ベクトルの意味を理解しておくといいかも.
・暗号処理をやってみよう!
・初期化ベクトルとは?暗号化で知っておくべき基礎知識を解説!
##解く
上記で述べたように,復号にはdeviceIDが必要.わかってる情報は2つ.
・deviceIDは15桁の数字で最初の8文字が「99999991」
・デバイスIDをASCIIコードに変換したものをSHA-256で暗号化して,さらに16進数に変換したものが「356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5」
SHA-256の特徴として,暗号化したデータから元のデータを計算することはほぼ不可能と言われており,これによって安全性の高い暗号化技術と言われている.つまり,「356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5」から元のデータを計算することはできない.
しかし,今回は元のデータの8桁はわかってるため,残りの7桁は総当たりでしらべればdeviceIDを取得できる.
15桁の総当たりとなると計算量が膨大になるけど7桁くらいなら大丈夫.
import java.math.BigInteger;
import java.security.MessageDigest;
public class device {
public static void main(String[] args) {
String value1 = "99999991";
String ans = "356280a58d3c437a45268a0b226d8cccad7b5dd28f5d1b37abf1873cc426a8a5";
try{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (int i=0; i<=9999999; i++){
//0~9999999ではなく,0000000~9999999で調べたい.
//%07dとして,7桁になるように0を左に詰める
String value2 = String.valueOf(String.format("%07d",i));
String deviceID = value1 + value2;
byte[] result = digest.digest(deviceID.getBytes());
String sha256 = String.format("%040x", new BigInteger(1, result));
if(sha256.equals(ans)){
System.out.println(deviceID);
break;
}
}
} catch (Exception e){
e.printStackTrace();
}
}
}
注意:%07dとして0を詰めないと999999910000123などの先頭に0がくる数字ができない.
10秒もしないくらいで計算でき,devideIDは999999913371337であることがわかった.
deviceIDが分かったので,次に復号を行う.
復号化のプログラムは,解析で得られたプログラムをほぼそのまま使って,最後に復号化したデータをFileOutputStreamで出力する.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Bruteforce {
public static void main(String[] args) {
String deviceId = "999999913371337";
try {
FileInputStream openRawResource = new FileInputStream("./Jewel_c.png");
byte[] bArr = new byte[openRawResource.available()];
openRawResource.read(bArr);
SecretKeySpec secretKeySpec = new SecretKeySpec(("!" + deviceId).getBytes("ASCII"), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec("kLwC29iMc4nRMuE5".getBytes());
Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding");
instance2.init(2, secretKeySpec, ivParameterSpec);
byte[] doFinal = instance2.doFinal(bArr);
FileOutputStream ans = new FileOutputStream("./Jewel_c_decrypt.png");
ans.write(doFinal);
} catch (Exception e) {
//TODO: handle exception
}
}
}
得られたJewel_c_decrypt.pngは以下のような画像であった.
ルビーの宝石の画像とともに,flagはこの画像のコメントにあるというメッセージがあった.
###画像のコメント見る方法
コメントはファイルプロパティからみれると思ったけど見れなかった.
いろいろ調べていると,画像のコメントを見れるサイトがあった.
https://image-convert.cman.jp/imgInfo/
ここに画像を投げてみると,コメントからflagが得られた.
ちなみに,mac,linuxだと,stringsコマンドとgrepコマンドを使って以下のように調べられる.
strings Jewel_c_decrypt.png | grep flag
windowsだとstringsに代わるコマンドがない?ため,まずcertutilで画像をバイナリ変換して,ファイルを開いてCtrl + F で検索するか,findstrコマンドで検索する必要がある.
certutil -encodehex Jewel_c_decrypt.png Jewel_c_decrypt.bin
#まとめ
apk解析ができた.
実際にプログラムを書いてみることで,AESやCBCなど,暗号化技術の勉強になった.