-
-
全国高校大学生软件创新大赛-软件系统安全赛 HappyLock复现
-
发表于: 2025-1-7 13:46 3891
-
前言
复现该题以学习师傅们的解题思路,Java层是个人思路,后续是复现部分
涉及frida java/native层 hook, dump dex/so, bindiff恢复符号, dex文件结构等知识
附件: attachments.zip 包括题目附件,和dump相关文件
Java层分析
可以发现StringFog包内的StringObf.decode方法,直接hook查看结果
1 2 3 4 5 6 7 | let StringObf = Java.use("StringFog.StringObf");StringObf["decode"].implementation = function (str) { let result = this["decode"](str); console.log(`StringObf.decode(${str})=${result}`); //showStacks(); return result;}; |
hook后滑动手势,打印部分信息如下
其中com.crackme.happylock.Check类比较可疑,但无法直接找到,可能有热加载dex操作

搜索字符串decode前的值,MainActivity中可以定位到PatternLockUtils.enc和Utils.cmp

hook PatternLockUtils.enc
1 2 3 4 5 6 7 | let PatternLockUtils = Java.use("com.andrognito.patternlockview.utils.PatternLockUtils");PatternLockUtils["enc"].implementation = function (arg1,arg2) { console.log(`PatternLockUtils.enc is called: null=${arg1}, null=${arg2}`); let result = this["enc"](arg1,arg2); console.log(`PatternLockUtils.enc result=${result}`); return result;}; |
可以发现和预期结果一致,该函数用于计算手势对应哈希

Utils.cmp通过反射调用clz.cmp方法进行验证

继续hook clz,打印相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | let Utils = Java.use("com.crackme.happylock.Utils"); let clz = Utils.clz.value; printInstance(clz); console.log("clz: ",clz); console.log("==========Methods==========") var methods=clz.getMethods(); for (let i=0;i<methods.length;i++){ console.log(methods[i]); } console.log("==========Fields==========") var fields=clz.getDeclaredFields(); if (fields.length===0) console.log("No fields!") else{ for (let i=0;i<fields.length;i++){ console.log(fields[i]); } } |
其中prinInstance是自己封装的实例打印函数
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 38 39 40 41 42 | function printClassFields(Class){ var fields=Class.class.getDeclaredFields(); //字段 console.log("\n==========fields=========="); for(var i=0;i<fields.length;i++){ var fieldName=fields[i].getName(); var fieldType=fields[i].getType(); console.log(fieldType,fieldName); }}function printClassMethods(Class){ var methods=Class.class.getDeclaredMethods(); //方法 console.log("\n==========methods=========="); for(var i=0;i<methods.length;i++){ var methodParams=methods[i].getParameterTypes() var methodReturnType=methods[i].getReturnType() var methodName=methods[i].getName(); console.log(methodReturnType+" "+methodName+"("+methodParams+")"); }}function printInstanceMethods(Instance){ printClassMethods(Instance)}function printInstanceFields(Instance){ var fields=Instance.class.getDeclaredFields(); //字段 console.log("\n==========fields=========="); for(var i=0;i<fields.length;i++){ var fieldName=fields[i].getName(); var fieldType=fields[i].getType(); var fieldValue=Instance[fieldName].value; console.log(fieldType,fieldName,"=",fieldValue); }}//打印实例详细信息function printInstance(Instance){ printInstanceFields(Instance) printInstanceMethods(Instance)}//打印类的详细信息function printClass(Class){ printClassFields(Class) printClassMethods(Class)} |
printInstance可以发现clz对应的ClassLoader,指明了dex文件路径

clz正是上面可疑的Check类,Utils.cmp实际是调用了Check.cmp

但无法直接使用frida hook到Check类,开始分析so层
so的init_array中可以发现多个函数进行字符串解密操作

JNI_OnLoad没找到关键逻辑,hook RegisterNatives也没有发现注册函数
接下来可以尝试dump内存中的dex和so,内存中的so已经解密了字符串,并且内存中的dex可能有Check类
Dump Dex
使用frida-dexdump得到的dex文件中,无法找到com.crackme.happylock.Check类


maps
查看app的内存映射情况进行观察
首先使用adb shell ps或frida-ps搜索app的pid

再使用maps查看内存映射情况

其中classes.dex标注了deleted, 下面开始dump dex文件
GDA dump
使用GDA可以,连接设备后搜索包名,在dex栏中可以定位,之后输入dex的起始地址和大小即可dump
如果无法正确dump可以查看文末问题解决

dump文件输出在GDA.exe同级目录dump/内,拖入jeb可以看到Check代码

getflag
一段简单的异或,可以getflag
1 2 3 4 5 6 7 8 | encrypted_data = [0x76, 17, 2, 80, 9, 0x7D, 6, 22, 0x71, 66, 0, 81, 94, 41, 87, 20, 0x7A, 65, 88, 5, 94, 41, 7, 19, 0x76, 22, 3, 2, 90, 41, 87, 71, 0x75, 68, 4, 7, 0x5F, 0x74, 4, 67]key = b"CrackMe!CrackMe!"decrypted_bytes = []for i in range(len(encrypted_data)): decrypted_byte = encrypted_data[i] ^ key[i % len(key)] decrypted_bytes.append(decrypted_byte)decrypted_text = bytes(decrypted_bytes).decode()print(decrypted_text) |
下面分析Native层做了什么操作
Native层
Dump SO
GDA dump操作同dump dex类似,但是需要手动使用3e3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6r3z5p5I4q4c8W2c8Q4x3V1k6e0L8@1k6A6P5r3g2J5i4@1f1@1i4@1u0r3i4@1q4q4i4@1f1#2i4@1p5@1i4K6S2p5M7$3!0Q4c8e0k6Q4z5e0k6Q4z5o6N6Q4c8e0c8Q4b7V1u0Q4b7U0j5`.

dump_so.py脚本可以自动dump并修复

恢复符号
分析dump并修复的so,可以发现shadowhook相关字符串,这是字节跳动的一个开源inline hook框架520K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1P5i4c8W2k6r3q4F1j5$3g2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1K9h3&6D9K9h3&6W2i4K6u0V1K9r3!0G2K9H3`.`.
下载抖音apk,提取libshadowhook.so,使用bindiff恢复符号(注意所有文件路径不能有中文):
ida打开libshadowhook.so,保存得到idb文件
ida打开dump出的libhappylock.so文件
ctrl+6调用bindiff,选择Diff Database,选择libshadowhook.so的idb文件

在Matched Functions窗口中ctrl+a选中所有匹配符号
再右键或者按ctrl+6,选择Import Symbols/Comments,设置匹配度

恢复符号后,JNI_OnLoad可以发现shadowhook相关操作

逻辑分析
参考ShadowHook 手册, shadowhook_hook_sym_name函数声明如下
1 2 3 | #include "shadowhook.h"// 参数: 目标库名,目标符号名,代理函数地址,返回原函数地址(可为kong)void *shadowhook_hook_sym_name(const char *lib_name, const char *sym_name, void *new_addr, void **orig_addr); |
hook_libc_execve
shadowhook_hook_func_addr中调用了shadowhook_hook_sym_name
hook了libc的execve
1 2 3 4 | __int64 shadowhook_hook_func_addr(){ return shadowhook_hook_sym_name(aLibcSo_1, aExecve, sub_121E4, &off_44758);} |
代理函数sub_121E4中判断系统调用是否为dex2oat,如果是则不执行,如果不是则执行
即执行除dex2oat外的系统调用

hook_libart_ClassLinker::LoadMethod
shadowhook_hook_sym_name_callback 中也调用了shadowhook_hook_sym_name
1 2 3 4 5 6 7 | __int64 shadowhook_hook_sym_name_callback(){ __int64 v0; // x0 v0 = __android_log_print(); return shadowhook_hook_sym_name(aLibartSo, v0, sub_12270, &qword_44770);} |
参考SWDD的[2025软件系统安全赛]HappyLock 直接hook shadowhook_hook_sym_name(0x12830)
注意要配合hook dlopen使用,保证加载so时立刻hook
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 38 39 | function hook_0x12380(){ var ModuleAddr= Module.findBaseAddress('libhappylock.so'); console.log("libhappylock.so: "+ModuleAddr) Interceptor.attach(ModuleAddr.add(0x12830), { onEnter: function (args) { console.log('lib_name:', args[0].readCString()); console.log('sym_name:', args[1].readCString()); console.log(`proxy_func: ${args[2]} (libhappylock.so+0x${(args[2]-ModuleAddr).toString(16)})`); console.log(`orig_addr: ${args[3]} (libhappylock.so+0x${(args[3]-ModuleAddr).toString(16)})\n`); }, onLeave: function (retval) { } });}function hook_dlopen_and_myhook() { var isHooking=false; var dlopen=Module.findExportByName(null,"dlopen"); var android_dlopen_ext=Module.findExportByName(null,"android_dlopen_ext"); console.log(`dlopen: ${dlopen}, android_dlopen_ext: ${android_dlopen_ext}`); if(android_dlopen_ext){ Interceptor.attach(android_dlopen_ext,{ onEnter:function(args){ var libPath=args[0].readCString(); if(libPath !== undefined && libPath != null&&libPath.indexOf("libhappylock") !== -1) { this.isCanHook = true; } },onLeave:function(args) { if(this.isCanHook&&!isHooking){ console.log("android_dlopen_ext libhappylock.so"); isHooking=true; hook_0x12380(); } } }) }}setImmediate(hook_dlopen_and_myhook) |

使用c++filt恢复函数符号名后,可以得知libart.so被hook的函数是ClassLinker::LoadMethod
1 | art::ClassLinker::LoadMethod(art::DexFile const&, art::ClassAccessor::Method const&, art::Handle<art::mirror::Class>, art::ArtMethod*) |
代理函数sub_12270 检测dex文件大小,如果匹配到目标dex则执行回填操作
(此处a1应是this指针,pdex是DexFile的引用,先调用了0x44770处的原始LoadMethod加载方法后再回填代码)

其中+0x217处是key的起始地址,后续为字符串表保存的字符串

+0x178处是Check.cmp的代码

综上所述,JNI_OnLoad没有注册Native方法,而是hook了libc和libart
当加载到目标dex文件时,回填代码
GDA无法正确dump问题
如果发现GDA dump无法正常使用(看不到app的内存映射情况等)
首先参考GDA关于android脱壳的问题说明,查看GDump是否正确推送至手机
其次GDA自带的adb可能会与系统adb冲突,将GDA自带的adb目录设置到系统adb环境变量之上即可
1 | 默认在 C:\Users\<User>\AppData\Roaming\GDA\gadtmp 目录 |

为了简化操作,可编写bat脚本,启动GDA时自动将gda自带的adb目录设置到环境变量最上方
@echo off set "DIR_TO_ADD=C:\Users\admin\AppData\Roaming\GDA\gdatmp" set "Path=%DIR_TO_ADD%;%Path%" start "" "E:\Tools\MobileTools\Decompilers\GDA\GDA4.11.exe"
但使用时依然要注意: 其他shell中运行adb命令仍然会kill GDA的adb server
References
感谢以下师傅的指导:
Shangwendada
PangBai
P1umH0