When dealing with a protocol where the source code and implementation are secret to us,we need to do certain kinds of testing to find out how to format data appropriately, as well as determine what can change behavior, what can be ignored, and what we need to pay attention to.
We want to poke at SessionID a bit, lets try to login repeatedly to get an idea of SessionIDs values. For this example, we login to the camera, then send a command, then print the SessionID.
0 = 0x00000001
1 = 0x00000002
2 = 0x00000003
3 = 0x00000004
4 = 0x00000005
5 = 0x00000006
6 = 0x00000007
7 = 0x00000008
8 = 0x00000009
[Done] exited with code=0 in 1.025 seconds
Since SessionID acts like a simple incrementer, we can easily write our own values. Getting an idea of how the camera might produce it's SessionID fields may help us later to develop an anti-client.
Let's make an attempt to send another command after logging in, we first will try to replay a commands SessionID, then try to replay the command with a randomized SessionID. This will give us greater insight into what works, and what doesn't.
For this command I'll be using SystemInfo which I found while sniffing! This one is used by the client to get a list of some settings and version numbers. Playing around with the SessionID field shows the camera doesn't care about it. I tried 0x00000007, 0x11111117, and a couple other random numbers and they all seemed to work!
There were also a couple values in the 20 byte header that I couldn't decipher, so I wrote a fuzzer to see if there was anything interesting about them. The results I got were, strange, to say the least. I used this method to make a basic command, including the header.
# Class that contains the basic process for making a message to/from the camera
class XMMessage
property type : UInt32
property session_id : UInt32
property unknown1 : UInt32
property unknown2 : UInt16
property magic : UInt16
property size : UInt32
property message : String
#TODO: Allow for spoofing of size, for example changing size to say that its 32 bytes, when its 0 or something
def self.from_s(string)
io = IO::Memory.new string
m = XMMessage.new
m.type = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
m.session_id = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
m.unknown1 = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
m.unknown2 = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian)
m.magic = io.read_bytes(UInt16, IO::ByteFormat::LittleEndian)
m.size = io.read_bytes(UInt32, IO::ByteFormat::LittleEndian)
m.message = string[20..string.size]
m
end
def initialize(@type = 0x000001ff_u32, @session_id = 0_u32, @unknown1 = 0_u32, @unknown2 = 0_u16, @magic = 0_u16, @size = 0_u32, @message = "")
end
def magic1 : UInt8
(magic & 0xFF).to_u8
end
def magic2 : UInt8
(magic >> 8).to_u8
end
def make_header
header_io = IO::Memory.new
header_io.write_bytes(type, IO::ByteFormat::LittleEndian)
header_io.write_bytes(session_id, IO::ByteFormat::LittleEndian)
header_io.write_bytes(unknown1, IO::ByteFormat::LittleEndian)
header_io.write_bytes(unknown2,IO::ByteFormat::LittleEndian)
header_io.write_bytes(magic, IO::ByteFormat::LittleEndian)
header_io.write_bytes(self.message.size, IO::ByteFormat::LittleEndian)
header_io.to_s
end
def make : String
(make_header + self.message)
end
end
Next, I made the main part of the fuzzer, which fills in every possible byte value, waits for a reply, then disconnects, and loops. I keep track of which magic return what reply.
If you'd like to view the code, and understand more about how the fuzzing process needs to work, check out the GitHub page for the project.
Some notes on this program,
- There are two bytes we want to target, the first magic1, and then magic2.
- The fuzzer itself has to be extremely fault tolerant, because lots of bad things happen when using it. There is a lot of trial and error in designing one of these.
Was the login connection refused? (because camera went offline) Was the command ever replied to? * Was the command reply's length zero?
When I want to run it I just do something like this,
class Command::SystemInfo < Command
def initialize(@session_id = 0)
super(magic1: 0xfc_u8, magic2: 0x03_u8, json: JSON.build do |json|
json.object do
json.field "Name", "SystemInfo"
json.field "SessionID", "0x#{@session_id.to_s(16).rjust(8, '0')}"
end
end)
end
end
File.open("logs/system_info.log", "w") do |file|
Fuzzer.run(
Command::SystemInfo.new(session_id: 0x11111117),
magic2: (0x3..0x6),
password: "password",
output: file
)
end
This will run for a while, eventually producing results, and while the results are accurate, it's too slow! It can take up to 24 hours to complete a single scan of the magic field 0x3 to 0x8, especially because camera turns off and doesn't turn back on for minutes, as well as many other system breaking bugs.
Luckily I had a couple ideas to make it faster.