We are provided with an APK file, which can be statically decompiled via tools like JADX or JEB.
From the APK file, we can obtain the native library that is used to validate the 20 character passcode (between 0x0 - 0xF)
We can reverse the passcode checking function to brute force many possible passcode combinations.
We can use each of this combination to decrypt the ciphertext, one will finally work to give us the flag.
The main user code can be found in be.dauntless.gettheplans.MainActivity
class of the program.
Essentially, this program takes in a 20 character PIN code which will be used to decrypt the flag.
The code is stored and reflected on the app via the updateCode
function, which stores the passcode in hex strings that are separated by spaces.
public MainActivity() {
this.code_size = 20;
}
private void updateCode() {
TextView textView0 = (TextView)this.findViewById(0x7F08017B); // id:txtCode
String s = "";
int v;
for(v = 0; v < this.code.size(); ++v) {
s = s + ((String)this.code.get(v)) + " ";
}
while(v < this.code_size) {
s = s + "• ";
++v;
}
textView0.setText(s);
}
On pressing the submit button, the sendCode
function is called.
This function checks the validity of the passcode by calling the sendCodeNative(s)
function. If the passcode is valid, it is used as an AES key to decrypt the final flag.
private void sendCode() {
TextView textView0 = (TextView)this.findViewById(0x7F08017B); // id:txtCode
if(this.code.size() != this.code_size) {
return;
}
String s = textView0.getText().toString();
if(this.sendCodeNative(s)) {
try {
Toast.makeText(this.getApplicationContext(), AESCrypt.decrypt(s.toUpperCase(), "WzqOeA3jbpkjs0hWE+ieL/bQyz+T/nQgnA2un8Y0TxCMK7132puDpSMKMvOA+MMV"), 1).show();
textView0.setTextColor(0xFF00FF00);
}
catch(Exception unused_ex) {
Toast.makeText(this.getApplicationContext(), "Blast \'m!", 1).show();
}
return;
}
Toast.makeText(this.getApplicationContext(), "Blast \'m!", 1).show();
textView0.setTextColor(0xFFFF0000);
this.code = new ArrayList();
this.updateCode();
}
The sendCodeNative
function is a native
function, which means that it is exported from a external native library which is called native-lib
in this case.
static {
System.loadLibrary("native-lib");
}
public native boolean sendCodeNative(String arg1) {
}
We can extract the native library by unzipping the APK and analyze it statically in a separate decompiler such as IDA.
The native library can be separated into a few parts.
The first part of the code converts the passcode from base16 (hexadecimal) and stores it into an integer array.
input_string = (*(*a1 + 1352LL))(a1, a3, 0LL);
input_string_2 = strdup(input_string);
v5 = strtok(input_string_2, " ");
if ( v5 )
{
i = 0LL;
do
{
input_array[i++] = strtol(v5, 0LL, 16);
v5 = strtok(0LL, " ");
}
while ( v5 );
if ( i == 21 )
return 0;
}
Subsequently, this array is shuffled in a detemrinistic manner by parsing the bits of some hardcoded integers.
j = 0LL;
v9 = 0x5D3ADLL;
v10 = 0x2E9D6LL;
LABEL_8:
// shuffle array in specific order
tmp = input_array[j];
input_array[j] = input_array[j + 1];
input_array[j + 1] = tmp;
while ( j != 18 )
{
if ( _bittest64(&v10, j) )
{
v12 = input_array[j + 1];
input_array[j + 1] = input_array[j + 2];
input_array[j + 2] = v12;
}
j += 2LL;
if ( _bittest64(&v9, j) )
goto LABEL_8;
}
The shuffled input array is then passed through a single pass bubble sort.
// one pass of a bubble sort
k = 2LL;
v14 = input_array[0];
while ( 1 )
{
v15 = input_array_minus_2[k + 1];
if ( v14 <= v15 )
break;
input_array_minus_2[k] = v15;
input_array_minus_2[k + 1] = v14;
if ( k == 20 )
goto LABEL_21;
LABEL_18:
v16 = input_array[k];
if ( v14 <= v16 )
{
v14 = input_array[k];
}
else
{
input_array_minus_2[k + 1] = v16;
input_array[k] = v14;
}
k += 2LL;
}
v14 = input_array_minus_2[k + 1];
if ( k != 20 )
goto LABEL_18;
LABEL_21:
Finally, the resulting array is compared against a bunch of xmmwords
and the result is added together and is the return value of this sendCodeNative
function.
v17 = _mm_add_epi32(
_mm_add_epi32(
_mm_add_epi32(
_mm_cmpeq_epi32(_mm_load_si128(&input_array[4]), xmmword_9B0),
_mm_cmpeq_epi32(_mm_load_si128(&input_array[12]), xmmword_9A0)),
_mm_add_epi32(
_mm_cmpeq_epi32(_mm_load_si128(input_array), xmmword_990),// 6000000000000000600000000
_mm_cmpeq_epi32(_mm_load_si128(&input_array[8]), xmmword_980))),
xmmword_9C0);
v18 = _mm_add_epi32(_mm_shuffle_epi32(v17, 0x4E), v17);
return (input_array[18] != 0xC)
+ (input_array[17] != 6)
+ (input_array[16] != 0xF) // input_long_arr[16:19] == [0xf, 0x6, 0xc, 0xf]
+ _mm_cvtsi128_si32(_mm_add_epi32(_mm_shuffle_epi32(v18, 0xE5), v18)) == -(input_array[19] != 0xF);
From the xmmword
s, we can retrieve the final expected value of the integer array.
We can then try to obtain the integer array before it is passed into the bubble sort. Since this integer array only consists of 20 elements, there is a total of 2**19 swaps which is trivial to bruteforce, although it will return multiple possible answers.
The array shuffling is deterministic and can also be reversed.
At the end, we will have multiple possible valid passcodes. However, only one of this passcode will properly decrypt the ciphertext.
// the AES IV is hardcoded to 16 null bytes
static {
AESCrypt.ivBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
AESCrypt.DEBUG_LOG_ENABLED = false;
}
// the AES key is the sha256 digest of the input key string
private static SecretKeySpec generateKey(String s) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest messageDigest0 = MessageDigest.getInstance("SHA-256");
byte[] arr_b = s.getBytes("UTF-8");
messageDigest0.update(arr_b, 0, arr_b.length);
byte[] arr_b1 = messageDigest0.digest();
AESCrypt.log("SHA-256 key ", arr_b1);
return new SecretKeySpec(arr_b1, "AES");
}
// the decryption is done via CBC with PKCS7Padding
public static byte[] decrypt(SecretKeySpec secretKeySpec0, byte[] arr_b, byte[] arr_b1) throws GeneralSecurityException {
Cipher cipher0 = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher0.init(2, secretKeySpec0, new IvParameterSpec(arr_b));
byte[] arr_b2 = cipher0.doFinal(arr_b1);
AESCrypt.log("decryptedBytes", arr_b2);
return arr_b2;
}
// take an input key string and a base64 encoded ciphertext to decrypt plaintext
public static String decrypt(String s, String s1) throws GeneralSecurityException {
try {
SecretKeySpec secretKeySpec0 = AESCrypt.generateKey(s);
AESCrypt.log("base64EncodedCipherText", s1);
byte[] arr_b = Base64.decode(s1, 2);
AESCrypt.log("decodedCipherText", arr_b);
byte[] arr_b1 = AESCrypt.decrypt(secretKeySpec0, AESCrypt.ivBytes, arr_b);
AESCrypt.log("decryptedBytes", arr_b1);
String s2 = new String(arr_b1, "UTF-8");
AESCrypt.log("message", s2);
return s2;
}
catch(UnsupportedEncodingException unsupportedEncodingException0) {
if(AESCrypt.DEBUG_LOG_ENABLED) {
Log.e("AESCrypt", "UnsupportedEncodingException ", unsupportedEncodingException0);
}
throw new GeneralSecurityException(unsupportedEncodingException0);
}
}
From above, we identify that we can decrypt our ciphertext via AES-CBC by taking the SHA256 digest of our brute-forced passcode with an IV of 16 null bytes.
Thanks Instructor Jeroen for the challenges!
The solve script is attached below!
