libclamav/iso9660.c
583cd65f
 /*
c442ca9c
  *  Copyright (C) 2013-2019 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
  *  Copyright (C) 2011-2013 Sourcefire, Inc.
583cd65f
  *
  *  Authors: aCaB <acab@clamav.net>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License version 2 as
  *  published by the Free Software Foundation.
  *
  *  This program is distributed in the hope that it will be useful,
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  *  MA 02110-1301, USA.
  */
 
 #include <string.h>
b2e7c931
 
60d8d2c3
 #include "clamav.h"
583cd65f
 #include "scanners.h"
 #include "iso9660.h"
 #include "fmap.h"
 #include "str.h"
 #include "hashtab.h"
 
 typedef struct {
     cli_ctx *ctx;
     size_t base_offset;
     unsigned int blocksz;
     unsigned int sectsz;
     unsigned int fileno;
     unsigned int joliet;
     char buf[260];
     struct cli_hashset dir_blocks;
 } iso9660_t;
 
 
f304dc68
 static const void *needblock(const iso9660_t *iso, unsigned int block, int temp) {
583cd65f
     cli_ctx *ctx = iso->ctx;
     size_t loff;
     unsigned int blocks_per_sect = (2048 / iso->blocksz);
     if(block > (((*ctx->fmap)->len - iso->base_offset) / iso->sectsz) * blocks_per_sect)
 	return NULL; /* Block is out of file */
     loff = (block / blocks_per_sect) * iso->sectsz;   /* logical sector */
612b9764
     loff += (block % blocks_per_sect) * iso->blocksz; /* logical block within the sector */
583cd65f
     if(temp)
 	return fmap_need_off_once(*ctx->fmap, iso->base_offset + loff, iso->blocksz);
     return fmap_need_off(*ctx->fmap, iso->base_offset + loff, iso->blocksz);
 }
 
 
 static int iso_scan_file(const iso9660_t *iso, unsigned int block, unsigned int len) {
     char *tmpf;
5e922674
     int fd, ret = CL_SUCCESS;
 
d81f9f18
     if(cli_gentempfd(iso->ctx->engine->tmpdir, &tmpf, &fd) != CL_SUCCESS)
612b9764
         return CL_ETMPFILE;
583cd65f
 
     cli_dbgmsg("iso_scan_file: dumping to %s\n", tmpf);
     while(len) {
612b9764
         const void *buf = needblock(iso, block, 1);
         unsigned int todo = MIN(len, iso->blocksz);
         if(!buf) {
             /* Block outside file */
             cli_dbgmsg("iso_scan_file: cannot dump block outside file, ISO may be truncated\n");
5e922674
             ret = CL_EFORMAT;
             break;
612b9764
         }
cd94be7a
         if((unsigned int)cli_writen(fd, buf, todo) != todo) {
5e922674
             cli_warnmsg("iso_scan_file: Can't write to file %s\n", tmpf);
             ret = CL_EWRITE;
             break;
612b9764
         }
         len -= todo;
         block++;
583cd65f
     }
 
5e922674
     if (!len)
d39cb658
         ret = cli_magic_scandesc(fd, tmpf, iso->ctx);
583cd65f
 
     close(fd);
     if(!iso->ctx->engine->keeptmp) {
 	if(cli_unlink(tmpf)) {
5e922674
 	    ret = CL_EUNLINK;
583cd65f
 	}
     }
 
     free(tmpf);
     return ret;
 }
 
f304dc68
 static char *iso_string(iso9660_t *iso, const void *src, unsigned int len) {
583cd65f
     if(iso->joliet) {
 	char *utf8;
3afedd07
         const char *uutf8;
b8cb08ff
 	if(len > (sizeof(iso->buf) - 2))
583cd65f
 	    len = sizeof(iso->buf) - 2;
 	memcpy(iso->buf, src, len);
 	iso->buf[len] = '\0';
 	iso->buf[len+1] = '\0';
 	utf8 = cli_utf16_to_utf8(iso->buf, len, UTF16_BE);
3afedd07
         uutf8 = utf8 ? utf8 : "";
 	strncpy(iso->buf, uutf8, sizeof(iso->buf));
583cd65f
 	iso->buf[sizeof(iso->buf)-1] = '\0';
 	free(utf8);
     } else {
 	memcpy(iso->buf, src, len);
 	iso->buf[len] = '\0';
     }
     return iso->buf;
 }
 
 
7cdb5c7f
 static int iso_parse_dir(iso9660_t *iso, unsigned int block, unsigned int len) {
583cd65f
     cli_ctx *ctx = iso->ctx;
     int ret = CL_CLEAN;
0dfe95a8
     int viruses_found = 0;
583cd65f
 
     if(len < 34) {
 	cli_dbgmsg("iso_parse_dir: Directory too small, skipping\n");
 	return CL_CLEAN;
     }
 
     for(; len && ret == CL_CLEAN; block++, len -= MIN(len, iso->blocksz)) {
f304dc68
 	const uint8_t *dir, *dir_orig;
583cd65f
 	unsigned int dirsz;
 
 	if(iso->dir_blocks.count > 1024) {
 	    cli_dbgmsg("iso_parse_dir: Breaking out due to too many dir records\n");
 	    return CL_BREAK;
 	}
 
 	if(cli_hashset_contains(&iso->dir_blocks, block))
 	    continue;
 
 	if((ret = cli_hashset_addkey(&iso->dir_blocks, block)) != CL_CLEAN)
 	    return ret;
 
 	dir = dir_orig = needblock(iso, block, 0);
 	if(!dir)
 	    return CL_CLEAN;
 
 	for(dirsz = MIN(iso->blocksz, len);;) {
 	    unsigned int entrysz = *dir, fileoff, filesz;
 	    char *sep;
 
 	    if(!dirsz || !entrysz) /* continuing on next block, if any */
 		break;
 	    if(entrysz > dirsz) { /* record size overlaps onto the next sector, no point in looking in there */
 		cli_dbgmsg("iso_parse_dir: Directory entry overflow, breaking out %u %u\n", entrysz, dirsz);
 		len = 0;
 		break;
 	    }
 	    if(entrysz < 34) { /* this shouldn't happen really*/
 		cli_dbgmsg("iso_parse_dir: Too short directory entry, attempting to skip\n");
 		dirsz -= entrysz;
 		dir += entrysz;
 		continue;
 	    }
 	    filesz = dir[32];
 	    if(filesz == 1 && (dir[33] == 0 || dir[33] == 1)) { /* skip "." and ".." */
 		dirsz -= entrysz;
 		dir += entrysz;
 		continue;
 	    }
 
 	    if(filesz + 33 > dirsz) {
 		cli_dbgmsg("iso_parse_dir: Directory entry name overflow, clamping\n");
 		filesz = dirsz - 33;
 	    }
 	    iso_string(iso, &dir[33], filesz);
 	    sep = memchr(iso->buf, ';', filesz);
 	    if(sep)
 		*sep = '\0';
 	    else
 		iso->buf[filesz] = '\0';
 	    fileoff = cli_readint32(dir+2);
 	    fileoff += dir[1];
 	    filesz = cli_readint32(dir+10);
 
 	    cli_dbgmsg("iso_parse_dir: %s '%s': off %x - size %x - flags %x - unit size %x - gap size %x - volume %u\n", (dir[25] & 2) ? "Directory" : "File", iso->buf, fileoff, filesz, dir[25], dir[26], dir[27], cli_readint32(&dir[28]) & 0xffff);
0dfe95a8
             ret = cli_matchmeta(ctx, iso->buf, filesz, filesz, 0, 0, 0, NULL);
             if (ret == CL_VIRUS) {
                 viruses_found = 1;
d7979d4f
                 if (!SCAN_ALLMATCHES)
0dfe95a8
                     break;
7cdb5c7f
                 ret = CL_CLEAN;
0dfe95a8
             }
583cd65f
 
 	    if(dir[26] || dir[27])
 		cli_dbgmsg("iso_parse_dir: Skipping interleaved file\n");
 	    else  {
 		/* TODO Handle multi-extent ? */
 		if(dir[25] & 2) {
7cdb5c7f
 		    ret = iso_parse_dir(iso, fileoff, filesz);
583cd65f
 		} else {
 		    if(cli_checklimits("ISO9660", ctx, filesz, 0, 0) != CL_SUCCESS)
 			cli_dbgmsg("iso_parse_dir: Skipping overlimit file\n");
 		    else
 			ret = iso_scan_file(iso, fileoff, filesz);
 		}
0dfe95a8
                 if (ret == CL_VIRUS) {
                     viruses_found = 1;
d7979d4f
                     if (!SCAN_ALLMATCHES)
0dfe95a8
                         break;
7cdb5c7f
                     ret = CL_CLEAN;
0dfe95a8
                 }
583cd65f
 	    }
 	    dirsz -= entrysz;
 	    dir += entrysz;
 	}
 
 	fmap_unneed_ptr(*ctx->fmap, dir_orig, iso->blocksz);
     }
0dfe95a8
     if (viruses_found == 1)
         return CL_VIRUS;
583cd65f
     return ret;
 }
 
 int cli_scaniso(cli_ctx *ctx, size_t offset) {
f304dc68
     const uint8_t *privol, *next;
583cd65f
     iso9660_t iso;
     int i;
 
     if(offset < 32768)
 	return CL_CLEAN; /* Need 16 sectors at least 2048 bytes long */
 
     privol = fmap_need_off(*ctx->fmap, offset, 2448 + 6);
     if(!privol)
 	return CL_CLEAN;
 
     next = (uint8_t *)cli_memstr((char *)privol + 2049, 2448 + 6 - 2049, "CD001", 5);
     if(!next)
 	return CL_CLEAN; /* Find next volume descriptor */
 
     iso.sectsz = (next - privol) - 1;
     if(iso.sectsz * 16 > offset)
 	return CL_CLEAN; /* Need room for 16 system sectors */
 
     iso.blocksz = cli_readint32(privol+128) & 0xffff;
     if(iso.blocksz != 512 && iso.blocksz != 1024 && iso.blocksz != 2048)
 	return CL_CLEAN; /* Likely not a cdrom image */
 
     iso.base_offset = offset - iso.sectsz * 16;
     iso.joliet = 0;
 
     for(i=16; i<32 ;i++) { /* scan for a joliet secondary volume descriptor */
 	next = fmap_need_off_once(*ctx->fmap, iso.base_offset + i * iso.sectsz, 2048);
 	if(!next)
 	    break; /* Out of disk */
 	if(*next == 0xff || memcmp(next+1, "CD001", 5))
 	    break; /* Not a volume descriptor */
 	if(*next != 2)
 	    continue; /* Not a secondary volume descriptor */
 	if(next[88] != 0x25 || next[89] != 0x2f)
 	    continue; /* Not a joliet descriptor */
 	if(next[156+26] || next[156+27])
 	    continue; /* Root is interleaved so we fallback to the primary descriptor */
 	switch(next[90]) {
 	case 0x40: /* Level 1 */
 	    iso.joliet = 1;
 	    break;
 	case 0x43: /* Level 2 */
 	    iso.joliet = 2;
 	    break;
 	case 0x45: /* Level 3 */
 	    iso.joliet = 3;
 	    break;
 	default: /* Not Joliet */
 	    continue;
 	}
 	break;
     }
 
     /* TODO rr, el torito, udf ? */
 
     /* NOTE: freeing sector now. it is still safe to access as we don't alloc anymore */
     fmap_unneed_off(*ctx->fmap, offset, 2448);
     if(iso.joliet)
 	privol = next;
 
     cli_dbgmsg("in cli_scaniso\n");
     if(cli_debug_flag) {
 	cli_dbgmsg("cli_scaniso: Raw sector size: %u\n", iso.sectsz);
 	cli_dbgmsg("cli_scaniso: Block size: %u\n", iso.blocksz);
 
 	cli_dbgmsg("cli_scaniso: Volume descriptor version: %u\n", privol[6]);
 
 #define ISOSTRING(src, len) iso_string(&iso, (src), (len))
 	cli_dbgmsg("cli_scaniso: System: %s\n", ISOSTRING(privol + 8, 32));
 	cli_dbgmsg("cli_scaniso: Volume: %s\n", ISOSTRING(privol + 40, 32));
 
 	cli_dbgmsg("cli_scaniso: Volume space size: 0x%x blocks\n", cli_readint32(&privol[80]));
 	cli_dbgmsg("cli_scaniso: Volume %u of %u\n", cli_readint32(privol+124) & 0xffff, cli_readint32(privol+120) & 0xffff);
 
 	cli_dbgmsg("cli_scaniso: Volume Set: %s\n", ISOSTRING(privol + 190, 128));
 	cli_dbgmsg("cli_scaniso: Publisher: %s\n", ISOSTRING(privol + 318, 128));
 	cli_dbgmsg("cli_scaniso: Data Preparer: %s\n", ISOSTRING(privol + 446, 128));
 	cli_dbgmsg("cli_scaniso: Application: %s\n", ISOSTRING(privol + 574, 128));
 
 #define ISOTIME(s,n) cli_dbgmsg("cli_scaniso: "s": %c%c%c%c-%c%c-%c%c %c%c:%c%c:%c%c\n", privol[n],privol[n+1],privol[n+2],privol[n+3], privol[n+4],privol[n+5], privol[n+6],privol[n+7], privol[n+8],privol[n+9], privol[n+10],privol[n+11], privol[n+12],privol[n+13])
 	ISOTIME("Volume creation time",813);
 	ISOTIME("Volume modification time",830);
 	ISOTIME("Volume expiration time",847);
 	ISOTIME("Volume effective time",864);
 
 	cli_dbgmsg("cli_scaniso: Path table size: 0x%x\n", cli_readint32(privol+132) & 0xffff);
 	cli_dbgmsg("cli_scaniso: LSB Path Table: 0x%x\n", cli_readint32(privol+140));
 	cli_dbgmsg("cli_scaniso: Opt LSB Path Table: 0x%x\n", cli_readint32(privol+144));
 	cli_dbgmsg("cli_scaniso: MSB Path Table: 0x%x\n", cbswap32(cli_readint32(privol+148)));
 	cli_dbgmsg("cli_scaniso: Opt MSB Path Table: 0x%x\n", cbswap32(cli_readint32(privol+152)));
 	cli_dbgmsg("cli_scaniso: File Structure Version: %u\n", privol[881]);
 
 	if(iso.joliet)
 	    cli_dbgmsg("cli_scaniso: Joliet level %u\n", iso.joliet);
     }
 
     if(privol[156+26] || privol[156+27]) {
 	cli_dbgmsg("cli_scaniso: Interleaved root directory is not supported\n");
 	return CL_CLEAN;
     }
 
     iso.ctx = ctx;
     i = cli_hashset_init(&iso.dir_blocks, 1024, 80);
     if(i != CL_SUCCESS)
 	return i;
7cdb5c7f
     i = iso_parse_dir(&iso, cli_readint32(privol+156+2) + privol[156+1], cli_readint32(privol+156+10));
583cd65f
     cli_hashset_destroy(&iso.dir_blocks);
     if(i == CL_BREAK)
 	return CL_CLEAN;
     return i;
 }