首页
社区
课程
招聘
[2025软件系统安全赛]HappyLock(对抗使用SHadowHook实现的类抽取)
发表于: 2025-1-6 13:00 3284

[2025软件系统安全赛]HappyLock(对抗使用SHadowHook实现的类抽取)

2025-1-6 13:00
3284

前言

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

题目要求

file

分析过程

初步分析

首先对于apk题目,二话不说肯定是Jadx开始梭:
于是乎
file

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

file

一大片的 goto语句,似乎使用了BlackObfuscator类似的混淆。

可以尝试把这个发给chatgpt试试

file

效果很不错,甚至gpt还帮我们解了一下字符串混淆。

另外虽然说jadx反编译不了,我们还可以试一试jeb,毕竟jeb的反编译能力是要强于jadx的,我们看看jeb的反编译结果:
file
file

可以发现jeb也可以正常看到逻辑,还能看到大量的类似于控制流平坦化的内容。

根据两边结果,cmp逻辑在Utils类中,那么主要逻辑就在这个cmp了

进一步分析处理逻辑

我们查看Utils中的tmp。

file

发现其通过new了一个class,然后调用了这个class里面的cmp方法,返回这个cmp的结果。

其实遇到这种动态调用,稍微有一点经验,我们就应该知道这个大概率在assets里面,但是不难发现一个细节,如果说真的在assets并且未加密的话,我们的jeb或者jadx也是同样可以识别到这个类的,这里并没有识别到,因此我们甚至不需要去看assets,他肯定是加密的,我们只需要查找他在哪儿加载的就可以了。

既然知道了,动态加载那么肯定离不开dexloader,我们直接搜索dexClassLoader:
file

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}`);
    }
});

file

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

defineClass源代码
file

从这里我们就可以看到其参数中是带有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) { }
})

file

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

file

好像字节码有问题,但是能看到有一个native方法。

接下来就要分析我们的Jni了

Native分析

file

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

file

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

file

这个很像是在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()));
        //args[1] = ptr(0);
    },
    onLeave: function (retval) {
    }
});

file

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

结果发现,在启动完之后,再也无法触发native的逻辑了。

对本题还有的疑问就是,他的log似乎也通过某种手段关闭了。

file

既然这样我们直接在调用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);
      //  console.log("    Message: " + (this.debugPtr3 - ModuleAddr));
    },
    onLeave: function (retval) {
 
      
    }
});

file

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

file

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

直接启动调试:
file
可以看到shadowhook 做 inlineHook的痕迹

基本到这里,我们就可以直接对如下classes.dex段做dump了

file

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):
    """
    导出指定段名的内存内容到文件。
 
    :param segment_name: 要导出的段名(字符串)
    :param 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反编译,就能看见逻辑。
file

或者我们直接根据之前dump下来的dex:
file
其中有一个大小是0x3ac
file
那么我们也可以手动填充需要填充的字符串

file
也就修复好了。

EXP

也就是说

file

异或一下再字符串输出就是我们的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转换为字节数组
    key_bytes = key.encode('utf-8')
 
    # 存储结果
    result = []
 
    # 遍历cmp数组并与key数组的字节进行异或操作
    for i in range(len(cmp)):
        # 使用key的字节,按循环方式访问
        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数组
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}")

file
算法助手验证是否正确:
file

解答jadx无法反编译转储的dex

结尾讲一下,为什么jadx无法反编译我们dump下来的内容,jadx反编译的时候会checksum,但是hook之后填充的字节实际上sum值变化了。
file
我们只需要根据dex文件结构对存储的sum值更改即可
file
修改为jadx计算出的值即可进入jadx反编译:
file

通过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");
        //args[1] = ptr(0);
    },
    onLeave: function (retval) {
    }
});

file
file
file
file


[注意]APP应用上架合规检测服务,协助应用顺利上架!

最后于 2025-1-9 16:28 被Shangwendada编辑 ,原因: 增加内容
上传的附件:
收藏
免费 8
支持
分享
最新回复 (5)
雪    币: 1978
活跃值: (1615)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
太強啦
2025-1-6 15:09
0
雪    币: 2598
活跃值: (3595)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
太帅了
2025-1-6 18:25
0
雪    币: 924
活跃值: (196)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
文章没有前言就像鱼失去了自行车
2025-1-6 18:49
1
雪    币: 105
活跃值: (4743)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
图裂了 哥们
2025-1-6 23:34
0
雪    币: 347
活跃值: (1272)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
6
朱老师
2025-1-8 14:42
0
游客
登录 | 注册 方可回帖
返回