吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 71|回复: 3
上一主题 下一主题
收起左侧

[Android 原创] 某道乐跑企业壳过检测脱壳分析

[复制链接]
跳转到指定楼层
楼主
buluo533 发表于 2026-2-4 22:03 回帖奖励

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关.本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责
一、样本信息
              
                       
              这个 app 的某梆企业壳和之前遇到的版本的不一样,他不是线程杀死的特征。
               我们直接开启一下 frida 来测试他的一个情况,直接做一个 dlopen 来测试一下
               
function hook_dlopen() {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var pathptr = args[0];
      if (pathptr !== undefined && pathptr != null) {
        var path = ptr(pathptr).readCString();
        console.log(path)
      }
    },
    onLeave: function (retval) {
    }
  });
libDexHelper.so
}

               

             可以看到他已经加载了壳特征的 so,libDexHelper.so,然后被杀死,看起来像是跳转到垃圾地址。这个样本难度不大,我跌跌撞撞搞了三套方案,都可以实现脱壳
第一套方案          
    ai 黑盒
// @FileName  :脱壳机.js
// @Time      :2025-12-14 16:10
// @AuThor    :Buluo
// =========================================================================
// ⚙️ 配置区域 (Configuration)
// =========================================================================
const CONFIG = {
  target_so: "libDexHelper.so", // 目标壳 SO 名字
  dump_dir: "/data/data/com.lptiyu.tanke/cache/", // Dump 落地目录
  sys_openat: 56 // AArch64 openat syscall number
};

// =========================================================================
// 🛠️ 基础工具模块 (Native Utils)
// =========================================================================
const NativeUtils = (() => {
  const libc = Process.getModuleByName("libc.so");
  const api = {
    open: new NativeFunction(libc.getExportByName("open"), 'int', ['pointer', 'int', 'int']),
    read: new NativeFunction(libc.getExportByName("read"), 'int', ['int', 'pointer', 'int']),
    write: new NativeFunction(libc.getExportByName("write"), 'int', ['int', 'pointer', 'int']),
    close: new NativeFunction(libc.getExportByName("close"), 'int', ['int']),
    malloc: new NativeFunction(libc.getExportByName("malloc"), 'pointer', ['int']),
    free: new NativeFunction(libc.getExportByName("free"), 'void', ['pointer'])
  };

  const O_RDONLY = 0, O_WRONLY = 1, O_CREAT = 64, O_TRUNC = 512;

  return {
    // 创建文件并写入内容
    writeFile: (path, content) => {
      const pathPtr = Memory.allocUtf8String(path);
      const fd = api.open(pathPtr, O_WRONLY | O_CREAT | O_TRUNC, 493); // 0755
      if (fd < 0) return false;
      const contentPtr = Memory.allocUtf8String(content);
      api.write(fd, contentPtr, content.length);
      api.close(fd);
      return true;
    },
    // 复制文件并过滤特定行 (核心逻辑)
    copyAndFilter: (srcPath, dstPath, filterKeywords) => {
      const srcPtr = Memory.allocUtf8String(srcPath);
      const dstPtr = Memory.allocUtf8String(dstPath);
      const fdIn = api.open(srcPtr, O_RDONLY, 0);
      if (fdIn < 0) return false;
      const fdOut = api.open(dstPtr, O_WRONLY | O_CREAT | O_TRUNC, 493);
      if (fdOut < 0) { api.close(fdIn); return false; }

      const bufSize = 4096;
      const buf = api.malloc(bufSize);
      let pending = "";

      while (true) {
        const n = api.read(fdIn, buf, bufSize);
        if (n <= 0) break;
        const chunk = buf.readUtf8String(n);
        const lines = (pending + chunk).split('\n');

        for (let i = 0; i < lines.length - 1; i++) {
          const line = lines[i];
          let blocked = false;
          for (let k of filterKeywords) {
            if (line.indexOf(k) !== -1) { blocked = true; break; }
          }
          if (!blocked) {
            const ptr = Memory.allocUtf8String(line + "\n");
            api.write(fdOut, ptr, line.length + 1);
                    }
                }
                pending = lines[lines.length - 1];
            }
            // 处理最后一行
            if (pending.length > 0) {
                 let blocked = false;
                 for (let k of filterKeywords) { if (pending.indexOf(k) !== -1) blocked = true; }
                 if (!blocked) {
                     const ptr = Memory.allocUtf8String(pending + "\n");
                     api.write(fdOut, ptr, pending.length + 1);
                 }
            }

            api.free(buf);
            api.close(fdIn);
            api.close(fdOut);
            return true;
        }
    };
})();

// =========================================================================
// 🎭 幻境构建模块 (Environment Mirage)
// =========================================================================
const MirageEnv = (() => {
    let fakeStatusPtr = null;
    let fakeMapsPtr = null;

    return {
        prepare: () => {
            console.log("Building Native Mirage...");

            // 1. 伪造 Status (TracerPid: 0)
            const statusPath = CONFIG.dump_dir + "native_status";
            const statusContent =
                `Name:\t${Process.currProcName}\nState:\tS (sleeping)\nTgid:\t${Process.id}\n` +
                `Ngid:\t0\nPid:\t${Process.id}\nPPid:\t1\nTracerPid:\t0\n` + // <--- 关键欺骗
                `Uid:\t10000\t10000\t10000\t10000\nGid:\t10000\t10000\t10000\t10000\n`;

            if (NativeUtils.writeFile(statusPath, statusContent)) {
                fakeStatusPtr = Memory.allocUtf8String(statusPath);
                console.log(`    [+] Fake Status ready: ${statusPath}`);
            }

            // 2. 伪造 Maps (过滤 Frida)
            const mapsPath = CONFIG.dump_dir + "native_maps";
            if (NativeUtils.copyAndFilter("/proc/self/maps", mapsPath, ["frida", "gum-js", "re.frida"])) {
                fakeMapsPtr = Memory.allocUtf8String(mapsPath);
                console.log(`    [+] Fake Maps ready: ${mapsPath}`);
            }
        },
        getStatusPtr: () => fakeStatusPtr,
        getMapsPtr: () => fakeMapsPtr
    };
})();

// =========================================================================
// 🛡️ 防御模块 (Anti-Suicide)
// =========================================================================
const AntiSuicide = (() => {
    return {
        activate: () => {
            const libc = Process.getModuleByName("libc.so");
            const funcs = ['raise', 'kill', 'tgkill', 'exit', '_exit'];

            funcs.forEach(name => {
                const addr = libc.findExportByName(name);
                if (addr) {
                    Interceptor.replace(addr, new NativeCallback((...args) => {
                        const sig = args[args.length - 1];
                        if (name.includes('exit')) {
                            console.warn(`[🛡️] BLOCKED ${name}! Sleeping thread.`);
                            Thread.sleep(1000000); return 0;
                        }
                        if (sig === 5) { // SIGTRAP
                            // console.warn(`[🛡️] BLOCKED ${name}(SIGTRAP)`);
                            return 0;
                        }
                        return new NativeFunction(addr, 'int', args.map(()=>'int'))(...args);
                    }, 'int', Array(name === 'raise' || name.includes('exit') ? 1 : (name === 'kill' ? 2 : 3)).fill('int')));
                }
            });
            console.log("[+] Anti-Suicide Shield activated.");
        }
    };
})();

// =========================================================================
// 🕵️‍♂️ 抓取模块 (DEX Dumper)
// =========================================================================
const DexDumper = (() => {
    // 自动保存 DEX 到文件
    const saveDex = (base, size) => {
        try {
            const magic = base.readCString(4);
            if (magic !== "dex\n") return; // 二次校验

            const path = `${CONFIG.dump_dir}dumped_${base}.dex`;
            const f = new File(path, "wb");
            f.write(base.readByteArray(size));
            f.flush();
            f.close();
            console.log(`[🎉] DEX CAPTURED! Path: ${path} (Size: ${size})`);
        } catch (e) { console.error("[-] Save failed: " + e); }
    };

    return {
        install: () => {
            // 扫描 libart.so 和 libdexfile.so
            const modules = Process.enumerateModules().filter(m => m.name.includes("libart") || m.name.includes("libdexfile"));
            let count = 0;

            modules.forEach(mod => {
                mod.enumerateSymbols().forEach(sym => {
                    // 模糊匹配 Open 函数 (OpenCommon, OpenMemory, Open)
                    if (sym.name.includes("DexFile") && sym.name.includes("Open") && !sym.name.includes("std")) {
                        try {
                            Interceptor.attach(sym.address, {
                                onEnter: function(args) {
                                    // 盲猜前两个参数,寻找 DEX 头 (dex\n035)
                                    for (let i = 0; i < 2; i++) {
                                        try {
                                            if (args[i].readU32() === 0x0a786564) { // 'dex\n'
                                                const size = args[i+1].toInt32();
                                                // 过滤过小的文件
                                                // if (size > 2048) saveDex(args[i], size);
                                            }
                                        } catch(e) {}
                                    }
                                }
                            });
                            count++;
                        } catch(e) {}
                    }
                });
            });
            console.log(`[+] DEX Dumper installed on ${count} targets.`);
        }
    };
})();

// =========================================================================
// 🚀 核心逻辑 (Syscall Interceptor)
// =========================================================================
function setup_bypass() {
    const module = Process.findModuleByName(CONFIG.target_so);
    if (!module) return;
    const jni_onload = module.findExportByName("JNI_OnLoad");

    console.log(`\nTarget Base: ${module.base}`);
    console.log("Strategy: Syscall Redirection (Stalker)");

    Interceptor.attach(jni_onload, {
        onEnter: function() {
            this.tid = Process.getCurrentThreadId();
            console.log(`[+] JNI_OnLoad entered. Stalker attached to thread ${this.tid}`);

            Stalker.follow(this.tid, {
                events: { call: false, ret: false, exec: false, block: false, compile: false },
                transform: function(iterator) {
                    let instruction = iterator.next();
                    do {
                        // 拦截 svc #0 指令
                        if (instruction.mnemonic === 'svc') {
                            iterator.putCallout(function(context) {
                                const sysNum = context.x8.toInt32();
                                // openat (56)
                                if (sysNum === CONFIG.sys_openat) {
                                    const ptrPath = context.x1; // x1 是路径参数
                                    try {
                                        const path = ptrPath.readCString();
                                        // 重定向 Status
                                        if (path && path.includes("/status")) {
                                            const fake = MirageEnv.getStatusPtr();
                                            if (fake) context.x1 = fake;
                                        }
                                        // 重定向 Maps
                                        if (path && path.includes("/maps")) {
                                            const fake = MirageEnv.getMapsPtr();
                                            if (fake) context.x1 = fake;
                                        }
                                    } catch(e) {}
                                }
                            });
                        }
                        iterator.keep();
                    } while ((instruction = iterator.next()) !== null);
                }
            });
        },
        onLeave: function(retval) {
            console.log(`[✅] JNI_OnLoad Completed! Ret: ${retval}`);
            Stalker.unfollow(this.tid);
        }
    });
}

// =========================================================================
// 🏁 主入口 (Main)
// =========================================================================
function main() {
    AntiSuicide.activate();
    MirageEnv.prepare();
    DexDumper.install();

    const dlopen = Module.findExportByName(null, "android_dlopen_ext");
    if (dlopen) {
        Interceptor.attach(dlopen, {
            onEnter: function(args) {
                const pathPtr = args[0];
                if (pathPtr) {
                    const path = pathPtr.readCString();
                    if (path && path.includes(CONFIG.target_so)) {
                        this.found = true;
                    }
                }
            },
            onLeave: function() {
                if (this.found) setup_bypass();
            }
        });
        console.log("Waiting for target SO to load...");
    }
}

setImmediate(main);
   这套方案使用 Gemini 黑盒,有报错就喂给他,不停的进行对抗,然后实现了过检测以及脱壳,但是这个仍然存在问题。但是也可以看出来一些常见的监测点,包括对 maps 、status 利用 frida 进行重定向,包括出现的 svc 指令特征去进行 hook,这些监测点也比较通用,在其他 app 也能遇到和用上。最后将dex 成功 dump 出来
第二套方案
      魔改 frida-service +端口转发,这是在无意间摸索出来的一套方案
         
[JavaScript] 纯文本查看 复制代码
./aaabbb -l 0.0.0.0:1314adb forward tcp:1314 tcp:1314
frida -H 127.0.0.1:1314 -f com.lptiyu.tanke -l .\bdyp.js

         使用的小佳大佬的魔改 aaabbb,做一个端口转发到 1314 端口,然后可以来试一下看看效果。
                  
                  
                    
         
         
            
         我们在启动之后,发现一个很有意思的现象libDexHelper.so 的特征壳已经加载成功了,进程还是被杀死了,看着就很像之前那一篇文章的梆梆,我们直接打印 clone 的线程,然后杀掉线程.
      
           
           
[JavaScript] 纯文本查看 复制代码
function hook_clone() {
  var clone = Module.findExportByName('libc.so', 'clone');

  Interceptor.attach(clone, {
    onEnter: function (args) {
      // 只有当 args[3] 不为 NULL 时,才说明上层确实把 “线程控制块指针” 传进来了
      if (args[3] != 0) {
        // 真正的用户线程函数地址
        var addr = args[3].add(96).readPointer()  // 读取指针
        var so_name = Process.findModuleByAddress(addr).name;
        // 获取该 so 在进程里的基址
        var so_base = Module.getBaseAddress(so_name);
        // 获取相对于 so_base 的偏移
        var offset = (addr - so_base);
        console.log("===============>", so_name, addr, ptr(offset));
      }
    },
    onLeave: function (retval) {

    }
  });
}
function hook_dlopen(so_name) {
   Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
        onEnter: function (args) {
            var pathptr = args[0];
            if (pathptr !== undefined && pathptr != null) {
                var path = ptr(pathptr).readCString();
                if (path.indexOf(so_name) !== -1) {
                    this.BookReader4Android = true
                }
            }
        },
        onLeave: function (retval) {
            if (this.BookReader4Android) {
                let base = Module.findBaseAddress(so_name);
                patch_func_nop(base.add(0x561d0));
                patch_func_nop(base.add(0x52cc0));
                patch_func_nop(base.add(0x5ded4));
                patch_func_nop(base.add(0x5e410));
                patch_func_nop(base.add(0x5b9f4));
                patch_func_nop(base.add(0x69470));
                patch_func_nop(base.add(0x5729c));
            }
        }
    });

}

               
                 这样就成功过掉了检测{:1_886:}
第三套方案  
            使用原生的 frida,定位加密大致位置,直接 trace 看在哪里被杀死的,定位加密位置可以看这张的一个思路吧
               

             还是老件套,因为是在libDexHelper.so 被杀死的,我们看他有没有完整的加载再去定位
            
               
            libDexHelper.so 是加载完成的
          那么检测点可能是在 jni 函数里面,看 jni_onload 加载情况
           
function hook_dlopen(so_name) {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var pathptr = args[0];
      if (pathptr !== undefined && pathptr != null) {
        var path = ptr(pathptr).readCString();
        if (path.indexOf(so_name) !== -1) {
          this.BookReader4Android = true
        }
      }
    },
    onLeave: function (retval) {
      if (this.BookReader4Android) {
        console.log(so_name,"加载成功!!!")
        const jni_onload =Process.findModuleByName(so_name).findExportByName("JNI_OnLoad")
        Interceptor.attach(jni_onload, {
          onLeave: function (retval) {
            console.log("JNI_OnLoad加载完成")
          }
        })
      }
    }
  });

}

           
说明检测是在 jni 函数中实现的逻辑,既然能 trace 出来,并被杀死,就一定有类似 svc 0 的相关操作
function hook_dlopen(so_name) {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var pathptr = args[0];
      if (pathptr !== undefined && pathptr != null) {
        var path = ptr(pathptr).readCString();
        if (path.indexOf(so_name) !== -1) {
          this.BookReader4Android = true
        }
      }
    },
    onLeave: function (retval) {
      if (this.BookReader4Android) {
        console.log(so_name, "加载成功!!!")
        const jni_onload = Process.findModuleByName(so_name).findExportByName("JNI_OnLoad")
        Interceptor.attach(jni_onload, {
          onEnter: function (args) {
            const thread_id = Process.getCurrentThreadId()
            Stalker.follow(thread_id, {
              transform: function (iterator) {
                while (true) {
                  let instruction = iterator.next();
                  if (instruction === null) {
                    break
                  }
                  iterator.keep()
                }
              }
            });
          },
          onLeave: function (retval) {
            console.log("JNI_OnLoad加载完成")
          }
        })
      }
    }
  });

}


       加入 trace 代码,确认没有对 app 启动流程造成影响,才能继续分析
         

     同样的闪退,这样的话就直接写 trace 代码逻辑看看输出情况
function hook_dlopen(so_name) {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var pathptr = args[0];
      if (pathptr !== undefined && pathptr != null) {
        var path = ptr(pathptr).readCString();
        if (path.indexOf(so_name) !== -1) {
          this.BookReader4Android = true
        }
      }
    },
    onLeave: function (retval) {
      if (this.BookReader4Android) {
        const targetModule = Process.findModuleByName(so_name);
        const jni_onload = targetModule.findExportByName("JNI_OnLoad");
        const modBase = targetModule.base;
        const modSize = targetModule.size;
        const modEnd = modBase.add(modSize);
        const modName = targetModule.name;
        Interceptor.attach(jni_onload, {
          onEnter: function (args) {
            this.tid = Process.getCurrentThreadId();

            Stalker.follow(this.tid, {
              transform: function (iterator) {
                let instruction = iterator.next();
                if (instruction === null) return;

                let offsetInBlock = 0;
                do {
                  const curAddr = instruction.address;

                  if (curAddr.compare(modBase) >= 0 && curAddr.compare(modEnd) < 0) {

                    const offset = curAddr.sub(modBase);

                    if (offsetInBlock === 0) {
                      console.log(`[transform] start: ${curAddr} name:${modName} offset: ${offset} base: ${modBase}`);
                    }
                    console.log("\t" + curAddr + " <+" + offsetInBlock + ">: " + instruction.toString());

                    offsetInBlock += instruction.size;
                  }

                  iterator.keep();

                  instruction = iterator.next();

                } while (instruction !== null);
              }
            });
          },
          onLeave: function (retval) {
            console.log("JNI_OnLoad加载完成");
            // Stalker.unfollow(this.tid);
          }
        });
      }
    }
  });

}

   直接梭哈

        

  日志会放在最后附件里面

        结合日志可以看到他是在0x31ec0 偏移地址最终死掉,提到了 x12 寄存器,我们在 ida 去看看这个地址的情况,需要注意的是,这个 so 文件也是混淆了,需要先 dump so 再去分析
           
         

            
      
        
        

     其实这里就能看到很多检测点了,直接 nop 掉这个判断,让他不走使我们报错的位置
   
[JavaScript] 纯文本查看 复制代码
var targetAddr = modBase.add(0x31CA0)Memory.patchCode(targetAddr, 4, function (code) {
  var cw = new Arm64Writer(code, {pc: targetAddr});
  cw.putNop();
  cw.flush();
});
      
       这时候就会发现,已经成功过掉了检测
     

总结
          这个案例还是比较简单,可能最开始没有摸到思路,ai操作太过于繁琐,但是很多检测点都有涉及,不失为一种思路,还是要有自己的分析思考过程。{:1_932:}总的来说还是学校的app分析起来香,趁着还没毕业好好分析一波。还得继续学习,还是太菜了{:1_932:}
trace1.zip (107.32 KB, 下载次数: 3)

免费评分

参与人数 1吾爱币 +2 热心值 +1 收起 理由
无名 + 2 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
无名 发表于 2026-2-4 22:16
大佬太牛了&#128077;,图有部分挂了?
3#
txsxcy 发表于 2026-2-4 22:43
4#
 楼主| buluo533 发表于 2026-2-4 22:57 |楼主
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-2-4 23:36

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表