Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[inet6] recognize unknown router advertisement options #4233

Merged
merged 1 commit into from
Jan 28, 2024

Conversation

evverx
Copy link
Contributor

@evverx evverx commented Jan 26, 2024

According to https://www.rfc-editor.org/rfc/rfc4861.html#section-4.6

All options are of the form:

        0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |     Type      |    Length     |              ...              |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

where

      Length         8-bit unsigned integer.  The length of the option
                     (including the type and length fields) in units of
                     8 octets.

This patch makes it possible to recognize/generate unknown options by introducing a new field where lengths are calculated according to the RFC and options are padded with zeroes if necessary. By default trailing zeroes aren't stripped to make it easier to process options where trailing zeroes can be mixed up with actual data (like the encrypted DNS option for example). The captive portal option was switched to the new field too but there trailing zeroes are still stripped.

fuzz(ICMPv6NDOptUnknown(type=...)) is much more effective now because it generates options valid enough to get past basic checks but nonsensical enough to trigger interesting issues like
systemd/systemd#30952 (comment)
systemd/systemd#30952 (comment)

ICMPv6NDOptUnknown is also used instead of Raw to guess payloads so router advertisements can be parsed even when unknown options pop up in the middle.

The patch was also cross-checked with Wireshark:

>>> tdecode(Ether()/IPv6()/ICMPv6ND_RA()/ICMPv6NDOptSrcLLAddr()/ICMPv6NDOptUnknown(data='watwatwat')/ICMPv6NDOptCaptivePortal())
...
    ICMPv6 Option (Source link-layer address : 00:00:00:00:00:00)
        Type: Source link-layer address (1)
        Length: 1 (8 bytes)
        Link-layer address: 00:00:00_00:00:00 (00:00:00:00:00:00)
    ICMPv6 Option (Unknown 0)
        Type: Unknown (0)
        Length: 2 (16 bytes)
        [Expert Info (Note/Undecoded): Dissector for ICMPv6 Option (0) code not implemented, Contact Wireshark developers if you want this supported]
            [Dissector for ICMPv6 Option (0) code not implemented, Contact Wireshark developers if you want this supported]
            [Severity level: Note]
            [Group: Undecoded]
        Data: 7761747761747761740000000000
    ICMPv6 Option (DHCP Captive-Portal)
        Type: DHCP Captive-Portal (37)
        Length: 1 (8 bytes)
        Captive Portal:

@evverx
Copy link
Contributor Author

evverx commented Jan 26, 2024

The CI seems to be failing because cryptography switched to Rust in pyca/cryptography@ba9131e and the _p attribute was removed. I pinned it to 41.0.7 in evverx#2 and all the tests passed there.

Copy link

codecov bot commented Jan 26, 2024

Codecov Report

Merging #4233 (14f928f) into master (ae79fcb) will decrease coverage by 0.01%.
The diff coverage is 100.00%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4233      +/-   ##
==========================================
- Coverage   81.77%   81.76%   -0.01%     
==========================================
  Files         331      331              
  Lines       76716    76722       +6     
==========================================
  Hits        62731    62731              
- Misses      13985    13991       +6     
Files Coverage Δ
scapy/layers/inet6.py 88.58% <100.00%> (+0.03%) ⬆️

... and 6 files with indirect coverage changes

@evverx evverx marked this pull request as draft January 26, 2024 01:53
@evverx evverx marked this pull request as ready for review January 26, 2024 01:53
@evverx evverx force-pushed the inet6-unknown-ra-option branch from 395884a to 6a68c74 Compare January 26, 2024 10:08
@evverx evverx marked this pull request as draft January 26, 2024 10:13
@evverx

This comment was marked as outdated.

@evverx evverx force-pushed the inet6-unknown-ra-option branch 2 times, most recently from eb6be56 to af571ba Compare January 27, 2024 00:45
@evverx evverx marked this pull request as ready for review January 27, 2024 00:47
@evverx evverx force-pushed the inet6-unknown-ra-option branch 2 times, most recently from ea33e99 to 7f503a0 Compare January 27, 2024 01:44
According to https://www.rfc-editor.org/rfc/rfc4861.html#section-4.6
All options are of the form:

        0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |     Type      |    Length     |              ...              |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

where
      Length         8-bit unsigned integer.  The length of the option
                     (including the type and length fields) in units of
                     8 octets.

This patch makes it possible to recognize/generate unknown options by
introducing a new field where lengths are calculated according to the
RFC and options are padded with zeroes if necessary. By default trailing
zeroes aren't stripped to make it easier to process options where
trailing zeroes can be mixed up with actual data (like the encrypted DNS
option for example). The captive portal option was switched to the new
field too but there trailing zeroes are still stripped.

fuzz(ICMPv6NDOptUnknown(type=...)) is much more effective now because it
generates options valid enough to get past basic checks but nonsensical
enough to trigger interesting issues like
systemd/systemd#30952 (comment)
systemd/systemd#30952 (comment)

ICMPv6NDOptUnknown is also used instead of Raw to guess payloads so
router advertisements can be parsed even when unknown options pop up in
the middle.

The patch was also cross-checked with Wireshark:
```
>>> tdecode(Ether()/IPv6()/ICMPv6ND_RA()/ICMPv6NDOptSrcLLAddr()/ICMPv6NDOptUnknown(data='watwatwat')/ICMPv6NDOptCaptivePortal())
...
    ICMPv6 Option (Source link-layer address : 00:00:00:00:00:00)
        Type: Source link-layer address (1)
        Length: 1 (8 bytes)
        Link-layer address: 00:00:00_00:00:00 (00:00:00:00:00:00)
    ICMPv6 Option (Unknown 0)
        Type: Unknown (0)
        Length: 2 (16 bytes)
        [Expert Info (Note/Undecoded): Dissector for ICMPv6 Option (0) code not implemented, Contact Wireshark developers if you want this supported]
            [Dissector for ICMPv6 Option (0) code not implemented, Contact Wireshark developers if you want this supported]
            [Severity level: Note]
            [Group: Undecoded]
        Data: 7761747761747761740000000000
    ICMPv6 Option (DHCP Captive-Portal)
        Type: DHCP Captive-Portal (37)
        Length: 1 (8 bytes)
        Captive Portal:
```
guedou
guedou previously approved these changes Jan 27, 2024
test/scapy/layers/inet6.uts Show resolved Hide resolved
@guedou
Copy link
Member

guedou commented Jan 27, 2024

Really cool PR! Thanks.

@guedou guedou merged commit 5a1abdc into secdev:master Jan 28, 2024
22 checks passed
evverx added a commit to evverx/scapy that referenced this pull request Mar 1, 2024
Without this patch the type of Neighbor Discovery options generated by
fuzz(ICMPv6NDOptUnknown()) is always 0. With this patch applied option
types are random.

It's a follow-up to secdev#4233
guedou pushed a commit that referenced this pull request Mar 1, 2024
Without this patch the type of Neighbor Discovery options generated by
fuzz(ICMPv6NDOptUnknown()) is always 0. With this patch applied option
types are random.

It's a follow-up to #4233
@@ -1737,18 +1737,41 @@ class _ICMPv6NDGuessPayload:

def guess_payload_class(self, p):
if len(p) > 1:
return icmp6ndoptscls.get(orb(p[0]), Raw) # s/Raw/ICMPv6NDOptUnknown/g ? # noqa: E501
return icmp6ndoptscls.get(orb(p[0]), ICMPv6NDOptUnknown)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would maybe be useful if the known options that can't be parsed properly turned into ICMPv6NDOptUnknown or something like that by analogy with tshark showing options and their bytes. One recent example where it could have helped would be https://lists.debian.org/debian-security-announce/2024/msg00123.html (which was (partly) powered by scapy FWIW). I'll try to figure out if it makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants