前言
文章没有前言就像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) {
}
});
|
[注意]APP应用上架合规检测服务,协助应用顺利上架!
最后于 2025-1-9 16:28
被Shangwendada编辑
,原因: 增加内容