| tags:Reversing categories:Writeups series:Insomnihack
Insomni'hack 2016 - SafeCRT
Hi!
Since there don’t seem to be any writeups yet for SafeCRT, here goes.
The challenge was an android APK and an encrypted file. I decompiled the APK using jadx. The interesting stuff was in ch.scrt.safecrt.Mainactivity
(irrelevant code has been removed):
package ch.scrt.safecrt;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.security.AlgorithmParameters;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class MainActivity extends AppCompatActivity {
public byte[] currentKey;
public int failCount = 0;
public String filename = "secfile.enc";
public String gSalt = "<-^->$$_D3ad_Be3f_$$<-^->";
public NumberPicker[] np;
public int npCount = 5;
public native String mixKey(byte[] bArr, byte[] bArr2);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) R.layout.activity_main);
getSupportActionBar().hide();
this.np = new NumberPicker[this.npCount];
this.np[0] = (NumberPicker) findViewById(R.id.numberPicker0);
this.np[1] = (NumberPicker) findViewById(R.id.numberPicker1);
this.np[2] = (NumberPicker) findViewById(R.id.numberPicker2);
this.np[3] = (NumberPicker) findViewById(R.id.numberPicker3);
this.np[4] = (NumberPicker) findViewById(R.id.numberPicker4);
for (int i = 0; i < this.npCount; i++) {
this.np[i].setMinValue(0);
this.np[i].setMaxValue(9);
setNumberPickerTextColor(this.np[i], -16711936);
}
((Button) findViewById(R.id.button)).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
int i;
String passcode = "";
for (i = 0; i < MainActivity.this.npCount; i++) {
passcode = passcode + MainActivity.this.np[i].getValue();
}
byte[] k1 = MainActivity.this.generateKey(passcode.toCharArray()).getEncoded();
byte[] k2 = new byte[k1.length];
MainActivity.this.mixKey(k1, k2);
MainActivity.this.currentKey = k2;
byte[] cipherFile = MainActivity.this.readFile(MainActivity.this.filename);
if (cipherFile != null) {
byte[] plainFile = MainActivity.this.decrypt(cipherFile, MainActivity.this.currentKey);
if (plainFile != null) {
MainActivity.this.failCount = 0;
i = new Intent(MainActivity.this.getApplicationContext(), TextActivity.class);
i.putExtra("plaintext", new String(plainFile));
MainActivity.this.startActivityForResult(i, 1);
return;
}
MainActivity mainActivity = MainActivity.this;
int i2 = mainActivity.failCount + 1;
mainActivity.failCount = i2;
if (i2 >= 5) {
MainActivity.this.startActivity(new Intent("android.intent.action.VIEW", Uri.parse("https://www.youtube.com/watch?v=XZxzJGgox_E")));
return;
} else if (MainActivity.this.failCount >= 3) {
Toast.makeText(v.getContext(), "Hey dude.. Are you drunk?", 0).show();
return;
} else {
Toast.makeText(v.getContext(), "Invalid passcode.. Try again!", 0).show();
return;
}
}
Intent i3 = new Intent(MainActivity.this.getApplicationContext(), TextActivity.class);
i3.putExtra("plaintext", "");
MainActivity.this.startActivityForResult(i3, 1);
}
});
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == -1) {
try {
String plaintext = data.getStringExtra("plaintext");
if (plaintext != null) {
writeFile(this.filename, encrypt(plaintext.getBytes("UTF-8"), this.currentKey));
Toast.makeText(this, "Saved!", 0).show();
}
} catch (UnsupportedEncodingException e) {
Toast.makeText(this, "UTF-8 is not supported...", 0).show();
}
}
}
public byte a2(byte b, int i) {
return (byte) (this.gSalt.charAt(i % this.gSalt.length()) ^ b);
}
public String bruteforce() {
int i;
String passcode = "";
byte[] cipherFile = readFile(this.filename);
byte[] ft = new byte[37];
for (i = 0; i < ft.length; i++) {
ft[i] = (byte) (f(i) % 255);
}
if (cipherFile != null) {
for (i = 6; i < 10; i++) {
for (int j = 2; j < 10; j++) {
for (int k = 2; k < 10; k++) {
for (int l = 0; l < 10; l++) {
for (int m = 0; m < 10; m++) {
byte[] k1 = generateKey(("" + i + j + k + l + m).toCharArray()).getEncoded();
byte[] k2 = new byte[k1.length];
jMixKey(k1, k2, ft);
this.currentKey = k2;
if (decrypt(cipherFile, this.currentKey) != null) {
Log.v("RESULT", "" + i + j + k + l + m);
}
}
}
}
}
}
}
return "";
}
public String jMixKey(byte[] inKey, byte[] outKey, byte[] ft) {
for (int i = 0; i < inKey.length; i++) {
outKey[i] = a2((byte) (inKey[i] ^ ft[i + 21]), i);
}
return "";
}
public int f(int n) {
if (n == 0) {
return 0;
}
if (n != 1) {
return f(n - 1) + f(n - 2);
}
return 1;
}
public SecretKey generateKey(char[] passphraseOrPin) {
SecretKey secretKey = null;
try {
secretKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret(new PBEKeySpec(passphraseOrPin, this.gSalt.getBytes(), 15, 128));
} catch (NoSuchAlgorithmException e) {
Log.v("Activity", "NoSuchAlgorithmException");
} catch (InvalidKeySpecException e2) {
Log.v("Activity", "InvalidKeySpecException");
}
return secretKey;
}
public byte[] encrypt(byte[] plaintext, byte[] key) {
try {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(1, skeySpec);
AlgorithmParameters params = cipher.getParameters();
return cipher.doFinal(plaintext);
} catch (Exception e) {
return null;
}
}
public byte[] decrypt(byte[] ciphertext, byte[] key) {
try {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(2, skeySpec);
AlgorithmParameters params = cipher.getParameters();
return cipher.doFinal(ciphertext);
} catch (Exception e) {
return null;
}
}
}
Basically what’s happening here, is that a 128 bit key is derived from a 5-digit numeric input using PKDF2-SHA1 and a custom mixing function with the fibonacci series and the flag is encrypted using aes-128-ecb.
This is easily brute-forcable, and since I don’t like java, I reimplemented the code in python. The custom fibonacci generator (m14f)
is terribly slow, and was probably meant as a brute-forcing-deterrent. Since it doesn’t depend on any input though, it can easily be precomputed and hardcoded in:
#!/usr/local/bin env python
import passlib
from base64 import b64decode
from Crypto.Cipher import AES
salt = "<-^->$$_D3ad_Be3f_$$<-^->"
ct = b64decode("zq2mD1j2l5vbKvprFfUx7TdAIRg7zVbVbn5ze60TSzQ=")
def is_ascii(s):
return all(ord(c) < 128 for c in s)
def a2(b, i):
return chr(ord(salt[i % len(salt)]) ^ b)
def m14f(n):
if n == 0 or n == 1:
return n
else:
return m14f(n-1)+m14f(n-2)
def mixKeys(key, ft):
assert len(key)==16
out = [None]*16
for i in range(16):
out[i] = a2(ord(key[i])^ft[i+21], i)
return ''.join(out)
def genkey(passcode):
from passlib.utils.pbkdf2 import pbkdf2
assert len(passcode)==5
return pbkdf2(''.join(passcode), salt, 15, 16)
def brute():
#ft = [m14f(i)%0xff for i in range(0,37)]
ft = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 122, 100, 222, 67, 34, 101, 135, 236, 116, 97, 213, 55, 13, 68, 81, 149, 230, 124, 99, 223, 67, 35, 102]
assert len(ft) == 16+21
for i in range(100000):
passcode = "%05d"%i
key = genkey(passcode)
key = mixKeys(key, ft)
aes = AES.new(key)
pt = aes.decrypt(ct)
if is_ascii(pt):
print pt
#if i%1000:
# print passcode
brute()
$ time python solve.py
INS{Jni_1sS0_funNy}
Congratz!
python solve.py 44.28s user 0.35s system 97% cpu 45.668 total
plonk