DOing More Harm: Part 2

Jun 18, 2022

11 mins read

Where we last left off, I had done an initial reverse engineering pass of Windows Update Delivery Optimization see: DOing Harm. I got familiar with the protocol, how peer discovery works, etc… but mainly only looked at the first handshake as that was most interesting to me at the time.

This time let’s dig a little deeper: Taking a look at how many systems are running this service on the public internet, the protocol itself, and doing a bit of fuzzing.

How many systems on the internet are running this?

As a quick Shodan.io query will show, 4 IP’s have this port open and are running a service that will respond to Shodan’s scanner.

shodan port 7680

Now, as someone who works with internet wide scanning on a daily basis, this number clearly is not accurately representing the number of systems with TCP port 7680. Rather, it’s representing the number of systems on the internet with port 7680 open that also are running a service that Shodan’s scanner can neogotiate.

So let’s do an inventory ourselves!

I’m familiar with zmap which

With a 10gigE connection and PF_RING, ZMap can scan the IPv4 address space in 5 minutes.

Now, while it would be really cool to have my answer in 5 minutes, there’s a lot of reasons that unless you’ve worked with you upstream provider’s they would probably shut you down almost instantly.

sudo zmap -B 3M -p 7680 -o results.csv

A much more modest scan at 3Mbps is started and I forget about it for a while and just let it chug away.

… 9 days later …

remy@remy-XPS-13-9310:~$ wc -l results.csv 
10726 results.csv

We have a list of ~10k IP addresses with TCP port 7680 now. Let’s enrich them with rDNS information!

There’s a million tools for doing this, but I’m in a lazy mode so https://pkg.go.dev/net#LookupAddr handles it just fine in a quick little program.

Of these ~10k IP’s, we can do a check of how many of them resolved to a certain TLD with a query on the SQLite database I threw the results into.

.mil TLD

Here we can see that 2372 of these IP’s with port 7680 have a .mil rDNS entry. I’m not keen on the miniscule chance of explaining at a future date “I was simply pissing in your ports to see if it had Roblox installed” to anyone operating a .mil site. I’m just going to remove all of those IP’s from the list and pretend they don’t exist.

This leaves us with 8354 IP’s.

Service Scanning

As mentioned in DOing Harm the following is a completely valid payload to ask the Windows Update Delivery Optimization (WUDO) service if it has the needed infohash for a copy of Roblox.

00000000: 0e50 4953 5350 4953 5350 4953 5350 4953  .PISSPISSPISSPIS
00000010: 5350 4953 5350 49d9 a589 6d90 2667 c875  SPISSPI...m.&g.u
00000020: bc5d 7cfe 8732 36f3 9ce5 a01e 11f2 7bfe  .]|..26.......{.
00000030: 5f18 feb7 fe23 f450 4953 5350 4953 5350  _....#.PISSPISSP
00000040: 4953 5350 4953 5350 4953 53              ISSPISSPISS

Also mentioned in the previous blog, I stated that if the above payload is sent only containing the first 31 bytes of the infohash WUDO will not close the connection. Upon sending the 32nd byte of the infohash, the WUDO service will close the connection. This behavior allows us to make a nice little service scanner to check whether the service listening on the port is actually WUDO.

The code looks like this:

import socket
import time
from tqdm import tqdm

message = ''
message += '0e50 4953 5350 4953 5350 4953 5350 4953'.replace(' ', '')
message += '5350 4953 5350 49d9 a589 6d90 2667 c875'.replace(' ', '')
message += 'bc5d 7cfe 8732 36f3 9ce5 a01e 11f2 7bfe'.replace(' ', '')
message += '5f18 feb7 fe23                         '.replace(' ', '')
message = bytes.fromhex(message)

def logWudo(ip):
    with open('wudo.txt', 'a') as f:
        f.write(ip+ '\n')

def scanip(ip):
    server_address = (ip, 7680)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(3)

    try:
        sock.connect(server_address)
        sock.sendall(message)
        sock.sendall(b'\xff')
        reply = sock.recv(1)
        #Socket was closed on 32nd byte of infohash
        if not reply:
            print("Real WUDO Service: " + ip)
            logWudo(ip)
    except:
        pass

def main():
    ips = []
    with open('no_mil.csv', 'r') as f:
        for ip in f:
            stripped_ip = ip.replace('\n', '')
            ips.append(stripped_ip)
    
    for ip in tqdm(ips):
        scanip(ip)

main()

And so… let’s fire it off! It’s not multi-threaded and has a default timeout of 3 seconds, so the current estimate says ~6 hours. This is fine because I’d like to have a beer and go read some documentation on 802.11 and WiFi-Direct specifications.

…the next evening…

We successfully detected 1245 instances of Windows Update Delivery Optimization (WUDO) service running on the public internet.

510 of them have an rDNS record. 735 of them do not.

1245 is not a big number of IP’s to be running a service on the public internet, but it isn’t particularly small either. I wont’ lie that i was kind of hoping for a much larger number, but I expect that the number of hosts with this port open fluctuate a lot depending on the day.

Additionally, while I checked the public internet, it’s the Windows default that this service be available on LAN, so that’s still worth investigating.

Reversing Engineering the Protocol

I had intended to reverse the protocol as part of the previous blog, but life happens and I simply didn’t have the time (and that blog was getting rather lengthy). I told myself “I’ll do that for the next blog.”

Then, I recieved a direct message on Twitter from Paolo @pmontesel:

Hey,
cool stuff on the Windows Update thing (: came across its weird traffic on our network and found 
nobody reversed it yet... 

Well, except you that is :D

I spent few hours looking at the code and have the TCP protocol reversed (it's pretty basic).
I can share it if you want.
  1. This makes me incredibly happy to read. This is why I write these blog posts. It’s a lot of extra effort to write a blog when I really just want to move on with the research.

  2. This saves me a lot of time. When I finally get my son to bed for the night I can just jump right into WUDO!

Let’s dig in!

Paolo’s WUDO Reversing notes are available here: https://github.com/thebabush/wudo-reversing/blob/main/README.md

Handshake

First we’ll review the Handshake, the same part of the protocol I spent the most time looking at in the previous blog. I’ll lay down relevant sections of the protcol alongside his notes (in block quotes).

According to Paolo’s notes:

The handshake packet is the only one that follows a different structure. It starts with a protocol “magic”:

htonl(len("Swarm protocol")) || "Swarm protocol"

Example:

00000000: 0e 53 77 61 72 6d 20 70 72 6f 74 6f 63 6f 6c    .Swarm protocol

where 0e is 14, and Swarm protocol is 14 bytes in length.

After the magic, comes the protocol version: 00 00 00 01 00 00 00 00.

This looks like version 1.0.0.0 encoded in a funny way.

This is followed by the 32 bytes of infohash / swarm hash which identifies the file requested for download from a peer.

Finally, the peer id (20 bytes).

I’m really glad Paolo figured this out because I was really confused by this.

And finally putting it all together:

00000000: 0e53 7761 726d 2070 726f 746f 636f 6c00  .Swarm protocol.
00000010: 0000 0000 1000 00d9 a589 6d90 2667 c875  ..........m.&g.u
00000020: bc5d 7cfe 8732 36f3 9ce5 a01e 11f2 7bfe  .]|..26.......{.
00000030: 5f18 feb7 fe23 f400 0102 0304 0506 0708  _....#..........
00000040: 090a 0b0c 0d0e 0f10 1112 13              ...........
  • 0e
    • Size: 1
    • htonl(len(“Swarm protocol”))
  • 537761726d2070726f746f636f6c
    • Size: 14
    • “Swarm protocol”
  • 0000000100000000
    • Size: 8
    • Version (1.0.0.0)
  • d9a5896d902667c875bc5d7cfe873236f39ce5a01e11f27bfe5f18feb7fe23f4
    • Size: 32
    • infohash of file (SHA256)
  • 000102030405060708090a0b0c0d0e0f10111213
    • Size: 20
    • Peer ID

Other Messages

Paolo lists several other types of messages including:

  • Keep Alive
  • Choke
  • Unchoke
  • Interested
  • Not Interested
  • Have Block
  • Bit Field
  • Cancel Block
  • Request Block
  • Start Block

Honestly, Paolo has covered just about everything I was interested in reversing. At the top of his notes he lists:

TODO: Kaitai Struct of the protocol

I love Kaitai! I’ve used it in several of my previous blogs and it’s my preferred first step for laying out the structure of a binary stream. However, Paolo’s notes are wonderful and have given me a huge advantage. Writing a kaitai struct almost feels like cheating. I’ve never written a wireshark dissector before and I intend to fuzz the protocol, so that seems like a good route to go down.

Writing a Wireshark dissector

While the internet-wide scan at the top of the blog was running, I reached out to a friend Yuu who was nice enough to drop a link to a repo he had of 2 Wireshark dissectors, one TCP and one UDP.

So, uh, I guess here we go? Disclaimer: I don’t know Lua.

Navigating the Wireshark dialog Help –> About Wireshark –> Folders shows where lua scripts are loaded from.

After some fiddling about and learning how to use the Wireshark debug console, most of my mistakes were due to not knowing how to write Lua, rather than difficulties with writing a dissector itself (Go figure).

I end up with the following basic dissector msdo.lua:

msdo_protocol = Proto("MSDO",  "Microsoft Delivery Optimization Protocol")

magic_length   = ProtoField.uint8("magic_length", "Magic Length", base.DEC)
magic_string   = ProtoField.string("magic_string", "Magic String")
version        = ProtoField.bytes("version", "Version", base.NONE)
infohash       = ProtoField.bytes("infohash", "Infohash", base.NONE)
peer_id        = ProtoField.bytes("peer_id", "Peer ID", base.NONE)

msdo_protocol.fields = {magic_length, magic_string, version, infohash, peer_id }

function msdo_protocol.dissector(buffer, pinfo, tree)
  length = buffer:len()
  if length == 0 then return end

  pinfo.cols.protocol = msdo_protocol.name

  local subtree = tree:add(msdo_protocol, buffer(), "MSDO Protocol Data")

  offset = 0

  --print("Magic Length: " .. buffer(offset,1))
  subtree:add_le(magic_length, buffer(offset,1))
  local length_of_magic_string = buffer(offset, 1):uint()
  offset = offset + 1

  --print("Magic String: " .. buffer(offset, length_of_magic_string))
  subtree:add_le(magic_string, buffer(offset, length_of_magic_string))
  offset = offset + length_of_magic_string

  --print("Version: " .. buffer(offset, 8))
  subtree:add_le(version, buffer(offset, 8))
  offset = offset + 8

  --print("Infohash: " .. buffer(offset, 32))
  subtree:add_le(infohash, buffer(offset, 32))
  offset = offset + 32

  --print("Peer ID: " .. buffer(offset, 20))
  subtree:add_le(peer_id, buffer(offset, 20))
  offset = offset + 20

end

local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(7680, msdo_protocol)


MSDO Wireshark Dissector

While this just dissects the first handshake, this is perfect to use with a fuzzer as typically (hopefully) bugs are found in the intial handshake. Bugs deeper within the protocol are usually a little harder to reach, so we’ll fill out the rest of the dissector if we don’t find anything interesting from just fuzzing the handshake.

Fuzzing

<insert 3 weeks of fuzzing here>

In part 1 of the blog, I’d managed to crash the DoSvc easily several times by fuzzing on a VM running Windows 10 Stable 1809 with radamsa and running the fuzzer from a raspi.

I made some optimizations this time around:

  • Fully updated Windows 10, Version 21H2 on a phsyical dedicated machine
  • Ran fuzzer on the same machine and fuzzed 127.0.0.1:7680 to reduce network latency
  • A health check service that monitors that the PID of DoSvc is still the same
  • A rotating wireshark capture so I always had recent network logs
  • Fuzzing with radamsa as well as boofuzz at the same time to cover “structured” fuzzing as well as “throw shit at the wall and see what sticks” fuzzing

Zero crashes or restarts.

Now clearly this can mean several things:

  • Maybe I just got incredibly lucky several times in a row the first time?
  • Maybe whatever bug I was triggering has been patched since Version 1809?
  • Maybe the DoSvc hits a different code path for private/local IP space and I can’t trigger it this way?
  • Maybe I’m just really bad at this?

Luckily my failure to reproduce a crash itself provided some clear direction to go in for next time (if there is a next time).

As fate would have it, Binary Golf Grand Prix 3 (BGGP3) was just announced. The theme this year is “crash”:

The goal of the 3rd Annual Binary Golf Grand Prix (BGGP3) is to find the smallest file which will crash a specific program.

With that, I’ll be wrapping up this edition of the blog and heading off to participate in BGGP3 which has structured rules and objectives, while dealing with the same tools and methodologies I’ll need if I’m to pursue MSDO further.

Hope you enjoyed the read

-remy

Sharing is caring!