作者简介:叶绍琛,网络安全攻防技术专家,极牛网技术委员会主席,CTFWAR网络安全攻防对抗联赛发起人,《网安观察》社群期刊总顾问,大中华区前50位RHCA系统架构师,曾任网易互娱云计算平台技术负责人,曾获国家科技部和教育部联合颁发的教育信息化发明创新奖。
Frida技术原理
Android脱壳的目的是从内存中dump下解密的应用dex文件,为了实现这个目的我们需要知道dex文件在内存中dex地址与dex文件大小。android系统的libart.so库文件中提供了一个导出的OpenMemory函数用来加载dex文件。
std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file,
std::string* error_msg) {
CHECK_ALIGNED(base, 4); // various dex file structures must be word aligned
std::unique_ptr<DexFile> dex_file(
new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
if (!dex_file->Init(error_msg)) {
dex_file.reset();
}
return std::unique_ptr<const DexFile>(dex_file.release());
}
这个函数的第一个参数指向了内存中的dex文件,如果我们可以Hook 这个函数,就可以得到dex文件加载进内存时的起始地址,再根据dex文件格式计算得到文件头保存的dex文件的长度fileSize。
获取函数签名
在编写Frida脚本之前我们需要在libart.so文件中找到OpenMemory的函数签名,这个签名根据android版本或者架构的不同会有些许差异,下面介绍如何从运行待脱壳应用的设备中提取libart.so文件:
$ adb pull /system/lib/libart.so /本地目录
$ adb pull /system/lib64/libart.so /本地目录
获取libart.so文件后有两个方法可以获得函数签名,第一种是利用IDA pro 分析so文件,搜索OpenMemory函数:
第二种方法实在linux系统下使用nm命令直接查看libart.so文件的函数签名:
$ nm libart.so | grep OpenMemory
编写脚本
Frida可以执行js脚本也可以执行python脚本:
· JS脚本:
‘use strict’;
Interceptor.attach(Module.findExportByName(“libart.so”, “_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_”), {
onEnter: function (args) {
//dex起始位置
var begin = args[1]
//打印dex文件头的魔数
console.log(“magic : ” + Memory.readUtf8String(begin))
//dex fileSize 属性的地址
var file_size_address = parseInt(begin,16) + 0x20
//dex 大小
var dex_size = Memory.readInt(ptr(file_size_address))
console.log(“dex_size :” + dex_size)
//将dex文件保存在sdcard下的unpack文件夹下
var file = new File(“/sdcard/unpack” + dex_size + “.dex”, “wb”)
file.write(Memory.readByteArray(begin, dex_size))
file.flush()
file.close()
},
onLeave: function (retval) {
if (retval.toInt32() > 0) {
/* do something */
}
}
});
· Python脚本:
import frida
import sysdef on_message(message, data):
base = message[‘payload’][‘base’]
size = int(message[‘payload’][‘size’])
print hex(base),size
package = sys.argv[1]
print “dex 导出目录为: /data/data/%s”%(package)
device = frida.get_usb_device()
pid = device.spawn(package)
session = device.attach(pid)
src = “””
Interceptor.attach(Module.findExportByName(“libart.so”, “_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_”), {
onEnter: function (args) {
//dex起始位置
var begin = args[1]
//打印dex文件头的魔数
console.log(“magic : ” + Memory.readUtf8String(begin))
//dex fileSize 属性的地址
var file_size_address = parseInt(begin,16) + 0x20
//dex 大小
var dex_size = Memory.readInt(ptr(file_size_address))
console.log(“dex_size :” + dex_size)
//将dex文件保存在sdcard下的unpack文件夹下
var file = new File(“/sdcard/unpack” + dex_size + “.dex”, “wb”)
file.write(Memory.readByteArray(begin, dex_size))
file.flush()
file.close()
},
onLeave: function (retval) {
if (retval.toInt32() > 0) {
/* do something */
}
}
});
“””%(package)
script = session.create_script(src)
script.on(“message” , on_message)
script.load()
device.resume(pid)
sys.stdin.read()
代码参考 https://github.com/dstmath/frida-unpack
准备Frida 环境
安装 frida客户端与服务端,建议使用Python的pip命令安装frida客户端:
$ pip install frida
$ pip install frida-tools
下载完毕后查看下载的客户端版本:
$ frida –version
根据客户端版本与设备CPU架构去github上下载对应的服务端:
客户端与服务端都下载完毕后,先把frida-server推送到设备中:
$ adb push frida-server /data/local/tmp
设置frida-server的权限并运行:
$ su
# cd /data/local/tmp
# chmod 777 frida-server
# ./frida-server
如果运行失败,可以尝试关闭android系统的SELinux:
# setenforce 0
我们打开另一个控制台,运行客户端命令查看与服务端的交互:
$ frida-ps -U
出现以下信息,则说明交互成功:
执行脱壳脚本
我们先将一个简单的测试程序丢到腾讯乐固上去加固,我们可以看到加固完成后的结果:
可以看到加固过后程序的原本代码被隐藏了,被替换成乐固的壳程序代码,将加固后的应用安装到手机上,赋予该应用读写sd卡权限:
在sd卡中创建unpack目录,执行命令运行JS脚本:
$ frida -U -f <被脱壳应用的包名> -l <js脚本名> –no-pause
-U参数表示我们使用了USB server进行链接,-f 参数表示在设备中启动一个指定的android程序,需要配合 –no-pause参数来使进程恢复。-l参数表示需要注入的js脚本。
Python脚本的运行就简单得多了:
$ python unpack.py <应用包名>
另外注意一个问题,如果应用运行过程中使用了64位的libart.so,脚本的这个地方需要进行一些小修改:
//dex起始位置
var begin = args[1]
获取dex起始位置的语句需要改成:
var begin = this.context.x0
不然可能会出现这样的问题:
脱壳完成后从sd卡中将unpack文件夹pull到本地:
$ adb pull /sdcard/unpack .
可以看到脱壳成功了。由于这一类脱壳法是将内存中的dex文件直接dump下来的,对于指令抽取等针对方法体进行处理的加固方式并没有做对应的修复工作。如果应用经过了指令抽取或者dex2c处理,这种方法拿到的dex文件就是残缺不全的,对于那些加固就需要通过对Android源码进行修改的脱壳方法比如dexhunter或者是比较新的FART脱壳机。
极牛网精选文章《Android安全指南 之 Frida脱壳实战》文中所述为作者独立观点,不代表极牛网立场。如若转载请注明出处:https://geeknb.com/12665.html