前言
虽然没有参加CISCN,但在初赛赛题中遇到一个APK逆向的题目,对于最近学习安卓技能的我来说,正好是个练习的机会。对此题目,锐评一下:出题人可能在测题时不够严谨,或者故意为之,导致很多人可以直接hook关键点,秒破,但部分人却频繁遇到崩溃问题,导致这道题在不同选手手中的难度不一致。然而正好我就是那个频繁崩溃的那一批,由此分析了一下崩溃的原因并且分享一下解决方法。
分析

看上面的MainActivity其实可以发现程序非常简单明了,感觉都可以一把秒。主要在legal里面,首先对flag格式做了判断,后续才对flag内容进行判断,如果格式不通过则不会触发jni的内容。
paramString.length() == 38 && paramString.startsWith("flag{") && paramString.charAt(paramString.length() - 1) == '}' && !inspect.inspect(paramString.substring(5, paramString.length() - 1));
如上则为判断语句,规定了当Flag长度为38,且格式满足flag{xxx}的时候才进入inspect,主要判断逻辑也是在inspect中完成。

审计inspect的代码,可以发现其实就是一个普通的DES,这道题的槽点和难点其实也就集中在这里了,我们会发现程序的Key和Iv都来自于Native层,其实审计Native层代码之后,大家都会发现算法繁琐且复杂,但是我们可以通过直接Hook的方法,去获取Iv与Key。
在这之前,我们得先看看运行效果。
发现如果格式不满足,程序正常返回Wrong

一旦格式满足,则程序直接崩溃了。
我们通过LogCat查看一下日志,究竟试什么原因崩溃。

查看日志,崩溃原因就有点显然了,总结如下:
程序在调用 JNI 函数 NewStringUTF 时传递了一个非法的 UTF-8 编码字符串。具体来说,错误日志指出输入的字节序列 0x9d 0x80 0x99 0xd4 0xac 0xc9 0xd3 0xf8 包含非法的 UTF-8 起始字节 0x9d。
NewStringUTF 函数期望接收一个合法的 UTF-8 编码的字符串,而不是任意字节序列。如果传入的字节序列不符合 UTF-8 编码规范,就会导致 JNI 检测到错误并抛出异常。
看到这里就有点抽象了为啥会有非UTF8编码的字符串呢,我们只能分析一下Native的逻辑了
在这之前我们先看一下正常来说如果Native层需要返回一个Jstring应该怎么写呢
extern "C" JNIEXPORT jstring JNICALL
Java_com_swdd_ahandroid_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
没错在return的时候 使用env->NewStringUTF(hello.c_str());来返回Jstring,所以这里出现的情况就是NewStringUTF抛出了异常,对应Native层代码如下:

首先我们得想办法知道这个导致崩溃的八个字节究竟是什么东西
多运行,多崩溃几次发现,如果多尝试几次使用Hook主动调用GetKey和程序自主触发产生的居然不一致,至此开始玄学起来了。


为了方便分析问题所在的地方,我们写脚本Hook NewStringUTF看看咋个事
hook脚本如下:
function hookNewStringUTF() {
Java.perform(function () {
var modules = Process.enumerateModules();
var newStringUTFAddr = null;
for (var i = 0; i < modules.length; i++) {
var module = modules[i];
try {
var symbols = module.enumerateSymbols();
for (var j = 0; j < symbols.length; j++) {
if (symbols[j].name.indexOf("NewStringUTF") >= 0) {
newStringUTFAddr = symbols[j].address;
console.log("Found NewStringUTF in module: " + module.name + " at: " + newStringUTFAddr + " with name: " + symbols[j].name);
break;
}
}
} catch (e) {
console.warn("Failed to enumerate symbols for module: " + module.name);
}
if (newStringUTFAddr) {
break;
}
}
if (newStringUTFAddr) {
// Hook NewStringUTF 函数
Interceptor.attach(newStringUTFAddr, {
onEnter: function(args) {
// 获取传入的 char* 参数
var inputStr = args[1].readCString();
console.log("NewStringUTF called with: " + inputStr);
},
onLeave: function(retval) {
// 如果需要修改返回值,可以在这里进行
console.log("NewStringUTF returning: " + retval);
}
});
} else {
console.log("NewStringUTF address not found!");
}
});
}
通过Hook得到的值如下,随后就崩溃了。

结尾
其实这个解决方法,看似无解,但是我发现当我们使用attach对于getkey时,程序则正常返回,并且变得可以调试进入,示例代码如下:
function hook() {
let jni = Java.use("com.example.re11113.jni");
var iv = jni.getiv();
var getKeyBase = Module.findExportByName("libSecret_entrance.so", "Java_com_example_re11113_jni_getkey");
Interceptor.attach(getKeyBase, {
onEnter: function(args) {
},
onLeave: function(retval) {
}
});
}
一旦我们附加这个代码,原本一调试就未响应的程序,变得可以调试了

调试之后也可以看到正确的值

那这样,整个程序就变得微妙了起来,测试了很多遍,都是只有在使用上述方法attach上之后程序就不再崩溃,并且目前没法找到原因。
既然正常,这个题的题的题解便是如此了
HOOK代码如下
function hook() {
let jni = Java.use("com.example.re11113.jni");
var iv = jni.getiv();
console.log("IV: "+iv);
var getKeyBase = Module.findExportByName("libSecret_entrance.so", "Java_com_example_re11113_jni_getkey");
Interceptor.attach(getKeyBase, {
onEnter: function(args) {
},
onLeave: function(retval) {
}
});
var key = jni.getKey();
console.log("Key: " + key);
}
最后解des即可

tql 哥哥呜呜呜