App解固脱壳方式
zsk Lv4

样例app下载链接

一、常见的壳

通常是看lib文件夹下so库特征,以下是市面上常见的不同厂商对APP的加固特征:
爱加密:libexec.so,libexecmain.so,ijiami.dat
梆梆: libsecexe.so,libsecmain.so , libDexHelper.so libSecShell.so
360:libprotectClass.so,libjiagu.so,libjiagu_art.so,libjiagu_x86.so
百度:libbaiduprotect.so
腾讯:libshellx-2.10.6.0.so,libBugly.so,libtup.so, libexec.so,libshell.so,stub_tengxun
网易易盾:libnesec.so
为什么要加固:
一定程度保护源代码
加固方式:
.dex加固 .so加固

二、加固原理

壳dex 读取源dex文件,加密后,写进一个新的dex文件

image


新APK运行
先加载壳APP—>壳APP读Dex文件末尾的源APKD大小—->在内存中壳APP解密出源APP—>运行源APP 壳APK有自己的Application对象
源APK有自己的Application对象
壳APK启动时 在AndroidMenifest.xml里找源APK的Application 执行它的oncreate方法 启动源APK
Dex文件格式: 任何类型的文件都有文件格式,对应的软件按照文件格式来就能解析出类型,.xml .json .jpeg

image

三、如何查看有没有壳

apktool对apk进行反编译,或者修改apk的后缀为zip,进行解压。查看解压后的文件下的lib目录,

image

可以看到这两个是用了腾讯的乐加固,或者我们不确定的话可以用jadx打开apk,也是可以发现的

image

都是壳的代码。不过即使怎么加壳,它都会去调源apk的Application,打开AndroidManifest.xml文件,

image

可以看到,上面一个是壳的,下面是源apk的Application。
逆向/脱壳方法
反编译/Hook技术和动态调试
Hook:先取得要Hook函数/方法的控制权,不用破坏程序
动态调试:反调试,汇编,计算内存地址

四、Hook技术

改变程序执行流程的一种技术 在函数被调用前,通过HOOK技术,先得到该函数的控制权,实现该函数的逻辑改写

image
Hook可以在在Java层、Native层(.so库)
在代码层 寻找要Hook的地方 进行Hook 改下代码逻辑

五、脱壳工具

脱壳原理:
在壳APK解密源APK后,源APK被加载前,拦截这个过程中的系统函数 把内存种Dex dump出来。

手动脱壳:
通过动态调试,跟踪计算Dex源文件的内存偏移地址,从内存中Dump出Dex文件 ,难度大,寄存器,汇编,反调试,反读写 IDA。

工具脱壳:
HOOK技术/内存特征寻找 简单易操作

基于xposed 脱壳工具
Fdex2:Hook ClassLoader loadClass方法
通用脱壳 dumpDex:https://github.com/WrBug/dumpDex
逆向框架: 筑好底层 提供开发接口
xposed(Java 编译,只能在java层)
frida(Python Javascript 代码注入,可以hook住java层、Native层)

六、开始脱壳(用FDex2脱壳)

首先我们打开xposed,把FDex2打开,并选择我们要的app

image

image
接下来就可以运行app,然后我们去/data/user/0/com.iCitySuzhou.suzhou001该目录下查看,多出来两个dex文件

image

把这两个文件拉到电脑用kadx打开,第一个是壳

image

打开第二个很明显看到这才是源apk的源码,而且可以发现它对源码进行混淆了

image

所以到这里我们的脱壳就成功了。

七、开始脱壳(用Frida脱壳)

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
function_address = Module.findExportByName(libname, function);
Interceptor.attach(address, func);
Interceptor.attach(address,
onEnter: function (args) {
},
onLeave: function (retval) {
}
)
File 模块 写文件流程
new File(filepath, mode)
write(data)
flush()
close()
file = new File("yuanrenxue.dex", "wb")
//data 是字符串或者 arrayBuffer // readByteArray() 返回的arrayBuffer
file.write(data)
file.flush()
file.close()
//把内存里的值转成字符串
Memory.readUtf8String()
//把内存里的值转换成整型
Memory.readInt()
//以begin为起始位置,从内存中读length长度的数据出来 返回ArrayBuffer类型
Memory.readByteArray(begin, length)
//把地址转换成NativePointer类型 frida里操作内存地址需要NativePointer类型
ptr()
JS api
#把其它进制转换成10进制
parseInt(num, radix)

加壳apk运行流程:app启动后–>壳dex先加载起来–>把源classes.dex读出来–>解密源classes.dex–>把源classes.dex给加载进内存–>源dex运行起来
下两篇文章都对dex进行了详解
Dex文件格式详解 https://www.jianshu.com/p/f7f0a712ddfe
ART 加载dex文件 https://www.jianshu.com/p/f81242ad8cb7

dex文件结构

数据名称 解释
header dex文件头部,记录整个dex文件的相关属性
string_ids 字符串数据索引,记录了每个字符串在数据区的偏移量
type_ids 类似数据索引,记录了每个类型的字符串索引
proto_ids 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表
field_ids 字段数据索引,记录了所属类,类型以及方法名
method_ids 类方法索引,记录方法所属类名,方法声明以及方法名等信息
class_defs 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量
data 数据区,保存了各个类的真是数据
link_data 连接数据区

简单记录了dex文件的一些基本信息,以及大致的数据分布。长度固定为0x70,其中每一项信息所占用的内存空间也是固定的,好处是虚拟机在处理dex时不用考虑dex文件的多样性

字段名称 偏移值 长度 说明
magic 0x0 8 魔数字段,值为”dex\
035\0”(固定的)
checksum 0x8 4 校验码
signature 0xc 20 sha-1签名
file_size 0x20 4 dex文件总长度
…… …… ….. ……

字段太多就不都展示出来,可以看到file_size这个就是我们要找的dex文件,因为源dex解密后会加载进内存,所以我们去Hook加载Dex的函数,把Dex从内存中dump出来。
下面这个函数就是把解密后的源dex加载进内存:
DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,//nullptr const OatDexFile* oat_dex_file,
std::string* error_msg)
OpenMemory()是在安卓系统/system/lib/libart.so里面,然后我们先把这个so文件拉到电脑用IDA打开

image

打开IDA选择静态调试,打开libart.so这个文件


image
image

这些就是so文件里面的函数,点击这个框,按ctrl+f进行搜索,输入OpenMemory这个函数,右键进行编辑,
_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_
这个字符串就是OpenMemory函数在内存中对外的方法名,我们打开IDA就是为了找这个方法名。

image

现在来写脚本:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import frida
import sys

package = "com.iCitySuzhou.suzhou001"

open_memory_6 = "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_"
#OpenMemory 在libart.so中 art虚拟机(安卓5) davlink虚拟机(安卓4)
#Hook OpenMemory的导出方法名
#用IDA 打开libart.so 查看OpenMemory的导出方法名
#OpenMemory的第一个参数是dex文件在内存中的起始位置
#根据dex文件格式 从起始位置开始 第32个字节 是该dex文件的大小
#知道dex起始位置和整个文件大小,只需要把这段内存dum出来即可
#适用于 安卓 6 7 8 9

#文件的起始位置 文件的大小 知道了文件的结束位置



def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)


src = """
//找so文件某个方法地址,找openMemory的内存地址
var openMemory_address = Module.findExportByName("libart.so", "_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_");
send('openMemory address:'+openMemory_address)

//hook openMemory地址
Interceptor.attach(openMemory_address, {
//一进入openMemory,就会调用onEnter方法
onEnter: function (args) {
//dex文件的起始位置
var dex_begin_address = args[1]

//dex文件的前8个字节是magic字段 看dex的文件格式说明
//打印magic(会显示 "dex 035") 三个字符 可以验证是否为dex文件
console.log("magic : " + Memory.readUtf8String(dex_begin_address))

//把地址转换成整型(十进制) 再加32
//因为dex文件的第32个字节处存放的是 dex文件的大小
var address = parseInt(dex_begin_address, 16) + 0x20

//把address地址指向的内存值读出来 该值就是dex的文件大小
//ptr(address)转换的原因是 frida只接受 NativePointer类型指针
var dex_size = Memory.readInt(ptr(address))
console.log("dex_size :" + dex_size)

//frida写文件 把内存中的数据 写到本地
var timestamp = new Date().getTime();
var file = new File("/data/data/%s/" + timestamp + ".dex", "wb")

//Memory.readByteArray(begin, length)
//把内存里的数据读出来,从begin开始读,取length长度
file.write(Memory.readByteArray(dex_begin_address, dex_size))
file.flush()
file.close()

send("dex begin address:"+parseInt(dex_begin_address,16))
send("dex file size:"+dex_size)
},
onLeave: function (retval) {
if (retval.toInt32() > 0) {
}
}
});
"""%(package)


print("dex 导出目录为: /data/data/%s"%(package))
device = frida.get_usb_device()
pid = device.spawn(package)
session = device.attach(pid)
script = session.create_script(src)
script.on("message" , on_message)
script.load()
device.resume(pid)
sys.stdin.read()

把frida-server运行起来
image

 

hook需要app运行,先把app打开,再运行脚本,

image

我们去手机/data/data/com.iCitySuzhou.suzhou001目录下查看,会多了几个dex文件

image

把这些dex文件拉到电脑用jadx打开看下,一个个查看后,找到了源dex

image

到这里用frida脱壳也成功了。。。

补充

壳的种类⾮常多,根据其种类不同,使⽤的技术也不同,这⾥稍微简单分个类:

  • ⼀代整体型壳:采⽤ Dex 整体加密,动态加载运⾏的机制;
  • ⼆代函数抽取型壳:粒度更细,将⽅法单独抽取出来,加密保存,解密执⾏;
  • 三代 VMP、Dex2C 壳:独⽴虚拟机解释执⾏、语义等价语法迁移,强度最⾼。

先说最难的 Dex2C ⽬前是没有办法还原的,只能跟踪进⾏分析; VMP 虚拟机解释执⾏保护的是映射表,只要⼼思细、功夫深,是可以将映射表还原的;

⼆代壳函数抽取⽬前是可以从根本上进⾏还原的, dump 出所有的运⾏时的⽅法体,填充到 dump 下来的 dex 中去的,这也是fart的核⼼原理。

frida-Dexdump

https://github.com/hluwa/FRIDA-DEXDump

利⽤ frida 的搜索内存,通过匹配 DEX ⽂件的特征,例如 DEX ⽂件的 文件头 中的 magic — dex.035 这个特征。 frida-Dexdump 便是这种脱壳⽅法的代表作。

  • 对于于完整的 dex,采⽤暴⼒搜索 dex035 即可找到。
  • ⽽对于抹头的 dex,通过匹配⼀些特征来找到,然后⾃动修复⽂件头。

抽取 -> invoke -> 还原 -> 再抽取

 评论