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.
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.
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.
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.
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.
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.
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.
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
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
537761726d2070726f746f636f6c
0000000100000000
d9a5896d902667c875bc5d7cfe873236f39ce5a01e11f27bfe5f18feb7fe23f4
000102030405060708090a0b0c0d0e0f10111213
Paolo lists several other types of messages including:
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.
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)
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.
<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:
127.0.0.1:7680
to reduce network latencyDoSvc
is still the sameradamsa
as well as boofuzz
at the same time to cover “structured” fuzzing as well as “throw shit at the wall and see what sticks” fuzzingZero crashes or restarts.
Now clearly this can mean several things:
DoSvc
hits a different code path for private/local IP space and I can’t trigger it this way?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!