Which attacks does PKCE defend against, and why do public clients in particular need it?
Authorization code interception and code injection, and it also provides CSRF protection; public clients need it because they have no client secret, so without PKCE anyone who obtains their code could redeem it.
PKCE was originally designed for mobile apps. On a phone, a malicious app can register the same custom URL scheme as the legitimate app and receive the redirect containing the code (code interception). Since the legitimate app is a public client with no secret, the attacker can redeem the stolen code at the token endpoint just as well as the real app could. With PKCE, the attacker lacks the code verifier, so the stolen code is useless.
PKCE also defeats authorization code injection: an attacker who stole a victim's code tries to slip it into their own session with the client, so the client logs the attacker in as the victim. The client sends the verifier from its own session, which does not match the challenge bound to the stolen code, so the exchange fails. For the same reason it gives CSRF protection: a forged callback carrying a code that was not started in this session cannot be redeemed.
That is why the current security best practice recommends PKCE for all clients, confidential ones included, and OAuth 2.1 requires it for every client that uses the authorization code flow.
Go deeper:
RFC 7636, Section 1: Introduction — the interception attack on public clients that PKCE was designed to stop.
RFC 9700, Section 4.5: Authorization Code Injection — the injection attack and why PKCE is the main countermeasure.