Frida

基本概念

Frida 是一个用来向目标进程动态注入指令的工具,它使用 python 编写,因此可在多种操作系统中使用,例如 Windows, MacOS, Linux, Android, iOS 等等;

当要在 Android 上面使用时,需要先使用 root 权限运行 frida-server 进程,然后将手机通过 USB 线连接到电脑上,开启调试模式,之后就可以通过在 PC 端运行脚本实现预期效果(原理:PC 端脚本会发送指令给 frida-server 执行);

代码示例

函数注入

先用 c 语言快速定义一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// hello.c
#include <stdio.h>
#include <unistd.h>

// 函数 func 接受一个参数,并打印出该参数值
void func (int n) {
printf ("Number: %d\n", n);
}

int main (int argc, char * argv[]) {
int i = 0;
// 函数在编译成可执行文件后,都会有一个虚拟内存地址,此处将该地址打印出来,以方便进行 hook
printf ("func() is at %p\n", func);

while (1)
{
func (i++);
sleep (1);
}
}

然后调用编译踌躇,编译成可运行的程序,用来测试:

gcc -Wall hello.c -o hello

读取函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# hook.py

from __future__ import print_function
import frida
import sys

session = frida.attach("hello") # 绑定名称为 hello 的进程
# 通过 ptr 指针值绑定内存中指定的位置
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toInt32());
}
});
""" % int(sys.argv[1], 16))

def on_message(message, data):
print(message)

script.on('message', on_message)
script.load()
sys.stdin.read()

修改函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# modify.py

import frida
import sys

session = frida.attach("hello")
# 此处脚本通过绑定指定内存地址中的函数,当进入该函数时,就将参数列表中的第一个参数的值修改为 1337
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
args[0] = ptr("1337");
}
});
""" % int(sys.argv[1], 16))

script.load()
sys.stdin.read()

替换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# call.py

import frida
import sys

session = frida.attach("hello")
# 调用 NativeFunction 在指定位置自定义了一个新函数,当该位置的函数被调用时,就会触发自定义的函数
# NativeFunction 函数的第二个参数用来指定返回值的类型,此处为 void,表示没有返回值
script = session.create_script("""
var func = new NativeFunction(ptr("%s"), 'void', ['int']);
func(1911);
func(1911);
func(1911);
""" % int(sys.argv[1], 16))

script.load()

注入字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// hi.c 定义一个接受字符串参数的函数,并编译成可执行程序

#include <stdio.h>
#include <unistd.h>

int func(const char *s) {
printf("String: %s\n", s);
return 0;
}

int main(int argc, char* argv[]) {
const char* s = "Testing!";

printf("f() is at %p\n", func);
printf("s is at %p\n", s);

while(1) {
func(s);
sleep(1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from __future__ import print_function
import frida
import sys

session = frida.attach("hi")
# NativeFunction 函数的第二个参数用来指定返回值的类型,此处为 int
# 第三个参数是一个列表,用来存放输入类型
# 注意:在注入字符串时,传递给自定义函数的值是字符串的指针
# 注意:此处使用了 Memory.allocUtf8String 来创建自定义字符串,事实上,还有很多相关的方法可用
# 例如:Memory.alloc(), Memory.protect() 等
script = session.create_script("""
var st = Memory.allocUtf8String('TESTMEPLZ!');
var func = new NativeFunction(ptr("%s"), 'int', ['pointer']);
func(st);
""" % int(sys.argv[1], 16))


def on_message(message, data):
print(message)


script.on('message', on_message)
script.load()

注入对象

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
81
82
// client.c

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
int sock_fd, i, n;
struct sockaddr_in serv_addr;
unsigned char* b;
const char* message;
char recv_buf[1024];

if (argc != 2) {
fprintf(stderr, "Usage: %s<ip of server>\n", argv[0]);
return 1;
}

printf("connect() is at: %p\n", connect);

if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Unable to create socket");
return 1;
}

bzero(&serv_addr, sizeof(serv_addr));

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5000);

if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Unable to parse IP address\n");
return 1;
}

printf("\nHere's the serv_addr buffer:\n");
b = (unsigned char *) &serv_addr;
for (i = 0; i != sizeof(serv_addr); i++) {
printf("%s%02x", (i != 0) ? " " : "", b[i]);
}

printf("\n\nPress ENTER key to Continue\n");
while(getchar() == EOF && ferror(stdin) && errno == EINTR) {
;
}

if (connect(sock_fd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) < 0) {
perror("Unable to connnect");
return 1;
}

message = "Hello there!";
if (send(sock_fd, message, strlen(message), 0) < 0) {
perror("Unable to send");
return 1;
}

while(1) {
n = recv(sock_fd, recv_buf, sizeof(recv_buf) - 1, 0);
if (n == -1 && errno == EINTR) {
continue;
} else if (n <= 0) {
break;
}
recv_buf[n] = 0;
fputs(recv_buf, stdout);
}

if (n < 0) {
perror("Unable to read");
}

return 0;
}

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
from __future__ import print_function
import frida
import sys

session = frida.attach("client")
script = session.create_script("""
send('Allocating memory and writing bytes...');
var st = Memory.alloc(16);
st.writeByteArray([0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]);
Interceptor.attach(Module.getExportByName(null, 'connect'), {
onEnter: function(args) {
send('Injecting malicious byte array:');
args[1] = st;
},
// onLeave: function(retval) {
// retval.replace(0);
// }
});
""")


def on_message(message, data):
if message['type'] == 'error':
print("[!]" + message['stack'])
elif message['type'] == 'send':
print("[i]" + message['payload'])
else:
print(message)


script.on('message', on_message)
script.load()
sys.stdin.read()

注入指令

可以直接使用 js 编写注入函数

步骤

  • 运行 frida-server,以便 PC 端的指令可发送到手机端;方法:adb shell 连通手机 shell,运行 frida-server 可执行文件,按官网教程,路径为 /data/local/tmp/frida-server
  • 运行 frida-ls-devices,列出当前连接的设备,得到 ID 号;
  • 运行 frida-ps -Ua,列出当前运行的进程,得到进程名称;
  • 运行 frida -D -f -l