A simpler solution is that the car should not accept a "skipped over" code. This attacks works by jamming code A and storing it for later use. The car owner assumes a signal loss and clicks again, opening their car with code B. If the car knows to reject code B in the future, why can't it also know to reject code A?
This is effectively how a rolling code works. You have a space of say, 2^16 keys that repeats. The car and keyfob share the same index to the keyspace, and increment one for every transaction.
If someone tried to "replay" an older key, the car would reject it. However, what if the user accidentally presses the button while not in range of the car?
To account for this, the system will "partially accept" keys "ahead of schedule" for about half the keyspace. The car advances its pointer (let's say N+5), and waits for N+6 to unlock.
Since most older keyfobs are simply "one way transmitters", PKI is out of the question. Newer keyfobs are able to receive as well, and could preform a handshake. However, car manufacturers are notoriously bad at security.
The car isn't being opened by B; it's being opened by A, and B is jammed and stored for later use. See other comment about the attacker always being "one [code] ahead" of the legitimate key fob user.
That's not how the attack works though: the fob sends A, but the car doesn't see it because of the jamming. The fob then sends B, and the car still doesn't see it. Then the attacker stops jamming the car's receiver, and replays A, so the doors open. B is stored by the attacker to replay later.
This would break if you once press unlock while out of range of the car, unless you have some way to reset the counter - in which case you start creating complex stuff to handle complex situations.
Not necessarily. The car could accept future codes, but not an older one. Assume codes A through Z, you've used A, B, and C, then when out of range of the vehicle you send codes D and E (Or these are jammed and stored for later use). That afternoon, you return to your car and issue code F, now codes D and E are no longer accepted (Until you get so far down the line that looping becomes a concern)