Talos Vulnerability Report

TALOS-2016-0095

Lhasa lha decode_level3_header Heap Corruption Vulnerability

March 31, 2016

Report ID

CVE-2016-2347

Summary

An exploitable integer underflow exists during calculation size for all headers in decode_level3_header function of Lhasa (lha) application.

Smaller value of header_len than LEVEL_3_HEADER_LEN ( 32 ) cause during subtraction integer underflow and lead later to memory corruption via heap based buffer overflow.

Tested Versions

Lhasa v0.3.0 command line LHA tool - Copyright (C) 2011,2012 Simon Howard
Lhasa v0.0.7 command line LHA tool - Copyright (C) 2011,2012 Simon Howard

Product URLs

https://github.com/fragglet/lhasa

Details

Code analysis:

lib\lha_file_header.c

Line 772  static int decode_level3_header(LHAFileHeader **header, LHAInputStream *stream)
Line 773  {
Line 774    unsigned int header_len;
Line 775
Line 776    // The first field at the start of a level 3 header is supposed to
Line 777    // indicate word size, with the idea being that the header format
Line 778    // can be extended beyond 32-bit words in the future. In practise,
Line 779    // nothing supports anything other than 32-bit (4 bytes), and neither
Line 780    // do we.
Line 781
Line 782    if (lha_decode_uint16(&RAW_DATA(header, 0)) != 4) {
Line 783      return 0;
Line 784    }
Line 785
Line 786    // Read the full header.
Line 787
Line 788    if (!extend_raw_data(header, stream,
Line 789               LEVEL_3_HEADER_LEN - RAW_DATA_LEN(header))) {
Line 790      return 0;
Line 791    }
Line 792
Line 793    // Read the header length field (including extended headers), and
Line 794    // extend to this full length. Because this is a 32-bit value,
Line 795    // we must place a sensible limit on the amount of data that will
Line 796    // be read, to avoid possibly allocating gigabytes of memory.
Line 797
Line 798    header_len = lha_decode_uint32(&RAW_DATA(header, 24));
Line 799
Line 800    if (header_len > LEVEL_3_MAX_HEADER_LEN) {
Line 801      return 0;
Line 802    }
Line 803
Line 804    if (!extend_raw_data(header, stream,
Line 805               header_len - RAW_DATA_LEN(header))) {
Line 806      return 0;
Line 807    }

header_len from line 798 is fully controllable 32bit value represents total size of headers (header + extended headers).

Because its value is only check to not exceed LEVEL_3_MAX_HEADER_LEN in line 804 for header_len values smaller than LEVEL_3_HEADER_LEN ( 32 ) integer underflow will occur with significant consequences for further code execution. For all header_len values in range <0;32) result of this subtraction from line 789 will be close to UINT_MAX.

Next we land inside :

Line 346  static uint8_t *extend_raw_data(LHAFileHeader **header,
Line 347                  LHAInputStream *stream,
Line 348                  size_t nbytes)
Line 349  {
Line 350    LHAFileHeader *new_header;
Line 351    size_t new_raw_len;
Line 352    uint8_t *result;
Line 353
Line 354    // Reallocate the header and raw_data area to be larger.
Line 355
Line 356    new_raw_len = RAW_DATA_LEN(header) + nbytes;
Line 357    new_header = realloc(*header, sizeof(LHAFileHeader) + new_raw_len);
Line 358
Line 359    if (new_header == NULL) {
Line 360      return NULL;
Line 361    }
Line 362
Line 363    // Update the header pointer to point to the new area.
Line 364
Line 365    *header = new_header;
Line 366    new_header->raw_data = (uint8_t *) (new_header + 1);
Line 367    result = new_header->raw_data + new_header->raw_data_len;
Line 368
Line 369    // Read data from stream into new area.
Line 370
Line 371    if (!lha_input_stream_read(stream, result, nbytes)) {
Line 372      return NULL;
Line 373    }

Here the result of mentioned above subtraction is of course nbytes argument.

First problem starts in Line 357, when header_len for e.g equal 0 new_raw_len will equal 0 and realloc instead of increase “header” memory area going to decrees its size.

In consequences “result” pointer will point on released memory.

Heap based buffer overflow appears in Line 371 where to “result” buffer is readed “nbytes” (~UINT_MAX) directly from file.

Attacker controlling length and content of file can fully controllable overwrite heap with specified content.

File format based on attached PoC:

04 00         - header size
2D 6C 68 37 2D    - compression method
21 00 00 01     - compressed_length
2E 00 00 00     - file length
92 91 85 40     - unix timestamp
20          - reserved
03          - level
41 41         - CRC
41          - Os ID
00 00 00 00     - extensions headers length

Crash analysis

Lets see this in gdb:

python sys.path.append('/home/icewall/tools/gdb-heap')
python import gdbheap

// Read data from stream into new area.

EIP-> if (!lha_input_stream_read(stream, result, nbytes)) {
  return NULL;
}

heap used
Used chunks of memory on heap
-----------------------------
   0: 0x0000000000615010 -> 0x000000000061524f      576 bytes uncategorized::576 bytes |.$......$P.....DT......P.....|
   1: 0x0000000000615250 -> 0x000000000061528f       64 bytes uncategorized::64 bytes |0.@......Pa...........-lh7-!....|
   2: 0x0000000000615290 -> 0x00000000006152df       80 bytes uncategorized::80 bytes |.Ra.............................|
   3: 0x00000000006152e0 -> 0x000000000061530f       48 bytes   C:string data:None |PRa.............................|
   4: 0x0000000000615310 -> 0x00000000006153bf      176 bytes uncategorized::176 bytes |................................|
   5: 0x00000000006153c0 -> 0x00000000006153df       32 bytes uncategorized::32 bytes |............AAAA........1.......|

p result

$2 = (uint8_t *) 0x6153d0 ""

heap all

All chunks of memory on heap (both used and free)
-------------------------------------------------

0: 0x0000000000615000 -> 0x000000000061523f  inuse: 576 bytes (<MChunkPtr chunk=0x615000 mem=0x615010 PREV_INUSE inuse chunksize=576 memsize=560>)

1: 0x0000000000615240 -> 0x000000000061527f  inuse: 64 bytes (<MChunkPtr chunk=0x615240 mem=0x615250 PREV_INUSE inuse chunksize=64 memsize=48>)

2: 0x0000000000615280 -> 0x00000000006152cf  inuse: 80 bytes (<MChunkPtr chunk=0x615280 mem=0x615290 PREV_INUSE inuse chunksize=80 memsize=64>)

3: 0x00000000006152d0 -> 0x00000000006152ff  inuse: 48 bytes (<MChunkPtr chunk=0x6152d0 mem=0x6152e0 PREV_INUSE inuse chunksize=48 memsize=32>)

4: 0x0000000000615300 -> 0x00000000006153af  inuse: 176 bytes (<MChunkPtr chunk=0x615300 mem=0x615310 PREV_INUSE inuse chunksize=176 memsize=160>)

5: 0x00000000006153b0 -> 0x00000000006153cf  inuse: 32 bytes (<MChunkPtr chunk=0x6153b0 mem=0x6153c0 PREV_INUSE inuse chunksize=32 memsize=16>)

p header
$3 = (LHAFileHeader **) 0x7fffffffdcd8
p *header
$4 = (LHAFileHeader *) 0x615310
p result
$5 = (uint8_t *) 0x6153d0 ""
heap all

All chunks of memory on heap (both used and free)
-------------------------------------------------

0: 0x0000000000615000 -> 0x000000000061523f  inuse: 576 bytes (<MChunkPtr chunk=0x615000 mem=0x615010 PREV_INUSE inuse chunksize=576 memsize=560>)

1: 0x0000000000615240 -> 0x000000000061527f  inuse: 64 bytes (<MChunkPtr chunk=0x615240 mem=0x615250 PREV_INUSE inuse chunksize=64 memsize=48>)

2: 0x0000000000615280 -> 0x00000000006152cf  inuse: 80 bytes (<MChunkPtr chunk=0x615280 mem=0x615290 PREV_INUSE inuse chunksize=80 memsize=64>)

3: 0x00000000006152d0 -> 0x00000000006152ff  inuse: 48 bytes (<MChunkPtr chunk=0x6152d0 mem=0x6152e0 PREV_INUSE inuse chunksize=48 memsize=32>)

4: 0x0000000000615300 -> 0x00000000006153af  inuse: 176 bytes (<MChunkPtr chunk=0x615300 mem=0x615310 PREV_INUSE inuse chunksize=176 memsize=160>)

5: 0x00000000006153b0 -> 0x00000000006153cf  inuse: 32 bytes (<MChunkPtr chunk=0x6153b0 mem=0x6153c0 PREV_INUSE inuse chunksize=32 memsize=16>)

heap free

Free chunks of memory on heap
-----------------------------

top
   0: 0x00000000006153e0 -> 0x000000000063600f   134192 bytes uncategorized::134192 bytes |................................|

fastbin 0
   1: 0x00000000006153c0 -> 0x00000000006153df       32 bytes uncategorized::32 bytes |............AAAA........1.......|

fastbin 1
fastbin 2
fastbin 3

x/10 result
0x6153d0: 0x00000000  0x00000000  0x00020c31  0x00000000
0x6153e0: 0x00000000  0x00000000  0x00000000  0x00000000
0x6153f0: 0x00000000  0x00000000  0x00000000  0x00000000
0x615400: 0x00000000  0x00000000  0x00000000  0x00000000
0x615410: 0x00000000  0x00000000  0x00000000  0x00000000
0x615420: 0x00000000  0x00000000  0x00000000  0x00000000
0x615430: 0x00000000  0x00000000  0x00000000  0x00000000

n (make step ) - trigger read from file and !!!! BUFFER OVERFLOW !!!!
heap free

Free chunks of memory on heap
-----------------------------

top
   0: 0x00000000006153e0 -> 0x4141414141a2951f 4702111234474983744 bytes   C:string data:None |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|

fastbin 0
   1: 0x00000000006153c0 -> 0x00000000006153df       32 bytes uncategorized::32 bytes |............AAAAAAAAAAAAAAAAAAAA|

fastbin 1
fastbin 2
fastbin 3
fastbin 4
fastbin 5

Total size: 4702111234474983776

r - run app

Starting program: /home/icewall/bugs/lhasa/bin/lha -xfw=/tmp /home/icewall/tools/afl-1.94b/out/crashes/id:000001,sig:11,src:002462,op:havoc,rep:2

Program received signal SIGSEGV, Segmentation fault.

0x00007ffff7a9412f in _int_free (av=0x7ffff7dd3760 <main_arena>, p=<optimized out>, have_lock=0) at malloc.c:3996

3996  malloc.c: No such file or directory.
(gdb) bt

#0  0x00007ffff7a9412f in _int_free (av=0x7ffff7dd3760 <main_arena>, p=<optimized out>, have_lock=0) at malloc.c:3996

#1  0x000000000040742b in lha_file_header_free (header=0x615310) at lha_file_header.c:1069
#2  0x0000000000407395 in lha_file_header_read (stream=0x615250) at lha_file_header.c:1044
#3  0x000000000040756f in lha_basic_reader_next_file (reader=0x6152e0) at lha_basic_reader.c:90
#4  0x000000000040478d in lha_reader_next_file (reader=0x615290) at lha_reader.c:310
#5  0x0000000000401c63 in lha_filter_next_file (filter=0x7fffffffde10) at filter.c:132
#6  0x00000000004035da in extract_archive (filter=0x7fffffffde10, options=0x7fffffffde60) at extract.c:588
#7  0x00000000004016fc in do_command (mode=MODE_EXTRACT,
  filename=0x7fffffffe2fd "/home/icewall/tools/afl-1.94b/out/crashes/id:000001,sig:11,src:002462,op:havoc,rep:2", options=0x7fffffffde60,

  filters=0x7fffffffdf80, num_filters=0) at main.c:102
#8  0x00000000004019da in main (argc=3, argv=0x7fffffffdf68) at main.c:266

You can see that free heap chunk get corrupted

Free chunks of memory on heap
-----------------------------

top
   0: 0x00000000006153e0 -> 0x4141414141a2951f 4702111234474983744 bytes   C:string data:None |AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|

VALGRIND:
==================

icewall@ubuntu:~/bugs/lhasa$ valgrind --leak-check=yes bin/lha -xfw=/tmp /home/icewall/bugs/lhasa/new_crashes/id\:000001\,sig\:11\,src\:002462\,op\:havoc\,rep\:2

==11621== Memcheck, a memory error detector
==11621== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==11621== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==11621== Command: bin/lha -xfw=/tmp /home/icewall/bugs/lhasa/new_crashes/id:000001,sig:11,src:002462,op:havoc,rep:2
==11621==
==11621== Invalid write of size 1
==11621==    at 0x4C31BFD: __GI_mempcpy (vg_replace_strmem.c:1508)
==11621==    by 0x4EB047D: _IO_file_xsgetn (fileops.c:1396)
==11621==    by 0x4EA593E: fread (iofread.c:42)
==11621==    by 0x4041FD: file_source_read (lha_input_stream.c:285)
==11621==    by 0x403EA9: do_read (lha_input_stream.c:135)
==11621==    by 0x4040E6: lha_input_stream_read (lha_input_stream.c:229)
==11621==    by 0x406481: extend_raw_data (lha_file_header.c:371)
==11621==    by 0x406E95: decode_level3_header (lha_file_header.c:804)
==11621==    by 0x4071F2: lha_file_header_read (lha_file_header.c:967)
==11621==    by 0x40756E: lha_basic_reader_next_file (lha_basic_reader.c:90)
==11621==    by 0x40478C: lha_reader_next_file (lha_reader.c:310)
==11621==    by 0x401C62: lha_filter_next_file (filter.c:132)
==11621==  Address 0x51fcaff is 1,023 bytes inside an unallocated block of size 4,192,480 in arena "client"
==11621==

References

  1. http://www.onicos.com/staff/iz/formats/lzh.html

Credit

Discovered by Marcin ‘Icewall’ Noga of Cisco TALOS