Skip to content

Commit 52c95a8

Browse files
authored
Merge pull request #3 from Nisitay/feature/fingerprint_uptime
Feature/fingerprint uptime
2 parents 10bb637 + a33ebfe commit 52c95a8

File tree

27 files changed

+432
-122
lines changed

27 files changed

+432
-122
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release History
22

3+
## pyp0f 0.3.0
4+
5+
* Added TCP timestamps uptime detection.
6+
37
## pyp0f 0.2.1
48

59
* Added impersonation utility.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ $ pip install pyp0f
2626
## Features
2727
- Full p0f fingerprinting (MTU, TCP, HTTP)
2828
- p0f spoofing - impersonation (MTU, TCP)
29+
- TCP timestamps uptime detection
2930

3031
## In Progress
3132
- Flow tracking
32-
- TCP uptime detection
3333
- NAT detection
3434

3535
## Getting Started

docs/README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,39 @@ print(result.match)
132132

133133
</details>
134134

135+
</details>
136+
137+
<details markdown="1">
138+
<summary>TCP uptime fingerprinting example</summary>
139+
140+
```python
141+
from scapy.layers.inet import IP, TCP
142+
from pyp0f.net.packet import parse_packet
143+
from pyp0f.net.signatures import TCPPacketSignature
144+
from pyp0f.fingerprint.uptime import fingerprint_uptime
145+
from pyp0f.utils.time import get_unix_time_ms
146+
147+
last_timestamp = IP() / TCP(seq=1, options=[("Timestamp", (1545573, 0))])
148+
current_timestamp = IP() / TCP(seq=2, options=[("Timestamp", (1545586, 0))])
149+
150+
last_packet_signature = TCPPacketSignature.from_packet(parse_packet(last_timestamp))
151+
152+
# Simulate different receive time
153+
last_packet_signature.received = get_unix_time_ms() - 130
154+
155+
result = fingerprint_uptime(current_timestamp, last_packet_signature)
156+
print(result.tps) # 100
157+
print(result.uptime)
158+
# Uptime(
159+
# raw_frequency=107.6923076923077,
160+
# frequency=100,
161+
# total_minutes=257,
162+
# modulo_days=497
163+
# )
164+
```
165+
166+
</details>
167+
135168
## Impersonation
136169
`pyp0f` provides functionality to modify Scapy packets so that `p0f` will think it has been sent by a specific OS.
137170

@@ -175,7 +208,7 @@ result = fingerprint_tcp(impersonated_packet) # TCPResult for "s:unix:OpenVMS:7
175208
</details>
176209

177210
## Real World Examples
178-
`pyp0f` can be used in real world scenarios, whether its to passivly fingerprint remote hosts,
211+
`pyp0f` can be used in real world scenarios, whether its to passively fingerprint remote hosts,
179212
or to deceive remote `p0f`.
180213

181214
### Sniff connection attempts

pyp0f/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "0.2.1"
1+
VERSION = "0.3.0"

pyp0f/database/records_database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def iter_values(
8686
self, key: Type[T], direction: Optional[Direction] = None
8787
) -> Iterator[T]:
8888
"""
89-
Safely iterate a list of values.
89+
Iterate a list of values.
9090
"""
9191
try:
9292
values = self._get(key, direction)

pyp0f/fingerprint/__init__.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
from .http import fingerprint as fingerprint_http
2-
from .mtu import fingerprint as fingerprint_mtu
3-
from .tcp import fingerprint as fingerprint_tcp
1+
from .http import fingerprint_http
2+
from .mtu import fingerprint_mtu
3+
from .tcp import fingerprint_tcp
4+
from .uptime import fingerprint_uptime
45

5-
__all__ = ["fingerprint_mtu", "fingerprint_tcp", "fingerprint_http"]
6+
__all__ = [
7+
"fingerprint_mtu",
8+
"fingerprint_tcp",
9+
"fingerprint_http",
10+
"fingerprint_uptime",
11+
]

pyp0f/fingerprint/http.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def headers_match(
2020
i = 0 # Index of packet header
2121

2222
for header in signature_headers:
23-
orig_index = i
23+
original_index = i
2424

2525
while (
2626
i < len(packet_headers)
@@ -34,12 +34,12 @@ def headers_match(
3434

3535
# Optional header -> check that it doesn't appear anywhere else
3636
if any(
37-
header.lower_name == pkt_header.lower_name
38-
for pkt_header in packet_headers
37+
header.lower_name == packet_header.lower_name
38+
for packet_header in packet_headers
3939
):
4040
return False
4141

42-
i = orig_index
42+
i = original_index
4343
continue
4444

4545
# Header found, validate values
@@ -49,15 +49,15 @@ def headers_match(
4949
return True
5050

5151

52-
def signatures_match(
52+
def http_signatures_match(
5353
signature: HTTPSignature, packet_signature: HTTPPacketSignature
5454
) -> bool:
5555
"""
5656
Check if HTTP signatures match by comparing the following criterias:
5757
- HTTP versions match.
5858
- All non-optional signature headers appear in the packet.
5959
- Absent headers in signature don't appear in the packet.
60-
- Order and values of headers match (this is relatively slow).
60+
- Order and values of headers match (relatively slow, hence why its last step).
6161
"""
6262
packet_headers = packet_signature.header_names
6363
return (
@@ -68,7 +68,7 @@ def signatures_match(
6868
)
6969

7070

71-
def find_match(
71+
def find_http_match(
7272
packet_signature: HTTPPacketSignature,
7373
direction: Direction,
7474
database: Database,
@@ -79,7 +79,7 @@ def find_match(
7979
generic_match: Optional[HTTPRecord] = None
8080

8181
for http_record in database.iter_values(HTTPRecord, direction):
82-
if not signatures_match(http_record.signature, packet_signature):
82+
if not http_signatures_match(http_record.signature, packet_signature):
8383
continue
8484

8585
if not http_record.is_generic:
@@ -91,7 +91,7 @@ def find_match(
9191
return generic_match
9292

9393

94-
def fingerprint(buffer: BufferLike, options: Options = OPTIONS) -> HTTPResult:
94+
def fingerprint_http(buffer: BufferLike, *, options: Options = OPTIONS) -> HTTPResult:
9595
"""
9696
Fingerprint the given HTTP 1.x payload.
9797
@@ -111,5 +111,5 @@ def fingerprint(buffer: BufferLike, options: Options = OPTIONS) -> HTTPResult:
111111
return HTTPResult(
112112
buffer,
113113
packet_signature,
114-
find_match(packet_signature, direction, options.database),
114+
find_http_match(packet_signature, direction, options.database),
115115
)

pyp0f/fingerprint/mtu.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,29 @@
55
from pyp0f.database.signatures import MTUSignature
66
from pyp0f.exceptions import PacketError
77
from pyp0f.fingerprint.results import MTUResult
8+
from pyp0f.net.layers.tcp import TCPFlag
89
from pyp0f.net.packet import Packet, PacketLike, parse_packet
910
from pyp0f.net.signatures import MTUPacketSignature
1011
from pyp0f.options import OPTIONS, Options
1112

1213

13-
def valid_for_fingerprint(packet: Packet) -> bool:
14+
def valid_for_mtu_fingerprint(packet: Packet) -> bool:
1415
"""
1516
Check if the given packet is valid for MTU fingerprint.
16-
Packets with MSS value are valid for fingerprint.
17+
SYN/SYN+ACK packets with MSS value are valid for fingerprint.
1718
"""
18-
return packet.should_fingerprint and packet.tcp.options.mss > 0
19+
return (
20+
packet.should_fingerprint
21+
and packet.tcp.options.mss > 0
22+
and packet.tcp.type
23+
in (
24+
TCPFlag.SYN,
25+
TCPFlag.SYN | TCPFlag.ACK,
26+
)
27+
)
1928

2029

21-
def signatures_match(
30+
def mtu_signatures_match(
2231
signature: MTUSignature, packet_signature: MTUPacketSignature
2332
) -> bool:
2433
"""
@@ -27,23 +36,19 @@ def signatures_match(
2736
return signature.mtu == packet_signature.mtu
2837

2938

30-
def find_match(
39+
def find_mtu_match(
3140
packet_signature: MTUPacketSignature, database: Database
3241
) -> Optional[MTURecord]:
3342
"""
3443
Search through the database for a match for the given MTU signature.
3544
"""
36-
return next(
37-
(
38-
mtu_record
39-
for mtu_record in database.iter_values(MTURecord)
40-
if signatures_match(mtu_record.signature, packet_signature)
41-
),
42-
None,
43-
)
45+
for mtu_record in database.iter_values(MTURecord):
46+
if mtu_signatures_match(mtu_record.signature, packet_signature):
47+
return mtu_record
48+
return None
4449

4550

46-
def fingerprint(packet: PacketLike, options: Options = OPTIONS) -> MTUResult:
51+
def fingerprint_mtu(packet: PacketLike, *, options: Options = OPTIONS) -> MTUResult:
4752
"""
4853
Fingerprint the given packet for MTU.
4954
@@ -57,13 +62,16 @@ def fingerprint(packet: PacketLike, options: Options = OPTIONS) -> MTUResult:
5762
Returns:
5863
MTU fingerprint result
5964
"""
60-
parsed_packet = parse_packet(packet)
65+
packet = parse_packet(packet)
6166

62-
if not valid_for_fingerprint(parsed_packet):
63-
raise PacketError("Packet is invalid for MTU fingerprint")
67+
if not valid_for_mtu_fingerprint(packet):
68+
raise PacketError(
69+
"Packet is invalid for MTU fingerprint. "
70+
"Packet must be SYN/SYN+ACK with MSS value."
71+
)
6472

65-
packet_signature = MTUPacketSignature.from_packet(parsed_packet)
73+
packet_signature = MTUPacketSignature.from_packet(packet)
6674

6775
return MTUResult(
68-
parsed_packet, packet_signature, find_match(packet_signature, options.database)
76+
packet, packet_signature, find_mtu_match(packet_signature, options.database)
6977
)

pyp0f/fingerprint/results/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,16 @@
22
from .http import HTTPResult
33
from .mtu import MTUResult
44
from .tcp import TCPMatch, TCPMatchType, TCPResult
5+
from .uptime import BAD_TPS, Uptime, UptimeResult
56

6-
__all__ = ["Result", "MTUResult", "TCPResult", "HTTPResult", "TCPMatchType", "TCPMatch"]
7+
__all__ = [
8+
"Result",
9+
"MTUResult",
10+
"TCPResult",
11+
"HTTPResult",
12+
"TCPMatchType",
13+
"TCPMatch",
14+
"Uptime",
15+
"UptimeResult",
16+
"BAD_TPS",
17+
]

pyp0f/fingerprint/results/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@
1414
@dataclass
1515
class Result(Generic[TMatch, TSignature], metaclass=ABCMeta):
1616
"""
17-
Fingerprint result
17+
Fingerprint result.
1818
"""
1919

2020
packet: Packet
21-
"""Origin packet"""
21+
"""Origin packet."""
2222

2323
packet_signature: TSignature
24-
"""Origin packet signature"""
24+
"""Origin packet signature."""
2525

2626
match: Optional[TMatch] = None
27-
"""Fingerprint match, if any"""
27+
"""Fingerprint match, if any."""

pyp0f/fingerprint/results/http.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
@dataclass
1313
class HTTPResult(Result[HTTPRecord, HTTPPacketSignature]):
1414
"""
15-
HTTP fingerprint result
15+
HTTP fingerprint result.
1616
"""
1717

1818
packet: BufferLike
19+
"""Origin HTTP payload."""
20+
1921
dishonest: bool = field(init=False)
22+
"""Software string (User-Agent or Server) looks forged?"""
2023

2124
def __post_init__(self):
2225
self.dishonest = (

pyp0f/fingerprint/results/mtu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
@dataclass
1212
class MTUResult(Result[MTURecord, MTUPacketSignature]):
1313
"""
14-
MTU fingerprint result
14+
MTU fingerprint result.
1515
"""

pyp0f/fingerprint/results/tcp.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,30 @@ class TCPMatchType(Enum):
1818
@dataclass
1919
class TCPMatch:
2020
"""
21-
Match for a TCP fingerprint
21+
Match for a TCP fingerprint.
2222
"""
2323

2424
type: TCPMatchType
25-
"""Match type"""
25+
"""Match type."""
2626

2727
record: TCPRecord
28-
"""Matched record"""
28+
"""Matched record."""
2929

3030
@property
3131
def is_fuzzy(self) -> bool:
32+
"""Approximate match?"""
3233
return self.type != TCPMatchType.EXACT
3334

3435

3536
@add_slots
3637
@dataclass
3738
class TCPResult(Result[TCPMatch, TCPPacketSignature]):
3839
"""
39-
TCP fingerprint result
40+
TCP fingerprint result.
4041
"""
4142

4243
distance: int = field(init=False)
43-
"""Estimated distance (TTL)"""
44+
"""Estimated distance (TTL)."""
4445

4546
def __post_init__(self):
4647
self.distance = (
@@ -52,7 +53,7 @@ def __post_init__(self):
5253

5354
def guess_distance(ttl: int) -> int:
5455
"""
55-
Guess number of hops (distance) for a given packet ttl.
56+
Figure out what the TTL distance might have been for a packet.
5657
"""
5758
return next(
5859
(initial_ttl - ttl for initial_ttl in (32, 64, 128) if ttl <= initial_ttl),

0 commit comments

Comments
 (0)