As described in this answer, I followed these steps to get a trace of the network stack:

netsh trace start provider=Microsoft-Windows-TCPIP level=5 keywords=ut:TcpipRoute ping -n 1 www.google.com netsh trace stop netsh trace convert %TEMP%\NetTraces\NetTrace.etl

The resulting text file contained the following line:

[2]0910.3710::2019-08-10 01:27:15.198580000 [Microsoft-Windows-TCPIP]IP: Address pair (::ffff:10.0.12.67, ::ffff:172.217.7.132) is preferred over (fd85:741f:6df1:212:50df:dc26:f469:4d4c, 2607:f8b0:4004:800::2004) by SortOptions: 0, Reason: Prefer Matching Label (Rule D 5.0).

The key part of that is

Reason: Prefer Matching Label

This is described in RFC 3484 Section 5 "Source Address Selection". Basically, prefixes can have "labels", and a source/destination address pair where the source and destination labels match is preferred over a pair where they do not match.

I can see the prefix/label mappings on my computer by running

netsh interface ipv6 show prefixpolicies

I get

Precedence Label Prefix ---------- ----- -------------------------------- 50 0 ::1/128 40 1 ::/0 35 4 ::ffff:0:0/96 30 2 2002::/16 5 5 2001::/32 3 13 fc00::/7 1 12 3ffe::/16 1 11 fec0::/10 1 3 ::/96

As my source IPv6 address, fd85:741f:6df1:212:50df:dc26:f469:4d4c, is a ULA address (I'm using NPt to allow failover between two different WAN connections), it falls within fc00::/7 and gets a label of 13. My destination address is within ::/0 and gets a label of 1. Those do not match, hence preferring IPv4 where they both fall within ::ffff:0:0/96 and get a label of 4.

To fix this, I just need to add a prefix policy that sets my source address's label to 1. I can do that by running the following command in an administrative command prompt:

netsh interface ipv6 add prefixpolicy fd00::/8 3 1

That adds a policy for fd00::/8 (the entire ULA prefix) which has a precedence of 3 and a label of 1. Here's the updated prefix policy table:

Precedence Label Prefix ---------- ----- -------------------------------- 50 0 ::1/128 40 1 ::/0 35 4 ::ffff:0:0/96 30 2 2002::/16 5 5 2001::/32 3 13 fc00::/7 3 1 fd00::/8 1 12 3ffe::/16 1 11 fec0::/10 1 3 ::/96

And now when I ping a dual-stack hostname, it prefers the IPv6 address.