ckolivas / lrzip

Long Range Zip

Home Page:http://lrzip.kolivas.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Multiple concurrency UAF bug between `zpaq_decompress_buf()` and `clear_rulist()` function

wcventure opened this issue · comments

Dear all,

Our tool report that there would be multiple concurrency use-after-free between zpaq_decompress_buf() function and clear_rulist() function, in the newest master branch 465afe8.

Brief Explanation

The related code simplified from stream.c and runzip.c are shown as follow:

// Thread T0                                | // Thread T1
// in clear_rulist() in runzip.c            | // in zpaq_decompress_buf() in steam.c
...                                         | ...
struct runzip_node *node = control->ruhead; | if (unlikely(dlen != ucthread->u_len)) {
struct stream_info *sinfo = node->sinfo;    |     ret = -1;
                                            | } else
dealloc(sinfo->ucthreads);                  |     dealloc(c_buf);
dealloc(sinfo);                             | out:
                                            |     if (ret == -1) {
                                            |         dealloc(ucthread->s_buf);
		                            |         ucthread->s_buf = c_buf;
	                                    |     }

Both thread T0 and thread T1 operate on a shared variable ucthread (i.e., T0 dealloc the a ucthread through dealloc(sinfo->ucthreads);, and T1 use the ucthread in all statements if (unlikely(dlen != ucthread->u_len)), dealloc(ucthread->s_buf);, and ucthread->s_buf = c_buf;).
However, a use-after-free can occur if the deallocation of ucthread before the use of ucthread.
For example, the following three thread interleaving can trigger three different UAFs:

Interleaving (a)

// Thread T0                                | // Thread T1
                                            |
...                                         | 
struct runzip_node *node = control->ruhead; | 
struct stream_info *sinfo = node->sinfo;    | 
                                            | 
dealloc(sinfo->ucthreads);                  | 
dealloc(sinfo);                             | 
----------------------------------------------------------------------------------------------------
	                                    | ...
                                            | if (unlikely(dlen != ucthread->u_len)) { // UAF here
                                            |     ret = -1;
                                            | } else
                                            |     dealloc(c_buf);
                                            | out:
                                            |     if (ret == -1) {
                                            |         dealloc(ucthread->s_buf);
		                            |         ucthread->s_buf = c_buf;
	                                    |     }

Interleaving (b)

// Thread T0                                | // Thread T1
                                            |
                                            | ...
                                            | if (unlikely(dlen != ucthread->u_len)) {
                                            |     ret = -1;
                                            | } else
                                            |     dealloc(c_buf);
                                            | out:
                                            |     if (ret == -1) {
----------------------------------------------------------------------------------------------------
...                                         | 
struct runzip_node *node = control->ruhead; | 
struct stream_info *sinfo = node->sinfo;    | 
                                            | 
dealloc(sinfo->ucthreads);                  | 
dealloc(sinfo);                             | 
----------------------------------------------------------------------------------------------------
                                            |         dealloc(ucthread->s_buf);  // UAF occur here
		                            |         ucthread->s_buf = c_buf;
	                                    |     }

Interleaving (c)

// Thread T0                                | // Thread T1
                                            |
                                            | ...
                                            | if (unlikely(dlen != ucthread->u_len)) {
                                            |     ret = -1;
                                            | } else
                                            |     dealloc(c_buf);
                                            | out:
                                            |     if (ret == -1) {
                                            |         dealloc(ucthread->s_buf); 
----------------------------------------------------------------------------------------------------
...                                         | 
struct runzip_node *node = control->ruhead; | 
struct stream_info *sinfo = node->sinfo;    | 
                                            | 
dealloc(sinfo->ucthreads);                  | 
dealloc(sinfo);                             | 
----------------------------------------------------------------------------------------------------
		                            |         ucthread->s_buf = c_buf; // UAF occur here
	                                    |     }

Reproduce through delay injection

To reproduce those use-after-free errors, we can insert two delays (e.g., sleep(1)) into the original source code.

For example, to reproduce interleaving (a) as mentioned earlier, you can insert a delay before dealloc(sinfo->ucthreads); statement in function in steam.c, and also a delay after, as shown as follows.

// In runzip.c, insert a delay after `dealloc(sinfo->ucthreads);`
static void clear_rulist(rzip_control *control)
{
	while (control->ruhead) {
		struct runzip_node *node = control->ruhead;
		struct stream_info *sinfo = node->sinfo;
	
		dealloc(sinfo->ucthreads);
		sleep(1); // delay here !!!!!!!!!!
		dealloc(node->pthreads);
		dealloc(sinfo->s);
		dealloc(sinfo);
		control->ruhead = node->prev;
		dealloc(node);
	}
}

// In steam.c, insert a delay after `dealloc(sinfo->ucthreads);`
static int zpaq_decompress_buf(rzip_control *control __UNUSED__, struct uncomp_thread *ucthread, long thread)
{
	...
	zpaq_decompress(ucthread->s_buf, &dlen, c_buf, ucthread->c_len,
			control->msgout, SHOW_PROGRESS ? true: false, thread);
	sleep(1); // delay here !!!!!!!!!!
	if (unlikely(dlen != ucthread->u_len)) {
		print_err("Inconsistent length after decompression. Got %ld bytes, expected %lld\n", dlen, ucthread->u_len);
		ret = -1;
	} else
		dealloc(c_buf);
    ...
}

image

compile the program:

CC=gcc CXX=g++ CFLAGS="-g -O0 -fsanitize=address" CXXFLAGS="-g -O0 -fsanitize=address" ./configure --enable-static-bin
make

Download the testcase (I upload the POC here, please unzip first).
POC.zip

Run with the testcase with the following command:

./lrzip -t -p2 POC

Then, you will see the use-after-free bug report. Here is the trace reported by ASAN:

=================================================================
==33325==ERROR: AddressSanitizer: heap-use-after-free on address 0x61d0000000e8 at pc 0x000000512b00 bp 0x7f9a30bfdb10 sp 0x7f9a30bfdb08

READ of size 8 at 0x61d0000000e8 thread T3
    #0 0x512aff in zpaq_decompress_buf /workdir/lrzip/stream.c:449:6
    #1 0x510381 in ucompthread /workdir/lrzip/stream.c:1554:11
    #2 0x7f9a3541b6da in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x76da)
    #3 0x7f9a3479771e in clone (/lib/x86_64-linux-gnu/libc.so.6+0x12171e)

0x61d0000000e8 is located 104 bytes inside of 2400-byte region [0x61d000000080,0x61d0000009e0)
freed by thread T0 here:
    #0 0x494e1d in free /home/brian/src/final/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:123:3
    #1 0x4faa0d in clear_rulist /workdir/lrzip/runzip.c:255:3
    #2 0x4f7ab2 in runzip_chunk /workdir/lrzip/runzip.c:384:2
    #3 0x4f4aae in runzip_fd /workdir/lrzip/runzip.c:404:7
    #4 0x4d84ce in decompress_file /workdir/lrzip/lrzip.c:845:6
    #5 0x4cb98b in main /workdir/lrzip/main.c:706:4
    #6 0x7f9a34697bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)

previously allocated by thread T0 here:
    #0 0x495212 in calloc /home/brian/src/final/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:154:3
    #1 0x5003e1 in open_stream_in /workdir/lrzip/stream.c:1084:33
    #2 0x4f6fa5 in runzip_chunk /workdir/lrzip/runzip.c:322:7
    #3 0x4f4aae in runzip_fd /workdir/lrzip/runzip.c:404:7
    #4 0x4d84ce in decompress_file /workdir/lrzip/lrzip.c:845:6
    #5 0x4cb98b in main /workdir/lrzip/main.c:706:4
    #6 0x7f9a34697bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)

Thread T3 created by T0 here:
    #0 0x47fe4a in pthread_create /home/brian/src/final/llvm-project/compiler-rt/lib/asan/asan_interceptors.cpp:214:3
    #1 0x4fbbd0 in create_pthread /workdir/lrzip/stream.c:125:6
    #2 0x507033 in fill_buffer /workdir/lrzip/stream.c:1713:6
    #3 0x504184 in read_stream /workdir/lrzip/stream.c:1800:8
    #4 0x4f9c88 in unzip_literal /workdir/lrzip/runzip.c:162:16
    #5 0x4f731c in runzip_chunk /workdir/lrzip/runzip.c:338:9
    #6 0x4f4aae in runzip_fd /workdir/lrzip/runzip.c:404:7
    #7 0x4d84ce in decompress_file /workdir/lrzip/lrzip.c:845:6
    #8 0x4cb98b in main /workdir/lrzip/main.c:706:4
    #9 0x7f9a34697bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)

SUMMARY: AddressSanitizer: heap-use-after-free /workdir/lrzip/stream.c:449:6 in zpaq_decompress_buf
Shadow bytes around the buggy address:
  0x0c3a7fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c3a7fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c3a7fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c3a7fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c3a7fff8000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c3a7fff8010: fd fd fd fd fd fd fd fd fd fd fd fd fd[fd]fd fd
  0x0c3a7fff8020: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c3a7fff8030: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c3a7fff8040: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c3a7fff8050: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c3a7fff8060: 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
==33325==ABORTING

I'm not sure if these use-after-free bugs could cause serious harm. I hope you can check whether it is necessary to fix these bugs.

Thanks.

Totally broken file.I use a validation function prior to decompress or test. It will catch this broken file before it even starts. Interesting academic exercise, but not worth it, IMHO.

$ ps2lrz -i POC
Showing file info only
POC is an lrzip version 0.6 file
POC is not encrypted
POC uncompressed file size is 5476377696771506395 bytes
Dumping magic header 24 bytes
Byte Offset      Description/Content
===========      ===================
Magic Bytes 0-3: 4C 52 5A 49 LRZI
Bytes 4-5:       LRZIP Major, Minor version: 00, 06
Bytes 6-13:      LRZIP Uncompressed Size bytes: DB 00 F0 07 80 00 00 4C 
Bytes 14 and 15: unused
Bytes 16-20:     LZMA Properties Bytes; 49 00 00 00 1B lc=1, lp=3, pb=1, Dictionary Size=452984832
Byte  21:        MD5 Sum at EOF: yes
Byte  22:        File is encrypted: no
Byte  23:        unused

Actually, the uploaded file is a well-crafted input. Given an illegal input file, concurrency use-after-free may occur. Because once the program failed to read chunk_bytes size in runzip_chunk function, the program will release the resources (i.e., free) and terminate. However, other threads still could access the released resources before termination.

All decompress functions perform the same way. As @ckolivas likes to say, patches welcome. if (!ptr) fatal...

 431 static int zpaq_decompress_buf(rzip_control *control __UNUSED__, struct uncomp_thread *ucthread, long thread)
. . .
 449         if (unlikely(dlen != ucthread->u_len)) {
 450                 print_err("Inconsistent length after decompression. Got %ld bytes, expected %lld\n", dlen, ucthread->u_len);
 451                 ret = -1;
 452         } else
 453                 dealloc(c_buf);
. . .
 462 static int bzip2_decompress_buf(rzip_control *control __UNUSED__, struct uncomp_thread *ucthread)
. . .
 483         if (unlikely(dlen != ucthread->u_len)) {
 484                 print_err("Inconsistent length after decompression. Got %d bytes, expected %lld\n", dlen, ucthread->u_len);
 485                 ret = -1;
 486         } else
 487                 dealloc(c_buf);
. . .
 496 static int gzip_decompress_buf(rzip_control *control __UNUSED__, struct uncomp_thread *ucthread)
. . .
 517         if (unlikely((i64)dlen != ucthread->u_len)) {
 518                 print_err("Inconsistent length after decompression. Got %ld bytes, expected %lld\n", dlen, ucthread->u_len);
 519                 ret = -1;
 520         } else
 521                 dealloc(c_buf);
. . .
 530 static int lzma_decompress_buf(rzip_control *control, struct uncomp_thread *ucthread)
. . .
 554         if (unlikely((i64)dlen != ucthread->u_len)) {
 555                 print_err("Inconsistent length after decompression. Got %lld bytes, expected %lld\n", (i64)dlen, ucthread->u_len);
 556                 ret = -1;
 557         } else
 558                 dealloc(c_buf);
. . .
 567 static int lzo_decompress_buf(rzip_control *control __UNUSED__, struct uncomp_thread *ucthread)
. . .
 588         if (unlikely((i64)dlen != ucthread->u_len)) {
 589                 print_err("Inconsistent length after decompression. Got %lu bytes, expected %lld\n", (unsigned long)dlen, ucthread->u_len);
 590                 ret = -1;
 591         } else
 592                 dealloc(c_buf);

Fixed in 4b39421 , thanks.