解题思路



获取到输入的字符串保存到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->}

随机推荐

  1. 【DataBase】MySQL 09 SQL函数 单行函数其三 日期函数

    日期函数 日期&时间函数 NOW 当前日期时间. CURDATE 当前日期. CURTIME 当前时间 -- NOW();返回系统日期+时间 SELECT NOW(); -- CURDATE( ...

  2. 【perl】01

    1.环境搭建 -- 解释器 / 编译器 Perl 在 Window 平台上有 ActiveStatePerl 和 Strawberry Perl 编译器. ActiveState Perl和 Stra ...

  3. 【Redis】RCMD 04 List 列表

    1.LPUSH 写入命令:   LPUSH 键 值1 值2 值3 值4 ... 127.0.0.1:6379[12]> LPUSH LIST-1 1 2 3 4 5 (integer) 5 2. ...

  4. linux测试cpu性能的命令

    linux测试cpu性能的命令 在Linux中,可以使用多种命令来测试CPU性能.以下是一些常用的命令: stress: 一个通用的压力测试工具,可以生成CPU.内存.IO等负载. 安装: sudo ...

  5. 代码随想录Day8

    344.反转字符串 编写一个函数,其作用是将输入的字符串反转过来.输入字符串以字符数组 s 的形式给出. 不要给另外的数组分配额外的空间,你必须原地修改输入数组.使用 \(O(1)\) 的额外空间解决 ...

  6. Dolphinscheduler不重启加载Oracle驱动

    转载自刘茫茫看山 问题背景 某天我们的租户反馈数据库连接缺少必要的驱动,我们通过日志查看确实是缺少部分数据库的驱动,因为DolphinScheduler默认只带了Oracle和MySQL的驱动,并且需 ...

  7. 【开启报名】同学看过来,Apache DolphinScheduler开源之夏课题任务正式发布!

    如果你还拥有着一张有效的"学生证",在这个充满机遇的夏天,我们诚邀你加入一个充满挑战和机遇的开源冒险--开源之夏. 这不仅是一个简单的编程开发活动,假如你成功参加并结项之后,还能获 ...

  8. .NET MAUI 里,为什么 FlexLayout 这么难用?

    管中窥豹,可见一斑 Layout: FlexLayout:

  9. 一文带你理解URI 和 URL 有什么区别?

    当我们打开浏览器,要访问一个网站或者一个ftp服务器的时候,一定要输入一串字符串, 比如: https://blog.csdn.net/ 或者: ftp://192.168.0.111/ 这样我们就可 ...

  10. JavaScript设计模式样例十五 —— 状态模式

    状态模式(State Pattern) 定义:创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象.目的:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类 ...