Frida的动态代码执行功能,主要是在它的核心引擎Gum中用C语言来实现的。
执行脚本:
import frida
session = frida.attach("cat")
print([x.name for x in session.enumerate_modules()])
输出
[u'cat', …, u'ld-2.15.so']
其中:
enumerate_modules() 函数枚举当前进程中所有加载的模块。
模块枚举
enumerate_modules()
枚举内存块
enumerate_ranges(mask)
enumerate_ranges() 的mask参数表示你要枚举的内存块的保护属性类型,支持 rwx,也可以直接填写 - 表示任意类型
执行:
print(s.enumerate_ranges('rw-'))
输出:
[Range(base_address=0x2d4160a06000, size=1019904, protection='rwx'), ...]
内存读写
read_bytes(address, n) 函数在目标进程中从 address 这个地址开始,连续读取 n 个字节。
write_bytes(address, n) 函数在目标进程中从 address这个地址开始,连续写入 n 个字节。
比如执行如下代码:
print(s.read_bytes(49758817247232, 10).encode("hex"))
执行完毕之后,应该能看到如下结果:
454c4602010100000000
执行如下代码:
s.write_bytes(49758817247232, "frida")
执行完毕之后, 相应的内存块的值会被更新成 frida 这个字符串的值
使用姿势
下面的内容分三点:
1.注入模式
方式:attach附加或hijack劫持
实际上,像
Interceptor.attach(Module.findExportByName(null, "connect")
就算是劫持了
注入模式的大致实现思路是这样的,带有GumJS的Frida核心引擎被打包成一个
动态连接库,然后把这个动态连接库注入到目标进程中,同时提供了一个双向通信通道,
这样你的控制端就可以和注入的模块进行通信了,在不需要的时候,
还可以在目标进程中把这个注入的模块给卸载掉。
除了上述功能,Frida还提供了枚举已经安装的App列表,
运行的进程列表已经已经连接的设备列表,这里所说的设备列表通常就是
frida-server 所在的设备。frida-server 是一个守护进程,
通过TCP和Frida核心引擎通信,默认的监听端口是27042。
2.嵌入模式
对于没有root的Android或没有越狱的IOS,frida-gadget
Frida提供了一个动态连接库组件 frida-gadget, 你可以把这个动态库集成到你程序里面
来使用Frida的动态执行功能。一旦你集成了gadget,你就可以和你的程序使用Frida进行交互,
并且使用 frida-trace 这样的功能,同时也支持从文件自动加载Js文件执行JS逻辑。
关于 Gadget 更多功能,请参考原文链接
https://www.frida.re/docs/modes
3.预加载模式
大家应该熟悉 LD_PRELOAD,或者 DYLDINSERTLIBRARIES吧,
如果有 JS_PRELOAD 这种东西是不是更爽了呢!事实上对于我们上面讨论的 Gadget 是
支持这种模式的, 这才是Gadget 的强大之处,通过这种模式你就可以达到自动加载Js文件
并执行的目的了。
调用程序
替换int
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
var f = new NativeFunction(ptr("%s"), 'void', ['int']);
f(1911);
f(1911);
f(1911);
""" % int(sys.argv[1], 16))
script.load()
执行输出:
Number: 1879
Number: 1911
Number: 1911
Number: 1911
Number: 1880
替换String
from __future__ import print_function
import frida
import sys
session = frida.attach("hi")
script = session.create_script("""
var st = Memory.allocUtf8String("TESTMEPLZ!");
var f = new NativeFunction(ptr("%s"), 'int', ['pointer']);
// In NativeFunction param 2 is the return value type,
// and param 3 is an array of input types
f(st);
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
使用类似的方法,比如 Memory.alloc()和Memory.protect()很容就能操作目标进程的内存,可以创建Python的 ctypes 和其他内存结构比如 structs,然后以字节数组的方式传递给目标函数。
下面我们来看这个脚本,这个脚本注入了一个指定格式的内存结构,然后劫持了libc.so中的 connect() 函数,在劫持的函数中用我们构造的结构体,替换 connect() 函数的第一个参数。创建文件 struct_mod.py,内容如下:
from __future__ import print_function
import frida
import sys
session = frida.attach("client")
script = session.create_script("""
// First, let's give ourselves a bit of memory to put our struct in:
send("Allocating memory and writing bytes...");
var st = Memory.alloc(16);
// Now we need to fill it - this is a bit blunt, but works...
Memory.writeByteArray(st,[0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]);
// Module.findExportByName() can find functions without knowing the source
// module, but it's slower, especially over large binaries! YMMV...
Interceptor.attach(Module.findExportByName(null, "connect"), {
onEnter: function(args) {
send("Injecting malicious byte array:");
args[1] = st;
}
//, onLeave: function(retval) {
// retval.replace(0); // Use this to manipulate the return value
//}
});
""")
这个脚本里同时还提到,我们可以使用 Module.findExportByName() 这个API来在目标进程中的指定模块中查找指定的导出函数,尤其是在比较大的可执行文件里面
接收消息
我们来演示一下向JavaScript中发送消息
from __future__ import print_function
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
recv('poke', function onMessage(pokeMessage) { send('pokeBack'); });
""")
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
script.post({"type": "poke"})
sys.stdin.read()
输出:
{u'type': u'send', u'payload': u'pokeBack'}
注意:
每一次recv的onMessage回调只能处理一条message。
recv方法是非阻塞的
在目标进程中以阻塞方式接收消息
from __future__ import print_function
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toString());
var op = recv('input', function(value) {
args[0] = ptr(value.payload);
});
op.wait();
}
});
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
val = int(message['payload'], 16)
script.post({'type': 'input', 'payload': str(val * 2)})
script.on('message', on_message)
script.load()
sys.stdin.read()
这里在on_message方法里面,首先将message[‘payload’]的值以16进制转换为整型int
然后再通过post方法将构建的这个json发送回frida服务端
在android终端执行时,通过recv方法读取这个json里的‘input’的值,用这个值替换Hook函数的形参
最后执行wait方法阻塞
在iOS上使用Frida
设置iOS设备
启动 Cydia 然后通过 Manage -> Sources -> Edit -> Add 这个操作步骤把 https://build.frida.re 这个代码仓库加入进去。然后你就可以在 Cydia 里面找到 Frida 的安装包了
冒烟测试,确认frida正常工作
frida-ps -U
这条命令枚举了进程列表
跟踪Twitter中的加密函数
frida-trace -U -i "CCCryptorCreate*" Twitter
输出:
Uploading data...
CCCryptorCreate: Auto-generated handler …/CCCryptorCreate.js
CCCryptorCreateFromData: Auto-generated handler …/CCCryptorCreateFromData.js
CCCryptorCreateWithMode: Auto-generated handler …/CCCryptorCreateWithMode.js
CCCryptorCreateFromDataWithMode: Auto-generated handler …/CCCryptorCreateFromDataWithMode.js
Started tracing 4 functions. Press Ctrl+C to stop.
目前, 很多App的加密、解密、哈希算法基本上都是使用 CCryptorCreate 和相关的一组加密函数。
没有越狱的iOS设备
为了让一个App能使用Frida,必须想办法让它加载一个 .dylib,就是一个 Gadget 模块。
即FridaGadget.dylib,让xCode集成frida
在Android上使用Frida
Frida本身是支持从4.2到6.0的版本的,但是目前来说对Art的支持还是有限的,所以我们建议最后还是用使用Dalvik虚拟机的系统设备或者模拟器来进行尝试。
跟踪Chrome里的Open()函数
$ frida-trace -U -i open com.android.chro
me
Uploading data...
open: Auto-generated handler …/linker/open.js
open: Auto-generated handler …/libc.so/open.js
Started tracing 2 functions. Press Ctrl+C to stop.