Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/scapy/sessions.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

261 statements  

1# SPDX-License-Identifier: GPL-2.0-only 

2# This file is part of Scapy 

3# See https://scapy.net/ for more information 

4 

5""" 

6Sessions: decode flow of packets when sniffing 

7""" 

8 

9from collections import defaultdict 

10import socket 

11import struct 

12 

13from scapy.config import conf 

14from scapy.data import MTU 

15from scapy.error import log_runtime 

16from scapy.packet import Packet 

17from scapy.pton_ntop import inet_pton 

18 

19# Typing imports 

20from typing import ( 

21 Any, 

22 Callable, 

23 DefaultDict, 

24 Dict, 

25 Iterator, 

26 List, 

27 Optional, 

28 Tuple, 

29 Type, 

30 cast, 

31 TYPE_CHECKING, 

32) 

33from scapy.compat import Self 

34if TYPE_CHECKING: 

35 from scapy.supersocket import SuperSocket 

36 

37 

38class DefaultSession(object): 

39 """Default session: no stream decoding""" 

40 

41 def __init__(self, supersession: Optional[Self] = None): 

42 if supersession and not isinstance(supersession, DefaultSession): 

43 supersession = supersession() 

44 self.supersession = supersession 

45 

46 def process(self, pkt: Packet) -> Optional[Packet]: 

47 """ 

48 Called to pre-process the packet 

49 """ 

50 # Optionally handle supersession 

51 if self.supersession: 

52 return self.supersession.process(pkt) 

53 return pkt 

54 

55 def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: 

56 """ 

57 Will be called by sniff() to ask for a packet 

58 """ 

59 pkt = sock.recv() 

60 if not pkt: 

61 return 

62 try: 

63 pkt = self.process(pkt) 

64 except Exception as ex: 

65 log_runtime.warning( 

66 "%s processing failed with '%s'. Skipping." % ( 

67 type(self), 

68 ex, 

69 ) 

70 ) 

71 if conf.debug_dissector: 

72 raise 

73 if pkt is not None: 

74 yield pkt 

75 return 

76 if pkt: 

77 yield pkt 

78 

79 

80class IPSession(DefaultSession): 

81 """Defragment IP packets 'on-the-flow'. 

82 

83 Usage: 

84 >>> sniff(session=IPSession) 

85 """ 

86 

87 def __init__(self, *args, **kwargs): 

88 # type: (*Any, **Any) -> None 

89 DefaultSession.__init__(self, *args, **kwargs) 

90 self.fragments = defaultdict(list) # type: DefaultDict[Tuple[Any, ...], List[Packet]] # noqa: E501 

91 

92 def process(self, packet: Packet) -> Optional[Packet]: 

93 from scapy.layers.inet import BadFragments, IP, _defrag_ip_pkt 

94 if not packet: 

95 return None 

96 if IP not in packet: 

97 return packet 

98 try: 

99 return _defrag_ip_pkt(packet, self.fragments)[1] # type: ignore 

100 except BadFragments: 

101 return None 

102 

103 

104class StringBuffer(object): 

105 """StringBuffer is an object used to re-order data received during 

106 a TCP transmission. 

107 

108 Each TCP fragment contains a sequence number, which marks 

109 (relatively to the first sequence number) the index of the data contained 

110 in the fragment. 

111 

112 If a TCP fragment is missed, this class will fill the missing space with 

113 zeros. 

114 

115 :param max_gap: the largest missing range that will be zero-filled, in bytes. 

116 A sequence number far from the data already buffered would otherwise 

117 allocate the whole distance. Defaults to MTU. 

118 """ 

119 

120 def __init__(self, max_gap: int = MTU) -> None: 

121 self.content = bytearray(b"") 

122 self.content_len = 0 

123 self.noff = 0 # negative offset 

124 self.incomplete = [] # type: List[Tuple[int, int]] 

125 self.max_gap = max_gap 

126 

127 def append(self, data: bytes, seq: Optional[int] = None) -> None: 

128 if not data: 

129 return 

130 data_len = len(data) 

131 if seq is None: 

132 seq = self.content_len 

133 seq = seq - 1 - self.noff 

134 if seq < 0: 

135 # Data is located before the start of the current buffer 

136 # (e.g. the first fragment was missing) 

137 if -seq - data_len > self.max_gap: 

138 log_runtime.warning( 

139 "Dropped data further than allowed per 'max_gap'." 

140 ) 

141 return 

142 self.content = bytearray(b"\x00" * (-seq)) + self.content 

143 self.content_len += (-seq) 

144 self.noff += seq 

145 seq = 0 

146 if seq + data_len > self.content_len: 

147 # Data is located after the end of the current buffer 

148 if seq - self.content_len > self.max_gap: 

149 log_runtime.warning( 

150 "Dropped data further than allowed per 'max_gap'." 

151 ) 

152 return 

153 self.content += b"\x00" * (seq - self.content_len + data_len) 

154 # As data was missing, mark it. 

155 # self.incomplete.append((self.content_len, seq)) 

156 self.content_len = seq + data_len 

157 assert len(self.content) == self.content_len 

158 # XXX removes empty space marker. 

159 # for ifrag in self.incomplete: 

160 # if [???]: 

161 # self.incomplete.remove([???]) 

162 memoryview(self.content)[seq:seq + data_len] = data 

163 

164 def shiftleft(self, i: int) -> None: 

165 self.content = self.content[i:] 

166 self.content_len -= i 

167 

168 def full(self): 

169 # type: () -> bool 

170 # Should only be true when all missing data was filled up, 

171 # (or there never was missing data) 

172 return bool(self) 

173 

174 def clear(self): 

175 # type: () -> None 

176 self.__init__() # type: ignore 

177 

178 def __bool__(self): 

179 # type: () -> bool 

180 return bool(self.content_len) 

181 __nonzero__ = __bool__ 

182 

183 def __len__(self): 

184 # type: () -> int 

185 return self.content_len 

186 

187 def __bytes__(self): 

188 # type: () -> bytes 

189 return bytes(self.content) 

190 

191 def __str__(self): 

192 # type: () -> str 

193 return cast(str, self.__bytes__()) 

194 

195 

196def streamcls(cls: Type[Packet]) -> Callable[ 

197 [bytes, Dict[str, Any], Dict[str, Any]], 

198 Optional[Packet], 

199]: 

200 """ 

201 Wraps a class for use when dissecting streams. 

202 """ 

203 if hasattr(cls, "tcp_reassemble"): 

204 return cls.tcp_reassemble # type: ignore 

205 else: 

206 # There is no tcp_reassemble. Just dissect the packet 

207 return lambda data, *_: data and cls(data) 

208 

209 

210class TCPSession(IPSession): 

211 """A Session that reconstructs TCP streams. 

212 

213 NOTE: this has the same effect as wrapping a real socket.socket into StreamSocket, 

214 but for all concurrent TCP streams (can be used on pcaps or sniffed sessions). 

215 

216 NOTE: only protocols that implement a ``tcp_reassemble`` function will be processed 

217 by this session. Other protocols will not be reconstructed. 

218 

219 DEV: implement a class-function `tcp_reassemble` in your Packet class:: 

220 

221 @classmethod 

222 def tcp_reassemble(cls, data, metadata, session): 

223 # data = the reassembled data from the same request/flow 

224 # metadata = empty dictionary, that can be used to store data 

225 # during TCP reassembly 

226 # session = a dictionary proper to the bidirectional TCP session, 

227 # that can be used to store anything 

228 [...] 

229 # If the packet is available, return it. Otherwise don't. 

230 # Whenever you return a packet, the buffer will be discarded. 

231 return pkt 

232 # Otherwise, maybe store stuff in metadata, and return None, 

233 # as you need additional data. 

234 return None 

235 

236 For more details and a real example, see: 

237 https://scapy.readthedocs.io/en/latest/usage.html#how-to-use-tcpsession-to-defragment-tcp-packets 

238 

239 :param app: Whether the socket is on application layer = has no TCP 

240 layer. This is identical to StreamSocket so only use this if your 

241 underlying source of data isn't a socket.socket. 

242 """ 

243 

244 def __init__(self, app=False, *args, **kwargs): 

245 # type: (bool, *Any, **Any) -> None 

246 super(TCPSession, self).__init__(*args, **kwargs) 

247 self.app = app 

248 if app: 

249 self.data = StringBuffer() 

250 self.metadata = {} # type: Dict[str, Any] 

251 self.session = {} # type: Dict[str, Any] 

252 else: 

253 # The StringBuffer() is used to build a global 

254 # string from fragments and their seq nulber 

255 self.tcp_frags = defaultdict( 

256 lambda: (StringBuffer(), {}) 

257 ) # type: DefaultDict[bytes, Tuple[StringBuffer, Dict[str, Any]]] 

258 self.tcp_sessions = defaultdict( 

259 dict 

260 ) # type: DefaultDict[bytes, Dict[str, Any]] 

261 # Setup stopping dissection condition 

262 from scapy.layers.inet import TCP 

263 self.stop_dissection_after = TCP 

264 

265 def _get_ident(self, pkt, session=False): 

266 # type: (Packet, bool) -> bytes 

267 underlayer = pkt["TCP"].underlayer 

268 af = socket.AF_INET6 if "IPv6" in pkt else socket.AF_INET 

269 src = underlayer and inet_pton(af, underlayer.src) or b"" 

270 dst = underlayer and inet_pton(af, underlayer.dst) or b"" 

271 if session: 

272 # Bidirectional 

273 def xor(x, y): 

274 # type: (bytes, bytes) -> bytes 

275 return bytes(a ^ b for a, b in zip(x, y)) 

276 return struct.pack("!4sH", xor(src, dst), pkt.dport ^ pkt.sport) 

277 else: 

278 # Uni-directional 

279 return src + dst + struct.pack("!HH", pkt.dport, pkt.sport) 

280 

281 def _strip_padding(self, pkt: Packet) -> Optional[bytes]: 

282 """Strip the packet of any padding, and return the padding. 

283 """ 

284 if isinstance(pkt, conf.padding_layer): 

285 return cast(bytes, pkt.load) 

286 pad = pkt.getlayer(conf.padding_layer) 

287 if pad is not None and pad.underlayer is not None: 

288 # strip padding 

289 del pad.underlayer.payload 

290 return cast(bytes, pad.load) 

291 return None 

292 

293 def process(self, 

294 pkt: Packet, 

295 cls: Optional[Type[Packet]] = None) -> Optional[Packet]: 

296 """Process each packet: matches the TCP seq/ack numbers 

297 to follow the TCP streams, and orders the fragments. 

298 """ 

299 packet = None # type: Optional[Packet] 

300 if self.app: 

301 # Special mode: Application layer. Use on top of TCP 

302 self.data.append(bytes(pkt)) 

303 if cls is None and not isinstance(pkt, bytes): 

304 cls = pkt.__class__ 

305 if "tcp_reassemble" in self.metadata: 

306 tcp_reassemble = self.metadata["tcp_reassemble"] 

307 elif cls is not None: 

308 self.metadata["tcp_reassemble"] = tcp_reassemble = streamcls(cls) 

309 else: 

310 return None 

311 if self.data.full(): 

312 min_len = self.metadata.get("tcp_min_len") 

313 if min_len is None or len(self.data) >= min_len: 

314 self.metadata.pop("tcp_min_len", None) 

315 packet = tcp_reassemble( 

316 bytes(self.data), 

317 self.metadata, 

318 self.session, 

319 ) 

320 if packet: 

321 padding = self._strip_padding(packet) 

322 if padding: 

323 # There is remaining data for the next payload. 

324 self.data.shiftleft(len(self.data) - len(padding)) 

325 # Skip full-padding 

326 if isinstance(packet, conf.padding_layer): 

327 return None 

328 else: 

329 # No padding (data) left. Clear 

330 self.data.clear() 

331 self.metadata.clear() 

332 return packet 

333 return None 

334 

335 _pkt = super(TCPSession, self).process(pkt) 

336 if _pkt is None: 

337 return None 

338 else: # Python 3.8 := would be nice 

339 pkt = _pkt 

340 

341 from scapy.layers.inet import IP, TCP 

342 if not pkt: 

343 return None 

344 if TCP not in pkt: 

345 return pkt 

346 pay = pkt[TCP].payload 

347 new_data = pay.original 

348 # Match packets by a unique TCP identifier 

349 ident = self._get_ident(pkt) 

350 data, metadata = self.tcp_frags[ident] 

351 tcp_session = self.tcp_sessions[self._get_ident(pkt, True)] 

352 # Handle TCP sequence numbers 

353 seq = pkt[TCP].seq 

354 if "seq" not in metadata: 

355 metadata["seq"] = seq 

356 if "next_seq" in metadata and seq < metadata["next_seq"]: 

357 # Retransmitted data (that we already returned) 

358 new_data = new_data[metadata["next_seq"] - seq:] 

359 if not new_data: 

360 return None 

361 seq = metadata["next_seq"] 

362 # Let's guess which class is going to be used 

363 if "pay_class" not in metadata: 

364 metadata["pay_class"] = pay_class = pkt[TCP].guess_payload_class(new_data) 

365 metadata["tcp_reassemble"] = tcp_reassemble = streamcls(pay_class) 

366 else: 

367 tcp_reassemble = metadata["tcp_reassemble"] 

368 

369 if pay: 

370 # Get a relative sequence number for a storage purpose 

371 relative_seq = metadata.get("relative_seq", None) 

372 if relative_seq is None: 

373 relative_seq = metadata["relative_seq"] = seq - 1 

374 seq = seq - relative_seq 

375 # Add the data to the buffer 

376 noff = data.noff 

377 data.append(new_data, seq) 

378 if data.noff != noff: 

379 # The negative offset has changed => data was prepended. 

380 # this means that our estimate min length may be wrong. 

381 metadata.pop("tcp_min_len", None) 

382 

383 # Check TCP FIN or TCP RESET 

384 if pkt[TCP].flags.F or pkt[TCP].flags.R: 

385 metadata["tcp_end"] = True 

386 elif not pay: 

387 # If there's no payload and the stream isn't ending, ignore. 

388 return pkt 

389 

390 # In case any app layer protocol requires it, 

391 # allow the parser to inspect TCP PSH flag 

392 if pkt[TCP].flags.P: 

393 metadata["tcp_psh"] = True 

394 # XXX TODO: check that no empty space is missing in the buffer. 

395 # XXX Currently, if a TCP fragment was missing, we won't notice it. 

396 if data.full(): 

397 min_len = metadata.get("tcp_min_len") 

398 if min_len is None or len(data) >= min_len: 

399 # Reassemble using all previous packets 

400 metadata["original"] = pkt 

401 metadata["ident"] = ident 

402 metadata.pop("tcp_min_len", None) 

403 packet = tcp_reassemble( 

404 bytes(data), 

405 metadata, 

406 tcp_session 

407 ) 

408 # Stack the result on top of the previous frames 

409 if packet: 

410 if "seq" in metadata: 

411 pkt[TCP].seq = metadata["seq"] 

412 # Clear TCP reassembly metadata 

413 metadata.clear() 

414 # Check for padding 

415 padding = self._strip_padding(packet) 

416 while padding: 

417 # There is remaining data for the next payload. 

418 full_length = data.content_len - len(padding) 

419 metadata["relative_seq"] = relative_seq + full_length 

420 data.shiftleft(full_length) 

421 # There might be a sub-payload hidden in the padding 

422 sub_packet = tcp_reassemble( 

423 bytes(data), 

424 metadata, 

425 tcp_session 

426 ) 

427 if sub_packet: 

428 packet.add_payload(sub_packet) 

429 padding = self._strip_padding(sub_packet) 

430 else: 

431 break 

432 else: 

433 # No padding (data) left. Clear 

434 data.clear() 

435 del self.tcp_frags[ident] 

436 # Minimum next seq 

437 metadata["next_seq"] = pkt[TCP].seq + len(new_data) 

438 # Skip full-padding 

439 if isinstance(packet, conf.padding_layer): 

440 return None 

441 # Rebuild resulting packet 

442 if pay: 

443 pay.underlayer.remove_payload() 

444 if IP in pkt: 

445 pkt[IP].len = None 

446 pkt[IP].chksum = None 

447 pkt = pkt / packet 

448 pkt.wirelen = None 

449 return pkt 

450 return None 

451 

452 def recv(self, sock: 'SuperSocket') -> Iterator[Packet]: 

453 """ 

454 Will be called by sniff() to ask for a packet 

455 """ 

456 pkt = sock.recv(stop_dissection_after=self.stop_dissection_after) 

457 _orig = pkt 

458 # Now handle TCP reassembly 

459 if self.app: 

460 while pkt is not None: 

461 try: 

462 pkt = self.process(pkt) 

463 except Exception as ex: 

464 log_runtime.warning( 

465 "%s processing failed with '%s'. Aborting." % ( 

466 type(self), 

467 ex, 

468 ) 

469 ) 

470 if conf.debug_dissector: 

471 raise 

472 return None 

473 if pkt: 

474 yield pkt 

475 # keep calling process as there might be more 

476 pkt = b"" # type: ignore 

477 else: 

478 try: 

479 pkt = self.process(pkt) # type: ignore 

480 except Exception as ex: 

481 log_runtime.warning( 

482 "%s processing failed with '%s'. Skipping." % ( 

483 type(self), 

484 ex, 

485 ) 

486 ) 

487 if conf.debug_dissector: 

488 raise 

489 if _orig is not None: 

490 yield _orig 

491 return None 

492 if pkt: 

493 yield pkt 

494 return None