首页
社区
课程
招聘
全国高校大学生软件创新大赛-软件系统安全赛 HappyLock复现
发表于: 2025-1-7 13:46 2606

全国高校大学生软件创新大赛-软件系统安全赛 HappyLock复现

2025-1-7 13:46
2606

前言

复现该题以学习师傅们的解题思路,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类

使用06fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9j5i4y4@1K9h3&6Y4i4K6u0V1P5h3q4F1k6#2)9J5c8X3k6J5K9h3c8S2i4K6g2X3k6s2g2E0M7q4!0q4z5q4)9^5y4q4)9&6b7g2!0q4y4W2)9&6b7#2!0m8b7#2!0q4y4g2!0n7c8g2)9&6y4#2!0q4y4g2)9^5z5q4!0n7x3q4!0q4y4#2)9&6b7g2)9^5y4r3c8W2P5q4!0q4y4W2)9&6y4W2)9^5y4#2!0q4y4q4!0n7b7W2!0n7y4W2!0q4z5q4)9^5x3#2!0n7c8q4!0q4y4W2)9^5z5g2!0n7c8g2!0q4y4g2)9^5z5q4!0n7x3p5y4Z5k6h3y4C8i4@1f1%4i4@1t1I4i4@1u0n7i4K6u0o6i4@1f1@1i4@1u0p5i4K6R3$3i4@1f1@1i4@1u0n7i4@1p5K6i4@1f1%4i4@1p5H3i4K6R3I4i4@1f1^5i4@1p5J5i4@1q4n7i4@1f1$3i4K6S2m8i4@1u0p5i4@1f1#2i4K6S2r3i4K6V1$3

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或e1fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9j5i4y4@1K9h3&6Y4i4K6u0V1P5h3q4F1k6#2)9J5c8X3k6J5K9h3c8S2i4K6g2X3k6s2g2E0M7q4!0q4y4#2)9&6b7g2)9^5y4r3c8#2L8i4m8Q4y4h3k6K6L8#2)9J5k6i4m8&6i4@1f1^5i4K6R3@1i4K6W2m8i4@1f1$3i4K6W2o6i4@1q4o6

GDA dump操作同dump dex类似,但是需要手动使用388K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6r3z5p5I4q4c8W2c8Q4x3V1k6e0L8@1k6A6P5r3g2J5i4@1f1@1i4@1u0r3i4@1q4q4i4@1f1#2i4@1p5@1i4K6S2p5M7$3!0Q4c8e0k6Q4z5e0k6Q4z5o6N6Q4c8e0c8Q4b7V1u0Q4b7U0j5`.

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

恢复符号

分析dump并修复的so,可以发现shadowhook相关字符串,这是字节跳动的一个开源inline hook框架ae7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1P5i4c8W2k6r3q4F1j5$3g2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1K9h3&6D9K9h3&6W2i4K6u0V1K9r3!0G2K9H3`.`.

下载抖音apk,提取libshadowhook.so,使用bindiff恢复符号(注意所有文件路径不能有中文):

  1. ida打开libshadowhook.so,保存得到idb文件

  2. ida打开dump出的libhappylock.so文件

    ctrl+6调用bindiff,选择Diff Database,选择libshadowhook.so的idb文件

  3. 在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


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 4
支持
分享
最新回复 (1)
雪    币: 301
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
好文章 难度还可以inlinehook 函数抽取壳
2025-1-8 15:35
0
游客
登录 | 注册 方可回帖
返回