-
Notifications
You must be signed in to change notification settings - Fork 0
/
parse_aim.rb
331 lines (291 loc) · 12.6 KB
/
parse_aim.rb
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
class Protos
# AIM connections are divided into two components - logon, and communication.
# The communication component exchanges SNAC data. That is to say, Channel
# ID is set to 2 and usually contains an FNAC. The logon component exchanges
# New Connection data, ie, Channel ID is set to 1. Parse the logon half.
def parse_aim_logon(data, state, dir)
return nil unless data
req = state.app_state[:req_struct]
res = state.app_state[:resp_struct]
# Hand off to the client or server parser as needed.
dir = state.app_state[dir][:type]
return _parse_aim_traffic(data, state, res, req, dir) if dir == :server
return _parse_aim_traffic(data, state, req, res, dir) if dir == :client
raise "AIM traffic not client or server"
end # of parse_aim_logon
# Parse AIM logon stream, server-to-client or client-to-server, keep state
# in res. 'dir' is either :server or :client
def _parse_aim_traffic(data, state, res, req, dir)
pos = 0
# Check our initialization condition
unless res
res = Struct.new(
:state, :buff, :maxlen, :terminator, :channel, :seq_number, :family,
:sub_family, :auth_cookie, :username, :flags, :client_version,
:language, :country, :email
).new
res.state = :find_channel
_prepare_to_copy(res, 6)
state.app_state[:resp_struct] = res if dir == :server
state.app_state[:req_struct] = res if dir == :client
end
while pos < data.length
case res.state
# Find the channel header (always six bytes)
when :find_channel
pos, ret = _copy_bytes(res, data, pos)
return true if ret == true
# We have our header, let's dissect it
raise "AIM traffic out of sync" unless ret[0] == 0x2A
res.channel = ret[1]
res.seq_number = _big_endian(ret[2,2])
_prepare_to_copy(res, _big_endian(ret[4,2])) # fine if it's 0
res.state = :channel_contents
# We've already parsed the channel header, now get its contents
when :channel_contents
pos, ret = _copy_bytes(res, data, pos)
return true if ret == true
# We have our blob of data. Report it.
@event_collector.send(:aim_raw_flap) do
{ :channel => res.channel,
:seq => res.seq_number, :snac => ret, :dir => dir,
:server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport] }
end
# Now break it up depending on channel ID
# Logon
if res.channel == 1
if ret.length >= 8
tlvs = _aim_tlvs_split(res, ret, 4) # first 4 bytes are version
res.auth_cookie = (tlvs.find { |t,_| t == 6 } || []).last
aim_cookies = (global_state[:aim_cookies] ||= {})
login_info = aim_cookies.delete res.auth_cookie
if login_info
res.username = login_info[:username]
req.username = login_info[:username] if req
res.email = login_info[:email]
req.email = login_info[:email] if req
res.client_version = login_info[:version]
req.client_version = login_info[:version] if req
res.language = login_info[:language]
req.language = login_info[:language] if req
res.country = login_info[:country]
req.country = login_info[:country] if req
@event_collector.send(:aim_login) do
{ :seq => res.seq_number, :dir => dir,
:server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport],
:username => res.username, :country => res.country,
:client_version => res.client_version,
:email => res.email, :language => res.language
}
end
end # of if login_info
end # of if proper login
# Logoff
elsif res.channel == 4
# Do nothing right now
# SNAC data
else res.channel == 2
_parse_aim_fnac(state, res, dir, req, ret) if ret.length >= 10
end # of channel type
# Time to get the next FLAP
res.state = :find_channel
_prepare_to_copy(res, 6)
end # of case state
end # of while data
true
end # of _parse_aim_traffic
# Take a blob containing TLVs (and optionally a position where to begin
# parsing) and return a list of TLVs. Format of returned TLV:
# [ tlv_type, tlv_data] (length can be inferred from tlv_data.length)
def _aim_tlvs_split(res, blob, pos = 0)
tlvs = []
# loop while there's data to be read
while blob.length - pos >= 4
tlv_type = _big_endian(blob[pos,2])
tlv_len = _big_endian(blob[pos+2,2])
pos += 4
if blob.length - pos < tlv_len
@event_collector.send(:aim_tlv_overflow) do
{ :channel => res.channel,
:seq => res.seq_number, :snac => res.buff, :dir => dir,
:server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport],
:should_be => blob.length - pos, :length => tlv_len }
end
break
end # of if overflow
# Add the TLV and increment position
tlvs << [ tlv_type, blob[pos, tlv_len] ]
pos += tlv_len
end # of while data
tlvs
end # of _aim_tlvs_split
# Grab SSI's, just like TLVs. Return an array. Format: [type, name]
def _aim_ssis_split(res, blob, pos = 0)
ssis = []
while blob.length - pos > 4
len = _big_endian(blob[pos, 2])
name = blob[pos+2, len]
pos = pos + len + 6
type = _big_endian(blob[pos, 2])
len = _big_endian(blob[pos+2, 2])
pos = pos + len + 4
ssis << [type, name] unless name.empty?
end
ssis
end
# Parse out an FNAC - some are TLVs, some aren't. All have 10-byte headers
def _parse_aim_fnac(state, res, dir, req, snac)
# First get the header
res.family = _big_endian(snac[0,2])
res.sub_family = _big_endian(snac[2,2])
res.flags = _big_endian(snac[4,2])
#res.fnac_id = _big_endian(snac[6,4])
# Now conditionally handle each FNAC that we understand.
case [ res.family, res.sub_family ]
# Signon/Logon (a bit redundant, ain't it?)
when [ 0x17, 0x2 ]
tlvs = _aim_tlvs_split(res, snac, 10)
# Get the username
if (tlv = tlvs.find { |t,_| t == 0x1 })
res.username = tlv.last
req.username = tlv.last if req
end
# Get the version string
if (tlv = tlvs.find { |t,_| t == 0x3 })
res.client_version = tlv.last
req.client_version = tlv.last if req
end
# Get the language
if (tlv = tlvs.find { |t,_| t == 0xF })
res.language = tlv.last
req.language = tlv.last if req
end
# Get the country
if (tlv = tlvs.find { |t,_| t == 0xE })
res.country = tlv.last
req.country = tlv.last if req
end
# Signon/Logon-Reply
when [ 0x17, 0x3 ]
tlvs = _aim_tlvs_split(res, snac, 10)
# Get the username
if (tlv = tlvs.find { |t,_| t == 0x1 })
res.username = tlv.last
req.username = tlv.last if req
end
# Get the auth_cookie
if (tlv = tlvs.find { |t,_| t == 0x6 })
res.auth_cookie = tlv.last
req.auth_cookie = tlv.last if req
end
# Get the user's official email address
if (tlv = tlvs.find { |t,_| t == 0x11 })
res.email = tlv.last
req.email = tlv.last if req
end
# Now add this object to our global state
res.username ||= req.username if req # just to be sure
if res.auth_cookie and res.username
aim_cookies = (global_state[:aim_cookies] ||= {})
# First let's delete all the old cookies just in case they're cluttering
aim_cookies.reject! { |_,v| state.last_seen - v[:time] > 3600 }
# Now add our new cookie
aim_cookies[res.auth_cookie] = { :username => res.username,
:email => res.email, :language => res.language,
:country => res.country, :version => res.client_version,
:time => state.last_seen }
end
# AIM-SSI/List
when [ 0x13, 0x6 ]
ssis = _aim_ssis_split(res, snac, 13)
ssis = ssis.select { |t,_| t == 0 }
ssis = ssis.collect { |_,x| x.downcase.gsub(' ', '') }
if ssis.length > 1
@event_collector.send(:aim_buddy_list) do
{ :server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport], :dir => dir,
:username => res.username, :country => res.country,
:client_version => res.client_version,
:email => res.email, :language => res.language,
:seq => res.seq_number, :buddies => ssis
}
end
end # of if buddy list
# Messaging/outgoing
when [ 0x4, 0x6 ]
buddylen = snac[20] # only one byte
buddy = snac[21, buddylen]
tlvs = _aim_tlvs_split(res, snac, 21+buddylen)
msg = tlvs.find { |t,_| t == 0x2 } # message block
if msg
msg = msg.last
# cut out msg header (which has an encoded length)
msg[0, _big_endian(msg[2,2]) + 12] = ''
@event_collector.send(:aim_message) do
{ :seq => res.seq_number,
:server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport], :dir => dir,
:sender => res.username, :country => res.country,
:client_version => res.client_version, :chat_dir => :outgoing,
:email => res.email, :language => res.language,
:recipient => buddy, :msg => _strip_html(msg)
}
end
@event_collector.send(:protos_chat_message) do
{ :server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport], :dir => dir,
:chat_dir => :outgoing, :recipient => buddy,
:sender => res.username, :protocol => :aim,
:content => _strip_html(msg) }
end
end # of if msg
# Messaging/incoming
when [ 0x4, 0x7 ]
buddylen = snac[20] # only one byte
buddy = snac[21, buddylen]
tlvs = _aim_tlvs_split(res, snac, 25+buddylen)
msg = tlvs.find { |t,_| t == 0x2 } # message block
if msg
msg = msg.last
# cut out msg header (which has an encoded length)
msg[0, _big_endian(msg[2,2]) + 12] = ''
@event_collector.send(:aim_message) do
{ :seq => res.seq_number, :chat_dir => :incoming,
:server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport], :dir => dir,
:recipient => res.username, :country => res.country,
:client_version => res.client_version,
:email => res.email, :language => res.language,
:sender => buddy, :msg => _strip_html(msg)
}
end
@event_collector.send(:protos_chat_message) do
{ :server_ip => str_ip(state.app_state[:dst]),
:client_ip => str_ip(state.app_state[:src]),
:server_port => state.app_state[:dport],
:client_port => state.app_state[:sport], :dir => dir,
:chat_dir => :incoming, :recipient => res.username,
:sender => buddy, :protocol => :aim,
:content => _strip_html(msg) }
end
end # of if msg
end # of case family/subfamily
end # of _parse_aim_fnac
end # of class Protos