前言
文章没有前言就像xxx没有xxx(暂时没想到比喻。
(题目在附件里)
题目要求

分析过程
初步分析
首先对于apk题目,二话不说肯定是Jadx开始梭:
于是乎

这一堆都是什么破玩意儿,看样子是被什么混淆影响了jadx的反编译,看看smali代码:

一大片的 goto语句,似乎使用了BlackObfuscator类似的混淆。
可以尝试把这个发给chatgpt试试

效果很不错,甚至gpt还帮我们解了一下字符串混淆。
另外虽然说jadx反编译不了,我们还可以试一试jeb,毕竟jeb的反编译能力是要强于jadx的,我们看看jeb的反编译结果:


可以发现jeb也可以正常看到逻辑,还能看到大量的类似于控制流平坦化的内容。
根据两边结果,cmp逻辑在Utils类中,那么主要逻辑就在这个cmp了
进一步分析处理逻辑
我们查看Utils中的tmp。

发现其通过new了一个class,然后调用了这个class里面的cmp方法,返回这个cmp的结果。
其实遇到这种动态调用,稍微有一点经验,我们就应该知道这个大概率在assets里面,但是不难发现一个细节,如果说真的在assets并且未加密的话,我们的jeb或者jadx也是同样可以识别到这个类的,这里并没有识别到,因此我们甚至不需要去看assets,他肯定是加密的,我们只需要查找他在哪儿加载的就可以了。
既然知道了,动态加载那么肯定离不开dexloader,我们直接搜索dexClassLoader:

dexClassLoader详解
既然加密了,那肯定有解密,类里面看到了一个decode,我们直接hook看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | Java.perform(function () {
try {
let Utils = Java.use("com.crackme.happylock.Utils");
let decodeOverloads = Utils.decode.overloads;
console.log(`Found ${decodeOverloads.length} overload(s) for decode method`);
Utils.decode.overload('[B').implementation = function (data) {
console.log(`Utils.decode(byte[]) is called`);
let dataArray = Java.array('byte', data);
console.log(`Input data: ${dataArray}`);
let result = this.decode(data);
console.log(`Utils.decode result: ${result}`);
return result;
};
} catch (err) {
console.error(`Error hooking decode method: ${err}`);
}
});
|

直接就发现了dex头,当然我们还可以通过hook defineClass来通杀所有的动态加载类大致方法如下:
defineClass源代码

从这里我们就可以看到其参数中是带有dexfile的,我们只需要hook上了之后解析就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Interceptor.attach(addr_DefineClass, {
onEnter: function (args) {
var dex_file = args[5]; var base = ptr(dex_file).add(Process.pointerSize).readPointer(); var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt(); if (dex_maps[base] == undefined) {
dex_maps[base] = size; var magic = ptr(base).readCString(); if (magic.indexOf("dex") == 0) {
var process_name = get_self_process_name(); if (process_name != "-1") {
var dex_dir_path = "/data/data/" + process_name + "/files/dump_dex_" + process_name; mkdir(dex_dir_path); var dex_path = dex_dir_path + "/class" + (dex_count == 1 ? "" : dex_count) + ".dex"; console.log("[find dex]:", dex_path); var fd = new File(dex_path, "wb"); if (fd && fd != null) {
dex_count++; var dex_buffer = ptr(base).readByteArray(size); fd.write(dex_buffer); fd.flush(); fd.close(); console.log("[dump dex]:", dex_path)
}
}
}
}
}
, onLeave: function (retval) { }
})
|

接下来我们需要分析动态加载的这个dex

好像字节码有问题,但是能看到有一个native方法。
接下来就要分析我们的Jni了
Native分析

最开始看到这个JniOnload,没找到register也没想太多 ,想着三下五除二直接上板子hook Register看看偏移,结果发生了如下事情:

欸嘿,还真hook不到,当时认为是自实现的register,也没想太多看看代码

这个很像是在Register,hook看看参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const ModuleAddr = Module.findBaseAddress('libhappylock.so');
console.log(ModuleAddr)
Interceptor.attach(ModuleAddr.add(0x12830), {
onEnter: function (args) {
console.log('arg0:', (args[0].readCString()));
console.log('arg1:', (args[1].readCString()));
console.log('arg2:', (args[2].readPointer()));
console.log('arg3:', (args[3].readPointer()));
},
onLeave: function (retval) {
}
});
|

奇怪,怎么是ClassLinker,(其实这个时候已经初步展露鸡脚了)。
但当时在做题的我没想太多,以为是我分析错了,就有oacia大佬的trace_so脚本梭了一把。

结果发现,在启动完之后,再也无法触发native的逻辑了。
对本题还有的疑问就是,他的log似乎也通过某种手段关闭了。

既然这样我们直接在调用logprint前看看参数,就能避免掉他用hook手段关闭log
插装一个log看看到底输出的啥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Interceptor.attach(ModuleAddr.add(0x127BC), {
onEnter: function (args) {
this.priority = args[0].toInt32();
this.tagPtr = args[1];
this.msgPtr = args[2];
this.debugPtr = args[3];
this.debugPtr2 = args[4];
this.debugPtr3 = args[5];
this.tag = safeReadCString(this.tagPtr);
this.msg = safeReadCString(this.msgPtr);
this.debug = safeReadCString(this.debugPtr);
this.debug2 = safeReadCString(this.debugPtr2);
console.log("[*] _android_log_print called:");
console.log(" Priority: " + this.priority);
console.log(" Tag: " + this.tag);
console.log(" Message: " + this.msg);
console.log(" Message: " + this.debug);
console.log(" Message: " + this.debug2);
},
onLeave: function (retval) {
}
});
|

好家伙,shadowhook,这下就能回想到

这个玩意实际上是在注册Hook了,那么根据之前分析的,他其实实现了一个类替换的过程,在defineClass前。
直接启动调试:

可以看到shadowhook 做 inlineHook的痕迹
基本到这里,我们就可以直接对如下classes.dex段做dump了

IDAPYTHON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import idautils
import idc
def dump_segment(segment_name, output_file):
for seg_ea in idautils.Segments():
seg = idaapi.getseg(seg_ea)
if seg is None:
continue
name = idc.get_segm_name(seg_ea)
if name == segment_name:
start = seg.start_ea
end = seg.end_ea
size = end - start
data = idc.get_bytes(start, size)
if data is None:
print(f"无法读取段 {segment_name} 的数据。")
return
try:
with open(output_file, 'wb') as f:
f.write(data)
print(f"段 {segment_name} 已成功导出到 {output_file}")
except IOError as e:
print(f"写入文件失败: {e}")
return
print(f"未找到段名为 {segment_name} 的段。")
dump_segment("classes.dex", r"E:\wechat\WeChat Files\wxid_sxslbee4x0m522\FileStorage\File\2025-01\classes.dex.dump")
|
然后这里注意使用Jadx会报错(保存原因后续分析),我们使用jeb反编译,就能看见逻辑。

或者我们直接根据之前dump下来的dex:

其中有一个大小是0x3ac

那么我们也可以手动填充需要填充的字符串

也就修复好了。
EXP
也就是说

异或一下再字符串输出就是我们的flag了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | def xor_with_key(cmp, key):
key_bytes = key.encode('utf-8')
result = []
for i in range(len(cmp)):
key_byte = key_bytes[i % len(key_bytes)]
cmp_byte = cmp[i]
xor_result = key_byte ^ cmp_byte
result.append(chr(xor_result))
return ''.join(result)
cmp = [
0x76, 0x11, 0x02, 0x50, 0x09, 0x7d, 0x06, 0x16, 0x71, 0x42,
0x00, 0x51, 0x5e, 0x29, 0x57, 0x14, 0x7a, 0x41, 0x58, 0x05,
0x5e, 0x29, 0x07, 0x13, 0x76, 0x16, 0x03, 0x02, 0x5a, 0x29,
0x57, 0x47, 0x75, 0x44, 0x04, 0x07, 0x5f, 0x74, 0x04, 0x43
]
key = "CrackMe!CrackMe!"
result_string = xor_with_key(cmp, key)
print(f"XOR result as string: {result_string}")
|

算法助手验证是否正确:

解答jadx无法反编译转储的dex
结尾讲一下,为什么jadx无法反编译我们dump下来的内容,jadx反编译的时候会checksum,但是hook之后填充的字节实际上sum值变化了。

我们只需要根据dex文件结构对存储的sum值更改即可

修改为jadx计算出的值即可进入jadx反编译:

通过hook打开log
1 2 3 4 5 6 7 8 9 | Interceptor.attach(ModuleAddr.add(0x126A8), {
onEnter: function (args) {
args[1] = ptr(1);
console.log("Debugable set True");
},
onLeave: function (retval) {
}
});
|




传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-1-9 16:28
被Shangwendada编辑
,原因: 增加内容