Skip to content

tcp: remove bias from the randomised initial sequence number - #408

Open
tinic wants to merge 1 commit into
eclipse-threadx:masterfrom
tinic:amiga-tcp-isn-entropy
Open

tcp: remove bias from the randomised initial sequence number#408
tinic wants to merge 1 commit into
eclipse-threadx:masterfrom
tinic:amiga-tcp-isn-entropy

Conversation

@tinic

@tinic tinic commented Jul 27, 2026

Copy link
Copy Markdown

_nxd_tcp_client_socket_connect() and _nx_tcp_server_socket_accept() compose a fresh ISN from two random draws with a bitwise OR:

socket_ptr -> nx_tcp_socket_tx_sequence =  (((ULONG)NX_RAND()) << NX_SHIFT_BY_16) & 0xFFFFFFFF;
socket_ptr -> nx_tcp_socket_tx_sequence |= (ULONG)NX_RAND();

The two draws overlap in bits 16-30, and an OR of two random bits is 1 with probability 3/4. rand() returns 0..RAND_MAX, so with the usual RAND_MAX of 0x7FFFFFFF the ISN has 17 bits at P(1) = 1/2 and 15 bits at P(1) = 3/4. That is 29.2 bits of Shannon entropy and 17 + 15*log2(4/3) = 23.2 bits of min-entropy: the likeliest ISN comes up 2^(32-23.2) = 438 times more often than under a uniform distribution, and an attacker who enumerates the dense upper values first faces 2^23 rather than 2^32.

This is a property of the composition, not of the generator. A port that points NX_RAND at a CSPRNG loses the same nine bits, because the loss is entirely in the overlap between the two draws.

Adding instead of ORing removes it, and matches the else branch immediately below -- the path every reused socket takes, which already adds. With a 31-bit draw the sum is exactly uniform over the whole 32-bit space: (a << 16) is uniform over the 65536 multiples of 65536, and adding an independent uniform value over [0, 2^31) leaves every 32-bit residue equally likely. Measured over 2e7 samples of a 31-bit uniform source:

           Shannon   min-entropy   likeliest ISN vs uniform
with |      29.168        23.220                      440x
with +      32.000        31.991                        1x

The consequence is blind in-window injection: an off-path attacker needs a sequence number inside the receive window to land a spoofed RST or segment, and nine bits of bias is nine bits fewer guesses. RFC 6528 asks that an ISN not be predictable. This does not implement 6528's four-tuple hash; it only removes a bias in a value the code already meant to be random.

Found while auditing SYN traces from an m68k AmigaOS port: bits 16-30 were set in 35 of 45 SYNs (0.78, predicted 0.75) before the change and 69 of 135 (0.51, predicted 0.50) after.

_nxd_tcp_client_socket_connect() and _nx_tcp_server_socket_accept() compose a
fresh ISN from two random draws with a bitwise OR:

    socket_ptr -> nx_tcp_socket_tx_sequence =  (((ULONG)NX_RAND()) << NX_SHIFT_BY_16) & 0xFFFFFFFF;
    socket_ptr -> nx_tcp_socket_tx_sequence |= (ULONG)NX_RAND();

The two draws overlap in bits 16-30, and an OR of two random bits is 1 with
probability 3/4. rand() returns 0..RAND_MAX, so with the usual RAND_MAX of
0x7FFFFFFF the ISN has 17 bits at P(1) = 1/2 and 15 bits at P(1) = 3/4. That
is 29.2 bits of Shannon entropy and 17 + 15*log2(4/3) = 23.2 bits of
min-entropy: the likeliest ISN comes up 2^(32-23.2) = 438 times more often
than under a uniform distribution, and an attacker who enumerates the dense
upper values first faces 2^23 rather than 2^32.

This is a property of the composition, not of the generator. A port that
points NX_RAND at a CSPRNG loses the same nine bits, because the loss is
entirely in the overlap between the two draws.

Adding instead of ORing removes it, and matches the else branch immediately
below -- the path every reused socket takes, which already adds. With a
31-bit draw the sum is exactly uniform over the whole 32-bit space:
(a << 16) is uniform over the 65536 multiples of 65536, and adding an
independent uniform value over [0, 2^31) leaves every 32-bit residue equally
likely. Measured over 2e7 samples of a 31-bit uniform source:

               Shannon   min-entropy   likeliest ISN vs uniform
    with |      29.168        23.220                      440x
    with +      32.000        31.991                        1x

The consequence is blind in-window injection: an off-path attacker needs a
sequence number inside the receive window to land a spoofed RST or segment,
and nine bits of bias is nine bits fewer guesses. RFC 6528 asks that an ISN
not be predictable. This does not implement 6528's four-tuple hash; it only
removes a bias in a value the code already meant to be random.

Found while auditing SYN traces from an m68k AmigaOS port: bits 16-30 were
set in 35 of 45 SYNs (0.78, predicted 0.75) before the change and 69 of 135
(0.51, predicted 0.50) after.

Signed-off-by: Tinic Uro <tinicuro@gmail.com>
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.

1 participant