[2025软件系统安全赛]HappyLock

前言

文章没有前言就像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看看:

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上了之后解析就好了

 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看看参数

  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看看到底输出的啥:

 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:

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了

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

    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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇