CVE-2018-10756: Transmission

by Tom Richards

Thursday, May 14, 2020

CVE-2018-10756: Transmission can crash or possibly execute arbitrary code when opening a maliciously crafted torrent file.

Any application which uses libtransmission, such as transmission-daemon, transmission-gtk, transmission-show, etc. is susceptible to this problem.

The official CVE text is as follows:

Use-after-free in libtransmission/variant.c in Transmission before 3.00 allows remote attackers to cause a denial of service (crash) or possibly execute arbitrary code via a crafted torrent file.

It is a well known fact that use-after-free errors can, at worst, be leveraged into execution of arbitrary code.

Reasons to look for security bugs

  1. Selfish reasons. Transmission is my favorite torrent client and I wanted to see if I could break it.
  2. Trust, but verify. I trust that my torrent client is built with security in mind, but I wanted to verify this for myself.
  3. Many people exist in this universe who are smarter than me. If I can break Transmission, chances are, somebody else can break it too. Maybe they can get it to run nasty code on my machine!

By performing this kind of research, I hope that I can…

  • Help make the Transmission project more secure and more stable
  • Use smart fuzzing tools
  • Learn some interesting things along the way

Security bugs often begin with a program crash. So let’s get to it!

How to discover a crash

The way to make a program crash is to simply throw a fuzzer at it for a while. For this particular bug, I happened to use afl-fuzz because (a) it was the first fuzzer I tried, and (b) it was easy to use. I didn’t need to write any additional code; I just needed to build the existing transmission-show utility using the AFL compiler.

First, I built transmission-show with AFL.

# Instrument builds with afl-fuzz
export CFLAGS="-O0 -g"
export CC=afl-clang-fast
export CXX=afl-clang-fast++

# Build transmission-show utility
mkdir -p build
cd build
cmake ..
make transmission-show

I then collected 6 of the smallest torrents I could find from academictorrents.com and used them as my starting corpus.

$ ls -1 testcase_dir/
07e05fc1229555e124df72160a01b2540d04cebf.torrent
25932ba42d983dd7b4474d8f59ab56cdc25d9107.torrent
3efc53f35d49669b89039f2b4ec9de11ec1d73fd.torrent
551952d08103200cf5034fb74adf71643aa0c643.torrent
968a3ff5e4182cdecd239980ecfd257a37451003.torrent
ba43f388cb372f72a91d7c08c54a3f8b36fe3505.torrent

When running the fuzzer, I made it utilize all CPU cores by launching multiple processes. Launch the parent fuzzer with the -M flag, and subsequent worker processes with the -S flag.

# Parent process (00)
afl-fuzz -m 128M -i testcase_dir -o findings_dir -M fuzzer00 -- ./utils/transmission-show @@

# Worker processes (01 - ...?)
afl-fuzz -m 128M -i testcase_dir -o findings_dir -S fuzzer01 -- ./utils/transmission-show @@
afl-fuzz -m 128M -i testcase_dir -o findings_dir -S fuzzer02 -- ./utils/transmission-show @@

After running for a number of hours, afl-fuzz produced a few dozen duplicate crash cases in the form of just-corrupt-enough .torrent files.

$ ls -1 findings_dir/
id:000000,sig:06,src:001779+001066,op:splice,rep:16
id:000000,sig:11,src:000907,op:havoc,rep:2
id:000000,sig:11,src:001047+001645,op:splice,rep:2
id:000000,sig:11,src:001294+001657,op:splice,rep:8
id:000000,sig:11,src:001627,op:havoc,rep:8
id:000000,sig:11,src:001633,op:havoc,rep:2
id:000001,sig:06,src:001580,op:havoc,rep:2
id:000001,sig:06,src:001604,op:havoc,rep:4
id:000001,sig:11,src:000926+001788,op:splice,rep:2
id:000001,sig:11,src:000994+001654,op:splice,rep:8
id:000001,sig:11,src:001047+001645,op:splice,rep:4
id:000001,sig:11,src:001609,op:havoc,rep:16
id:000001,sig:11,src:001627,op:havoc,rep:8
...

Running transmission-show against these generated input files produced a few interesting crashes.

1. Null pointer dereference in utils/show.c

The transmission-show utility did not check the return value of localtime.

utils/show.c

111
112
113
114
115
116
117
118
119
if (inf->dateCreated == 0)
{
    printf("  Created on: Unknown\n");
}
else
{
    struct tm tm = *localtime(&inf->dateCreated);
    printf("  Created on: %s", asctime(&tm));
}

When localtime fails to parse the date due to mangled or invalid data (as was the case with the fuzzer’s generated crash cases), it returns NULL. Dereferencing this NULL value is known as a null pointer dereference.

This issue only affected the transmission-show utility and was patched a while ago.

2. Assert / abort in libtransmission/ptrarray.h

Running transmission-show against some generated crash cases resulted in a failed assertion, ultimately causing the program to abort().

$ ./transmission-show 'id:000000,sig:06,src:001779+001066,op:splice,rep:16'
assertion failed: i >= 0 (/home/tom/transmission/libtransmission/ptrarray.h:53)
Aborted (core dumped)

Transmission’s array accessor function begins with a number of assertions which try to prevent bad things from happening. Fortunately for us, afl-fuzz is smart enough to get around them, as we’ll see in a moment.

48
49
50
51
52
53
54
55
56
57
/** @brief Return the nth item in a tr_ptrArray
    @return the nth item in a tr_ptrArray */
static inline void* tr_ptrArrayNth(tr_ptrArray* array, int i)
{
    TR_ASSERT(array != NULL);
    TR_ASSERT(i >= 0);
    TR_ASSERT(i < array->n_items);

    return array->items[i];
}

I chose not to investigate these abort() cases, because they’re really just a sign that something more interesting is happening.

3. Segfault in libtransmission/variant.c

Running transmission-show against some generated crash cases resulted in a segmentation fault:

$ ./transmission-show 'id:000009,sig:11,src:001800,op:havoc,rep:8'
Segmentation fault (core dumped)

Now this looks promising! The segmentation fault is a sign of bad memory behavior. The next steps are to (a) determine what kind of bad behavior it is, and (b) understand why the behavior occurs.

Minimizing the crash cases

In the collected crash cases directory, I found that afl-fuzz had generated torrent files which are over 30KB in size! This is way too much data to step through by hand. Fortunately, there were some smaller crash cases as well.

I settled on a handful of crashes which all basically crashed the same way with a segfault, and selected the one with the smallest filesize. To further simplify this crash input file, we hand it over to afl-tmin, the automatic test case minimizer.

afl-tmin performs a few optimization passes in an attempt to remove unimportant bytes of data from the input file.

The command-line invocation looks like this:

$ afl-tmin -m 128M -i crashes/segfault.torrent -o minimized-crash.torrent -- ./transmission-show @@
afl-tmin 2.52b by <lcamtuf@google.com>

[+] Read 662 bytes from 'crashes/segfault.torrent'.
[*] Performing dry run (mem limit = 128 MB, timeout = 1000 ms)...
[+] Program exits with a signal, minimizing in crash mode.
[*] Stage #0: One-time block normalization...
[+] Block normalization complete, 310 bytes replaced.
[*] --- Pass #1 ---
[*] Stage #1: Removing blocks of data...
    Block length = 64, remaining size = 662
    Block length = 32, remaining size = 448
    Block length = 16, remaining size = 384
    Block length = 8, remaining size = 368
    Block length = 4, remaining size = 352
    Block length = 2, remaining size = 352
    Block length = 1, remaining size = 352
[+] Block removal complete, 310 bytes deleted.
[*] Stage #2: Minimizing symbols (12 code points)...
[+] Symbol minimization finished, 3 symbols (70 bytes) replaced.
[*] Stage #3: Character minimization...
[+] Character minimization done, 0 bytes replaced.
[*] --- Pass #2 ---
[*] Stage #1: Removing blocks of data...
    Block length = 32, remaining size = 352
    Block length = 16, remaining size = 352
    Block length = 8, remaining size = 352
    Block length = 4, remaining size = 352
    Block length = 2, remaining size = 352
    Block length = 1, remaining size = 352
[+] Block removal complete, 0 bytes deleted.

     File size reduced by : 46.83% (to 352 bytes)
    Characters simplified : 107.95%
     Number of execs done : 1308
          Fruitless execs : path=1256 crash=0 hang=0

[*] Writing output to 'minimized-crash.torrent'...
[+] We're done here. Have a nice day!

In stage 1, it somehow figured out how to remove blocks while balancing the start/end bencoding markers. Pretty impressive!

In stage 2, the minimizer changes unique symbols into one or more duplicate symbols. In our bencoded torrent file, the minimizer ended up homogenizing the dictionary keys to all be "0".

# Before minimization:
{ "a": { "b": { "c": ... } } }

# After minimization:
{ "0": { "0": { "0": ... } } }

Overall result: Not bad! The minimizer automatically reduced the crash case input by almost half (662 bytes -> 352 bytes).

Verifying the use-after-free with ASan

AddressSanitizer, or “ASan”, is a memory error detection feature of the Clang compiler. When compiling your program with ASan, Clang instruments your compiled binary with special instructions to help detect incorrect memory usage.

ASan will terminate your program with a big fancy error message if your program…

  • Makes an out-of-bounds access to the heap, stack, and/or globals,
  • Attempts to use a memory address after it was free()d, or,
  • Does other weird things as listed on the AddressSanitizer page.

Building transmission with ASan

# Instrument builds with AddressSanitizer
export CC="clang"
export CXX="clang++"
export CFLAGS="-O1 -g -fsanitize=address -fno-omit-frame-pointer -pthread"
export LDFLAGS="-g -fsanitize=address"

# Build transmission-show utility
mkdir -p build
cd build
cmake ..
make transmission-show

Here’s what transmission-show looks like when crashing under ASan:

# Run asan-enabled program against the generated crash case
$ ./transmission-show crashes/segfault.torrent
=================================================================
==402959==ERROR: AddressSanitizer: heap-use-after-free on address 0x622000000108 at pc 0x562b86e68dd4 bp 0x7ffd170f4bc0 sp 0x7ffd170f4bb8
READ of size 1 at 0x622000000108 thread T0
    #0 0x562b86e68dd3 in tr_variantIsList /transmission-2.94/libtransmission/variant.h:215:28
    #1 0x562b86e6b03d in tr_variantIsContainer /transmission-2.94/libtransmission/variant.c:114:12
    #2 0x562b86e6a8bf in tr_variantWalk /transmission-2.94/libtransmission/variant.c:836:18
    #3 0x562b86ee1907 in tr_variantToBufBenc /transmission-2.94/libtransmission/variant-benc.c:392:5
    #4 0x562b86e6bed4 in tr_variantToBuf /transmission-2.94/libtransmission/variant.c:1132:9
    #5 0x562b86e6bf7d in tr_variantToStr /transmission-2.94/libtransmission/variant.c:1151:28
    #6 0x562b86e48c87 in tr_metainfoParseImpl /transmission-2.94/libtransmission/metainfo.c:516:22
    #7 0x562b86e48a9d in tr_metainfoParse /transmission-2.94/libtransmission/metainfo.c:671:26
    #8 0x562b86e4f6f7 in torrentParseImpl /transmission-2.94/libtransmission/torrent.c:1094:16
    #9 0x562b86e4f51f in tr_torrentParse /transmission-2.94/libtransmission/torrent.c:1137:12
    #10 0x562b86e4649d in main /transmission-2.94/utils/show.c:360:11
    #11 0x7f679781f022 in __libc_start_main (/usr/lib/libc.so.6+0x27022)
    #12 0x562b86d6ae1d in _start (/transmission-2.94/build/utils/transmission-show+0x81e1d)

0x622000000108 is located 8 bytes inside of 5120-byte region [0x622000000100,0x622000001500)
freed by thread T0 here:
    #0 0x562b86e0e6a2 in realloc (/transmission-2.94/build/utils/transmission-show+0x1256a2)
    #1 0x562b86e63806 in tr_realloc /transmission-2.94/libtransmission/utils.c:138:32
    #2 0x562b86e6abec in tr_variantWalk /transmission-2.94/libtransmission/variant.c
    #3 0x562b86ee1907 in tr_variantToBufBenc /transmission-2.94/libtransmission/variant-benc.c:392:5
    #4 0x562b86e6bed4 in tr_variantToBuf /transmission-2.94/libtransmission/variant.c:1132:9
    #5 0x562b86e6bf7d in tr_variantToStr /transmission-2.94/libtransmission/variant.c:1151:28
    #6 0x562b86e48c87 in tr_metainfoParseImpl /transmission-2.94/libtransmission/metainfo.c:516:22
    #7 0x562b86e48a9d in tr_metainfoParse /transmission-2.94/libtransmission/metainfo.c:671:26
    #8 0x562b86e4f6f7 in torrentParseImpl /transmission-2.94/libtransmission/torrent.c:1094:16
    #9 0x562b86e4f51f in tr_torrentParse /transmission-2.94/libtransmission/torrent.c:1137:12
    #10 0x562b86e4649d in main /transmission-2.94/utils/show.c:360:11
    #11 0x7f679781f022 in __libc_start_main (/usr/lib/libc.so.6+0x27022)

previously allocated by thread T0 here:
    #0 0x562b86e0e319 in malloc (/transmission-2.94/build/utils/transmission-show+0x125319)
    #1 0x562b86e637bd in tr_malloc /transmission-2.94/libtransmission/utils.c:128:24
    #2 0x562b86e6a7d2 in tr_variantWalk /transmission-2.94/libtransmission/variant.c:822:30
    #3 0x562b86ee1907 in tr_variantToBufBenc /transmission-2.94/libtransmission/variant-benc.c:392:5
    #4 0x562b86e6bed4 in tr_variantToBuf /transmission-2.94/libtransmission/variant.c:1132:9
    #5 0x562b86e6bf7d in tr_variantToStr /transmission-2.94/libtransmission/variant.c:1151:28
    #6 0x562b86e48c87 in tr_metainfoParseImpl /transmission-2.94/libtransmission/metainfo.c:516:22
    #7 0x562b86e48a9d in tr_metainfoParse /transmission-2.94/libtransmission/metainfo.c:671:26
    #8 0x562b86e4f6f7 in torrentParseImpl /transmission-2.94/libtransmission/torrent.c:1094:16
    #9 0x562b86e4f51f in tr_torrentParse /transmission-2.94/libtransmission/torrent.c:1137:12
    #10 0x562b86e4649d in main /transmission-2.94/utils/show.c:360:11
    #11 0x7f679781f022 in __libc_start_main (/usr/lib/libc.so.6+0x27022)

SUMMARY: AddressSanitizer: heap-use-after-free /transmission-2.94/libtransmission/variant.h:215:28 in tr_variantIsList
Shadow bytes around the buggy address:
  0x0c447fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c447fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c447fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c447fff8000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c447fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c447fff8020: fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c447fff8030: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c447fff8040: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c447fff8050: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c447fff8060: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c447fff8070: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==402959==ABORTING

During the torrentParseImpl process, AddressSanitizer shows us that some memory is first allocated with malloc, then freed with realloc, and finally accessed again. This is bad news. So why does this happen?

Understanding bencoding

Bencoding is a the format which is used to encode torrent files. It provides an efficient, dictionary-like storage mechanism which allows for fast, endian-neutral decoding, as well as storage of binary data.

Bencoding resembles JSON, but bencoding was specified and implemented at a time before JSON became widely popular.

Decoding (or “interpreting”) a bencode object can be implemented naively in a recursive fashion:

# Decodes a single bencode object
function decode_next_object() {
  do {
    char := read_character()
    if char == 'd' # Dictionary start
      allocate_dictionary()
      decode_next_object()
    else if char == 'l' # List start
      allocate_list()
      decode_next_object()
    else if char == 'i' # Number start
      decode_number()
    else if char in ('1'..'9') # Strings start with length
      decode_string()
    else
      exit("invalid input")
  } (while char != null)
}

With this recursive algorithm, a problem appears when dealing with arbitrarily-deeply nested objects. Since a piece of bencoded data can have dictionaries wrapped in lists wrapped in dictionaries and so on, pushing a function call onto the stack for each level of data nesting is just asking for trouble. C developers should be especially careful about interpreting untrusted input using an unbounded recursive algorithm.

Transmission used to use a recursive implementation for walking a bencoding object. It was vulnerable to a “smash-stacking” (sic) attack, however, and was changed.

libtransmission/variant.c

813
814
815
816
817
818
/**
 * This function's previous recursive implementation was
 * easier to read, but was vulnerable to a smash-stacking
 * attack via maliciously-crafted data. (#667)
 */
void tr_variantWalk(tr_variant const* v, struct VariantWalkFuncs const* walkFuncs, void* user_data, bool sort_dicts)

How do we defend against this?

Transmission is written in C. Some defenses against stack overflow in this language are:

1. Add a nesting limit.

Choose an arbitrary recursion limit (let’s say, 1000) for your decoder. The nesting limit of the object will be limited to 1000 levels deep. Upon reaching the limit, you may choose to ignore deeper values, or simply reject the input as invalid altogether.

2. Don’t use recursion.

Use an iterative algorithm where the decoding state is not stored on the stack. For example, such a decoder might allocate its own “stack” on the heap, to keep track of where it is in the decoding process. This eliminates the potential for stack overflow, albeit with the new possibility of memory consumption. :) Again, creating an artificial nesting limit can help eliminate this unbounded memory growth.

Transmission’s current decoder does allocate its own decoding stack, and handles resizing the stack when it becomes full. As far as I’m aware, it does not have a nesting limit.

Understanding why the program crashes

When opening a .torrent file, Transmission performs the following actions to decode the data in it:

Step 1: Pre-allocate a stack of 64 elements.

libtransmission/variant.c

820
821
822
int stackSize = 0;
int stackAlloc = 64;
struct SaveNode* stack = tr_new(struct SaveNode, stackAlloc);

Step 2: Walk the bencode tree, pushing SaveNodes onto the decoding stack when encountering “container” types such as lists and dictionaries.

libtransmission/variant.c

882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
case TR_VARIANT_TYPE_LIST:
    if (v == node->v)
    {
        walkFuncs->listBeginFunc(v, user_data);
    }
    else
    {
        if (stackAlloc == stackSize)
        {
            stackAlloc *= 2;
            stack = tr_renew(struct SaveNode, stack, stackAlloc);
        }

        nodeConstruct(&stack[stackSize++], v, sort_dicts);
    }

    break;

Step 3: When the stack fills up, double its size by using tr_renew, a custom realloc wrapper.

libtransmission/variant.c

882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
case TR_VARIANT_TYPE_LIST:
    if (v == node->v)
    {
        walkFuncs->listBeginFunc(v, user_data);
    }
    else
    {
        if (stackAlloc == stackSize)
        {
            stackAlloc *= 2;
            stack = tr_renew(struct SaveNode, stack, stackAlloc);
        }

        nodeConstruct(&stack[stackSize++], v, sort_dicts);
    }

    break;

Step 4: While constructing the SaveNode, a pointer to the current stack is attached to the node (oops!). Can you spot the bug?

libtransmission/variant.c

766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
static void nodeConstruct(struct SaveNode* node, tr_variant const* v, bool sort_dicts)
{
    node->isVisited = false;
    node->childIndex = 0;

    if (sort_dicts && tr_variantIsDict(v))
    {
        /* make node->sorted a sorted version of this dictionary */

        size_t const n = v->val.l.count;
        struct KeyIndex* tmp = tr_new(struct KeyIndex, n);

        for (size_t i = 0; i < n; i++)
        {
            tmp[i].val = v->val.l.vals + i;
            tmp[i].keystr = tr_quark_get_string(tmp[i].val->key, NULL);
        }

        qsort(tmp, n, sizeof(struct KeyIndex), compareKeyIndex);

        tr_variantInitDict(&node->sorted, n);

        for (size_t i = 0; i < n; ++i)
        {
            node->sorted.val.l.vals[i] = *tmp[i].val;
        }

        node->sorted.val.l.count = n;

        tr_free(tmp);

        node->v = &node->sorted;
    }
    else
    {
        node->v = v;
    }
}

Because the stack was reallocated, the SaveNode’s variant pointer v points to the previously allocated (now freed!) memory.

Step 5: To finish decoding, walk back down the stack, passing v to a few different functions such as tr_variantIsContainer.

libtransmission/variant.c

826
827
828
829
830
831
832
833
834
835
836
837
838
839
while (stackSize > 0)
{
    struct SaveNode* node = &stack[stackSize - 1];
    tr_variant const* v;

    if (!node->isVisited)
    {
        v = node->v;
        node->isVisited = true;
    }
    else if (tr_variantIsContainer(node->v) && node->childIndex < node->v->val.l.count)
    {
        int const index = node->childIndex;
        ++node->childIndex;

One more level…

libtransmission/variant.c

112
113
114
115
static bool tr_variantIsContainer(tr_variant const* v)
{
    return tr_variantIsList(v) || tr_variantIsDict(v);
}

Before finally landing at a line of code which dereferences the pointer.

libtransmission/variant.h

213
214
215
216
static inline bool tr_variantIsList(tr_variant const* v)
{
    return v != NULL && v->type == TR_VARIANT_TYPE_LIST;
}

Oops! v->type dereferences the pointer. It points to the previous stack (in a freed region of memory), causing a crash (use-after-free).

Why did the patch take so long?

Transmission is an open source project run by a handful of kind volunteers in their spare time.

  • Could I have demanded that the maintainers immediately release a new version to address this issue? Yes.
  • Would it have been a huge jerk move to demand other people’s time like that? Also yes.

Admittedly, two years from report to release is a just a tiny bit longer than I was expecting. Maybe next time I’ll explicitly set a 90-day deadline.

Improving the project for the future

More fuzzing with a good set of test corpora can help identify problems like this. Ideal future fuzz targets might include code which uses untrusted data and/or network client(s) that touch the public internet, such as:

  • Announcer (HTTP/UDP)
  • Distributed Hash Table (DHT)
  • Magnet URI parsing
  • Micro Transport Protocol (uTP) client
  • Torrent file parsing
  • UDP client
  • Webseed fetcher

How to mitigate this issue

  1. Update Transmission to 3.00.
  2. If you can’t update for some reason, avoid using .torrent files, especially untrusted ones. Instead, initiate your downloads via magnet link.

Exercises left to the reader :)

  • What is the smallest possible amount of input data (bytes) required to reproduce this crash?
  • Leverage the crash into arbitrary code execution.

Timeline of events

Event Date
Initial report May 5, 2018
CVE filed May 5, 2018
Bug confirmed May 10, 2018
Patch written May 11, 2018
Patch tested May 12, 2018
Patch released (Thanks @mikedld!) Apr 29, 2020
This blog post published May 14, 2020
Transmission 3.00 released May 22, 2020

External references