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 attackers to cause a denial of service (crash) or possibly execute arbitrary code via a specially 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
if (inf->dateCreated == 0)
112
{
113
    printf("  Created on: Unknown\n");
114
}
115
else
116
{
117
    struct tm tm = *localtime(&inf->dateCreated);
118
    printf("  Created on: %s", asctime(&tm));
119
}

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.

libtransmission/ptrarray.h

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

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
 * This function's previous recursive implementation was
815
 * easier to read, but was vulnerable to a smash-stacking
816
 * attack via maliciously-crafted data. (#667)
817
 */
818
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
int stackSize = 0;
821
int stackAlloc = 64;
822
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
case TR_VARIANT_TYPE_LIST:
883
    if (v == node->v)
884
    {
885
        walkFuncs->listBeginFunc(v, user_data);
886
    }
887
    else
888
    {
889
        if (stackAlloc == stackSize)
890
        {
891
            stackAlloc *= 2;
892
            stack = tr_renew(struct SaveNode, stack, stackAlloc);
893
        }
894
895
        nodeConstruct(&stack[stackSize++], v, sort_dicts);
896
    }
897
898
    break;

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

libtransmission/variant.c

882
case TR_VARIANT_TYPE_LIST:
883
    if (v == node->v)
884
    {
885
        walkFuncs->listBeginFunc(v, user_data);
886
    }
887
    else
888
    {
889
        if (stackAlloc == stackSize)
890
        {
891
            stackAlloc *= 2;
892
            stack = tr_renew(struct SaveNode, stack, stackAlloc);
893
        }
894
895
        nodeConstruct(&stack[stackSize++], v, sort_dicts);
896
    }
897
898
    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
static void nodeConstruct(struct SaveNode* node, tr_variant const* v, bool sort_dicts)
767
{
768
    node->isVisited = false;
769
    node->childIndex = 0;
770
771
    if (sort_dicts && tr_variantIsDict(v))
772
    {
773
        /* make node->sorted a sorted version of this dictionary */
774
775
        size_t const n = v->val.l.count;
776
        struct KeyIndex* tmp = tr_new(struct KeyIndex, n);
777
778
        for (size_t i = 0; i < n; i++)
779
        {
780
            tmp[i].val = v->val.l.vals + i;
781
            tmp[i].keystr = tr_quark_get_string(tmp[i].val->key, NULL);
782
        }
783
784
        qsort(tmp, n, sizeof(struct KeyIndex), compareKeyIndex);
785
786
        tr_variantInitDict(&node->sorted, n);
787
788
        for (size_t i = 0; i < n; ++i)
789
        {
790
            node->sorted.val.l.vals[i] = *tmp[i].val;
791
        }
792
793
        node->sorted.val.l.count = n;
794
795
        tr_free(tmp);
796
797
        node->v = &node->sorted;
798
    }
799
    else
800
    {
801
        node->v = v;
802
    }
803
}

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
while (stackSize > 0)
827
{
828
    struct SaveNode* node = &stack[stackSize - 1];
829
    tr_variant const* v;
830
831
    if (!node->isVisited)
832
    {
833
        v = node->v;
834
        node->isVisited = true;
835
    }
836
    else if (tr_variantIsContainer(node->v) && node->childIndex < node->v->val.l.count)
837
    {
838
        int const index = node->childIndex;
839
        ++node->childIndex;

One more level…

libtransmission/variant.c

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

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

libtransmission/variant.h

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

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