文章目录
  1. 1. 前言
  2. 2. 源码分析
  3. 3. 协议分析

前言

最近在玩一款叫做XXXX2的手游,是个休闲类的餐厅游戏.
这个游戏有个增强玩家之间互动的功能,每天都要去给别人浇水.
每天上百次的浇水次数,一个一个的去浇,就要花掉半小时到一小时.
嗨呀好气呀,这怎么能是一个程序员干的事呢.
于是决定写一个一键浇水的脚本,并记录之.

源码分析

拆包发现这居然是个libgdx的游戏,游戏逻辑在动态加载的一个jar包里,而且这个jar包没有加密和混淆,那么我们直接看代码就可以了解网络请求的协议了.
经过一些分析,发现游戏所有的请求都继承自一个Request类,而Request则有个方法通过反射读取所有的字段,
按照(字段名,字段类型,字段值)的格式,写进一个DataInputStream.
那么我们可以依照写出Ruby的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
TYPE_ARR = ['boolean', 'byte', 'short','int', 'float','long', 'double', 'string']
PACK_ARR = ['c', 'c', 's>', 'i>', 'g', 'l>', 'G', 'a*']
def convert(key,type,value)
if type == 'string'
type_idx = TYPE_ARR.index(type)
[key.size,key,type_idx,value.size,value].pack("s>A*cs>#{PACK_ARR[type_idx]}")
else
type_idx = TYPE_ARR.index(type)
[key.size,key,type_idx,value].pack("s>A*c#{PACK_ARR[type_idx]}")
end
end

需要注意的是Java的DataInputStream的writeUTF方法,前2个byte位是后面实际所写byte位的长度.
上面的方法中用key.size代替是因为这里所有的string都是英文,所以实际所占的byte位数就是字符串的长度.
其中short,int,long,float,double需要用network byte order,所以要加上’>’(float和double不支持’>’).

当然还有一些自定义类型和上述类型的一维数组,二维数组,全实现太麻烦了,后面我们需要什么,再来添加什么.

事成之后模拟了一个请求发送过去,发现服务器没有鸟我们.那么协议格式一定是有问题,所以我们先抓个包,看看正常的包是怎么样的.

协议分析

手机抓包太麻烦,要Root,代理的话只能抓HTTP的包.
所以我们用WireShark和模拟器.
把电脑上要联网的东西能关掉的全关掉,以减少干扰.
打开模拟器,进入游戏,随便做一些操作,然后寻找我们需要的包.
找到后随便取一组请求和返回,数据如下:

请求:

1
2
3
4
5
00 00 00 5D 05 75 72 00 00 02 5B 42 AC F3 17 F8 06 08 54 E0 02 00
00 78 70 00 00 00 44 1F 8B 08 00 00 00 00 00 00 00 63 60 60 56 64
60 CD 2D 4E F7 4C 61 66 60 60 38 C9 C0 0E 64 87 54 16 A4 02 79 40
19 96 12 08 93 81 9B 81 AD B4 38 B5 08 A4 4A B8 57 95 81 31 EA 3F
00 EA A8 91 67 3A 00 00 00

返回:

1
2
3
4
5
6
7
8
9
10
11
12
00 00 01 03 05 75 72 00 00 02 5b 42 ac f3 17 f8 06 08 54 e0 02 00
00 78 70 00 00 00 ea 1f 8b 08 00 00 00 00 00 00 00 63 60 60 56 64
e0 2c 2d 4e 2d 0a 2e 49 4c 4b 93 65 60 60 60 04 e2 73 40 ac c6 a0
db da 34 cf fb 4b 1d 50 48 07 c8 67 72 fc c0 00 03 52 40 2c 04 c4
02 0c 38 c0 7f 1c 00 28 c5 cc 80 c4 81 2b 46 d1 2d 98 93 58 5c 12
92 5f 9a 9c e1 91 9a 98 12 92 99 9b ca 0a 97 e3 4c ad 28 29 4a 0c
28 ca 4f 62 01 f3 59 72 f2 93 b3 41 74 72 62 5a 2a 90 66 03 e9 75
0f 41 68 60 8c fa cf c0 9a 9c 9f 99 57 cc cc c0 e4 f9 81 81 2f 29
bf 34 3d a3 c4 b9 a8 b2 b8 24 31 07 28 08 02 7c 45 a9 e5 89 45 29
c8 82 5c 0c 5c 69 45 99 a9 79 29 c5 19 99 05 10 55 ec b9 c5 e9 21
95 05 a9 40 1e 30 d8 58 92 f3 53 c0 cc 17 0c ac 40 09 cf 14 90 a2
93 0c 1c 20 61 cf bc b4 7c 76 88 e5 00 7a 8b 40 e6 63 01 00 00

请求包前4个字节是0x5d,等于93,而data长度是97.返回包前4个字节是0x01,0x03,等于259,而data长度是263.
所以前4个字节标识后面内容的长度.

再往后是请求包和返回包都有的一段
05 75 72 00 00 02 5B 42 AC F3 17 F8 06 08 54 E0 02 00 00 78 70
这段经观察每个请求都有,并且都相同,但是不知道是什么,大概是一些乱七八糟的头信息.

后面又是一个标识长度的int位,再往后是1F 8B,这是gzip格式的标识位.说明后面的内容都是gzip格式了.
我们把后面这段gzip的内容解压出来,发现就是我们上面所提到的流格式.
至此,我们就可以模拟整个请求了.

全部代码如下:

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
require 'eventmachine'
require 'zlib'
require 'stringio'
TYPE_ARR = ['boolean', 'byte', 'short','int', 'float','long', 'double', 'string']
PACK_ARR = ['c', 'c', 's>', 'i>', 'g', 'l>', 'G', 'a*']
def convert(key,type,value)
if type == 'string'
type_idx = TYPE_ARR.index(type)
[key.size,key,type_idx,value.size,value].pack("s>A*cs>#{PACK_ARR[type_idx]}")
else
type_idx = TYPE_ARR.index(type)
[key.size,key,type_idx,value].pack("s>A*c#{PACK_ARR[type_idx]}")
end
end
def gen_request(arr)
# 头4个字节是msgType
msgType = arr.find{|x|
x[0] == 'msgType'
}[2]
data = [msgType].pack("i>")
# 按key排序
arr.sort!{|a,b|
a[0] <=> b[0]
}
arr.each{|x|
data += convert(x[0],x[1],x[2])
}
# 结尾标识符,"Z"加上255
data += [1,"Z",-1].pack('s>A*c')
# gzip压缩
io = StringIO.new
z = Zlib::GzipWriter.new(io)
z.write(data)
z.close
# 最后加上头信息
data = io.string
data = data.force_encoding('utf-8')
size = data.bytes.size
data = [size].pack("i>") + data
data = [5,117,114,0,0,2,91,66,172,243,23,248,6,8,84,224,2,0,0,120,112].pack("c*").force_encoding('utf-8') + data
size = data.bytes.size
data = [size].pack("i>") + data
data
end
login_url = "mgc2.login.cn.cat-studio.net"
login_port = 6010
class TestClient < EventMachine::Connection
def post_init
p "connect success"
request = []
request << ['msgId','int',1]
request << ['msgType','int',101]
request << ['userId','int',-1]
request << ['lan','int',0]
request << ['source','int',0]
request << ['version','int',10920]
data = gen_request(request)
send_data data
end
def receive_data(data)
p data.bytes.join(',')
end
end
EventMachine.run {
EventMachine::connect login_url, login_port, TestClient
}

上述代码实现了一次请求服务器列表的操作.服务器返回的格式同客户端一样.解析代码和其他请求代码不再赘述.

文章目录
  1. 1. 前言
  2. 2. 源码分析
  3. 3. 协议分析