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
- It will get the input text from the password field (Should be flag)
- It pass it to
transform
function - Assign it to a
transform
byte array - Call
UnlockActivity
and pass thetransform
byte array asDIGEST
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):
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')
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')