Skip to content

Instantly share code, notes, and snippets.

@caprinux
Last active November 19, 2024 07:48
Show Gist options
  • Save caprinux/0223a402aa3931a293b6978e8fb249b0 to your computer and use it in GitHub Desktop.
Save caprinux/0223a402aa3931a293b6978e8fb249b0 to your computer and use it in GitHub Desktop.
SANS SEC575 Practice — Get The Plans (Hard)

TLDR

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.

Reversing the APK

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.

Reversing the Native Library

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);

Putting the pieces together

From the xmmwords, 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!

{DF9733D0-4111-4B7C-AABB-03684B5070E2}
from pwn import b64d
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def one_pass_bubble(arr):
x = arr.copy()
for i in range(len(x)-1):
if x[i+1] < x[i]:
x[i+1], x[i] = x[i], x[i+1]
return x
v9 = 0x5D3AD
v10 = 0x2E9D6
ct = b64d("WzqOeA3jbpkjs0hWE+ieL/bQyz+T/nQgnA2un8Y0TxCMK7132puDpSMKMvOA+MMV")
# we restore the final state of our input here
input = [0 for i in range(20)]
input[0:4] = [0, 6, 0, 6]
input[4:8] = [0xa, 0xb, 0xd, 8]
input[8:12] = [0xa, 0x7, 0xa, 3]
input[12:16] = [5, 4, 0, 1]
input[16:20] = [0xf, 0x6, 0xc, 0xf]
# reimplement bittest
def _bittest64(val, bitpos):
return (val >> bitpos) & 0x1
# reimplement the reverse of the array swap
def reverse_swap(arr):
x = arr.copy()
j = 18
if _bittest64(v9, j):
x[j], x[j+1] = x[j+1], x[j]
j -= 2
while (j != 0):
if _bittest64(v10, j):
x[j+1], x[j+2] = x[j+2], x[j+1]
if _bittest64(v9, j):
x[j+1], x[j] = x[j], x[j+1]
j -= 2
return x
# we brute force the one pass bubble swap
for i in range(pow(2, 19)):
x = input.copy()
bswaps = bin(i)[2:].zfill(19)
for j in range(len(bswaps)):
if bswaps[j] == '1':
x[19-j], x[19-j-1] = x[19-j-1], x[19-j]
if one_pass_bubble(x) == input:
# now we reverse the swap
res = reverse_swap(x)
# then now we decrypt
key = " ".join([hex(i)[2:].upper() for i in res]).encode() + b" "
key_digest = hashlib.sha256(key).digest()
cipher = AES.new(key=key_digest, mode=AES.MODE_CBC, iv=b"\x00"*16)
try:
print(unpad(cipher.decrypt(ct), 16).decode())
print(key)
except:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment