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
- Selfish reasons. Transmission is my favorite torrent client and I wanted to see if I could break it.
- Trust, but verify. I trust that my torrent client is built with security in mind, but I wanted to verify this for myself.
- 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
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.
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
|
|
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
Step 2: Walk the bencode tree, pushing SaveNode
s onto the decoding stack when encountering “container” types such as lists and dictionaries.
libtransmission/variant.c
|
|
Step 3: When the stack fills up, double its size by using tr_renew
, a custom realloc
wrapper.
libtransmission/variant.c
|
|
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
|
|
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
|
|
One more level…
libtransmission/variant.c
Before finally landing at a line of code which dereferences the pointer.
libtransmission/variant.h
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
- Update Transmission to 3.00.
- 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 |