ChallengeMobile
解题思路

获取到输入的字符串保存到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->}
随机推荐
- 我出一道面试题,看看你能拿 3k 还是 30k!
大家好,我是程序员鱼皮.欢迎屏幕前的各位来到今天的模拟面试现场,接下来我会出一道经典的后端面试题,你只需要进行 4 个简单的选择,就能判断出来你的水平是新手(3k).初级(10k).中级(15k)还是 ...
- linux中grep的用法详解
linux中grep的用法详解 grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是 ...
- SEO自动外链蜘蛛池工具促进百度快速收录怎么样 跟大家详谈一下
此工具集成市面上所有自动外链网站的资源链接,经过合并.去重.筛选.验证 总结出最终的外链资源 ,软件实时更新 本软件将您繁杂的外链推广转为自动化进行,并且加入站群的支持,您只需要将你的站群域名粘贴到软 ...
- 【Vue】分组类型排名查询功能
一.书接上回: https://www.cnblogs.com/mindzone/p/17749725.html 这个效果被否决了,产品要求一定要UI的来,UI效果如图: 按人为主体的时候,固定有4个 ...
- 【SpringMVC】06 转发 & 重定向
除了快速入门的视图解析器方式处理, 我们还可以使用原生的Servlet转发方式执行 访问测试 还有重定向 访问 测试 使用SpringMVC的转发&重定向 和原生的重定向,有一点不同,MVC的 ...
- 计算机领域:学术写作中的conducive的含义表示
"Conducive" 的意思是"有助于"或"有益于".在学术和正式的写作中,"conducive" 常用于描述某种情况 ...
- Docker 之 RabbitMQ安装教程 基于腾讯云
由于需要学习RabbitMQ(主要是项目启动了mq本地却没有服务控制台老是在连mq,致使我热部署都不知道到底成功没),所以才有了这个教程,下面介绍RabbitMQ准备工作 下载所需环境 yum -y ...
- 结构开发笔记(三):solidworks软件(二):小试牛刀,绘制一个立方体
前言 solidworks草图大师,基本的使用过程. 所有的零件基础都是从平面绘制开始,然后凸出来厚度. 本篇绘制一个简单的立方体,熟悉基本操作. 立方体绘制过程 选取一个平面绘制一个 ...
- AI阅读助手ChatDOC:基于 AI 与文档对话、重新定义阅读方式的AI文献阅读和文档处理工具
让 AI 真正成为你的生产力超级助手 AI 时代降临,我们需要积极拥抱 AI 工具 在过去的 2 个多月里,以 ChatGPT 为代表的 AI 风靡全球.随着 GPT 模型的不断优化,ChatGPT ...
- worktree的路径的文件夹自己重命名后发现没有git
1. worktree title: worktree的路径的文件夹自己重命名(修改名称)后发现没有git了 keyword: git worktree repair prune 快速方法 问题:父级 ...