Participate this CTF as SKR this year, here are some of my writeups

Qualifier

Droid Check

Challenges files:

We got an APK file (Android application file)

We can unzip it and decompile the classes.dex it using dex2jar, view it using JD-GUI

Or can use online APK decompiler

This case I use online decompiler and download it

Static Analysis

After viewing the source folder, notice got two java files is important:

  • UnlockActivity.java
  • MainActivity.java

When android start the app, it will start with MainActivity first, so we look at it first:

public class MainActivity extends AppCompatActivity {
    public native byte[] transform(String str);

    static {
        System.loadLibrary("native-lib");
    }

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) C0273R.layout.activity_main);
        ((Button) findViewById(C0273R.C0275id.unlock)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                byte[] transform = MainActivity.this.transform(((EditText) MainActivity.this.findViewById(C0273R.C0275id.password)).getText().toString());
                Intent intent = new Intent(MainActivity.this, UnlockActivity.class);
                intent.putExtra("DIGEST", transform);
                MainActivity.this.startActivity(intent);
            }
        });
    }
}

We can see it defind a OnClickListener, when the button is clicked, it will call the onClick function

  1. It will get the input text from the password field (Should be flag)
  2. It pass it to transform function
  3. Assign it to a transform byte array
  4. Call UnlockActivity and pass the transform byte array as DIGEST

Look at the UnlockActivity:

public class UnlockActivity extends AppCompatActivity {
    private static final byte[] TARGET = {-74, 56, -99, -111, 95, 98, -38, -116, -5, 76, -18, -84, -65, -112, 31, -81};

    private static byte[][] split(byte[] bArr, int i) {
        int length = bArr.length % i;
        int i2 = 0;
        int length2 = (bArr.length / i) + (length > 0 ? 1 : 0);
        byte[][] bArr2 = new byte[length2][];
        while (true) {
            if (i2 >= (length > 0 ? length2 - 1 : length2)) {
                break;
            }
            int i3 = i2 * i;
            bArr2[i2] = Arrays.copyOfRange(bArr, i3, i3 + i);
            i2++;
        }
        if (length > 0) {
            int i4 = length2 - 1;
            int i5 = i * i4;
            bArr2[i4] = Arrays.copyOfRange(bArr, i5, length + i5);
        }
        return bArr2;
    }

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) C0273R.layout.activity_unlock);
        byte[] byteArrayExtra = getIntent().getByteArrayExtra("DIGEST");
        WebView webView = (WebView) findViewById(C0273R.C0275id.canvas);
        if (Arrays.equals(byteArrayExtra, TARGET)) {
            webView.loadData("<h3>Congrats! Now go submit your flag :)</h3>", "text/html; charset=utf-8", "UTF-8");
        } else {
            webView.loadData("<h3>WRONG PASSWORD!</h3>", "text/html; charset=utf-8", "UTF-8");
        }
    }
}

When it start UnlockActivity, it get the DIGEST from previous activity (MainActivity) then assign to byteArrayExtra

Then obviously if the byteArrayExtra equal to TARGET then our password is the correct flag

But the TARGET seems like not ASCII character, so how can we even solve this??

Solving

Remember our password first pass into a function? That is the key to find the correct flag!

We can see it at the start of MainActivity:

public class MainActivity extends AppCompatActivity {
    public native byte[] transform(String str);

    static {
        System.loadLibrary("native-lib");
    }

But where is the function definition?

You can see it calling native-lib means it loads library from outside of the code (Actually is including C and C++ code https://en.wikipedia.org/wiki/Android_NDK)

You can find the compiled binary inside lib folder, you can see got many types of binary, for this case I choose x86_64 one

Open it at IDA or Ghidra to decompile it

After that, viewing a function called Java_com_test_locked_MainActivity_transform:

undefined8
Java_com_test_locked_MainActivity_transform(long *param_1,undefined8 param_2,undefined8 param_3)

{
  byte bVar1;
  byte bVar2;
  char *__s;
  size_t sVar3;
  undefined8 uVar4;
  ulong uVar5;
  long in_FS_OFFSET;
  byte local_38 [16];
  long local_28;
  
  local_28 = *(long *)(in_FS_OFFSET + 0x28);
  // Turn our password to string pointer
  __s = (char *)(**(code **)(*param_1 + 0x548))(param_1,param_3,0);
  // Get the length of password
  sVar3 = strlen(__s);
  // Set local_38 to all 0
  __memset_chk(local_38,0,sVar3);
  if (sVar3 != 0) {
    bVar2 = 0;
    if (sVar3 == 1) {
      uVar5 = 0;
    }
    else {
      uVar5 = 0;
      // Doing some transfromation with XORing 
      do {
         bVar1 = transform_table[uVar5];
         local_38[uVar5] = transform_table[(byte)__s[uVar5]] ^ bVar1 ^ bVar2;
         bVar2 = transform_table[uVar5 + 1] ^ bVar1 ^ bVar2;
         local_38[uVar5 + 1] = transform_table[(byte)__s[uVar5 + 1]] ^ bVar2;
         uVar5 = uVar5 + 2;
      } while ((sVar3 & 0xfffffffffffffffe) != uVar5);
    } 
    // Can ignore this part (Because it will never go here)
    if ((sVar3 & 1) != 0) {
      local_38[uVar5] = bVar2 ^ transform_table[uVar5] ^ transform_table[(byte)__s[uVar5]];
    }
  }
  // Not sure but I think should be change our password to local_38
  (**(code **)(*param_1 + 0x550))(param_1,param_3,__s);
  uVar4 = (**(code **)(*param_1 + 0x580))(param_1,0x10);
  (**(code **)(*param_1 + 0x680))(param_1,uVar4,0,0x10,local_38);
  if (*(long *)(in_FS_OFFSET + 0x28) == local_28) {
    return uVar4;
  }
                      /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

Translate it to python code basically is like this:

s = "password"
l = "00000000"
b1 = 0
b2 = 0
for i in range(0,len(password),2):
	b1 = transform_table[i]
	l[i] = transform_table[s[i]] ^ b1 ^ b2
	b2 = transform_table[i+1] ^ b1 ^ b2
	l[i+1] = transform_table[s[i+1]] ^ b2

Got two ways to solve this: Brute force or Decrypt

Brute force method

First copy the transform_table from Ghidra by highlight and right click > Copy special > Byte String (No spaces):

image1

Then paste it into python script like this:

transform_table = bytes.fromhex("c649139a6709de2b581e48534f9d35ae81d8c477ad96c1ee0c16321faa08e5ca8783fe45e01454ff5e107fd3202d2ea77b3e64a2846f91bfb441d6ef75aced5b3c50740f045d714b25ba9f3fe1608c33e7c7f41bc5bce2ecb3b143231a9c247ecdda826cd038707d0afd01114e7a97ce408826b7a086cb1799306e63988accd2025a56348ba4807c19429521b9c28e6690550d47b6e4d9d4a18d93db6d92361261f0e3f573f1c9c872c0f2aba885f8afd52ff90beb9e4cdc94bbd1a6298f374aa35122e939e6c31c0076523b65fb0344f305a95c46e857f74d3d0627cf153af65fddb8b2fc68d7bd629b0789592a6b31a51d0eb5282cfabe79186a7869eab0")

For target just copy and paste but need to convert negative to positive, just calculate by modular 256:

target = [i%256 for i in [-74, 56, -99, -111, 95, 98, -38, -116, -5, 76, -18, -84, -65, -112, 31, -81]]

Then brute force all combinations, if equal to TARGET then its the correct flag:

for i in range(0,len(target),2):
  b1 = transform_table[i]
  if transform_table[flag[i]] ^ b1 ^ b2 != target[i]:
    for j in range(256):
      if transform_table[j] ^ b1 ^ b2 == target[i]:
        flag[i] = j
        break

  b2 = transform_table[i + 1] ^ b1 ^ b2;
  if transform_table[flag[i + 1]] ^ b2 != target[i+1]:
    for j in range(256): 
      if transform_table[j] ^ b2 == target[i+1]:
        flag[i+1] = j
        break
print(flag)
# bytearray(b'fsjn1hck1ngcyber')

Full python script

Decrypt method

We know that XOR is reversable, like A ^ B = C then A = C ^ B and B = C ^ A

b1 and b2 is static no matter what is our password, so we can decrypt back the password easily

Look back the algorithm:

for i in range(0,len(password),2):
  b1 = transform_table[i]
  l[i] = transform_table[s[i]] ^ b1 ^ b2
  b2 = transform_table[i+1] ^ b1 ^ b2
  l[i+1] = transform_table[s[i+1]] ^ b2

We need to find s, our flag. If TARGET equal l then:

transform_table[s] = target ^ b1 ^ b2

To find s in Python can use function index to find the corresponding index:

s = transform_table.index(target ^ b1 ^ b2)

Write in to Python script, produce the same flag:

for i in range(0,len(target),2):
  b1 = transform_table[i]
  flag[i] = transform_table.index(target[i] ^ b1 ^ b2)
  b2 = transform_table[i+1] ^ b1 ^ b2
  flag[i+1] = transform_table.index(target[i+1] ^ b2)

print(flag)
# bytearray(b'fsjn1hck1ngcyber')

Full python script