Talos Vulnerability Report


Yi Technology Home Camera 27US nonce reuse authentication bypass vulnerability

October 31, 2018
CVE Number



An exploitable code execution vulnerability exists in the firmware update functionality of Yi Home Camera 27US A specially crafted set of UDP packets can cause a logic flaw, resulting in an authentication bypass. An attacker can sniff network traffic and send a set of packets to trigger this vulnerability.

Tested Versions

Yi Technology Home Camera 27US

Product URLs


CVSSv3 Score

9.0 – CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H


CWE-323: Reusing a Nonce, Key Pair in Encryption


Yi Home Camera is an internet of things home camera sold globally. The 27US version is one of the newer models sold in the U.S., and is the most basic model out of the Yi Technology camera lineup. It still, however, includes all the functionality that one would expect from an IoT device: the ability to view video from anywhere, offline and subscription-based cloud storage, and ease of setup.

When the Yi Camera communicates with the ‘Yi Home’ phone app, in order to provide video feeds and status updates, the network daemon in charge is the p2p_tnp binary. This daemon provides network discovery, authorization, and also settings manipulation functionality, all over UDP. The custom protocol handling this is rather simple to start, and a basic overview is given below:

| Magic Byte |  Opcode Byte |  MsgLen Short |  Message (max 0x500)
|       \xF1      |    \x00-\xFF    |   <len of msg> |   <msg>

If the camera and the phone running the ‘Yi Home’ app are on the same subnet, a handshake occurs with the custom TNP protocol before any other operations can happen. The phone sends a MSG_LAN_SEARCH (\x30) packet to UDP port 31208, to which the phone will respond with a MSG_LAN_NOTIFY (\x31) packet from a new source port. It should be noted that this MSG_LAN_SEARCH can be sent to a broadcast address or a single IP address, and also that the MSG_LAN_SEARCH response contains the Device ID (DID) of the camera, which is needed to pass certain checks. A sample DID consists of a prefix, serial, and suffix. For example: TNPUSAC-112233-NMEDP.

After the initial packet exchange, the camera will respond with a MSG_P2P_RDY (\x42) packet and the thread controlling this socket enters the PPPP_thread_recv_Proto_device function. This function contains a large jump table for dealing with a subset of the possible TNP opcodes. Of most interest for this writeup is a set of basic opcodes (MSG_P2P_NOTIFY_ACK, MSG_ALIVE) that can be sent to enter another jump table of opcodes inside of the tnp::PPPP_thread_recv_DRW function.

For more context, each time a user connects to the Yi Camera and initiates a live camera feed, or editing of settings, the code path enters the tnp::PPPP_thread_recv_DRW, and a large amount of data is cleared out for this new user. A max of 10 of these sessions can occur simultaneously, any more connections will be dropped until a slot frees. Any connection that is talking with the recv_DRW thread and taking up one of these session slots will be henceforth referred to as a TNP_Session.

Inside of this new jumptable, a set of fewer opcodes is available, however the complexity of some of them is far greater.


In examining the MSG_RCV_DRW opcode (\xD0), a lot more validation must be passed in order to reach actually useful code. A summary of the MSG_RCV_DRW packet is given below:

+0x0   : “\xF1”              # Magic Byte    
+0x1   : “\xD0”              # Opcode (MSG_RCV_DRW) 
+0x2   : <len of msg>          # Max=>0x638, absolutely must match actual packet len recv’ed
+0x6   :“\xD1”               # Another Magic byte
+0x7   : <Channel>           # Max=>0x5
+0x8   : <LL_Index>           
+0xA  : <Version>               # 0x01030000 
+0xE  : <Size_Of_LL_Data>         # Max=>0x3FE 
# <Begin myDoIOControl> //[1]
+0x13 : <myDoIOControl Opcode> 
+0x15 : <used in p2p_send_ctrl_data>
+0x17 : <readOffset>
+0x19 : <Useless>   
+0x21 : <DATA>

If the MSG_RCV_DRW message passes the required checks mentioned in the comments up until [1], then the program will append all data below [1] into a linked list structure that is used for ordering of the UDP messages (essentially reimplementing TCP). The structure of the singly linked list (SLL) is shown here:

Struct SLL {
        +0x00 : First_Node,
        +0x04 : Last_Node,
        +0x08 : Node_Count,
        +0x0C : Total_NodeBody_Size,
        +0x10 : Max_Index (0x10000)

Struct SLL_Node {
        +0x00 : last_node,
        +0x04 : size_of_data_block,
        +0x08 : data_block_ptr,
        +0x0C : bk_node,
        +0x10 : fwd_node

It’s worth mentioning that each TNP_Session allocates four different linked lists, each one serving a different queuing purpose. The best approximation/naming conventions are listed here, along with a set offset that is used to find them from the TNP_Session’s base address:

+0x1F8 =>  LL_Control queue 
+0x2A0 =>  LL_ToSend
+0x348  => LL_Sent
+0x3FC => LL_Recv

Regardless, for the “\xD0” opcode, the linked list node is put into the LL_Control queue. Assuming that there is not already a node with the given LL_Index (which results in a packet drop/free), the SLL_node is inserted into the list at the index provided in the packet. A malloc then occurs, of size Size_Of_LL_Data, and that new address is copied into the data_block_ptr field of the allocated SLL_Node. Everything from offset 0x13 down is copied into this new heap chunk, and then sits around until the entire SLL has all of its indexes filled, or a set timeout has occurred.

If one sends a MSG_RCV_DRW packet with an LL_Index field of 0x4, MSG_RCV_DRW packets of index 0x0-0x3 must also be received, or else the packet will be discarded. If there is a gap in the LL_Indexes given, the camera will read the linked list data packets until the gap occurs, and nothing more.

After the MSG_RCV_DRW packets have all been received, or the timeout (approximately 10 seconds) occurs, another thread inside of the tnp_worker function reads the LL_Control queue, walking the linked list inside of the parse_msg function. Inside of parse_msg, the next parts of the packets are examined:

+0xA  : <Version>                    # 0x01030000    [1]
+0xE  : <Size_Of_LL_Data>         # Max=>0x3FE  [2]
# <Begin myDoIOControl>             [3]
+0x13 : <myDoIOControl Opcode> 
+0x15 : <used in p2p_send_ctrl_data>
+0x17 : <readOffset>
+0x19 : <Useless>   
+0x21 : <DATA>  [4]

First, the PPPP_Read function is called, which will copy the first eight bytes from offset 0xA of our MSG_RCV_DRW packet ([1] and [2]) into a stack buffer of parse_msg. If [2] is larger than 0x3FE, or if [2] is larger than the actual amount of bytes in the rest of the message, it is considered invalid and discarded. If this size check passes, then a second call to PPPP_Read happens, this time reading SizeOf_LL_Data [2] bytes from the LL_Nodes’ data blocks. Assuming nothing funky occurs, the program returns to parse_msg and checks the Version field’s first byte, which must be less than or equal to one.

Assuming that all the above checks have been passed, the program takes everything from [3] down and passes it as the data to the myDoIOControl function, which can toggle settings and initiate live camera feeds, among other important features. Appropriately, it is where the first actual authentication check also occurs. Before any more processing occurs, the Yi Camera will examine the first 0x20 bytes of the field [4] for a nonce and an encrypted and encoded hash generated from the nonce:

“%s,%s\x00” : H3S022DXHiw1DKI,G/5ueoPY6x9steD\x00

Quick note: The key used to generate the HmacSha1 is reset to a random value on every reboot.

In order to prevent a nonce-replay attack (as these MSG_RCV_DRW messages are sent cleartext over UDP and also potentially over the internet), the TNP_Session stores a list of the last 0x64 good nonces that were used. This list is checked whenever a new request is received, and the Yi Camera will drop any requests that reuse any nonce in this list.

Unfortunately, there are a couple of issues with this nonce scheme, mainly because the good_nonce list is stored inside of the TNP_Session struct, which is on a per-connection basis. Thus, if an attacker sniffs a valid nonce and initiates a new TNP_Session while the previous session is still active (thus taking up a new TNP\Session slot), the list of previously used nonces is empty, allowing for a successful nonce-replay attack.

Even more concerning though is that upon the ending of a TNP_Session, the tnp_worker will clean up everything that it needs to, including the TNP_Session itself. At the end of the myReleaseUser function that the tnp_worker calls, a large memset call is made, setting the entire TNP_Session struct (size 0xD38) to null, clearing out the previously used nonce list, allowing for replays to occur in the same TNP_Session slot. Combining all the above, it results in being able to log in with any old nonce until the device reboots, removing the timing restriction of needing another TNP_Session connected.


2018-06-13 - Vendor disclosure
2018-09-03 - Vendor submitted build to Talos for testing
2018-09-05 - Talos confirmed issue patched
2018-10-22 - Vendor released new firmware
2018-10-31 - Public release


Discovered by Lilith ¯\_(ツ)_/¯ of Cisco Talos.