0%

【逆向神器Frida】第一章 潜龙勿用

frida框架使用js语言编写脚本,因此只要把js运行所需要的v8引擎在目标进程中启动, 就可以运行frida插件了.

frida具体使用步骤如下:

1. 在github上下载frida作者发布的引擎:

1
2
3
4
5
6
7
8
9
frida github地址: https://github.com/frida/frida

frida 官网地址: https://frida.re

frida提供的js api接口文档地址: https://frida.re/docs/javascript-api/

linux平台下载地址:https://github.com/frida/frida/releases/download/12.11.13/frida-gadget-12.11.13-linux-x86.so.xz

windows平台下载地址:https://github.com/frida/frida/releases/download/15.1.27/frida-gadget-15.1.27-windows-x86.dll.xz

解压后, 将12.11.13/frida-gadget-12.11.13-linux-x86.so文件放到df_game_r目录下,将frida-gadget-15.1.27-windows-x86.dll文件放到游戏目录下

2. 在目标进程中启动frida引擎:

将so或dll注入目标进程后frida就自动启动了。 注入的方式有很多种, 这里使用比较常用的方式:
linux平台: 修改run脚本, 添加LD_PRELOAD启动df_game_r, 即:

1
LD_PRELOAD=./frida-gadget-12.11.13-linux-x86.so ./df_game_r siroco11 start &

windows平台, 将frida-gadget-15.1.27-windows-x86.dll放到游戏目录下, 使用补丁大合集自带的注入dll功能在游戏进程启动时注入.

3. 新建配置文件, 设置需要执行的js脚本路径:

在frida引擎同级目录下, 新建一个同名的后缀为.config文件(.config文件必须与so文件同名) :

linux平台: 在frida-gadget-12.11.13-linux-x86.so目录下新建 frida-gadget-12.11.13-linux-x86.config 文件, 内容如下:

1
2
3
4
5
6
7
{
"interaction": {
"type": "script",
"path": "server_helper.js",
"on_change": "reload"
}
}

path为插件脚本文件路径, 默认为so的同级目录相对路径, 也可以填写绝对路径. server_helper.js为脚本名, 可以任意命名.

on_change表示js脚本文件发生改变时自动热重载脚本.

windows平台: 在frida-gadget-15.1.27-windows-x86.dll目录下新建frida-gadget-15.1.27-windows-x86.config文件, 内容如下:

1
2
3
4
5
6
7
{
"interaction": {
"type": "script",
"path": "client_helper.js",
"on_change": "reload"
}
}

4. 编写js脚本:

在config文件中指定的位置创建服务器插件脚本server_helper.js, 内容如下:

1
2
3
4
5
6
7
8
9
10
rpc.exports = {
//脚本加载时执行
init: function (stage, parameters) {
console.log('[init] stage=' + stage + ', parameters=' + JSON.stringify(parameters));
},
//脚本卸载时执行
dispose: function () {
console.log('[dispose]');
}
};

客户端目录下新建client_helper.js, 内容相同.

启动服务器, 登录游戏. 不出意外的话, 可以在服务器控制台看到init日志打印.
init为插件入口函数, js插件加载时自动执行.
dispose为卸载插件时执行的最后一个函数.重新保存server_helper.js, 服务器控制台可以看到dispose日志和热重载脚本后的init日志打印.

完成以上步骤, 就可以开始用js脚本写插件了.

因为js脚本运行在js的虚拟机里, 所以js插件想穿透虚拟机实现游戏中的功能就必须使用frida提供的三个接口:

1、读写任意内存: js通过NativePointer类可以操作内存, 除了读写内存该类提供了很多实用的接口, 包括修改内存保护属性, 特征码搜索, 从内存中读取字符串等.

2、hook任意函数: js通过Interceptor.attach接口可以实现对汇编的hook. 使用该接口时不必考虑32位或者64位程序, 也不必考虑被hook函数的调用约定, 甚至可以在函数内部进行hook.

通过Interceptor.attach可以拿到函数的入参和返回值, 并可以对函数返回值进行修改.

此外还可以使用更高级的Interceptor.replace接口, 该接口可以重写被hook的函数, 彻底改变程序执行流程.

3、执行任意函数: js通过NativeFunction类, 可以在js脚本内执行游戏进程内的所有C函数.

5. frida插件的写法.

下面通过几个小功能说明一下frida插件的写法

1.每日登录奖励/离线奖励(服务端插件):

需求: 玩家每日首次登录时, 根据自身等级获取一定数量的道具

玩家离线后再次上线, 根据离线时长获取离线经验奖励和离线金币奖励.

分析: 编写服务器插件要寻找合适的游戏事件处理函数作为Hook点, 在对应事件触发时再执行插件逻辑, 可以使对服务器的原有逻辑影响最小, 降低bug发生的可能.

游戏每日为玩家重置疲劳时会同时发送特殊副本的门票, hook这里可以保证玩家每日首次登录时触发, 同时也可以保证玩家在线时也能触发.

离线奖励的发送, 可以选择玩家选择角色时的函数进行hook. 注意离线奖励的上下限处理, 避免出现bug.

本例中将展示如何调用服务器函数完成需求, 插件代码如下:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
//获取角色名字
var CUserCharacInfo_getCurCharacName = new NativeFunction(ptr(0x8101028), 'pointer', ['pointer'], {"abi":"sysv"});
//给角色发消息
var CUser_SendNotiPacketMessage = new NativeFunction(ptr(0x86886CE), 'int', ['pointer', 'pointer', 'int'], {"abi":"sysv"});
//获取角色上次退出游戏时间
var CUserCharacInfo_getCurCharacLastPlayTick = new NativeFunction(ptr(0x82A66AA), 'int', ['pointer'], {"abi":"sysv"});
//获取角色等级
var CUserCharacInfo_get_charac_level = new NativeFunction(ptr(0x80DA2B8), 'int', ['pointer'], {"abi":"sysv"});
//获取角色当前等级升级所需经验
var CUserCharacInfo_get_level_up_exp = new NativeFunction(ptr(0x0864E3BA), 'int', ['pointer', 'int'], {"abi":"sysv"});
//角色增加经验
var CUser_gain_exp_sp = new NativeFunction(ptr(0x866A3FE), 'int', ['pointer', 'int', 'pointer', 'pointer', 'int', 'int', 'int'], {"abi":"sysv"});
//发送道具
var CUser_AddItem = new NativeFunction(ptr(0x867B6D4), 'int', ['pointer', 'int', 'int', 'int', 'pointer', 'int'], {"abi":"sysv"});
//获取角色背包
var CUserCharacInfo_getCurCharacInvenW = new NativeFunction(ptr(0x80DA28E), 'pointer', ['pointer'], {"abi":"sysv"});


//减少金币
var CInventory_use_money = new NativeFunction(ptr(0x84FF54C), 'int', ['pointer', 'int', 'int', 'int'], {"abi":"sysv"});
//增加金币
var CInventory_gain_money = new NativeFunction(ptr(0x84FF29C), 'int', ['pointer', 'int', 'int', 'int', 'int'], {"abi":"sysv"});
//通知客户端道具更新(客户端指针, 通知方式[仅客户端=1, 世界广播=0, 小队=2, war room=3], itemSpace[装备=0, 时装=1], 道具所在的背包槽)
var CUser_SendUpdateItemList = new NativeFunction(ptr(0x867C65A), 'int', ['pointer', 'int', 'int', 'int'], {"abi":"sysv"});

//获取系统时间
var CSystemTime_getCurSec = new NativeFunction(ptr(0x80CBC9E), 'int', ['pointer'], {"abi":"sysv"});
var GlobalData_s_systemTime_ = ptr(0x941F714);

//获取系统UTC时间(秒)
function api_CSystemTime_getCurSec()
{
return GlobalData_s_systemTime_.readInt();
}

//给角色发经验
function api_CUser_gain_exp_sp(user, exp)
{
var a2 = Memory.alloc(4);
var a3 = Memory.alloc(4);
CUser_gain_exp_sp(user, exp, a2, a3, 0, 0, 0);
}

//给角色发道具
function api_CUser_AddItem(user, item_id, item_cnt)
{
var item_space = Memory.alloc(4);
var slot = CUser_AddItem(user, item_id, item_cnt, 6, item_space, 0);

if(slot >= 0)
{
//通知客户端有游戏道具更新
CUser_SendUpdateItemList(user, 1, item_space.readInt(), slot);
}

return;
}

//获取角色名字
function api_CUserCharacInfo_getCurCharacName(user)
{
var p = CUserCharacInfo_getCurCharacName(user);
if(p.isNull())
{
return '';
}

return p.readUtf8String(-1);
}

//给角色发消息
function api_CUser_SendNotiPacketMessage(user, msg, msg_type)
{
var p = Memory.allocUtf8String(msg);
CUser_SendNotiPacketMessage(user, p, msg_type);

return;
}

//发送离线奖励
function send_offline_reward(user)
{
//当前系统时间
var cur_time = api_CSystemTime_getCurSec();

//用户上次退出游戏时间
var user_last_play_time = CUserCharacInfo_getCurCharacLastPlayTick(user);

//新创建的角色首次登陆user_last_play_time为0
if(user_last_play_time > 0)
{
//离线时长(分钟)
var diff_time = (cur_time - user_last_play_time) / 60;

//离线10min后开始计算
if(diff_time < 10)
return;

//离线奖励最多发送3天
if(diff_time > 3*24*60)
diff_time = 3*24*60;

//经验奖励: 每分钟当前等级经验的0.2%
var REWARD_EXP_PER_MIN = 0.002;
//金币奖励: 每分钟当前等级*100
var REWARD_GOLD_PER_MIN = 100;

//计算奖励
var cur_level = CUserCharacInfo_get_charac_level(user);
var reward_exp = Math.floor(CUserCharacInfo_get_level_up_exp(user, cur_level) * REWARD_EXP_PER_MIN * diff_time);
var reward_gold = Math.floor(cur_level * REWARD_GOLD_PER_MIN * diff_time);

//发经验
api_CUser_gain_exp_sp(user, reward_exp);
//发金币
CInventory_gain_money(CUserCharacInfo_getCurCharacInvenW(user), reward_gold, 0, 0, 0);
//通知客户端有游戏道具更新
CUser_SendUpdateItemList(user, 1, 0, 0);

//发消息通知客户端奖励已发送
api_CUser_SendNotiPacketMessage(user, '离线奖励已发送(经验奖励:' + reward_exp + ', 金币奖励:' + reward_gold + ')', 6);
}
}

//发送每日首次登陆奖励
function send_first_login_reward(user)
{
//奖励道具列表(道具id, 每级奖励数量)
var REWARD_LIST = [[8, 0.1], [3037, 10]];

//获取玩家登录
var cur_level = CUserCharacInfo_get_charac_level(user);
for(var i=0; i<REWARD_LIST.length; i++)
{
//道具id
var reward_item_id = REWARD_LIST[i][0];
//道具数量
var reward_item_cnt = 1 + Math.floor((cur_level * REWARD_LIST[i][1]));

//发送道具到玩家背包
api_CUser_AddItem(user, reward_item_id, reward_item_cnt);
}
}

//角色登入登出处理
function hook_user_inout_game_world()
{
//选择角色处理函数 Hook GameWorld::reach_game_world
Interceptor.attach(ptr(0x86C4E50), {
//函数入口, 拿到函数参数args
onEnter: function (args) {
//保存函数参数
this.user = args[1];

//console.log('[GameWorld::reach_game_world] this.user=' + this.user);
},
//原函数执行完毕, 这里可以得到并修改返回值retval
onLeave: function (retval) {
//给角色发消息问候
api_CUser_SendNotiPacketMessage(this.user, 'Hello ' + api_CUserCharacInfo_getCurCharacName(this.user), 2);

//离线奖励处理
send_offline_reward(this.user);
}
});

//角色退出时处理函数 Hook GameWorld::leave_game_world
Interceptor.attach(ptr(0x86C5288), {

onEnter: function (args) {

var user = args[1];

//console.log('[GameWorld::leave_game_world] user=' + user);
},
onLeave: function (retval) {
}
});
}

//角色每日首次登录奖励
function hook_user_first_login()
{
//角色每日重置处理函数 Hook CUser::AddDailyItem
Interceptor.attach(ptr(0x8656CAA), {

onEnter: function (args) {
//保存函数参数
var user = args[0];

//console.log('[CUser::AddDailyItem] user=' + user);

//发送每日首次登陆奖励
send_first_login_reward(user);
},
onLeave: function (retval) {
}
});
}

rpc.exports = {
init: function (stage, parameters) {
console.log('[init]');

//角色登入登出处理
hook_user_inout_game_world();

//每日首次登录处理
hook_user_first_login();
},
dispose: function () {

console.log('[dispose]');
}
};

这个功能只需要215行代码, 而且其中有一半的代码是在定义api函数, 这些定义的api函数是可以被重复使用的. 如果一次性将需要的所有api全部定义, 后面再编写插件时就只需要写少量的逻辑代码了.

2. 处理聊天信息, 定制GM命令(服务端插件):

需求: 玩家在客户端发送以”//“开头的游戏命令, 服务器收到消息后进行对应的逻辑处理.

分析: 在服务器端, 处理正常的聊天信息和处理以”//“开头的GM命令的函数是分开的, 这里hook处理GM命令的函数, 实现定制GM命令.

本例中将展示如何读取客户端上传的明文封包. 插件代码如下(重复的代码将不再展示, 下面是实现此需求所需的代码):

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//从客户端封包中读取数据
var PacketBuf_get_byte = new NativeFunction(ptr(0x858CF22), 'int', ['pointer', 'pointer'], {"abi":"sysv"});
var PacketBuf_get_short = new NativeFunction(ptr(0x858CFC0), 'int', ['pointer', 'pointer'], {"abi":"sysv"});
var PacketBuf_get_int = new NativeFunction(ptr(0x858D27E), 'int', ['pointer', 'pointer'], {"abi":"sysv"});
var PacketBuf_get_binary = new NativeFunction(ptr(0x858D3B2), 'int', ['pointer', 'pointer', 'int'], {"abi":"sysv"});

//服务器组包
var PacketGuard_PacketGuard = new NativeFunction(ptr(0x858DD4C), 'int', ['pointer'], {"abi":"sysv"});
var InterfacePacketBuf_put_header = new NativeFunction(ptr(0x80CB8FC), 'int', ['pointer', 'int', 'int'], {"abi":"sysv"});
var InterfacePacketBuf_put_byte = new NativeFunction(ptr(0x80CB920), 'int', ['pointer', 'uint8'], {"abi":"sysv"});
var InterfacePacketBuf_put_short = new NativeFunction(ptr(0x80D9EA4), 'int', ['pointer', 'uint16'], {"abi":"sysv"});
var InterfacePacketBuf_put_int = new NativeFunction(ptr(0x80CB93C), 'int', ['pointer', 'int'], {"abi":"sysv"});
var InterfacePacketBuf_put_binary = new NativeFunction(ptr(0x811DF08), 'int', ['pointer', 'pointer', 'int'], {"abi":"sysv"});
var InterfacePacketBuf_finalize = new NativeFunction(ptr(0x80CB958), 'int', ['pointer', 'int'], {"abi":"sysv"});
var Destroy_PacketGuard_PacketGuard = new NativeFunction(ptr(0x858DE80), 'int', ['pointer'], {"abi":"sysv"});

//从客户端封包中读取数据(失败会抛异常, 调用方必须做异常处理)
function api_PacketBuf_get_byte(packet_buf)
{
var data = Memory.alloc(1);

if(PacketBuf_get_byte(packet_buf, data))
{
return data.readU8();
}

throw new Error('PacketBuf_get_byte Fail!');
}
function api_PacketBuf_get_short(packet_buf)
{
var data = Memory.alloc(2);

if(PacketBuf_get_short(packet_buf, data))
{
return data.readShort();
}

throw new Error('PacketBuf_get_short Fail!');
}
function api_PacketBuf_get_int(packet_buf)
{
var data = Memory.alloc(4);

if(PacketBuf_get_int(packet_buf, data))
{
return data.readInt();
}


throw new Error('PacketBuf_get_int Fail!');
}
function api_PacketBuf_get_binary(packet_buf, len)
{
var data = Memory.alloc(len);

if(PacketBuf_get_binary(packet_buf, data, len))
{
return data.readByteArray(len);
}

throw new Error('PacketBuf_get_binary Fail!');
}

//获取原始封包数据
function api_PacketBuf_get_buf(packet_buf)
{
return packet_buf.add(20).readPointer().add(13);
}

//获取GameWorld实例
var G_GameWorld = new NativeFunction(ptr(0x80DA3A7), 'pointer', [], {"abi":"sysv"});
//根据server_id查找user
var GameWorld_find_from_world = new NativeFunction(ptr(0x86C4B9C), 'pointer', ['pointer', 'int'], {"abi":"sysv"});
//城镇瞬移
var GameWorld_move_area = new NativeFunction(ptr(0x86C5A84), 'pointer', ['pointer', 'pointer', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int', 'int'], {"abi":"sysv"});


//处理GM信息
function hook_gm_command()
{
//HOOK Dispatcher_New_Gmdebug_Command::dispatch_sig
Interceptor.attach(ptr(0x820BBDE), {

onEnter: function (args) {

//获取原始封包数据
var raw_packet_buf = api_PacketBuf_get_buf(args[2]);

//解析GM DEBUG命令
var msg_len = raw_packet_buf.readInt();
var msg = raw_packet_buf.add(4).readUtf8String(msg_len);

var user = args[1];


console.log('收到GM_DEBUG消息: ['+ api_CUserCharacInfo_getCurCharacName(user) + '] ' + msg);

//去除命令开头的 '//'
msg = msg.slice(2);

if(msg == 'test')
{
//向客户端发送消息
api_CUser_SendNotiPacketMessage(user, '这是一条测试命令', 1);

//执行一些测试代码

return;
}
else if(msg.indexOf('move ') == 0)
{
//城镇瞬移
var msg_group = msg.split(' ');
if(msg_group.length == 5)
{
var village = parseInt(msg_group[1]);
var area = parseInt(msg_group[2]);
var pos_x = parseInt(msg_group[3]);
var pos_y = parseInt(msg_group[4]);
GameWorld_move_area(G_GameWorld(), user, village, area, pos_x, pos_y, 0, 0, 0, 0, 0);
}
else
{
api_CUser_SendNotiPacketMessage(user, '格式错误. 使用示例: //move 2 1 100 100', 2);
}
}
},
onLeave: function (retval) {
}
});
}

GM命令作为客户端与服务器通信的桥梁, 从上面代码可以看到服务器可以从GM命令中解析出客户端上传的参数, 根据这些参数可以在服务器扩展客户端无法实现的功能.

3.赛利亚房间玩家互相可见:

需求: 玩家进入赛丽亚旅馆时, 可以看见在同一旅馆内的其他玩家(服务器插件+客户端插件)

分析:一个服务器中每个区域的赛利亚房间是玩家共享的, 之所以玩家间互相看不见, 是因为服务器没有把玩家进出赛利亚区域的消息广播出去. 通过服务器插件可以实现进入赛利亚房间后看到其他玩家.

但是仅在服务端做插件, 是看不到其他玩家在赛利亚房间的移动的, 因为客户端屏蔽了在赛利亚房间移动时向服务器发送的消息. 因此还需要编写客户端插件, 移除限制.

服务端插件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//允许赛利亚房间的人互相可见
function share_seria_room()
{
//Hook Area::insert_user
Interceptor.attach(ptr(0x86C25A6), {

onEnter: function (args) {
//修改标志位, 让服务器广播赛利亚旅馆消息
args[0].add(0x68).writeInt(0);
},
onLeave: function (retval) {
}
});
}

客户端插件代码:

1
2
3
4
5
6
7
8
9
10
11
12
//允许赛利亚房间的人互相可见
function share_seria_room()
{
//patch地址 屏蔽客户端在赛利亚房间移动时不发送封包
//0110BBB6 - 83 78 24 01 - cmp dword ptr [eax+24],01 { 1 }
//0110BBBA - 74 32 - je 0110BBEE
var patch_addr = ptr(0x0110BBBA);
//修改内存可写
Memory.protect(patch_addr, 1024, 'rwx');
//写入nop nop
patch_addr.writeShort(0x9090);
}

本章插件实现的功能比较简单, 主要是熟悉插件的编写流程. 下一章将尝试稍微复杂的功能, 编写插件实现nut扩展以及时装镶嵌.
https://i0.hdslb.com/bfs/article/5203df7170578472bd71154755b575b207900753.gif