脱壳入门

hook , 抓包,调试和反调试等安卓逆向基础知识都学了,刚准备成就一番事业,出门遇到大boss 360加固,要么就是梆梆加固企业版,打开app就是网络错误,或闪退或连不上网,源码是看不到的,frida一启动就是process terminated,根本无从下手,深感无力,感觉还是功力尚浅,故欲深究加壳和脱壳。

简要原理和过程

主流是写一个代理Application,加载so文件,so里面写dex文件的解密逻辑,再用DexClassLoader加载解密后的dex文件。

要懂一点双亲委派机制,Dex文件结构等知识,具体就不多写了,可以见:[原创]Android漏洞之战(11)——整体加壳原理和脱壳技巧详解-Android安全-看雪-安全社区|安全招聘|kanxue.com 以及这篇文章里的提到的其他链接

例如梆梆加固免费版,这里我用的apk是吾爱破解论坛安卓逆向这档事的demo,梆梆加固注册个账号,把apk传上去加固下再下下来就行,jadx打开

image-20250205174421050

image-20250205174436101

尝试IDA打开,函数名基本都是没啥规律的,点开也是JUMP_OUT(地址),要么是一堆LABEL混淆过的,看不明白。

咋办呢,那就从内存里下手呗。例如frida-dexdump。

原理是这样的,/proc/<pid>/maps放了这个进程分配的所有内存块索引,然后在这些内存块里寻找dex文件特征的部分,如果能匹配到就dump到文件里。

梆梆加固免费版会有frida的检测,可以用frida魔改版server:Release 16.6.6 · Ylarod/Florida

大致的魔改原理可以见:《安卓逆向这档事》十八、表哥,你也不想你的Frida被检测吧!(上) - 吾爱破解 - 52pojie.cn

究其根源还是要看frida的源码,看看可能会有什么特征。如果未来遇到魔改server也不起作用的话,还要深入研究一下这块。

frida-dexdump -U -f com.zj.wuaipojie

拿到20个dex文件,jadx打开:

image-20250205175652330

可以看到成功脱壳。

frida-dexdump脚本原理

frida-dexdump/agent/src/index.ts at master · hluwa/frida-dexdump

这里只写关键函数,主要看searchDex,分为普通搜索和深度搜索,先看普通的:

image-20250205181410649

先检查文件头特征”64 65 78 0a 30….”,假如有的话,先检测路径是否为/data/dalvik-cache或/system,如果是的话直接return(系统的dex),然后调用verify函数验证。

image-20250205181718691

先看0x70是否比这块内存大(0x70是dex文件头大小,如果都没这个大,那这块肯定不是dex文件),然后根据maps来判断,关键在verify_by_maps方法。

image-20250205182030276

0x34DEX 文件头(Dex Header)中的 map_off 偏移量,用于获取 map_list 的地址,然后遍历。

1
2
3
4
5
6
7
8
9
10
11
struct DexMapList {
uint32_t size; // 记录 `DexMapItem` 的数量
DexMapItem list[size]; // MapItem 列表
};

struct DexMapItem {
uint16_t type; // **数据类型**(4096 表示 `TYPE_MAP_LIST`)
uint16_t unused;
uint32_t size; // 该数据项的数量
uint32_t offset; // 该数据项的偏移量
};

遍历的是DexMapItem结构,每个大小为0xC,然后判断类型是否为”TYPE_MAP_LIST”。也就是item_type === 4096这一行。这个类型里的offset表示map_list本身的偏移地址。

所以把之前获取的和TYPE_MAP_LIST里的对比下就能判断是否为完整DEX文件了。

image-20250205184720794

deepsearch部分,检测70 00 00 00

在 DEX 文件头(Header)中,**偏移 0x0C 处存储了 header_size**,它表示 DEX 头部的大小,永远是0x70 (112)

并且还考虑了magic字段被抹去的情况dex_base.readCString(4) != "dex\n",还多了一个verify_ids_off

image-20250205184807830

这里是dex文件里各种重要字段的偏移,要比dex文件本身小,比文件头大(0x70)

剩下的就不写了。

其他方法

假如打乱了内存里dex文件的内容,这该怎么办?frida-dexdump肯定就没辙了。在App的启动流程里,DexFile数据结构是一个重要的角色,不管怎么加密解密,随便折腾,最后都要经过这个数据结构。所以hook dexfile相关的函数,也可以实现脱壳。

ida打开libart.so,看雪里的文章中挑选的是这个函数。

image-20250205194643722

第一个参数就是Dexfile,ida应该是自动处理了?得找到原来的名字。

image-20250205194903640

鼠标放上面,按X查看引用下,然后copy出来即可。

frida脚本:

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
function hook() {
var libartModule = Process.getModuleByName("libart.so"); // 获取 libart.so 模块
var hookName = "_ZN3art11ClassLinker10SetupClassERKNS_7DexFileERKNS_3dex8ClassDefENS_6HandleINS_6mirror5ClassEEENS_6ObjPtrINS9_11ClassLoaderEEE"; // 要查找的函数名

// 遍历模块的符号(包括导出的函数和变量)
var addr = libartModule.getExportByName(hookName)
if(addr != null) {
Interceptor.attach(addr, {
onEnter: function(args) {
var dexFileAddr = args[1]
console.log("dexFileAddr:" + dexFileAddr)
var begin = ptr(dexFileAddr).add(Process.pointerSize).readPointer()
var size = ptr(dexFileAddr).add(Process.pointerSize * 2).readU32()
save(begin, size)
}
})
}
}

function save(begin, size) {
var path = '/storage/emulated/0/Download/'+size+'.dex'
var file = new File(path, 'wb')
file.write(ptr(begin).readByteArray(size))
file.flush()
file.close()
console.log("dump success!")
}

function main() {
hook()
}

setImmediate(main)

frida -f -U com.zj.wuaipojie -l unpack.js

运行成功后pull出来

image-20250205195144404

成功拿到源码!

image-20250205195218166

思考总结

假如,假如说,直接重写DexFile的加载,这该怎么办?也看了一些其他文章,说目前最新的技术是啥vmp保护。总之要走的路还很长,慢慢学吧。


脱壳入门
http://example.com/2025/02/05/脱壳入门/
Aŭtoro
zhattatey
Postigita
February 5, 2025
Lizenta