解题思路
获取到输入的字符串保存到s,调用Jformat方法对s进行验证,返回true则代表输入字符串正确反之错误。
Jformat方法分析:
首先看到使用了LoadData加载了”ming“给了a方法,a方法的返回值赋值给了arr_b。
接着判断SDK_INT是否小于29:
意思就是判断Android版本是否小于10,如果不是则
使用InMemoryDexClassLoader从内存中动态加载DEX,加载方法在"challengemobile"文件中,这个是SO文件。
根据inMemoryDexClassLoader0的结果动态加载com.example.challengemobile下的Checker类。猜测是加载后的DEX文件。
method0是class0通过getMethod获取到Checker类下的"isflag"方法
如果以上操作都完成了则进入到if判断这里
很明显,取s字符串前五个判断是否是:
flag{,去最后一个判断是否是:}
如果都满足则继续进入if里:
首先获取s除去前五个字符的字符串赋值给了s1
通过反射机制调用了method0类的isflag方法,把s1字符串除去最后一个字符作为参数传递给了isflag方法。其实就是把flga{}括号中的字符串作为参数给了isflag方法。
根据isflag方法的返回值走return代码
如果返回了null则直接返回false,否则返回boolean0的布尔值。
其实说白了就是:
输入字符串--->isflag方法执行--->返回null可能代表isfhlag方法执行异常--->放回false则代码输入字符串不满足isflag方法的判断,true则证明输入的字符串满足了判断。同时就是正确的flag了
a方法分析
native方法,在"challengemobile"so文件中
解包APK在lib目录下使用IDA分析so文件
分析不难发现使用”ming“文件的数据去异或genrand_int32()函数的返回值,赋值给v10,v10的赋值的v8,那么可以说明这个循环执行完毕之后v8的数据就是动态加载的DEX文件了。这里有两个方法dump到v8的值:
1:动调SO文件,执行完循环查看V8的值;
2:使用Frida直接HOOK a方法的返回值;
我使用Frida去拿到DEX,因为比较快。
let MainActivity = Java.use("com.example.challengemobile.MainActivity");
MainActivity["a"].implementation = function (bArr) {
// console.log(`MainActivity.a is called: bArr=${bArr}`);
let result = this["a"](bArr);
// console.log(`MainActivity.a result = ${result}`);
// 将 result 转换为十六进制字符串
let hexResult = Array.from(result).map(num => {
// 对每个数值进行十六进制转换,并确保其为两个字符
// (例如,0xA 会变成 0x0A)
return ('0' + (num & 0xFF).toString(16)).slice(-2);
}).join(' '); // 以空格分隔每个十六进制数
console.log(`MainActivity.a result (hex): ${hexResult}`);
return result;
};
HOOK脚本可以直接在jadx右键复制,我这里改了输出结果为十六进制。
保存十六进制数据到txt中,使用010打开导入十六进制数据另存为dex文件
使用JADX打开dex文件分析:
可以看到就是Chekcer类
isflag方法,str就是{}中的数据,调用encryptToBase64String方法传入str和getKey方法的返回值。
getKey方法
也是个native方法,调用了example so文件
通过getKey方法定位到这里发现是使用了Java_example_Checker_getKey方法把v4复制给了v5,接着调用AES_ECB_decrypt方法对v5进行解密,其实就是v4经过AES_ECB_decrypt解密后就是key了,这个so文件是通过另一个so文件调用的,可能IDA无法调试。
还是使用Ffrida来获取getKey的返回值
Java.perform(function () {
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
if (loader.findClass("com.example.challengemobile.Checker")) {
Java.classFactory.loader = loader;
// console.log(loader);
}
} catch (error) {
}
}, onComplete: function () {
}
});
let Checker = Java.use("com.example.challengemobile.Checker");
console.log("Key---> " + Checker["getKey"]())
}
使用enumerateClassLoaders遍历所有类,找到Checker类,在使用use来获取到Checker类下的所有对象,通过Checker来主动调用getKey方法获取到返回值。
``Key---> oM51I504n137gp2~
获取到key后继续分析
encryptToBase64String方法
调用encrypt方法,str与str2作为参数传递过去
encrypt方法
调用另一个encrypt方法并把str与str2的字节数组作为参数传递过去
encrypt方法(第二个)
如果bArr长度为0则直接返回bArr的值,否则调用
toByteArray(encrypt(toIntArray(bArr, true), toIntArray(fixKey(bArr2), $assertionsDisabled)), $assertionsDisabled);
这一串方法调用,按顺序分析
encrypt方法(第三个)
很经典的一个XXTEA加密。
iArr为输入字符串的整数数组。
iArr2为key的整数数组。
那么不难理解:
toIntArray方法就是把字节数组转为对应的整数数组。
toByteArray方法把整数数组转为字节数组。
所有加密执行完之后回到encryptToBase64String方法
也就是说把XXTEA加密后的结果进行了Base64加密。所以
Ckh/PFCSS/i4kMVw1lswyghOZbIg+W5SymREHNcRg721Tm9w
就是密文了
分析到这里就可以去解密了,没有魔改的XXTEA加密,有了Key和密文,直接解密后在进行Base64解密就是flag了。
使用java实现
package CTF.ISCC.ChallengeMobile;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
public class Exp {
private static int MX(int i, int i2, int i3, int i4, int i5, int[] iArr) {
return (((i3 >>> 5) ^ (i2 << 2)) + ((i2 >>> 3) ^ (i3 << 4))) ^ ((i ^ i2) + (iArr[(i4 & 3) ^ i5] ^ i3));
}
private static int[] toIntArray(byte[] bArr, boolean z) {
int[] iArr;
int length = (bArr.length & 3) == 0 ? bArr.length >>> 2 : (bArr.length >>> 2) + 1;
if (z) {
iArr = new int[length + 1];
iArr[length] = bArr.length;
} else {
iArr = new int[length];
}
int length2 = bArr.length;
for (int i = 0; i < length2; i++) {
int i2 = i >>> 2;
iArr[i2] = iArr[i2] | ((bArr[i] & 255) << ((i & 3) << 3));
}
return iArr;
}
private static byte[] fixKey(byte[] bArr) {
if (bArr.length != 16) {
byte[] bArr2 = new byte[16];
if (bArr.length < 16) {
System.arraycopy(bArr, 0, bArr2, 0, bArr.length);
} else {
System.arraycopy(bArr, 0, bArr2, 0, 16);
}
return bArr2;
}
return bArr;
}
private static byte[] toByteArray(int[] iArr, boolean z) {
int i;
int length = iArr.length << 2;
if (z) {
i = iArr[iArr.length - 1];
int i2 = length - 4;
if (i < i2 - 3 || i > i2) {
return null;
}
} else {
i = length;
}
byte[] bArr = new byte[i];
for (int i3 = 0; i3 < i; i3++) {
bArr[i3] = (byte) (iArr[i3 >>> 2] >>> ((i3 & 3) << 3));
}
return bArr;
}
private static int[] encrypt(int[] iArr, int[] iArr2) {
int length = iArr.length - 1;
if (length >= 1) {
int i = (52 / (length + 1)) + 6;
int i2 = iArr[length];
int i3 = 0;
while (true) {
int i4 = i - 1;
if (i <= 0) {
break;
}
i3 -= 1640531527;
int i5 = (i3 >>> 2) & 3;
int i6 = 0;
while (i6 < length) {
i2 = iArr[i6] + MX(i3, iArr[i6 + 1], i2, i6, i5, iArr2);
iArr[i6] = i2;
i6++;
}
i2 = iArr[length] + MX(i3, iArr[0], i2, i6, i5, iArr2);
iArr[length] = i2;
i = i4;
}
}
return iArr;
}
/**
*
* @param iArr
* @param iArr2
* @return
*/
private static int[] decrypt(int[] iArr, int[] iArr2) {
int length = iArr.length - 1;
if (length >= 1) {
int i = (52 / (length + 1)) + 6;
int i3 = 0;
for (int j = 0; j < i; ++j)
i3 -= 0x61c88647;
for (int j = 0; j < i; ++j) {
int i6 = length;
int i2 = iArr[length - 1];
int i5 = (i3 >>> 2) & 3;
iArr[length] = iArr[length] - MX(i3, iArr[0], i2, i6, i5, iArr2);
while (i6 > 0) {
--i6;
i2 = iArr[i6 - 1 < 0 ? length : i6 - 1];
iArr[i6] = iArr[i6] - MX(i3, iArr[i6 + 1], i2, i6, i5, iArr2);
}
i3 += 0x61c88647;
}
}
return iArr;
}
public static void main(String[] args) {
int[] decBase64 = toIntArray(Base64.getDecoder().decode("Ckh/PFCSS/i4kMVw1lswyghOZbIg+W5SymREHNcRg721Tm9w"),false);
// System.out.println(Arrays.toString(decBase64));
int[] decKey = toIntArray(fixKey("oM51I504n137gp2~".getBytes()),true);
// System.out.println(Arrays.toString(decKey));
byte[] flag = toByteArray(decrypt(decBase64, decKey), true);
// System.out.println(Arrays.toString(flag));
assert flag != null;
String str = new String(flag,StandardCharsets.UTF_8);
System.out.println("flag{" + str + "}");
}
}
flag:
flag{ZVDK$8m|/;&6L6#zYJa3?Ming%a[Qt->}