1. 这个标题到底在解决什么问题:加固、解密与ELF修复的关系
做移动端安全分析的朋友,应该都见过这种场景:一个APK拿到手里,用常规手段一跑,jadx或者dex2jar打开的 classes.dex 里全是com.stub.StubApp、com.secneo.apkwrapper这类壳入口,真正业务代码根本看不见。这个时候你就知道,这个APK上了加固,最常见的就是360加固、腾讯乐固、爱加密、梆梆这一类。而标题里把“360加固DEX解密”和“ELF修复”放在一起,背后是一条完整的技术链路:先脱壳拿到真实DEX,再把加固过的SO文件修到能用的状态。
先给新手一个概念上的对照:APK加固的本质是做“运行时还原”。加壳器在打包阶段把原始DEX加密、抽取、或者隐藏起来,App启动时由壳的入口代码完成解密和加载。这个过程一旦发生在内存里,就给了分析者机会——你可以让App自己把真实代码吐出来,再在内存里抓取、转储、修复。360加固在几代版本里用了不同的方案,早年主要是整体加密 + 自定义ClassLoader加载,后来演变成DEX抽取、So加固、VMP虚拟机并存。不管哪种方案,最终都会落到两个对象上:一个是DEX,一个是ELF。
DEX解密,说的是把壳隐藏的真实字节码还原成可静态分析的DEX文件;ELF修复,说的是被加壳改造过的Native库(.so文件)在解密、dump之后,需要把ELF结构补全,才能被IDA、Ghidra这类工具正常加载。这两个环节经常同时出现,因为360加固的很多版本里,DEX的还原逻辑就在Native层,你把SO修好了,脱壳点才找得准,DEX才能完整dump出来。所以这不是两件独立的事,而是同一条分析流水线的上下站。
这条内容适合谁看?三类人最需要:一类是做移动端安全评估、合规测试的工程师;一类是做App崩溃治理、兼容性适配的Android开发,遇到加固SDK导致的Native崩溃,需要理解ELF改动到底动了什么;还有一类是刚入门逆向、想系统搞懂“加固 ≠ 无敌”的朋友。前提只有一条:分析的样本必须是你自己开发的App,或者你有明确授权的测试目标。没有授权去拆别人的App,这不是技术问题,是法律问题,下面讲的思路全部默认建立在合规场景上。
2. DEX解密流程:从脱壳点定位到指令修复
2.1 脱壳的基本思路:什么时候dump最关键
DEX脱壳,核心在于找准dump时机。壳再复杂,也要在某个时间点把真实DEX加载到内存里交给虚拟机执行。你要做的,就是在“DEX完整存在于内存”的那一刻,把这块内存抠出来。
360加固的脱壳点,不同版本差异很大。老版本里,壳代码会先加载一个加密的DEX文件,解密后放到内存里,再调用DexClassLoader或者自定义的ClassLoader去加载。这种方案下,只要hook住ClassLoader的构造过程,或者直接hookdalvik.system.DexFile的构造函数,就能拿到解密后的DEX。新版360加固喜欢用抽取+动态还原,也就是把DEX的CodeItem(方法体里的指令)抽走,运行到某个方法时再去Native层还原。这个时候就不能光靠ClassLoader hook了,得配合dump下运行时的完整DEX,回来再修被抽掉的CodeItem。
实际操作里,遇到360加固的样本,我会先用手机或模拟器装上目标App,然后起一个Frida脚本,把常见脱壳点全部hook一遍:
dalvik.system.DexFile的构造函数(拿到DexFile对象后直接调getBytes());java.lang.ClassLoader的loadClass方法,这里能观察到类加载的时机;libdexfile.so里的OpenMemory、DexFileLoader::Open这类Native函数;- 如果是Android 8.0以上,重点看
art::DexFileLoader::OpenCommon或者art::DexFile::DexFile。
Frida脚本里比较保守的做法是,先用Java.perform把Java层ClassLoader相关的hook挂上,启动App自动跑一遍业务逻辑,触发大部分类加载,然后在内存里搜dex\n035\0这个DEX魔数。搜到就按地址和大小dump出来。这个办法对整体加密型的加固基本是通杀,但对抽取型加固,dump到的DEX虽然文件头完备,指令却被抽空了,这时候就需要下一步修复。
还有一条经验:dump最好在App启动后延迟1到2秒再做,别一上来就dump。因为360加固的解密是分阶段触发的,主界面没起来,很多DEX根本还没解密。实测中,很多朋友dump出的DEX只有几十KB,就是因为时机太早,只抓到了壳的启动模块,真实业务DEX还没进内存。
2.2 修复DEX文件头:拿到dump后第一件事
你dump下来的内存镜像,跟一个标准DEX文件之间,差着几个关键字段。要让它能被jadx、baksmali这类工具正常解析,第一步是把DEX header修复干净。一个标准DEX header共112字节(0x70),关键在于下面这些字段:
magic:前8字节必须是dex\n035\0或者dex\n037\0、dex\n038\0,对应不同DEX版本。360加固有的版本会在运行时把魔数临时改掉,或者内存里出现多个DEX拼接,这时候要根据实际情况裁剪。file_size:整个DEX文件的长度。Dump内存时你只能按区域抠,这个值经常不对,要么是0,要么是虚大。修复方法很简单,用十六进制工具找到文件尾部的实际偏移,直接改这个字段。header_size:通常是0x70,这个值一般不用动。map_off:MAP列表的偏移。很多工具修复时会忽略map_off,但baksmali校验严格的时候会报错。稳妥的做法是用baksmali的fixDex参数,或者用现成的DEX修复工具把map补一遍。data_off/data_size:数据区偏移和大小,抽取型加固修完指令后这两个字段经常要跟着调。link_off/link_size:一般没用到静态链接,默认0。
实际操作中,我最常用的工具组合是:先用frida-dexdump(GitHub上开源的那个)自动dump,再用010 Editor加载DEX模板手动核对头部字段,最后用baksmali反汇编验证。如果dump出来的是正常的完整DEX,jadx直接打开就能看到代码;如果提示“DEX file size mismatch”或者“unexpected end of file”,基本就是file_size和实际长度对不上,改一下就好。
这里有个坑:有时候dump出来的DEX文件头是好的,但打开后类列表是空的,或者一堆方法体缺失。这种情况多半不是文件头的问题,而是抽取型加固造成的指令缺失,得做下一步指令修复。
2.3 指令修复:抽取型加固的硬骨头
抽取型加固,是把DEX里各个方法的CodeItem指令区抽走,运行时通过Native代码按需还原。你dump到的DEX,类的字段、方法签名都在,但每个方法体里只剩下空壳,指令区全是0或者被填充成NOP。静态分析工具看到的就是“方法存在但没有代码”。
修复抽取型加固的思路很直接:既然运行时每个方法执行前都会把指令填回去,那就hook住这个填充过程。360加固的抽取实现早期在Java层,后来移到Native层。在Native层,关键函数是art::ArtMethod::Invoke,它在真正执行方法前会检查CodeItem是否存在,不存在就去解密恢复。分析的时候,可以hookArtMethod::Invoke,拿到ArtMethod指针后,直接读取它指向的DexCache和ClassLinker,定位到对应的CodeItem在DEX文件中的位置,再把内存里的真实指令写回dump下来的DEX副本。
这套流程听起来复杂,实操上有一个取巧的技巧:只要你能完整dump到一份“运行过足够多方法”的内存DEX,那么完整度已经很高了。怎么让App运行足够多方法?跑一遍核心业务流程,该点的页面点一遍,该触发的方法都触发一遍。此时dump下来的DEX,跟静态修复出来的结果差别不大,再用FART(Frida ART)这类半自动脱壳机去主动调用遍历,把每个方法都主动Invoke一遍,就能把指令全部补回来。
FART的基本思路是主动调用:hook住ClassLoader.loadClass,拿到每个Class后遍历所有方法,一个个触发ArtMethod::Invoke。方法执行完,指令肯定被还原到了内存里,这时候统一dump。实测下来,360加固的抽取型方案,用主动调用方式能修出80%以上的方法,剩下20%有JNI动态注册、反射调用、或者极端混淆的方法,需要手工补。这一块没有银弹,只能靠经验和耐心一点点对。
3. ELF修复:从文件格式到重定位
3.1 ELF文件结构速览:用010 Editor看懂So文件
说完了DEX,现在进入标题的另一半:ELF修复。Android的Native库(.so)就是ELF格式,和Linux上的可执行文件同源。加固过的SO,静态看往往是无害的加载器,真正的逻辑要么被加密,要么被打散。分析的第一步,是用010 Editor打开SO文件,加载ELF模板,把结构读懂。
ELF文件分成几个层面:ELF Header在最前面,定义文件的基本属性;接下来是Program Header Table(程序头),描述运行时如何加载;再往后是Section Header Table(节头),描述静态链接时的各个节(.text、.data、.dynsym、.rel.dyn等)。加固壳经常做的事情是:把敏感的节加密成自定义数据块,只留一个极小的加载器,运行时解密后按正确的加载地址映射到内存。
用010 Editor看ELF时,优先确认这几项:
e_type:是ET_DYN(共享目标文件),Android的.so基本都是这个,如果是ET_EXEC就要注意是不是被改过了。e_machine:EM_ARM(40)是32位ARM,EM_AARCH64(183)是64位ARM。32位和64位的修复逻辑不一样,别混。e_entry:入口点地址。加固SO的入口通常被改成壳的初始化函数,原始入口被隐藏了。脱壳后要观察它是否指向了合理的导出函数。e_phoff/e_phnum:Program Header偏移和数量。PT_LOAD段必须覆盖整个运行时代码区,如果PT_LOAD段数量或范围不对,加载器就起不来。e_shoff/e_shnum:Section Header偏移和数量。很多加固SO会把section header表删掉或者加密,导致IDA加载时报“The file is not a valid ELF”或者解析不到符号。
网上搜“relocations in generic elf”,经常出现的是IDA或Ghidra在加载某些ELF时给出的警告。意思是文件中存在重定位项,但IDA的通用ELF加载器无法解析它们。这类情况十有八九不是文件坏了,而是节表缺失、重定位表被加密或者section header里对应的字符串表被清空了。修复的目标就是把这些被壳隐藏的表补回来。
3.2 加固SO的解密流程:壳到底改了哪些东西
360加固的Native部分,通常有两条处理路径。一条是在Java层通过System.loadLibrary加载时,SO里的JNI_OnLoad负责解密DEX;另一条是把SO自身的代码段也加密,运行到init_array时在内存中解密,这种叫做“自解密SO”。
自解密SO的工作方式是这样的:链接器加载SO时,会先执行PT_INIT_ARRAY里的初始化函数。加固壳把自己的解密函数放进init_array的最前面,执行时先解密被加密的代码段,然后跳转到原始入口。如果你只是一股脑从内存里把SO抠出来,拿到的是解密后的内存镜像,但它的ELF头和程序头描述的加载方式还是加密前的状态,直接保存成文件给IDA加载,就会出现各种解析异常。
修复ELF,在脱壳场景下要做的事情很明确:
- 从内存中dump出解密后的SO镜像(完整映射到
data段的区域); - 修正ELF Header里的
e_entry、e_shoff、e_shnum; - 根据内存布局重建Program Header Table,确保PT_LOAD段的范围和权限与运行时一致;
- 重建或修复Section Header Table,让IDA能正确识别节和符号表;
- 修复重定位表,让静态分析工具能把GOT/PLT的跳转关系看清。
前两步相对机械,难的是后三步,因为加固壳通常不会保留原始的section信息。
3.3 修复重定位与GOT/PLT:热词背后的坑
“relocations in generic elf”这个报错之所以频繁出现在跟360加固相关的讨论里,是因为加固SO里重定位表的处理往往不标准。Android的.so文件在链接时,会生成.rel.dyn和.rel.plt(64位是.rela.dyn和.rela.plt)两类重定位表。.rel.dyn保存绝对地址重定位(如R_ARM_ABSOLUTE、R_AARCH64_ABS64),.rel.plt保存函数跳转重定位(如R_ARM_JUMP_SLOT、R_AARCH64_JUMP_SLOT)。
加固壳为了不让分析者一眼看出函数之间的调用关系,会把这些重定位表加密或删除。运行时,壳自己维护一份重定位信息,在解密代码段后动态修正GOT表。你从内存dump出来的SO,GOT表已经是修复后的运行时状态了,但静态文件里没有对应的.rel.plt段,IDA就无从得知哪个GOT项对应哪个外部函数。于是它给出“relocations in generic elf”的警告,后续的F5反编译结果也就变得非常不可靠。
修复重定位表的思路,是依赖“运行时GOT已经正确”这一点反推:GOT表里每一个有效指针,都对应一个被解析过的外部函数。通过对比so文件的动态符号表.dynsym和运行时GOT表的内容,可以手工建出一份重定位表。具体做法:
- 用
readelf -S查看SO的section列表,找到.dynsym(动态符号表)和.dynstr(动态字符串表)。 - 用
readelf -r看看现有的重定位表是否为空或者缺失。 - 用010 Editor打开文件的GOT区域(通常在
.got或.data.rel.ro节中),逐个读取指针值。 - 将每个GOT项的地址偏移与动态符号表里的符号地址做匹配,匹配上的,就能还原出一条重定位记录。
这个过程工作量不小,但修复完后IDA的分析质量会明显提升,可以正常看出SO调用了哪些系统API(如mmap、open、dlopen等),对还原加固逻辑非常关键。
另外,静态修复时需要特别关注GOT表的权限位。Android的RELRO机制会将GOT表设为只读,如果修复后的SO文件里GOT表所在段的权限标记错误(比如标成可写),运行时可能导致崩溃。你要是做的是“修复后能在设备上跑起来”的目标,这个细节必须检查。
4. 实操中的高频问题与排查方法
4.1 bad ELF interpreter与文件权限:最常见但最容易被忽略
“bad ELF interpreter: No such file or directory”报错,在热词里跟修复相关,但它其实不是加固导致的,而是在Linux/Android环境里跑ELF时,系统找不到动态链接器的经典错误。32位ELF需要32位的/lib/ld-linux.so.2,64位ELF需要64位的/lib64/ld-linux-x86-64.so.2。如果你把一个32位的加固SO或者旧的命令行工具放到64位系统上直接运行,没装32位运行库,就会报这个错。
处理方式很简单:确认ELF位数和系统架构,然后安装对应的兼容库。Debian/Ubuntu系装libc6-i386或者lib32z1,CentOS/RHEL系装glibc.i686。在Android设备上,如果自己写的Native工具报这个错,检查一下是不是用错了NDK的toolchain编出了错误架构的so。
另一个跟文件权限相关的问题是Permission denied。这个看起来简单,但很多人在修复或分析ELF时都会踩:从内存dump出来的so文件,默认权限可能是600,直接拿去执行就报权限不够。chmod +x一把梭就能解决。还有的情况是NTFS或FAT32挂载分区不支持可执行权限,把so拷到ext4或tmpfs上再跑就正常了。这种“环境问题”排查起来有时候比技术问题还费时间,先打个file xxx.so和ls -l xxx.so,能省不少冤枉路。
对于文件权限修复,分布式场景下还常见一个坑:如果你通过CI/CD管道把so文件分发到多台测试机,压缩包或Git仓库可能把可执行位丢了。Android的AGP构建其实会自动处理so的执行权限,但你要是手动去改APK里的so,一定要用zip工具带权限属性,别用普通的文件管理器重新打包。
4.2 加固DEX和ELF修复后的运行崩溃:从日志反推问题
费了半天劲把DEX和ELF都修好了,结果一运行就崩,这是家常便饭。关键要区分:是修复文件的问题,还是原App本身的代码就敏感。
如果崩溃发生在启动早期,logcat里看到ClassNotFoundException或者NoSuchMethodError,通常是DEX修复不完整,类或方法没补全。先回DEX文件头检查file_size和map_off,再用baksmali重新反编译一次,看报错的具体类在哪里断档。这个方法很笨但很管用:baksmali的报错信息通常会指出某个类的某个方法索引越界,你就能定位到是哪一部分指令还没修复。
如果崩溃发生在Native层,logcat里能看到Fatal signal 11 (SIGSEGV)或者Abort message: 'art::ArtMethod::...',要优先怀疑ELF修复的问题。常见原因有三种:
- SO的PT_LOAD段范围不对,运行时访问到未映射区域;
- GOT表修复错误,导致某个外部函数指针指向了无效地址;
.init_array被改了但顺序不对,壳的初始化函数比真实业务代码晚执行。
自己在分析的时候,先用IDA加载修复后的SO,看它能不能正确识别导入导出函数。如果F5出来的伪代码里一堆off_xxxxxx,说明GOT/PLT关系没修好。这个阶段别急着继续往下分析,先把重定位表补完,否则后面全是无用功。
另外补充一点:热词里的“双系统引导修复”“麒麟系统修复助手”这类场景,跟ELF修复其实是同一类底层思维——引导程序、加载器、文件系统三者的关系没对上。Windows的双系统引导修复是修BCD,Linux的ELF加载失败是修动态链接器路径,Android的加固So修复是修ELF头和重定位表。核心逻辑都一样:让加载器能找到下一个要执行的东西。
4.3 工具选型与操作流程整理
做360加固DEX解密和ELF修复,工具不需要多,但要配合顺。我自己长期用的组合是:
| 环节 | 工具 | 用途 |
|---|---|---|
| 动态hook | Frida、FART | 找脱壳点、主动调用、dump内存 |
| 十六进制编辑 | 010 Editor(带ELF/DEX模板) | 查看、修复文件头、节表 |
| 二进制分析 | readelf、llvm-readelf、Ghidra | 查看节表、符号表、重定位表 |
| 静态反编译 | jadx、baksmali | 验证DEX修复成果 |
| Native分析 | IDA Pro、Ghidra | 分析SO逻辑,验证ELF修复成果 |
用010 Editor看ELF文件时,很多人上来就加载官方ELF模板,但Android的SO很多字段在模板里显示正常,实际却被加固壳改得面目全非。建议对照readelf -h和readelf -l的输出逐项核对,工具之间交叉验证,能发现不少问题。
如果你做的是大批量样本分析,建议把这套流程脚本化。Frida先做自动化dump,然后写个Python脚本自动修正DEX的header字段,批处理跑下来,最后再用baksmali全量反编译做质量校验。人工只处理那些自动修复失败的特例。这样效率能提升好几倍,也减少手误。
注意:所有操作必须在授权范围内。加固技术本身是合法的商业产品,分析和解密只应该用于你自己拥有、或有权测试的App。写在授权测试报告里的脱壳分析是技术成果,拿去拆别人商业App就是另外一回事了,这个边界自己想清楚。
5. 合规边界与一点个人体会
聊完技术细节,我多说几句大白话。360加固这类产品,本质上是一种软件保护技术,它存在的意义是延缓分析者理解App逻辑的速度。解密和修复技术,则是从“被保护对象”和“运行环境”的关系中找突破口。这个对抗关系跟猫鼠游戏一样,没有一劳永逸的方案,只有不断升级的手段。
我个人在实际操作中的体会有三点。第一,不要迷信任何“一键脱壳工具”。那些工具不是没用,而是只能覆盖一部分加固版本,遇到新版本、新抽取策略,工具就失灵了。真正值钱的能力是理解原理,能定位到“壳必须在某个时机还原真实代码”这个不变的事实,然后顺着这个事实设计自己的方案。第二,修复工作要沉得住气。DEX修到一半、ELF重定位表修到想吐的时候,最容易出错。建议每修完一步就做一次验证:修完header就baksmali一把,修完重定位就重新用IDA看导入表,验证通过再进行下一步。第三,保留好dump原始内存镜像。修复过程中改坏了,随时回到原始dump重新来,别在已经改过的文件上反复改,越改越乱。
如果后续你想深入扩展,建议往两个方向走:一个是VMP(虚拟机保护)方向,代码被编译成自定义字节码,在虚拟机解释器里执行,那已经不是“还原DEX”的问题了,而是“还原解释器”的问题;另一个是ART虚拟机内部结构,搞清楚ArtMethod、DexCache、ClassLinker这些运行时对象到底怎么关联的,很多表面上的加密手段一看就能理解它为什么这么设计。这两个方向啃下来,再回头看360加固这类方案,你会觉得它只是同一套思路下的一个具体实现。