clamav/sigtool/sigtool.c
Valerie Snyder 13c4788f36
FIPS & FIPS-like limits on hash algs for cryptographic uses
ClamAV will not function when using a FIPS-enabled OpenSSL 3.x.
This is because ClamAV uses MD5 and SHA1 algorithms for a variety of
purposes including matching for malware detection, matching to prevent
false positives on known-clean files, and for verification of MD5-based
RSA digital signatures for determining CVD (signature database archive)
authenticity.

Interestingly, FIPS had been intentionally bypassed when creating hashes
based whole buffers and whole files (by descriptor or `FILE`-pointer):
78d4a9985a
Note: this bypassed FIPS the 1.x way with:
`EVP_MD_CTX_set_flags(ctx, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);`

It was NOT disabled when using `cl_hash_init()` / `cl_update_hash()` /
`cl_finish_hash()`. That likely worked by coincidence in that the hash
was already calculated most of the time. It certainly would have made
use of those functions if the hash had not been calculated prior:
78d4a9985a/libclamav/matcher.c (L743)

Regardless, bypassing FIPS entirely is not the correct solution.
The FIPS restrictions against using MD5 and SHA1 are valid, particularly
when verifying CVD digital siganatures, but also I think when using a
hash to determine if the file is known-clean (i.e. the "clean cache" and
also MD5-based and SHA1-based FP signatures).

This commit extends the work to bypass FIPS using the newer 3.x method:
`md = EVP_MD_fetch(NULL, alg, "-fips");`

It does this for the legacy `cl_hash*()` functions including
`cl_hash_init()` / `cl_update_hash()` / `cl_finish_hash()`.
It also introduces extended versions that allow the caller to choose if
they want to bypass FIPS:
- `cl_hash_data_ex()`
- `cl_hash_init_ex()`
- `cl_update_hash_ex()`
- `cl_finish_hash_ex()`
- `cl_hash_destroy_ex()`
- `cl_hash_file_fd_ex()`
See the `flags` parameter for each.

Ironically, this commit does NOT use the new functions at this time.
The rational is that ClamAV may need MD5, SHA1, and SHA-256 hashes of
the same files both for determining if the file is malware, and for
determining if the file is clean.

So instead, this commit will do a checks when:

1. Creating a new ClamAV scanning engine. If FIPS-mode enabled, it will
   automatically toggle the "FIPS limits" engine option.
   When loading signatures, if the engine "FIPS limits" option is enabled,
   then MD5 and SHA1 FP signatures will be skipped.

2. Before verifying a CVD (e.g. also for loading, unpacking when
   verification enabled).
   If "FIPS limits" or FIPS-mode are enabled, then the legacy MD5-based RSA
   method is disabled.

   Note: This commit also refactors the interface for `cl_cvdverify_ex()`
   and `cl_cvdunpack_ex()` so they take a `flags` parameters, rather than a
   single `bool`. As these functions are new in this version, it does not
   break the ABI.

The cache was already switched to use SHA2-256, so that's not a concern
for checking FIPS-mode / FIPS limits options.

This adds an option for `freshclam.conf` and `clamd.conf`:

   FIPSCryptoHashLimits yes

And an equivalent command-line option for `clamscan` and `sigtool`:

   --fips-limits

You may programmatically enable FIPS-limits for a ClamAV engine like this:
```C
   cl_engine_set_num(engine, CL_ENGINE_FIPS_LIMITS, 1);
```

CLAM-2792
2025-08-14 22:39:15 -04:00

4335 lines
137 KiB
C

/*
* Copyright (C) 2013-2025 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
* Copyright (C) 2007-2013 Sourcefire, Inc.
* Copyright (C) 2002-2007 Tomasz Kojm <tkojm@clamav.net>
*
* CDIFF code (C) 2006 Sensory Networks, Inc.
*
* Author: Tomasz Kojm <tkojm@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.
*/
#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>
#include <time.h>
#include <locale.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <ctype.h>
#include <libgen.h>
#ifndef _WIN32
#include <sys/resource.h>
#endif
// libclamav
#include "clamav.h"
#include "matcher.h"
#include "cvd.h"
#include "dsig.h"
#include "str.h"
#include "ole2_extract.h"
#include "htmlnorm.h"
#include "textnorm.h"
#include "default.h"
#include "fmap.h"
#include "readdb.h"
#include "others.h"
#include "pe.h"
#include "entconv.h"
#include "clamav_rust.h"
// common
#include "output.h"
#include "optparser.h"
#include "misc.h"
#include "tar.h"
#include "vba.h"
#define MAX_DEL_LOOKAHEAD 5000
// global variable for the absolute path of the --cvdcertsdir option
char *g_cvdcertsdir = NULL;
// struct s_info info;
short recursion = 0, bell = 0;
short printinfected = 0, printclean = 1;
static const struct dblist_s {
const char *ext;
unsigned int count;
} dblist[] = {
/* special files */
{"info", 0},
{"cfg", 0},
{"ign", 0},
{"ign2", 0},
{"ftm", 0},
/* databases */
{"db", 1},
{"hdb", 1},
{"hdu", 1},
{"hsb", 1},
{"hsu", 1},
{"mdb", 1},
{"mdu", 1},
{"msb", 1},
{"msu", 1},
{"ndb", 1},
{"ndu", 1},
{"ldb", 1},
{"ldu", 1},
{"sdb", 1},
{"zmd", 1},
{"rmd", 1},
{"idb", 0},
{"fp", 1}, // TODO Should count be 0 here? We don't count others like this
{"sfp", 0},
{"gdb", 1},
{"pdb", 1},
{"wdb", 0},
{"crb", 1},
{"cdb", 1},
{"imp", 1},
// TODO Should we add .ioc, .yar, .yara, and .pwdb so that sigtool will
// include these sigs in a build (just in case we need this functionality
// in the future?)
{NULL, 0}};
static char *getdbname(const char *str, char *dst, int dstlen)
{
int len = strlen(str);
if (cli_strbcasestr(str, ".cvd") || cli_strbcasestr(str, ".cld") || cli_strbcasestr(str, ".cud"))
len -= 4;
if (dst) {
strncpy(dst, str, MIN(dstlen - 1, len));
dst[MIN(dstlen - 1, len)] = 0;
} else {
dst = (char *)malloc(len + 1);
if (!dst)
return NULL;
strncpy(dst, str, len - 4);
dst[MIN(dstlen - 1, len - 4)] = 0;
}
return dst;
}
static int hexdump(void)
{
char buffer[FILEBUFF], *pt;
int bytes;
while ((bytes = read(0, buffer, FILEBUFF)) > 0) {
pt = cli_str2hex(buffer, bytes);
if (write(1, pt, 2 * bytes) == -1) {
mprintf(LOGG_ERROR, "hexdump: Can't write to stdout\n");
free(pt);
return -1;
}
free(pt);
}
if (bytes == -1)
return -1;
return 0;
}
static int hashpe(const char *filename, unsigned int class, cli_hash_type_t type)
{
int status = -1;
STATBUF sb;
const char *fmptr;
struct cl_engine *engine = NULL;
cli_ctx ctx = {0};
struct cl_scan_options options = {0};
cl_fmap_t *new_map = NULL;
int fd = -1;
cl_error_t ret;
/* Prepare file */
fd = open(filename, O_RDONLY);
if (fd < 0) {
mprintf(LOGG_ERROR, "hashpe: Can't open file %s!\n", filename);
goto done;
}
lseek(fd, 0, SEEK_SET);
FSTAT(fd, &sb);
new_map = fmap_new(fd, 0, sb.st_size, filename, filename);
if (NULL == new_map) {
mprintf(LOGG_ERROR, "hashpe: Can't create fmap for open file\n");
goto done;
}
/* build engine */
if (!(engine = cl_engine_new())) {
mprintf(LOGG_ERROR, "hashpe: Can't create new engine\n");
goto done;
}
cl_engine_set_num(engine, CL_ENGINE_AC_ONLY, 1);
if (cli_initroots(engine, 0) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "hashpe: cli_initroots() failed\n");
goto done;
}
if (cli_add_content_match_pattern(engine->root[0], "test", "deadbeef", 0, 0, 0, "*", NULL, 0) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "hashpe: Can't parse signature\n");
goto done;
}
if (cl_engine_compile(engine) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "hashpe: Can't compile engine\n");
goto done;
}
/* prepare context */
ctx.engine = engine;
ctx.options = &options;
ctx.options->parse = ~0;
ctx.dconf = (struct cli_dconf *)engine->dconf;
ctx.recursion_stack_size = ctx.engine->max_recursion_level;
ctx.recursion_stack = calloc(sizeof(cli_scan_layer_t), ctx.recursion_stack_size);
if (!ctx.recursion_stack) {
goto done;
}
// ctx was memset, so recursion_level starts at 0.
ctx.recursion_stack[ctx.recursion_level].fmap = new_map;
ctx.recursion_stack[ctx.recursion_level].type = CL_TYPE_ANY; // ANY for the top level, because we don't yet know the type.
ctx.recursion_stack[ctx.recursion_level].size = new_map->len;
ctx.fmap = ctx.recursion_stack[ctx.recursion_level].fmap;
fmptr = fmap_need_off_once(ctx.fmap, 0, sb.st_size);
if (!fmptr) {
mprintf(LOGG_ERROR, "hashpe: fmap_need_off_once failed!\n");
goto done;
}
cl_debug();
/* Send to PE-specific hasher */
switch (class) {
case 1:
ret = cli_genhash_pe(&ctx, CL_GENHASH_PE_CLASS_SECTION, type, NULL);
break;
case 2:
ret = cli_genhash_pe(&ctx, CL_GENHASH_PE_CLASS_IMPTBL, type, NULL);
break;
default:
mprintf(LOGG_ERROR, "hashpe: unknown classification(%u) for pe hash!\n", class);
goto done;
}
/* THIS MAY BE UNNECESSARY */
switch (ret) {
case CL_CLEAN:
break;
case CL_VIRUS:
mprintf(LOGG_DEBUG, "hashpe: CL_VIRUS after cli_genhash_pe()!\n");
break;
case CL_BREAK:
mprintf(LOGG_DEBUG, "hashpe: CL_BREAK after cli_genhash_pe()!\n");
break;
case CL_EFORMAT:
mprintf(LOGG_ERROR, "hashpe: Not a valid PE file!\n");
break;
default:
mprintf(LOGG_ERROR, "hashpe: Other error %d inside cli_genhash_pe.\n", ret);
break;
}
status = 0;
done:
/* Cleanup */
if (NULL != new_map) {
fmap_free(new_map);
}
if (ctx.recursion_stack[ctx.recursion_level].evidence) {
evidence_free(ctx.recursion_stack[ctx.recursion_level].evidence);
}
if (NULL != ctx.recursion_stack) {
free(ctx.recursion_stack);
}
if (NULL != engine) {
cl_engine_free(engine);
}
if (-1 != fd) {
close(fd);
}
return status;
}
static int hashsig(const struct optstruct *opts, unsigned int class, cli_hash_type_t type)
{
char *hash;
unsigned int i;
STATBUF sb;
if (opts->filename) {
for (i = 0; opts->filename[i]; i++) {
if (CLAMSTAT(opts->filename[i], &sb) == -1) {
perror("hashsig");
mprintf(LOGG_ERROR, "hashsig: Can't access file %s\n", opts->filename[i]);
return -1;
} else {
if ((sb.st_mode & S_IFMT) == S_IFREG) {
if ((class == 0) && (hash = cli_hashfile(opts->filename[i], NULL, type))) {
mprintf(LOGG_INFO, "%s:%u:%s\n", hash, (unsigned int)sb.st_size, basename(opts->filename[i]));
free(hash);
} else if ((class > 0) && (hashpe(opts->filename[i], class, type) == 0)) {
/* intentionally empty - printed in cli_genhash_pe() */
} else {
mprintf(LOGG_ERROR, "hashsig: Can't generate hash for %s\n", opts->filename[i]);
return -1;
}
}
}
}
} else { /* stream */
if (class > 0) {
mprintf(LOGG_ERROR, "hashsig: Can't generate requested hash for input stream\n");
return -1;
}
hash = cli_hashstream(stdin, NULL, type);
if (!hash) {
mprintf(LOGG_ERROR, "hashsig: Can't generate hash for input stream\n");
return -1;
}
mprintf(LOGG_INFO, "%s\n", hash);
free(hash);
}
return 0;
}
static int fuzzy_img_file(char *filename)
{
int status = -1;
int target_fd = -1;
FFIError *fuzzy_hash_calc_error = NULL;
uint8_t *mem = NULL;
image_fuzzy_hash_t hash = {0};
STATBUF st;
ssize_t bytes_read;
if ((target_fd = open(filename, O_RDONLY)) == -1) {
char err[128];
mprintf(LOGG_ERROR, "%s: Can't open file: %s\n", basename(filename), cli_strerror(errno, err, sizeof(err)));
goto done;
}
if (FSTAT(target_fd, &st)) {
char err[128];
mprintf(LOGG_ERROR, "%s: fstat() failed: %s\n", basename(filename), cli_strerror(errno, err, sizeof(err)));
goto done;
}
if (NULL == (mem = malloc((size_t)st.st_size))) {
mprintf(LOGG_ERROR, "%s: Malloc failed, buffer size: %zu\n", basename(filename), (size_t)st.st_size);
goto done;
}
bytes_read = read(target_fd, mem, (size_t)st.st_size);
if (bytes_read == -1) {
char err[128];
mprintf(LOGG_ERROR, "%s: Failed to read file: %s\n", basename(filename), cli_strerror(errno, err, sizeof(err)));
goto done;
}
if (bytes_read < (ssize_t)st.st_size) {
mprintf(LOGG_ERROR, "%s: Read fewer bytes than expected. The file may have been modified while attempting to process it.\n", basename(filename));
goto done;
}
if (!fuzzy_hash_calculate_image(mem, (size_t)st.st_size, hash.hash, 8, &fuzzy_hash_calc_error)) {
mprintf(LOGG_ERROR, "%s: Failed to calculate image fuzzy hash: %s\n",
basename(filename),
ffierror_fmt(fuzzy_hash_calc_error));
goto done;
}
char hashstr[17];
snprintf(hashstr, 17, "%02x%02x%02x%02x%02x%02x%02x%02x",
hash.hash[0], hash.hash[1], hash.hash[2], hash.hash[3],
hash.hash[4], hash.hash[5], hash.hash[6], hash.hash[7]);
mprintf(LOGG_INFO, "%s: %s\n", basename(filename), hashstr);
status = 0;
done:
if (NULL != mem) {
free(mem);
}
if (NULL != fuzzy_hash_calc_error) {
ffierror_free(fuzzy_hash_calc_error);
}
if (target_fd != -1) {
close(target_fd);
}
return status;
}
static int fuzzy_img(const struct optstruct *opts)
{
int status = 0;
int ret;
size_t i;
if (!opts->filename) {
mprintf(LOGG_ERROR, "You must provide one or more files to generate a hash.");
status = -1;
goto done;
}
for (i = 0; opts->filename[i]; i++) {
ret = fuzzy_img_file(opts->filename[i]);
if (ret != 0) {
// report failure if any of the files fail
status = -1;
}
}
done:
return status;
}
static cli_ctx *convenience_ctx(int fd, const char *filepath)
{
cl_error_t status = CL_EMEM;
cli_ctx *ctx = NULL;
struct cl_engine *engine = NULL;
cl_fmap_t *new_map = NULL;
/* build engine */
engine = cl_engine_new();
if (NULL == engine) {
printf("convenience_ctx: engine initialization failed\n");
goto done;
}
cl_engine_set_num(engine, CL_ENGINE_AC_ONLY, 1);
if (cli_initroots(engine, 0) != CL_SUCCESS) {
printf("convenience_ctx: cli_initroots() failed\n");
goto done;
}
if (cli_add_content_match_pattern(engine->root[0], "test", "deadbeef", 0, 0, 0, "*", NULL, 0) != CL_SUCCESS) {
printf("convenience_ctx: Can't parse signature\n");
goto done;
}
if (CL_SUCCESS != cl_engine_compile(engine)) {
printf("convenience_ctx: failed to compile engine.");
goto done;
}
/* fake input fmap */
new_map = fmap_new(fd, 0, 0, NULL, filepath);
if (NULL == new_map) {
printf("convenience_ctx: fmap failed\n");
goto done;
}
/* prepare context */
ctx = calloc(1, sizeof(cli_ctx));
if (!ctx) {
printf("convenience_ctx: ctx allocation failed\n");
goto done;
}
ctx->engine = (const struct cl_engine *)engine;
ctx->dconf = (struct cli_dconf *)engine->dconf;
ctx->recursion_stack_size = ctx->engine->max_recursion_level;
ctx->recursion_stack = calloc(sizeof(cli_scan_layer_t), ctx->recursion_stack_size);
if (!ctx->recursion_stack) {
status = CL_EMEM;
goto done;
}
// ctx was calloc'd, so recursion_level starts at 0.
ctx->recursion_stack[ctx->recursion_level].fmap = new_map;
ctx->recursion_stack[ctx->recursion_level].type = CL_TYPE_ANY; // ANY for the top level, because we don't yet know the type.
ctx->recursion_stack[ctx->recursion_level].size = new_map->len;
ctx->fmap = ctx->recursion_stack[ctx->recursion_level].fmap;
ctx->options = calloc(1, sizeof(struct cl_scan_options));
if (!ctx->options) {
printf("convenience_ctx: scan options allocation failed\n");
goto done;
}
ctx->options->general |= CL_SCAN_GENERAL_HEURISTICS;
ctx->options->parse = ~(0);
status = CL_SUCCESS;
done:
if (CL_SUCCESS != status) {
if (NULL != new_map) {
fmap_free(new_map);
}
if (NULL != ctx) {
if (NULL != ctx->options) {
free(ctx->options);
}
if (ctx->recursion_stack[ctx->recursion_level].evidence) {
evidence_free(ctx->recursion_stack[ctx->recursion_level].evidence);
}
if (NULL != ctx->recursion_stack) {
free(ctx->recursion_stack);
}
free(ctx);
ctx = NULL;
}
if (NULL != engine) {
cl_engine_free(engine);
}
}
return ctx;
}
static void destroy_ctx(cli_ctx *ctx)
{
if (NULL != ctx) {
if (NULL != ctx->recursion_stack) {
/* Clean up any fmaps */
while (ctx->recursion_level > 0) {
if (NULL != ctx->recursion_stack[ctx->recursion_level].fmap) {
fmap_free(ctx->recursion_stack[ctx->recursion_level].fmap);
ctx->recursion_stack[ctx->recursion_level].fmap = NULL;
}
ctx->recursion_level -= 1;
}
if (NULL != ctx->recursion_stack[0].fmap) {
fmap_free(ctx->recursion_stack[0].fmap);
ctx->recursion_stack[0].fmap = NULL;
}
if (NULL != ctx->recursion_stack[0].evidence) {
evidence_free(ctx->recursion_stack[0].evidence);
ctx->recursion_stack[0].evidence = NULL;
}
free(ctx->recursion_stack);
ctx->recursion_stack = NULL;
}
if (NULL != ctx->engine) {
cl_engine_free((struct cl_engine *)ctx->engine);
ctx->engine = NULL;
}
if (NULL != ctx->options) {
free(ctx->options);
ctx->options = NULL;
}
free(ctx);
}
}
static int htmlnorm(const struct optstruct *opts)
{
int fd;
cli_ctx *ctx = NULL;
if ((fd = open(optget(opts, "html-normalise")->strarg, O_RDONLY | O_BINARY)) == -1) {
mprintf(LOGG_ERROR, "htmlnorm: Can't open file %s\n", optget(opts, "html-normalise")->strarg);
return -1;
}
if (NULL != (ctx = convenience_ctx(fd, optget(opts, "html-normalise")->strarg))) {
html_normalise_map(ctx, ctx->fmap, ".", NULL, NULL);
fmap_free(ctx->fmap);
} else
mprintf(LOGG_ERROR, "fmap failed\n");
close(fd);
destroy_ctx(ctx);
return 0;
}
static int asciinorm(const struct optstruct *opts)
{
const char *fname;
unsigned char *norm_buff;
struct text_norm_state state;
size_t map_off;
fmap_t *map;
int fd, ofd;
fname = optget(opts, "ascii-normalise")->strarg;
fd = open(fname, O_RDONLY | O_BINARY);
if (fd == -1) {
mprintf(LOGG_ERROR, "asciinorm: Can't open file %s\n", fname);
return -1;
}
if (!(norm_buff = malloc(ASCII_FILE_BUFF_LENGTH))) {
mprintf(LOGG_ERROR, "asciinorm: Can't allocate memory\n");
close(fd);
return -1;
}
if (!(map = fmap_new(fd, 0, 0, fname, fname))) {
mprintf(LOGG_ERROR, "fmap: Could not map fd %d\n", fd);
close(fd);
free(norm_buff);
return -1;
}
if (map->len > MAX_ASCII_FILE_SIZE) {
mprintf(LOGG_ERROR, "asciinorm: File size of %zu too large\n", map->len);
close(fd);
free(norm_buff);
fmap_free(map);
return -1;
}
ofd = open("./normalised_text", O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR);
if (ofd == -1) {
mprintf(LOGG_ERROR, "asciinorm: Can't open file ./normalised_text\n");
close(fd);
free(norm_buff);
fmap_free(map);
return -1;
}
text_normalize_init(&state, norm_buff, ASCII_FILE_BUFF_LENGTH);
map_off = 0;
while (map_off != map->len) {
size_t written;
if (!(written = text_normalize_map(&state, map, map_off))) break;
map_off += written;
if (write(ofd, norm_buff, state.out_pos) == -1) {
mprintf(LOGG_ERROR, "asciinorm: Can't write to file ./normalised_text\n");
close(fd);
close(ofd);
free(norm_buff);
fmap_free(map);
return -1;
}
text_normalize_reset(&state);
}
close(fd);
close(ofd);
free(norm_buff);
fmap_free(map);
return 0;
}
static int utf16decode(const struct optstruct *opts)
{
const char *fname;
char *newname, buff[512], *decoded;
int fd1, fd2, bytes;
fname = optget(opts, "utf16-decode")->strarg;
if ((fd1 = open(fname, O_RDONLY | O_BINARY)) == -1) {
mprintf(LOGG_ERROR, "utf16decode: Can't open file %s\n", fname);
return -1;
}
newname = malloc(strlen(fname) + 7);
if (!newname) {
mprintf(LOGG_ERROR, "utf16decode: Can't allocate memory\n");
close(fd1);
return -1;
}
sprintf(newname, "%s.ascii", fname);
if ((fd2 = open(newname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR)) < 0) {
mprintf(LOGG_ERROR, "utf16decode: Can't create file %s\n", newname);
free(newname);
close(fd1);
return -1;
}
while ((bytes = read(fd1, buff, sizeof(buff))) > 0) {
decoded = cli_utf16toascii(buff, bytes);
if (decoded) {
if (write(fd2, decoded, strlen(decoded)) == -1) {
mprintf(LOGG_ERROR, "utf16decode: Can't write to file %s\n", newname);
free(decoded);
close(fd1);
close(fd2);
unlink(newname);
free(newname);
return -1;
}
free(decoded);
}
}
free(newname);
close(fd1);
close(fd2);
return 0;
}
static char *sha2_256_file(const char *file, unsigned int *size)
{
FILE *fh;
unsigned int i, bytes;
unsigned char digest[32], buffer[FILEBUFF];
char *sha;
void *ctx;
ctx = cl_hash_init("sha2-256");
if (!(ctx))
return NULL;
if (!(fh = fopen(file, "rb"))) {
mprintf(LOGG_ERROR, "sha2_256_file: Can't open file %s\n", file);
cl_hash_destroy(ctx);
return NULL;
}
if (size)
*size = 0;
while ((bytes = fread(buffer, 1, sizeof(buffer), fh))) {
cl_update_hash(ctx, buffer, bytes);
if (size)
*size += bytes;
}
cl_finish_hash(ctx, digest);
sha = (char *)malloc(65);
if (!sha) {
fclose(fh);
return NULL;
}
for (i = 0; i < 32; i++)
sprintf(sha + i * 2, "%02x", digest[i]);
fclose(fh);
return sha;
}
static int writeinfo(const char *dbname, const char *builder, const char *header, const struct optstruct *opts, char *const *dblist2, unsigned int dblist2cnt)
{
FILE *fh;
unsigned int i, bytes;
char file[4096], *pt, dbfile[4096];
unsigned char digest[32], buffer[FILEBUFF];
void *ctx;
snprintf(file, sizeof(file), "%s.info", dbname);
if (!access(file, R_OK)) {
if (unlink(file) == -1) {
mprintf(LOGG_ERROR, "writeinfo: Can't unlink %s\n", file);
return -1;
}
}
if (!(fh = fopen(file, "wb+"))) {
mprintf(LOGG_ERROR, "writeinfo: Can't create file %s\n", file);
return -1;
}
if (fprintf(fh, "%s\n", header) < 0) {
mprintf(LOGG_ERROR, "writeinfo: Can't write to %s\n", file);
fclose(fh);
return -1;
}
if (dblist2cnt) {
for (i = 0; i < dblist2cnt; i++) {
if (!(pt = sha2_256_file(dblist2[i], &bytes))) {
mprintf(LOGG_ERROR, "writeinfo: Can't generate SHA2-256 for %s\n", file);
fclose(fh);
return -1;
}
if (fprintf(fh, "%s:%u:%s\n", dblist2[i], bytes, pt) < 0) {
mprintf(LOGG_ERROR, "writeinfo: Can't write to info file\n");
fclose(fh);
free(pt);
return -1;
}
free(pt);
}
}
if (!dblist2cnt || optget(opts, "hybrid")->enabled) {
for (i = 0; dblist[i].ext; i++) {
snprintf(dbfile, sizeof(dbfile), "%s.%s", dbname, dblist[i].ext);
if (strcmp(dblist[i].ext, "info") && !access(dbfile, R_OK)) {
if (!(pt = sha2_256_file(dbfile, &bytes))) {
mprintf(LOGG_ERROR, "writeinfo: Can't generate SHA2-256 for %s\n", file);
fclose(fh);
return -1;
}
if (fprintf(fh, "%s:%u:%s\n", dbfile, bytes, pt) < 0) {
mprintf(LOGG_ERROR, "writeinfo: Can't write to info file\n");
fclose(fh);
free(pt);
return -1;
}
free(pt);
}
}
}
if (!optget(opts, "unsigned")->enabled) {
rewind(fh);
ctx = cl_hash_init("sha2-256");
if (!(ctx)) {
fclose(fh);
return -1;
}
while ((bytes = fread(buffer, 1, sizeof(buffer), fh)))
cl_update_hash(ctx, buffer, bytes);
cl_finish_hash(ctx, digest);
if (!(pt = cli_getdsig(optget(opts, "server")->strarg, builder, digest, 32, 3))) {
mprintf(LOGG_ERROR, "writeinfo: Can't get digital signature from remote server\n");
fclose(fh);
return -1;
}
fprintf(fh, "DSIG:%s\n", pt);
free(pt);
}
fclose(fh);
return 0;
}
static int diffdirs(const char *old, const char *new, const char *patch);
static int verifydiff(const struct optstruct *opts, const char *diff, const char *cvd, const char *incdir);
static int qcompare(const void *a, const void *b)
{
return strcmp(*(char *const *)a, *(char *const *)b);
}
char *createTempDir(const struct optstruct *opts);
void removeTempDir(const struct optstruct *opts, char *dir)
{
if (!optget(opts, "leave-temps")->enabled) {
cli_rmdirs(dir);
}
}
/**
* @brief Determine the name of the sign file based on the target file name.
*
* If the target file name ends with '.cvd', '.cud', or '.cld' then the sign file name will be
* 'name-version.cvd.sign', 'name-version.cud.sign', or 'name-version.cld.sign' respectively.
*
* If the target file name does not end with '.cvd', '.cud', or '.cld' then the sign file name will be
* 'target.sign'.
*
* The sign file name will be placed in the same directory as the target file.
*
* If the target file is a CVD file, the version number will be extracted from the CVD file.
*
* The caller is responsible for freeing the memory allocated for the sign file name.
*
* @param target The target file name.
* @return char*
*/
static char *get_sign_file_name(char *target)
{
char *sign_file_name = NULL;
char *dir = NULL;
FFIError *cvd_open_error = NULL;
uint32_t cvd_version = 0;
char *cvd_name = NULL;
cvd_t *cvd = NULL;
char *target_dup = NULL;
cvd_type cvd_type = CVD_TYPE_UNKNOWN;
const char *cvd_extension = NULL;
// If the target filename ends with '.cvd',
// the sign file name will be 'name-version.cvd.sign'
if (cli_strbcasestr(target, ".cvd")) {
cvd_type = CVD_TYPE_CVD;
cvd_extension = "cvd";
} else if (cli_strbcasestr(target, ".cld")) {
cvd_type = CVD_TYPE_CLD;
cvd_extension = "cld";
} else if (cli_strbcasestr(target, ".cud")) {
cvd_type = CVD_TYPE_CUD;
cvd_extension = "cud";
}
if (cvd_type != CVD_TYPE_UNKNOWN) {
// Signing a signature archive. We need to open the CVD file to get the version to use in the .sign file name.
cvd = cvd_open(target, &cvd_open_error);
if (NULL == cvd) {
mprintf(LOGG_ERROR, "get_sign_file_name: Failed to open CVD file '%s': %s\n", target, ffierror_fmt(cvd_open_error));
goto done;
}
cvd_version = cvd_get_version(cvd);
cvd_name = cvd_get_name(cvd);
// get the directory from the target filename
target_dup = strdup(target);
if (NULL == target_dup) {
mprintf(LOGG_ERROR, "get_sign_file_name: Failed to duplicate target filepath.\n");
goto done;
}
dir = dirname(target_dup);
if (NULL == dir) {
mprintf(LOGG_ERROR, "get_sign_file_name: Failed to get directory from target filename.\n");
goto done;
}
sign_file_name = calloc(1, strlen(dir) + 1 + strlen(cvd_name) + 1 + (3 * sizeof(uint32_t) + 2) + 1 + strlen(cvd_extension) + strlen(".sign") + 1);
if (NULL == sign_file_name) {
mprintf(LOGG_ERROR, "get_sign_file_name: Memory allocation error.\n");
goto done;
}
sprintf(sign_file_name, "%s/%s-%u.%s.sign", dir, cvd_name, cvd_version, cvd_extension);
} else {
// sign file name will just be the target filename with an added '.sign' extension
sign_file_name = malloc(strlen(target) + strlen(".sign") + 1);
if (NULL == sign_file_name) {
mprintf(LOGG_ERROR, "get_sign_file_name: Memory allocation error.\n");
goto done;
}
strcpy(sign_file_name, target);
strcat(sign_file_name, ".sign");
}
done:
if (NULL != cvd_name) {
ffi_cstring_free(cvd_name);
}
if (NULL != cvd) {
cvd_free(cvd);
}
if (NULL != target_dup) {
free(target_dup);
}
if (NULL != cvd_open_error) {
ffierror_free(cvd_open_error);
}
return sign_file_name;
}
static int sign(const struct optstruct *opts)
{
int ret = -1;
const struct optstruct *opt;
char *target = NULL;
char *sign_file_name = NULL;
char *signing_key = NULL;
char **certs = NULL;
size_t certs_count = 0;
bool sign_result = false;
FFIError *sign_file_error = NULL;
bool append = false;
target = optget(opts, "sign")->strarg;
if (NULL == target) {
mprintf(LOGG_ERROR, "sign: No target file specified.\n");
mprintf(LOGG_ERROR, "To sign a file with sigtool, you must specify a target file and use the --key and --cert options.\n");
mprintf(LOGG_ERROR, "For example: sigtool --sign myfile.cvd --key /path/to/private.key --cert /path/to/public.crt --cert /path/to/intermediate.crt --cert /path/to/root-ca.crt\n");
goto done;
}
sign_file_name = get_sign_file_name(target);
if (NULL == sign_file_name) {
mprintf(LOGG_ERROR, "sign: Failed to determine sign file name from target: %s\n", target);
goto done;
}
signing_key = optget(opts, "key")->strarg;
if (NULL == target) {
mprintf(LOGG_ERROR, "sign: No private key specified.\n");
mprintf(LOGG_ERROR, "To sign a file with sigtool, you must specify a target file and use the --key and --cert options.\n");
mprintf(LOGG_ERROR, "For example: sigtool --sign myfile.cvd --key /path/to/private.key --cert /path/to/public.crt --cert /path/to/intermediate.crt --cert /path/to/root-ca.crt\n");
goto done;
}
opt = optget(opts, "cert");
if (NULL == opt) {
mprintf(LOGG_ERROR, "sign: No signing or intermediate certificates specified.\n");
mprintf(LOGG_ERROR, "To sign a file with sigtool, you must specify a target file and use the --key and --cert options.\n");
mprintf(LOGG_ERROR, "For example: sigtool --sign myfile.cvd --key /path/to/private.key --cert /path/to/public.crt --cert /path/to/intermediate.crt --cert /path/to/root-ca.crt\n");
goto done;
}
while (opt) {
if (!opt->strarg) {
mprintf(LOGG_ERROR, "sign: The --cert option requires a path value to a signing or intermediate certificate.\n");
mprintf(LOGG_ERROR, "To sign a file with sigtool, you must specify a target file and use the --key and --cert options.\n");
mprintf(LOGG_ERROR, "For example: sigtool --sign myfile.cvd --key /path/to/private.key --cert /path/to/public.crt --cert /path/to/intermediate.crt --cert /path/to/root-ca.crt\n");
goto done;
}
certs_count += 1;
certs = realloc(certs, certs_count * sizeof(char *));
if (NULL == certs) {
mprintf(LOGG_ERROR, "sign: Memory allocation error.\n");
goto done;
}
certs[certs_count - 1] = opt->strarg;
opt = opt->nextarg;
}
if (optget(opts, "append")->enabled) {
append = true;
}
sign_result = codesign_sign_file(
target,
sign_file_name,
signing_key,
(const char **)certs,
certs_count,
append,
&sign_file_error);
if (!sign_result) {
mprintf(LOGG_ERROR, "sign: Failed to sign file '%s': %s\n", target, ffierror_fmt(sign_file_error));
goto done;
}
mprintf(LOGG_INFO, "sign: Successfully signed file '%s', and placed the signature in '%s'\n", target, sign_file_name);
ret = 0;
done:
if (NULL != sign_file_name) {
free(sign_file_name);
}
if (NULL != certs) {
free(certs);
}
if (NULL != sign_file_error) {
ffierror_free(sign_file_error);
}
return ret;
}
static int verify(const struct optstruct *opts)
{
int ret = -1;
char *target = NULL;
char *sign_file_name = NULL;
char *signer_name = NULL;
bool verify_result = false;
FFIError *verify_file_error = NULL;
void *verifier = NULL;
FFIError *new_verifier_error = NULL;
target = optget(opts, "verify")->strarg;
if (NULL == target) {
mprintf(LOGG_ERROR, "verify: No target file specified.\n");
mprintf(LOGG_ERROR, "To verify a file signed with sigtool, you must specify a target file. "
"You may also override the default certificates directory using --cvdcertsdir. "
"For example: sigtool --verify myfile.cvd --cvdcertsdir /path/to/certs/\n");
goto done;
}
sign_file_name = get_sign_file_name(target);
if (NULL == sign_file_name) {
mprintf(LOGG_ERROR, "verify: Failed to determine sign file name.\n");
goto done;
}
if (!codesign_verifier_new(g_cvdcertsdir, &verifier, &new_verifier_error)) {
mprintf(LOGG_ERROR, "verify: Failed to create verifier: %s\n", ffierror_fmt(new_verifier_error));
goto done;
}
verify_result = codesign_verify_file(
target,
sign_file_name,
verifier,
&signer_name,
&verify_file_error);
if (!verify_result) {
mprintf(LOGG_ERROR, "verify: Failed to verify file '%s': %s\n", target, ffierror_fmt(verify_file_error));
goto done;
}
mprintf(LOGG_INFO, "verify: Successfully verified file '%s' with signature '%s', signed by '%s'\n", target, sign_file_name, signer_name);
ret = 0;
done:
if (NULL != signer_name) {
ffi_cstring_free(signer_name);
}
if (NULL != sign_file_name) {
free(sign_file_name);
}
if (NULL != verify_file_error) {
ffierror_free(verify_file_error);
}
if (NULL != verifier) {
codesign_verifier_free(verifier);
}
if (NULL != new_verifier_error) {
ffierror_free(new_verifier_error);
}
return ret;
}
static int build(const struct optstruct *opts)
{
int ret, bc = 0, hy = 0;
size_t bytes;
unsigned int i, sigs = 0, oldsigs = 0, entries = 0, version, real_header, fl, maxentries;
STATBUF foo;
unsigned char buffer[FILEBUFF];
char *tarfile, header[513], smbuff[32], builder[33], *pt, olddb[512];
char patch[50], broken[57], dbname[32], dbfile[4096];
const char *newcvd, *localdbdir = NULL;
struct cl_engine *engine;
FILE *cvd, *fh;
gzFile tar;
time_t timet;
struct tm *brokent;
struct cl_cvd *oldcvd;
char **dblist2 = NULL;
unsigned int dblist2cnt = 0;
DIR *dd;
struct dirent *dent;
unsigned int dboptions = 0;
#define FREE_LS(x) \
for (i = 0; i < dblist2cnt; i++) \
free(x[i]); \
free(x);
if (!optget(opts, "server")->enabled && !optget(opts, "unsigned")->enabled) {
mprintf(LOGG_ERROR, "build: --server is required for --build\n");
return -1;
}
if (optget(opts, "datadir")->active)
localdbdir = optget(opts, "datadir")->strarg;
if (CLAMSTAT("COPYING", &foo) == -1) {
mprintf(LOGG_ERROR, "build: COPYING file not found in current working directory.\n");
return -1;
}
getdbname(optget(opts, "build")->strarg, dbname, sizeof(dbname));
if (!strcmp(dbname, "bytecode"))
bc = 1;
if (optget(opts, "hybrid")->enabled)
hy = 1;
if (!(engine = cl_engine_new())) {
mprintf(LOGG_ERROR, "build: Can't initialize antivirus engine\n");
return 50;
}
if (NULL != g_cvdcertsdir) {
if ((ret = cl_engine_set_str(engine, CL_ENGINE_CVDCERTSDIR, g_cvdcertsdir))) {
logg(LOGG_ERROR, "cli_engine_set_str(CL_ENGINE_CVDCERTSDIR) failed: %s\n", cl_strerror(ret));
cl_engine_free(engine);
return -1;
}
}
dboptions = CL_DB_STDOPT | CL_DB_PUA | CL_DB_SIGNED;
if (optget(opts, "fips-limits")->enabled) {
dboptions |= CL_DB_FIPS_LIMITS;
}
if ((ret = cl_load(".", engine, &sigs, dboptions))) {
mprintf(LOGG_ERROR, "build: Can't load database: %s\n", cl_strerror(ret));
cl_engine_free(engine);
return -1;
}
cl_engine_free(engine);
if (!sigs) {
mprintf(LOGG_ERROR, "build: There are no signatures in database files\n");
} else {
if (bc || hy) {
if ((dd = opendir(".")) == NULL) {
mprintf(LOGG_ERROR, "build: Can't open current directory\n");
return -1;
}
while ((dent = readdir(dd))) {
if (dent->d_ino) {
if (cli_strbcasestr(dent->d_name, ".cbc")) {
dblist2 = (char **)realloc(dblist2, (dblist2cnt + 1) * sizeof(char *));
if (!dblist2) { /* dblist2 leaked but we don't really care */
mprintf(LOGG_ERROR, "build: Memory allocation error\n");
closedir(dd);
return -1;
}
dblist2[dblist2cnt] = strdup(dent->d_name);
if (!dblist2[dblist2cnt]) {
FREE_LS(dblist2);
mprintf(LOGG_ERROR, "build: Memory allocation error\n");
return -1;
}
dblist2cnt++;
}
}
}
closedir(dd);
entries += dblist2cnt;
if (dblist2 != NULL) {
qsort(dblist2, dblist2cnt, sizeof(char *), qcompare);
}
if (!access("last.hdb", R_OK)) {
if (!dblist2cnt) {
mprintf(LOGG_ERROR, "build: dblist2 == NULL (no .cbc files?)\n");
return -1;
}
dblist2 = (char **)realloc(dblist2, (dblist2cnt + 1) * sizeof(char *));
if (!dblist2) {
mprintf(LOGG_ERROR, "build: Memory allocation error\n");
return -1;
}
dblist2[dblist2cnt] = strdup("last.hdb");
if (!dblist2[dblist2cnt]) {
FREE_LS(dblist2);
mprintf(LOGG_ERROR, "build: Memory allocation error\n");
return -1;
}
dblist2cnt++;
entries += countlines("last.hdb");
}
}
if (!bc || hy) {
for (i = 0; dblist[i].ext; i++) {
snprintf(dbfile, sizeof(dbfile), "%s.%s", dbname, dblist[i].ext);
if (dblist[i].count && !access(dbfile, R_OK))
entries += countlines(dbfile);
}
}
if (entries != sigs)
mprintf(LOGG_WARNING, "build: Signatures in %s db files: %u, loaded by libclamav: %u\n", dbname, entries, sigs);
maxentries = optget(opts, "max-bad-sigs")->numarg;
if (maxentries) {
if (!entries || (sigs > entries && sigs - entries >= maxentries)) {
mprintf(LOGG_ERROR, "Bad number of signatures in database files\n");
FREE_LS(dblist2);
return -1;
}
}
}
/* try to read cvd header of current database */
if (opts->filename) {
if (cli_strbcasestr(opts->filename[0], ".cvd") || cli_strbcasestr(opts->filename[0], ".cld") || cli_strbcasestr(opts->filename[0], ".cud")) {
strncpy(olddb, opts->filename[0], sizeof(olddb));
olddb[sizeof(olddb) - 1] = '\0';
} else {
mprintf(LOGG_ERROR, "build: Not a CVD/CLD/CUD file\n");
FREE_LS(dblist2);
return -1;
}
} else {
pt = freshdbdir();
snprintf(olddb, sizeof(olddb), "%s" PATHSEP "%s.cvd", localdbdir ? localdbdir : pt, dbname);
if (access(olddb, R_OK))
snprintf(olddb, sizeof(olddb), "%s" PATHSEP "%s.cld", localdbdir ? localdbdir : pt, dbname);
if (access(olddb, R_OK))
snprintf(olddb, sizeof(olddb), "%s" PATHSEP "%s.cud", localdbdir ? localdbdir : pt, dbname);
free(pt);
}
if (!(oldcvd = cl_cvdhead(olddb)) && !optget(opts, "unsigned")->enabled) {
mprintf(LOGG_WARNING, "build: CAN'T READ CVD HEADER OF CURRENT DATABASE %s (wait 3 s)\n", olddb);
sleep(3);
}
if (oldcvd) {
version = oldcvd->version + 1;
oldsigs = oldcvd->sigs;
cl_cvdfree(oldcvd);
} else if (optget(opts, "cvd-version")->numarg != 0) {
version = optget(opts, "cvd-version")->numarg;
} else {
mprintf(LOGG_INFO, "Version number: ");
if (scanf("%u", &version) == EOF) {
mprintf(LOGG_ERROR, "build: scanf() failed\n");
FREE_LS(dblist2);
return -1;
}
}
mprintf(LOGG_INFO, "Total sigs: %u\n", sigs);
if (sigs > oldsigs)
mprintf(LOGG_INFO, "New sigs: %u\n", sigs - oldsigs);
strcpy(header, "ClamAV-VDB:");
/* time */
time(&timet);
brokent = localtime(&timet);
setlocale(LC_TIME, "C");
strftime(smbuff, sizeof(smbuff), "%d %b %Y %H-%M %z", brokent);
strcat(header, smbuff);
/* version */
sprintf(header + strlen(header), ":%u:", version);
/* number of signatures */
sprintf(header + strlen(header), "%u:", sigs);
/* functionality level */
fl = (unsigned int)(optget(opts, "flevel")->numarg);
sprintf(header + strlen(header), "%u:", fl);
real_header = strlen(header);
/* add fake MD5 and dsig (for writeinfo) */
strcat(header, "X:X:");
if ((pt = getenv("SIGNDUSER"))) {
strncpy(builder, pt, sizeof(builder));
builder[sizeof(builder) - 1] = '\0';
} else {
mprintf(LOGG_INFO, "Builder name: ");
if (scanf("%32s", builder) == EOF) {
mprintf(LOGG_ERROR, "build: Can't get builder name\n");
free(dblist2);
return -1;
}
}
/* add builder */
strcat(header, builder);
/* add current time */
sprintf(header + strlen(header), ":" STDu64, (uint64_t)timet);
if (writeinfo(dbname, builder, header, opts, dblist2, dblist2cnt) == -1) {
mprintf(LOGG_ERROR, "build: Can't generate info file\n");
FREE_LS(dblist2);
return -1;
}
header[real_header] = 0;
if (!(tarfile = cli_gentemp("."))) {
mprintf(LOGG_ERROR, "build: Can't generate temporary name for tarfile\n");
FREE_LS(dblist2);
return -1;
}
if ((tar = gzopen(tarfile, "wb9f")) == NULL) {
mprintf(LOGG_ERROR, "build: Can't open file %s for writing\n", tarfile);
free(tarfile);
FREE_LS(dblist2);
return -1;
}
if (tar_addfile(-1, tar, "COPYING") == -1) {
mprintf(LOGG_ERROR, "build: Can't add COPYING to tar archive\n");
gzclose(tar);
unlink(tarfile);
free(tarfile);
FREE_LS(dblist2);
return -1;
}
if (bc || hy) {
if (!hy && tar_addfile(-1, tar, "bytecode.info") == -1) {
gzclose(tar);
unlink(tarfile);
free(tarfile);
FREE_LS(dblist2);
return -1;
}
for (i = 0; i < dblist2cnt; i++) {
if (tar_addfile(-1, tar, dblist2[i]) == -1) {
gzclose(tar);
unlink(tarfile);
free(tarfile);
FREE_LS(dblist2);
return -1;
}
}
}
if (!bc || hy) {
for (i = 0; dblist[i].ext; i++) {
snprintf(dbfile, sizeof(dbfile), "%s.%s", dbname, dblist[i].ext);
if (!access(dbfile, R_OK)) {
if (tar_addfile(-1, tar, dbfile) == -1) {
gzclose(tar);
unlink(tarfile);
free(tarfile);
FREE_LS(dblist2);
return -1;
}
}
}
}
gzclose(tar);
FREE_LS(dblist2);
/* MD5 + dsig */
if (!(fh = fopen(tarfile, "rb"))) {
mprintf(LOGG_ERROR, "build: Can't open file %s for reading\n", tarfile);
unlink(tarfile);
free(tarfile);
return -1;
}
if (!(pt = cli_hashstream(fh, buffer, CLI_HASH_MD5))) {
mprintf(LOGG_ERROR, "build: Can't generate MD5 checksum for %s\n", tarfile);
fclose(fh);
unlink(tarfile);
free(tarfile);
return -1;
}
// Check if the MD5 starts with 00. If it does, we'll return CL_ELAST_ERROR. The caller may try again for better luck.
// This is to avoid a bug in hash verification with ClamAV 1.1 -> 1.4. The bug was fixed in 1.5.0.
// TODO: Remove this workaround when no one is using those versions.
if (pt[0] == '0' && pt[1] == '0') {
// print out the pt hash
mprintf(LOGG_INFO, "The tar.gz MD5 starts with 00, which will fail to verify in ClamAV 1.1 -> 1.4: %s\n", pt);
fclose(fh);
unlink(tarfile);
free(tarfile);
free(pt);
return CL_ELAST_ERROR;
}
rewind(fh);
sprintf(header + strlen(header), "%s:", pt);
free(pt);
if (!optget(opts, "unsigned")->enabled) {
if (!(pt = cli_getdsig(optget(opts, "server")->strarg, builder, buffer, 16, 1))) {
mprintf(LOGG_ERROR, "build: Can't get digital signature from remote server\n");
fclose(fh);
unlink(tarfile);
free(tarfile);
return -1;
}
sprintf(header + strlen(header), "%s:", pt);
free(pt);
} else {
sprintf(header + strlen(header), "X:");
}
/* add builder */
strcat(header, builder);
/* add current time */
sprintf(header + strlen(header), ":" STDu64, (uint64_t)timet);
/* fill up with spaces */
while (strlen(header) < sizeof(header) - 1)
strcat(header, " ");
/* build the final database */
newcvd = optget(opts, "build")->strarg;
if (!(cvd = fopen(newcvd, "wb"))) {
mprintf(LOGG_ERROR, "build: Can't create final database %s\n", newcvd);
fclose(fh);
unlink(tarfile);
free(tarfile);
return -1;
}
if (fwrite(header, 1, 512, cvd) != 512) {
mprintf(LOGG_ERROR, "build: Can't write to %s\n", newcvd);
fclose(fh);
unlink(tarfile);
free(tarfile);
fclose(cvd);
unlink(newcvd);
return -1;
}
while ((bytes = fread(buffer, 1, FILEBUFF, fh)) > 0) {
if (fwrite(buffer, 1, bytes, cvd) != bytes) {
mprintf(LOGG_ERROR, "build: Can't write to %s\n", newcvd);
fclose(fh);
unlink(tarfile);
free(tarfile);
fclose(cvd);
unlink(newcvd);
return -1;
}
}
fclose(fh);
fclose(cvd);
if (unlink(tarfile) == -1) {
mprintf(LOGG_WARNING, "build: Can't unlink %s\n", tarfile);
unlink(tarfile);
free(tarfile);
unlink(newcvd);
return -1;
}
free(tarfile);
mprintf(LOGG_INFO, "Created %s\n", newcvd);
if (!oldcvd || optget(opts, "no-cdiff")->enabled) {
mprintf(LOGG_INFO, "Skipping .cdiff creation\n");
return 0;
}
/* generate patch */
if (!(pt = createTempDir(opts))) {
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(olddb, pt, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "build: Can't unpack CVD file %s\n", olddb);
removeTempDir(opts, pt);
free(pt);
unlink(newcvd);
return -1;
}
strncpy(olddb, pt, sizeof(olddb));
olddb[sizeof(olddb) - 1] = '\0';
free(pt);
if (!(pt = createTempDir(opts))) {
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(newcvd, pt, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "build: Can't unpack CVD file %s\n", newcvd);
removeTempDir(opts, pt);
free(pt);
cli_rmdirs(olddb);
unlink(newcvd);
return -1;
}
snprintf(patch, sizeof(patch), "%s-%u.script", dbname, version);
ret = diffdirs(olddb, pt, patch);
removeTempDir(opts, pt);
free(pt);
if (ret == -1) {
cli_rmdirs(olddb);
unlink(newcvd);
return -1;
}
ret = verifydiff(opts, patch, NULL, olddb);
cli_rmdirs(olddb);
if (ret == -1) {
snprintf(broken, sizeof(broken), "%s.broken", patch);
if (rename(patch, broken)) {
unlink(patch);
mprintf(LOGG_ERROR, "Generated file is incorrect, removed");
} else {
mprintf(LOGG_ERROR, "Generated file is incorrect, renamed to %s\n", broken);
}
return ret;
}
if (optget(opts, "unsigned")->enabled)
return 0;
if (!script2cdiff(patch, builder, optget(opts, "server")->strarg)) {
ret = -1;
} else {
ret = 0;
}
return ret;
}
static int unpack(const struct optstruct *opts)
{
char name[512], *dbdir;
const char *localdbdir = NULL;
const char *certs_directory = NULL;
uint32_t dboptions = 0;
if (optget(opts, "datadir")->active)
localdbdir = optget(opts, "datadir")->strarg;
if (optget(opts, "unpack-current")->enabled) {
dbdir = freshdbdir();
snprintf(name, sizeof(name), "%s" PATHSEP "%s.cvd", localdbdir ? localdbdir : dbdir, optget(opts, "unpack-current")->strarg);
if (access(name, R_OK)) {
snprintf(name, sizeof(name), "%s" PATHSEP "%s.cld", localdbdir ? localdbdir : dbdir, optget(opts, "unpack-current")->strarg);
if (access(name, R_OK)) {
mprintf(LOGG_ERROR, "unpack: Couldn't find %s CLD/CVD database in %s\n", optget(opts, "unpack-current")->strarg, localdbdir ? localdbdir : dbdir);
free(dbdir);
return -1;
}
}
free(dbdir);
} else {
strncpy(name, optget(opts, "unpack")->strarg, sizeof(name));
name[sizeof(name) - 1] = '\0';
}
certs_directory = optget(opts, "cvdcertsdir")->strarg;
if (NULL == certs_directory) {
// Check if the CVD_CERTS_DIR environment variable is set
certs_directory = getenv("CVD_CERTS_DIR");
// If not, use the default value
if (NULL == certs_directory) {
certs_directory = OPT_CERTSDIR;
}
}
if (optget(opts, "fips-limits")->enabled) {
dboptions |= CL_DB_FIPS_LIMITS;
}
if (cl_cvdverify_ex(name, g_cvdcertsdir, dboptions) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "unpack: %s is not a valid CVD\n", name);
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(name, ".", NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "unpack: Can't unpack file %s\n", name);
return -1;
}
return 0;
}
static int cvdinfo(const struct optstruct *opts)
{
struct cl_cvd *cvd;
char *pt;
int ret;
const char *certs_directory = NULL;
uint32_t dboptions = 0;
pt = optget(opts, "info")->strarg;
if ((cvd = cl_cvdhead(pt)) == NULL) {
mprintf(LOGG_ERROR, "cvdinfo: Can't read/parse CVD header of %s\n", pt);
return -1;
}
mprintf(LOGG_INFO, "File: %s\n", pt);
pt = strchr(cvd->time, '-');
if (!pt) {
cl_cvdfree(cvd);
return -1;
}
*pt = ':';
mprintf(LOGG_INFO, "Build time: %s\n", cvd->time);
mprintf(LOGG_INFO, "Version: %u\n", cvd->version);
mprintf(LOGG_INFO, "Signatures: %u\n", cvd->sigs);
mprintf(LOGG_INFO, "Functionality level: %u\n", cvd->fl);
mprintf(LOGG_INFO, "Builder: %s\n", cvd->builder);
pt = optget(opts, "info")->strarg;
if (cli_strbcasestr(pt, ".cvd")) {
mprintf(LOGG_INFO, "MD5: %s\n", cvd->md5);
mprintf(LOGG_INFO, "Digital signature: %s\n", cvd->dsig);
}
cl_cvdfree(cvd);
certs_directory = optget(opts, "cvdcertsdir")->strarg;
if (NULL == certs_directory) {
// Check if the CVD_CERTS_DIR environment variable is set
certs_directory = getenv("CVD_CERTS_DIR");
// If not, use the default value
if (NULL == certs_directory) {
certs_directory = OPT_CERTSDIR;
}
}
if (optget(opts, "fips-limits")->enabled) {
dboptions |= CL_DB_FIPS_LIMITS;
}
if (cli_strbcasestr(pt, ".cud")) {
mprintf(LOGG_INFO, "Verification: Unsigned container\n");
} else if ((ret = cl_cvdverify_ex(pt, certs_directory, dboptions))) {
mprintf(LOGG_ERROR, "cvdinfo: Verification: %s\n", cl_strerror(ret));
return -1;
} else {
mprintf(LOGG_INFO, "Verification OK.\n");
}
return 0;
}
static int listdb(const struct optstruct *opts, const char *filename, const regex_t *regex);
static int listdir(const struct optstruct *opts, const char *dirname, const regex_t *regex)
{
DIR *dd;
struct dirent *dent;
char *dbfile;
if ((dd = opendir(dirname)) == NULL) {
mprintf(LOGG_ERROR, "listdir: Can't open directory %s\n", dirname);
return -1;
}
while ((dent = readdir(dd))) {
if (dent->d_ino) {
if (strcmp(dent->d_name, ".") && strcmp(dent->d_name, "..") &&
(cli_strbcasestr(dent->d_name, ".db") ||
cli_strbcasestr(dent->d_name, ".hdb") ||
cli_strbcasestr(dent->d_name, ".hdu") ||
cli_strbcasestr(dent->d_name, ".hsb") ||
cli_strbcasestr(dent->d_name, ".hsu") ||
cli_strbcasestr(dent->d_name, ".mdb") ||
cli_strbcasestr(dent->d_name, ".mdu") ||
cli_strbcasestr(dent->d_name, ".msb") ||
cli_strbcasestr(dent->d_name, ".msu") ||
cli_strbcasestr(dent->d_name, ".ndb") ||
cli_strbcasestr(dent->d_name, ".ndu") ||
cli_strbcasestr(dent->d_name, ".ldb") ||
cli_strbcasestr(dent->d_name, ".ldu") ||
cli_strbcasestr(dent->d_name, ".sdb") ||
cli_strbcasestr(dent->d_name, ".zmd") ||
cli_strbcasestr(dent->d_name, ".rmd") ||
cli_strbcasestr(dent->d_name, ".cdb") ||
cli_strbcasestr(dent->d_name, ".cbc") ||
cli_strbcasestr(dent->d_name, ".cld") ||
cli_strbcasestr(dent->d_name, ".cvd") ||
cli_strbcasestr(dent->d_name, ".crb") ||
cli_strbcasestr(dent->d_name, ".imp"))) {
dbfile = (char *)malloc(strlen(dent->d_name) + strlen(dirname) + 2);
if (!dbfile) {
mprintf(LOGG_ERROR, "listdir: Can't allocate memory for dbfile\n");
closedir(dd);
return -1;
}
sprintf(dbfile, "%s" PATHSEP "%s", dirname, dent->d_name);
if (listdb(opts, dbfile, regex) == -1) {
mprintf(LOGG_ERROR, "listdb: Error listing database %s\n", dbfile);
free(dbfile);
closedir(dd);
return -1;
}
free(dbfile);
}
}
}
closedir(dd);
return 0;
}
static int listdb(const struct optstruct *opts, const char *filename, const regex_t *regex)
{
FILE *fh;
char *buffer, *pt, *start, *dir;
const char *dbname, *pathsep = PATHSEP;
unsigned int line = 0;
if ((fh = fopen(filename, "rb")) == NULL) {
mprintf(LOGG_ERROR, "listdb: Can't open file %s\n", filename);
return -1;
}
if (!(buffer = (char *)malloc(CLI_DEFAULT_LSIG_BUFSIZE + 1))) {
mprintf(LOGG_ERROR, "listdb: Can't allocate memory for buffer\n");
fclose(fh);
return -1;
}
/* check for CVD file */
if (!fgets(buffer, 12, fh)) {
mprintf(LOGG_ERROR, "listdb: fgets failed\n");
free(buffer);
fclose(fh);
return -1;
}
rewind(fh);
if (!strncmp(buffer, "ClamAV-VDB:", 11)) {
free(buffer);
fclose(fh);
if (!(dir = createTempDir(opts))) {
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(filename, dir, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "listdb: Can't unpack CVD file %s\n", filename);
removeTempDir(opts, dir);
free(dir);
return -1;
}
/* list extracted directory */
if (listdir(opts, dir, regex) == -1) {
mprintf(LOGG_ERROR, "listdb: Can't list directory %s\n", filename);
removeTempDir(opts, dir);
free(dir);
return -1;
}
removeTempDir(opts, dir);
free(dir);
return 0;
}
if (!(dbname = strrchr(filename, *pathsep))) {
mprintf(LOGG_ERROR, "listdb: Invalid filename %s\n", filename);
fclose(fh);
free(buffer);
return -1;
}
dbname++;
if (cli_strbcasestr(filename, ".db")) { /* old style database */
while (fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh)) {
if (regex) {
cli_chomp(buffer);
if (!cli_regexec(regex, buffer, 0, NULL, 0))
mprintf(LOGG_INFO, "[%s] %s\n", dbname, buffer);
continue;
}
line++;
if (buffer && buffer[0] == '#')
continue;
pt = strchr(buffer, '=');
if (!pt) {
mprintf(LOGG_ERROR, "listdb: Malformed pattern line %u (file %s)\n", line, filename);
fclose(fh);
free(buffer);
return -1;
}
start = buffer;
*pt = 0;
if ((pt = strstr(start, " (Clam)")))
*pt = 0;
mprintf(LOGG_INFO, "%s\n", start);
}
} else if (cli_strbcasestr(filename, ".crb")) {
while (fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh)) {
cli_chomp(buffer);
if (buffer[0] == '#')
continue;
if (regex) {
if (!cli_regexec(regex, buffer, 0, NULL, 0))
mprintf(LOGG_INFO, "[%s] %s\n", dbname, buffer);
continue;
}
line++;
mprintf(LOGG_INFO, "%s\n", buffer);
}
} else if (cli_strbcasestr(filename, ".hdb") || cli_strbcasestr(filename, ".hdu") || cli_strbcasestr(filename, ".mdb") || cli_strbcasestr(filename, ".mdu") || cli_strbcasestr(filename, ".hsb") || cli_strbcasestr(filename, ".hsu") || cli_strbcasestr(filename, ".msb") || cli_strbcasestr(filename, ".msu") || cli_strbcasestr(filename, ".imp")) { /* hash database */
while (fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh)) {
cli_chomp(buffer);
if (regex) {
if (!cli_regexec(regex, buffer, 0, NULL, 0))
mprintf(LOGG_INFO, "[%s] %s\n", dbname, buffer);
continue;
}
line++;
if (buffer && buffer[0] == '#')
continue;
start = cli_strtok(buffer, 2, ":");
if (!start) {
mprintf(LOGG_ERROR, "listdb: Malformed pattern line %u (file %s)\n", line, filename);
fclose(fh);
free(buffer);
return -1;
}
if ((pt = strstr(start, " (Clam)")))
*pt = 0;
mprintf(LOGG_INFO, "%s\n", start);
free(start);
}
} else if (cli_strbcasestr(filename, ".ndb") || cli_strbcasestr(filename, ".ndu") || cli_strbcasestr(filename, ".ldb") || cli_strbcasestr(filename, ".ldu") || cli_strbcasestr(filename, ".sdb") || cli_strbcasestr(filename, ".zmd") || cli_strbcasestr(filename, ".rmd") || cli_strbcasestr(filename, ".cdb")) {
while (fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh)) {
cli_chomp(buffer);
if (regex) {
if (!cli_regexec(regex, buffer, 0, NULL, 0))
mprintf(LOGG_INFO, "[%s] %s\n", dbname, buffer);
continue;
}
line++;
if (buffer && buffer[0] == '#')
continue;
if (cli_strbcasestr(filename, ".ldb") || cli_strbcasestr(filename, ".ldu"))
pt = strchr(buffer, ';');
else
pt = strchr(buffer, ':');
if (!pt) {
mprintf(LOGG_ERROR, "listdb: Malformed pattern line %u (file %s)\n", line, filename);
fclose(fh);
free(buffer);
return -1;
}
*pt = 0;
if ((pt = strstr(buffer, " (Clam)")))
*pt = 0;
mprintf(LOGG_INFO, "%s\n", buffer);
}
} else if (cli_strbcasestr(filename, ".cbc")) {
if (fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh) && fgets(buffer, CLI_DEFAULT_LSIG_BUFSIZE, fh)) {
pt = strchr(buffer, ';');
if (!pt) { /* not a real sig */
fclose(fh);
free(buffer);
return 0;
}
if (regex) {
if (!cli_regexec(regex, buffer, 0, NULL, 0)) {
mprintf(LOGG_INFO, "[%s BYTECODE] %s", dbname, buffer);
}
} else {
*pt = 0;
mprintf(LOGG_INFO, "%s\n", buffer);
}
}
}
fclose(fh);
free(buffer);
return 0;
}
static int listsigs(const struct optstruct *opts, int mode)
{
int ret;
const char *name;
char *dbdir;
STATBUF sb;
regex_t reg;
const char *localdbdir = NULL;
if (optget(opts, "datadir")->active)
localdbdir = optget(opts, "datadir")->strarg;
if (mode == 0) {
name = optget(opts, "list-sigs")->strarg;
if (access(name, R_OK) && localdbdir)
name = localdbdir;
if (CLAMSTAT(name, &sb) == -1) {
mprintf(LOGG_INFO, "--list-sigs: Can't get status of %s\n", name);
return -1;
}
mprintf_stdout = 1;
if (S_ISDIR(sb.st_mode)) {
if (!strcmp(name, OPT_DATADIR)) {
dbdir = freshdbdir();
ret = listdir(opts, localdbdir ? localdbdir : dbdir, NULL);
free(dbdir);
} else {
ret = listdir(opts, name, NULL);
}
} else {
ret = listdb(opts, name, NULL);
}
} else {
if (cli_regcomp(&reg, optget(opts, "find-sigs")->strarg, REG_EXTENDED | REG_NOSUB) != 0) {
mprintf(LOGG_INFO, "--find-sigs: Can't compile regex\n");
return -1;
}
mprintf_stdout = 1;
dbdir = freshdbdir();
ret = listdir(opts, localdbdir ? localdbdir : dbdir, &reg);
free(dbdir);
cli_regfree(&reg);
}
return ret;
}
char *createTempDir(const struct optstruct *opts)
{
const struct optstruct *opt;
const char *tempdir = NULL;
char *ret = NULL;
if (opts && ((opt = optget(opts, "tempdir"))->enabled)) {
tempdir = opt->strarg;
} else {
tempdir = cli_gettmpdir();
}
ret = (char *)cli_gentemp_with_prefix(tempdir, "sigtool");
if (NULL == ret) {
logg(LOGG_ERROR, "Can't create temporary directory name.\n");
goto done;
}
if (mkdir(ret, 0700)) {
logg(LOGG_ERROR, "Can't create temporary directory for scan: %s.\n", tempdir);
free((char *)ret);
ret = NULL;
}
done:
return ret;
}
static bool setTempDir(struct cl_engine *engine, const struct optstruct *opts)
{
bool bRet = false;
int ret = -1;
const char *tempdir = createTempDir(opts);
if (NULL == tempdir) {
goto done;
}
if ((ret = cl_engine_set_str(engine, CL_ENGINE_TMPDIR, tempdir))) {
logg(LOGG_ERROR, "cli_engine_set_str(CL_ENGINE_TMPDIR) failed: %s\n", cl_strerror(ret));
goto done;
}
bRet = true;
done:
if (tempdir) {
free((char *)tempdir);
}
return bRet;
}
static void scanfile(const char *filename, struct cl_engine *engine, const struct optstruct *opts, struct cl_scan_options *options)
{
cl_error_t ret = CL_SUCCESS;
int fd = -1;
const char *virname = NULL;
char *real_filename = NULL;
unsigned long int blocks = 0;
if (NULL == filename || NULL == engine || NULL == opts || NULL == options) {
logg(LOGG_INFO, "scanfile: Invalid args.\n");
ret = CL_EARG;
goto done;
}
ret = cli_realpath((const char *)filename, &real_filename);
if (CL_SUCCESS != ret) {
logg(LOGG_DEBUG, "Failed to determine real filename of %s.\n", filename);
logg(LOGG_DEBUG, "Quarantine of the file may fail if file path contains symlinks.\n");
} else {
filename = real_filename;
}
if ((fd = safe_open(filename, O_RDONLY | O_BINARY)) == -1) {
logg(LOGG_WARNING, "Can't open file %s: %s\n", filename, strerror(errno));
goto done;
}
if (CL_CLEAN != cl_scandesc_callback(fd, filename, &virname, &blocks, engine, options, NULL)) {
logg(LOGG_ERROR, "Error parsing '%s'\n", filename);
goto done;
}
done:
if (NULL != real_filename) {
free(real_filename);
}
return;
}
static int vba_callback(const unsigned char *const metaString, const size_t metaStringLen, void *context)
{
size_t i = 0;
UNUSEDPARAM(context);
if (NULL == metaString) {
return -1;
}
for (i = 0; i < metaStringLen; i++) {
#ifndef _WIN32
if ('\r' == metaString[i]) {
continue;
}
#endif
printf("%c", metaString[i]);
}
printf("\n");
return 0;
}
static int vbadump(const struct optstruct *opts)
{
int ret = -1;
struct cl_scan_options options;
struct cl_engine *engine = NULL;
#ifndef _WIN32
struct rlimit rlim;
#endif
const char *filename = NULL;
/* Initialize scan options struct */
memset(&options, 0, sizeof(struct cl_scan_options));
if ((ret = cl_init(CL_INIT_DEFAULT))) {
logg(LOGG_ERROR, "Can't initialize libclamav: %s\n", cl_strerror(ret));
ret = 2;
goto done;
}
if (!(engine = cl_engine_new())) {
logg(LOGG_ERROR, "Can't initialize antivirus engine\n");
ret = 2;
goto done;
}
cl_engine_set_clcb_vba(engine, vba_callback);
if (!setTempDir(engine, opts)) {
ret = 2;
goto done;
}
if ((ret = cl_engine_compile(engine)) != 0) {
logg(LOGG_ERROR, "Database initialization error: %s\n", cl_strerror(ret));
ret = 2;
goto done;
}
#ifndef _WIN32
if (getrlimit(RLIMIT_FSIZE, &rlim) == 0) {
if (rlim.rlim_cur < (rlim_t)cl_engine_get_num(engine, CL_ENGINE_MAX_FILESIZE, NULL))
logg(LOGG_WARNING, "System limit for file size is lower than engine->maxfilesize\n");
if (rlim.rlim_cur < (rlim_t)cl_engine_get_num(engine, CL_ENGINE_MAX_SCANSIZE, NULL))
logg(LOGG_WARNING, "System limit for file size is lower than engine->maxscansize\n");
} else {
logg(LOGG_WARNING, "Cannot obtain resource limits for file size\n");
}
#endif
options.general |= CL_SCAN_GENERAL_COLLECT_METADATA;
/* Have the engine unpack everything so that we don't miss anything. We'll clean it up later. */
options.parse = ~0;
/*
* Need to explicitly set this to always keep temps, since we are scanning extracted ooxml/ole2 files
* from our scan directory to print all the vba content. Remove it later if leave-temps was not specified.
*/
cl_engine_set_num(engine, CL_ENGINE_KEEPTMP, 1);
cl_engine_set_num(engine, CL_ENGINE_TMPDIR_RECURSION, 1);
filename = optget(opts, "vba")->strarg;
if (NULL == filename) {
filename = optget(opts, "vba-hex")->strarg;
}
scanfile(filename, engine, opts, &options);
removeTempDir(opts, engine->tmpdir);
ret = 0;
done:
/* free the engine */
cl_engine_free(engine);
return ret;
}
static int comparesha(const char *diff)
{
char info[32], buff[FILEBUFF], *sha, *pt, *name;
const char *tokens[3];
FILE *fh;
int ret = 0, tokens_count;
name = strdup(diff);
if (!name) {
mprintf(LOGG_ERROR, "verifydiff: strdup() failed\n");
return -1;
}
if (!(pt = strrchr(name, '-')) || !isdigit(pt[1])) {
mprintf(LOGG_ERROR, "verifydiff: Invalid diff name\n");
free(name);
return -1;
}
*pt = 0;
if ((pt = strrchr(name, *PATHSEP)))
pt++;
else
pt = name;
snprintf(info, sizeof(info), "%s.info", pt);
free(name);
if (!(fh = fopen(info, "rb"))) {
mprintf(LOGG_ERROR, "verifydiff: Can't open %s\n", info);
return -1;
}
if (!fgets(buff, sizeof(buff), fh) || strncmp(buff, "ClamAV-VDB", 10)) {
mprintf(LOGG_ERROR, "verifydiff: Incorrect info file %s\n", info);
fclose(fh);
return -1;
}
while (fgets(buff, sizeof(buff), fh)) {
cli_chomp(buff);
tokens_count = cli_strtokenize(buff, ':', 3, tokens);
if (tokens_count != 3) {
if (!strcmp(tokens[0], "DSIG"))
continue;
mprintf(LOGG_ERROR, "verifydiff: Incorrect format of %s\n", info);
ret = -1;
break;
}
if (!(sha = sha2_256_file(tokens[0], NULL))) {
mprintf(LOGG_ERROR, "verifydiff: Can't generate SHA2-256 for %s\n", buff);
ret = -1;
break;
}
if (strcmp(sha, tokens[2])) {
mprintf(LOGG_ERROR, "verifydiff: %s has incorrect checksum\n", buff);
ret = -1;
free(sha);
break;
}
free(sha);
}
fclose(fh);
return ret;
}
static int rundiff(const struct optstruct *opts)
{
int ret;
unsigned short mode;
const char *diff;
FFIError *cdiff_apply_error = NULL;
void *verifier = NULL;
FFIError *new_verifier_error = NULL;
diff = optget(opts, "run-cdiff")->strarg;
if (strstr(diff, ".cdiff")) {
mode = 1;
} else if (strstr(diff, ".script")) {
mode = 0;
} else {
mprintf(LOGG_ERROR, "rundiff: Incorrect file name (no .cdiff/.script extension)\n");
return -1;
}
if (!codesign_verifier_new(g_cvdcertsdir, &verifier, &new_verifier_error)) {
cli_errmsg("rundiff: Failed to create a new code-signature verifier: %s\n", ffierror_fmt(new_verifier_error));
ret = -1;
goto done;
}
if (!cdiff_apply(
diff,
verifier,
mode,
&cdiff_apply_error)) {
mprintf(LOGG_ERROR, "rundiff: Error applying '%s': %s\n",
diff, ffierror_fmt(cdiff_apply_error));
ret = -1;
goto done;
}
// success. compare the SHA2-256 checksums of the files
ret = comparesha(diff);
done:
if (NULL != verifier) {
codesign_verifier_free(verifier);
}
if (NULL != new_verifier_error) {
ffierror_free(new_verifier_error);
}
if (NULL != cdiff_apply_error) {
ffierror_free(cdiff_apply_error);
}
return ret;
}
static int maxlinelen(const char *file)
{
int fd, bytes, n = 0, nmax = 0, i;
char buff[512];
if ((fd = open(file, O_RDONLY | O_BINARY)) == -1) {
mprintf(LOGG_ERROR, "maxlinelen: Can't open file %s\n", file);
return -1;
}
while ((bytes = read(fd, buff, 512)) > 0) {
for (i = 0; i < bytes; i++, ++n) {
if (buff[i] == '\n') {
if (n > nmax)
nmax = n;
n = 0;
}
}
}
if (bytes == -1) {
mprintf(LOGG_ERROR, "maxlinelen: Can't read file %s\n", file);
close(fd);
return -1;
}
close(fd);
return nmax + 1;
}
static int compare(const char *oldpath, const char *newpath, FILE *diff)
{
FILE *old, *new;
char *obuff, *nbuff, *tbuff, *pt, *omd5, *nmd5;
unsigned int oline = 0, tline, found, i, badxchg = 0;
int l1 = 0, l2;
long opos;
if (!access(oldpath, R_OK) && (omd5 = cli_hashfile(oldpath, NULL, CLI_HASH_MD5))) {
if (!(nmd5 = cli_hashfile(newpath, NULL, CLI_HASH_MD5))) {
mprintf(LOGG_ERROR, "compare: Can't get MD5 checksum of %s\n", newpath);
free(omd5);
return -1;
}
if (!strcmp(omd5, nmd5)) {
free(omd5);
free(nmd5);
return 0;
}
free(omd5);
free(nmd5);
l1 = maxlinelen(oldpath);
}
l2 = maxlinelen(newpath);
if (l1 == -1 || l2 == -1)
return -1;
l1 = MAX(l1, l2) + 1;
obuff = malloc(l1);
if (!obuff) {
mprintf(LOGG_ERROR, "compare: Can't allocate memory for 'obuff'\n");
return -1;
}
nbuff = malloc(l1);
if (!nbuff) {
mprintf(LOGG_ERROR, "compare: Can't allocate memory for 'nbuff'\n");
free(obuff);
return -1;
}
tbuff = malloc(l1);
if (!tbuff) {
mprintf(LOGG_ERROR, "compare: Can't allocate memory for 'tbuff'\n");
free(obuff);
free(nbuff);
return -1;
}
if (l1 > CLI_DEFAULT_LSIG_BUFSIZE)
fprintf(diff, "#LSIZE %u\n", l1 + 32);
fprintf(diff, "OPEN %s\n", newpath);
if (!(new = fopen(newpath, "rb"))) {
mprintf(LOGG_ERROR, "compare: Can't open file %s for reading\n", newpath);
free(obuff);
free(nbuff);
free(tbuff);
return -1;
}
old = fopen(oldpath, "rb");
while (fgets(nbuff, l1, new)) {
i = strlen(nbuff);
if (i >= 2 && (nbuff[i - 1] == '\r' || (nbuff[i - 1] == '\n' && nbuff[i - 2] == '\r'))) {
mprintf(LOGG_ERROR, "compare: New %s file contains lines terminated with CRLF or CR\n", newpath);
if (old)
fclose(old);
fclose(new);
free(obuff);
free(nbuff);
free(tbuff);
return -1;
}
cli_chomp(nbuff);
if (!old) {
fprintf(diff, "ADD %s\n", nbuff);
} else {
if (fgets(obuff, l1, old)) {
oline++;
cli_chomp(obuff);
if (!strcmp(nbuff, obuff)) {
continue;
} else {
tline = 0;
found = 0;
opos = ftell(old);
while (fgets(tbuff, l1, old)) {
tline++;
cli_chomp(tbuff);
if (tline > MAX_DEL_LOOKAHEAD)
break;
if (!strcmp(tbuff, nbuff)) {
found = 1;
break;
}
}
fseek(old, opos, SEEK_SET);
if (found) {
strncpy(tbuff, obuff, l1);
tbuff[l1 - 1] = '\0';
for (i = 0; i < tline; i++) {
tbuff[MIN(16, l1 - 1)] = 0;
if ((pt = strchr(tbuff, ' ')))
*pt = 0;
fprintf(diff, "DEL %u %s\n", oline + i, tbuff);
if (!fgets(tbuff, l1, old))
break;
}
oline += tline;
} else {
if (!*obuff || *obuff == ' ') {
badxchg = 1;
break;
}
obuff[MIN(16, l1 - 1)] = 0;
if ((pt = strchr(obuff, ' ')))
*pt = 0;
fprintf(diff, "XCHG %u %s %s\n", oline, obuff, nbuff);
}
}
} else {
fclose(old);
old = NULL;
fprintf(diff, "ADD %s\n", nbuff);
}
}
}
if (old) {
if (!badxchg) {
while (fgets(obuff, l1, old)) {
oline++;
cli_chomp(obuff);
obuff[MIN(16, l1 - 1)] = 0;
if ((pt = strchr(obuff, ' ')))
*pt = 0;
fprintf(diff, "DEL %u %s\n", oline, obuff);
}
}
fclose(old);
}
fprintf(diff, "CLOSE\n");
free(obuff);
free(tbuff);
if (badxchg) {
fprintf(diff, "UNLINK %s\n", newpath);
fprintf(diff, "OPEN %s\n", newpath);
rewind(new);
while (fgets(nbuff, l1, new)) {
cli_chomp(nbuff);
fprintf(diff, "ADD %s\n", nbuff);
}
fprintf(diff, "CLOSE\n");
}
free(nbuff);
fclose(new);
return 0;
}
static int compareone(const struct optstruct *opts)
{
if (!opts->filename) {
mprintf(LOGG_ERROR, "makediff: --compare requires two arguments\n");
return -1;
}
return compare(optget(opts, "compare")->strarg, opts->filename[0], stdout);
}
static int dircopy(const char *src, const char *dest)
{
DIR *dd;
struct dirent *dent;
STATBUF sb;
char spath[512], dpath[512];
if (CLAMSTAT(dest, &sb) == -1) {
if (mkdir(dest, 0755)) {
/* mprintf(LOGG_ERROR, "dircopy: Can't create temporary directory %s\n", dest); */
return -1;
}
}
if ((dd = opendir(src)) == NULL) {
/* mprintf(LOGG_ERROR, "dircopy: Can't open directory %s\n", src); */
return -1;
}
while ((dent = readdir(dd))) {
if (dent->d_ino) {
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
continue;
snprintf(spath, sizeof(spath), "%s" PATHSEP "%s", src, dent->d_name);
snprintf(dpath, sizeof(dpath), "%s" PATHSEP "%s", dest, dent->d_name);
if (filecopy(spath, dpath) == -1) {
/* mprintf(LOGG_ERROR, "dircopy: Can't copy %s to %s\n", spath, dpath); */
cli_rmdirs(dest);
closedir(dd);
return -1;
}
}
}
closedir(dd);
return 0;
}
static int verifydiff(const struct optstruct *opts, const char *diff, const char *cvd, const char *incdir)
{
char *tempdir = NULL;
char cwd[512];
int ret = -1;
unsigned short mode;
FFIError *cdiff_apply_error = NULL;
void *verifier = NULL;
FFIError *new_verifier_error = NULL;
bool created_temp_dir = false;
cl_error_t cl_ret;
char *real_diff = NULL;
if (strstr(diff, ".cdiff")) {
mode = 1;
} else if (strstr(diff, ".script")) {
mode = 0;
} else {
mprintf(LOGG_ERROR, "verifydiff: Incorrect file name (no .cdiff/.script extension)\n");
goto done;
}
if (!(tempdir = createTempDir(opts))) {
goto done;
}
created_temp_dir = true;
if (cvd) {
if (CL_SUCCESS != cl_cvdunpack_ex(cvd, tempdir, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "verifydiff: Can't unpack CVD file %s\n", cvd);
goto done;
}
} else {
if (dircopy(incdir, tempdir) == -1) {
mprintf(LOGG_ERROR, "verifydiff: Can't copy dir %s to %s\n", incdir, tempdir);
goto done;
}
}
if (!getcwd(cwd, sizeof(cwd))) {
mprintf(LOGG_ERROR, "verifydiff: getcwd() failed\n");
goto done;
}
cl_ret = cli_realpath((const char *)diff, &real_diff);
if (CL_SUCCESS != cl_ret) {
mprintf(LOGG_ERROR, "verifydiff: Failed to determine real filename of %s: %s\n", diff, cl_strerror(cl_ret));
goto done;
}
diff = real_diff;
if (chdir(tempdir) == -1) {
mprintf(LOGG_ERROR, "verifydiff: Can't chdir to %s\n", tempdir);
goto done;
}
if (!codesign_verifier_new(g_cvdcertsdir, &verifier, &new_verifier_error)) {
cli_errmsg("verifydiff: Failed to create a new code-signature verifier: %s\n", ffierror_fmt(new_verifier_error));
ret = -1;
goto done;
}
if (!cdiff_apply(
diff,
verifier,
mode,
&cdiff_apply_error)) {
mprintf(LOGG_ERROR, "verifydiff: Can't apply %s: %s\n", diff, ffierror_fmt(cdiff_apply_error));
if (chdir(cwd) == -1) {
mprintf(LOGG_WARNING, "verifydiff: Can't chdir to %s\n", cwd);
}
goto done;
}
ret = comparesha(diff);
if (chdir(cwd) == -1) {
mprintf(LOGG_WARNING, "verifydiff: Can't chdir to %s\n", cwd);
}
if (0 == ret) {
if (cvd) {
mprintf(LOGG_INFO, "Verification: %s correctly applies to %s\n", diff, cvd);
} else {
mprintf(LOGG_INFO, "Verification: %s correctly applies to the previous version\n", diff);
}
}
done:
if (created_temp_dir) {
removeTempDir(opts, tempdir);
}
if (NULL != tempdir) {
free(tempdir);
}
if (NULL != cdiff_apply_error) {
ffierror_free(cdiff_apply_error);
}
if (NULL != real_diff) {
free(real_diff);
}
if (NULL != verifier) {
codesign_verifier_free(verifier);
}
if (NULL != new_verifier_error) {
ffierror_free(new_verifier_error);
}
return ret;
}
/**
* @brief Match a given "signature" in the file fd and return the offset.
*
* The "signature" may be a subsignature to include things like a PCRE special
* subsignature.
*
* @param sig
* @param offset
* @param fd
*/
static void matchsig(char *sig, const char *offset, int fd)
{
struct cli_ac_result *acres = NULL, *res;
STATBUF sb;
unsigned int matches = 0;
struct cl_engine *engine = NULL;
cli_ctx ctx = {0};
struct cl_scan_options options = {0};
cl_fmap_t *new_map = NULL;
struct cli_lsig_tdb tdb = {0};
mprintf(LOGG_INFO, "SUBSIG: %s\n", sig);
/* Prepare file */
lseek(fd, 0, SEEK_SET);
FSTAT(fd, &sb);
new_map = fmap_new(fd, 0, sb.st_size, NULL, NULL);
if (NULL == new_map) {
goto done;
}
/* build engine */
if (!(engine = cl_engine_new())) {
mprintf(LOGG_ERROR, "matchsig: Can't create new engine\n");
goto done;
}
cl_engine_set_num(engine, CL_ENGINE_AC_ONLY, 1);
if (cli_initroots(engine, 0) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "matchsig: cli_initroots() failed\n");
goto done;
}
if (readdb_parse_ldb_subsignature(engine->root[0], "test", sig, "*", NULL, 0, 0, 1, &tdb) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "matchsig: Can't parse signature\n");
goto done;
}
if (cl_engine_compile(engine) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "matchsig: Can't compile engine\n");
goto done;
}
ctx.engine = engine;
ctx.options = &options;
ctx.options->parse = ~0;
ctx.dconf = (struct cli_dconf *)engine->dconf;
ctx.recursion_stack_size = ctx.engine->max_recursion_level;
ctx.recursion_stack = calloc(sizeof(cli_scan_layer_t), ctx.recursion_stack_size);
if (!ctx.recursion_stack) {
goto done;
}
// ctx was memset, so recursion_level starts at 0.
ctx.recursion_stack[ctx.recursion_level].fmap = new_map;
ctx.recursion_stack[ctx.recursion_level].type = CL_TYPE_ANY; // ANY for the top level, because we don't yet know the type.
ctx.recursion_stack[ctx.recursion_level].size = new_map->len;
ctx.fmap = ctx.recursion_stack[ctx.recursion_level].fmap;
(void)cli_scan_fmap(&ctx, CL_TYPE_ANY, false, NULL, AC_SCAN_VIR, &acres);
res = acres;
while (res) {
matches++;
res = res->next;
}
if (matches) {
/* TODO: check offsets automatically */
mprintf(LOGG_INFO, "MATCH: ** YES%s ** (%u %s:", offset ? "/CHECK OFFSET" : "", matches, matches > 1 ? "matches at offsets" : "match at offset");
res = acres;
while (res) {
mprintf(LOGG_INFO, " %u", (unsigned int)res->offset);
res = res->next;
}
mprintf(LOGG_INFO, ")\n");
} else {
mprintf(LOGG_INFO, "MATCH: ** NO **\n");
}
done:
/* Cleanup */
while (acres) {
res = acres;
acres = acres->next;
free(res);
}
if (NULL != new_map) {
fmap_free(new_map);
}
if (ctx.recursion_stack[ctx.recursion_level].evidence) {
evidence_free(ctx.recursion_stack[ctx.recursion_level].evidence);
}
if (NULL != ctx.recursion_stack) {
free(ctx.recursion_stack);
}
if (NULL != engine) {
cl_engine_free(engine);
}
}
static char *decodehexstr(const char *hex, unsigned int *dlen)
{
uint16_t *str16;
char *decoded;
unsigned int i, p = 0, wildcard = 0, len = strlen(hex) / 2;
str16 = cli_hex2ui(hex);
if (!str16)
return NULL;
for (i = 0; i < len; i++)
if (str16[i] & CLI_MATCH_WILDCARD)
wildcard++;
decoded = calloc(len + 1 + wildcard * 32, sizeof(char));
if (!decoded) {
free(str16);
mprintf(LOGG_ERROR, "decodehexstr: Can't allocate memory for decoded\n");
return NULL;
}
for (i = 0; i < len; i++) {
if (str16[i] & CLI_MATCH_WILDCARD) {
switch (str16[i] & CLI_MATCH_WILDCARD) {
case CLI_MATCH_IGNORE:
p += sprintf(decoded + p, "{WILDCARD_IGNORE}");
break;
case CLI_MATCH_NIBBLE_HIGH:
p += sprintf(decoded + p, "{WILDCARD_NIBBLE_HIGH:0x%x}", str16[i] & 0x00f0);
break;
case CLI_MATCH_NIBBLE_LOW:
p += sprintf(decoded + p, "{WILDCARD_NIBBLE_LOW:0x%x}", str16[i] & 0x000f);
break;
default:
mprintf(LOGG_ERROR, "decodehexstr: Unknown wildcard (0x%x@%u)\n", str16[i] & CLI_MATCH_WILDCARD, i);
free(decoded);
free(str16);
return NULL;
}
} else {
decoded[p] = str16[i];
p++;
}
}
if (dlen)
*dlen = p;
free(str16);
return decoded;
}
inline static char *get_paren_end(char *hexstr)
{
char *pt;
int level = 0;
pt = hexstr;
while (*pt != '\0') {
if (*pt == '(') {
level++;
} else if (*pt == ')') {
if (!level)
return pt;
level--;
}
pt++;
}
return NULL;
}
static char *decodehexspecial(const char *hex, unsigned int *dlen)
{
char *pt, *start, *hexcpy, *decoded, *h, *e, *c, op, lop;
unsigned int len = 0, hlen, negative;
int level;
char *buff;
hexcpy = NULL;
buff = NULL;
hexcpy = strdup(hex);
if (!hexcpy) {
mprintf(LOGG_ERROR, "decodehexspecial: strdup(hex) failed\n");
return NULL;
}
pt = strchr(hexcpy, '(');
if (!pt) {
free(hexcpy);
return decodehexstr(hex, dlen);
} else {
buff = calloc(strlen(hex) + 512, sizeof(char));
if (!buff) {
mprintf(LOGG_ERROR, "decodehexspecial: Can't allocate memory for buff\n");
free(hexcpy);
return NULL;
}
start = hexcpy;
do {
negative = 0;
*pt++ = 0;
if (!start) {
mprintf(LOGG_ERROR, "decodehexspecial: Unexpected EOL\n");
free(hexcpy);
free(buff);
return NULL;
}
if (pt >= hexcpy + 2) {
if (pt[-2] == '!') {
negative = 1;
pt[-2] = 0;
}
}
if (!(decoded = decodehexstr(start, &hlen))) {
mprintf(LOGG_ERROR, "Decoding failed (1): %s\n", pt);
free(hexcpy);
free(buff);
return NULL;
}
memcpy(&buff[len], decoded, hlen);
len += hlen;
free(decoded);
if (!(start = get_paren_end(pt))) {
mprintf(LOGG_ERROR, "decodehexspecial: Missing closing parenthesis\n");
free(hexcpy);
free(buff);
return NULL;
}
*start++ = 0;
if (!strlen(pt)) {
mprintf(LOGG_ERROR, "decodehexspecial: Empty block\n");
free(hexcpy);
free(buff);
return NULL;
}
if (!strcmp(pt, "B")) {
if (!*start) {
if (negative)
len += sprintf(buff + len, "{NOT_BOUNDARY_RIGHT}");
else
len += sprintf(buff + len, "{BOUNDARY_RIGHT}");
continue;
} else if (pt - 1 == hexcpy) {
if (negative)
len += sprintf(buff + len, "{NOT_BOUNDARY_LEFT}");
else
len += sprintf(buff + len, "{BOUNDARY_LEFT}");
continue;
}
} else if (!strcmp(pt, "L")) {
if (!*start) {
if (negative)
len += sprintf(buff + len, "{NOT_LINE_MARKER_RIGHT}");
else
len += sprintf(buff + len, "{LINE_MARKER_RIGHT}");
continue;
} else if (pt - 1 == hexcpy) {
if (negative)
len += sprintf(buff + len, "{NOT_LINE_MARKER_LEFT}");
else
len += sprintf(buff + len, "{LINE_MARKER_LEFT}");
continue;
}
} else if (!strcmp(pt, "W")) {
if (!*start) {
if (negative)
len += sprintf(buff + len, "{NOT_WORD_MARKER_RIGHT}");
else
len += sprintf(buff + len, "{WORD_MARKER_RIGHT}");
continue;
} else if (pt - 1 == hexcpy) {
if (negative)
len += sprintf(buff + len, "{NOT_WORD_MARKER_LEFT}");
else
len += sprintf(buff + len, "{WORD_MARKER_LEFT}");
continue;
}
} else {
if (!strlen(pt)) {
mprintf(LOGG_ERROR, "decodehexspecial: Empty block\n");
free(hexcpy);
free(buff);
return NULL;
}
/* TODO: analyze string alternative for typing */
if (negative)
len += sprintf(buff + len, "{EXCLUDING_STRING_ALTERNATIVE:");
else
len += sprintf(buff + len, "{STRING_ALTERNATIVE:");
level = 0;
h = e = pt;
op = '\0';
while ((level >= 0) && (e = strpbrk(h, "()|"))) {
lop = op;
op = *e;
*e++ = 0;
if (op != '(' && lop != ')' && !strlen(h)) {
mprintf(LOGG_ERROR, "decodehexspecial: Empty string alternative block\n");
free(hexcpy);
free(buff);
return NULL;
}
// mprintf(LOGG_INFO, "decodehexspecial: %s\n", h);
if (!(c = cli_hex2str(h))) {
mprintf(LOGG_ERROR, "Decoding failed (3): %s\n", h);
free(hexcpy);
free(buff);
return NULL;
}
memcpy(&buff[len], c, strlen(h) / 2);
len += strlen(h) / 2;
free(c);
switch (op) {
case '(':
level++;
negative = 0;
if (e >= pt + 2) {
if (e[-2] == '!') {
negative = 1;
e[-2] = 0;
}
}
if (negative)
len += sprintf(buff + len, "{EXCLUDING_STRING_ALTERNATIVE:");
else
len += sprintf(buff + len, "{STRING_ALTERNATIVE:");
break;
case ')':
level--;
buff[len++] = '}';
break;
case '|':
buff[len++] = '|';
break;
default:;
}
h = e;
}
if (!(c = cli_hex2str(h))) {
mprintf(LOGG_ERROR, "Decoding failed (4): %s\n", h);
free(hexcpy);
free(buff);
return NULL;
}
memcpy(&buff[len], c, strlen(h) / 2);
len += strlen(h) / 2;
free(c);
buff[len++] = '}';
if (level != 0) {
mprintf(LOGG_ERROR, "decodehexspecial: Invalid string alternative nesting\n");
free(hexcpy);
free(buff);
return NULL;
}
}
} while ((pt = strchr(start, '(')));
if (start) {
if (!(decoded = decodehexstr(start, &hlen))) {
mprintf(LOGG_ERROR, "Decoding failed (2)\n");
free(buff);
free(hexcpy);
return NULL;
}
memcpy(&buff[len], decoded, hlen);
len += hlen;
free(decoded);
}
}
free(hexcpy);
if (dlen)
*dlen = len;
return buff;
}
static int decodehex(const char *hexsig)
{
char *pt, *hexcpy, *start, *n, *decoded, *wild;
int asterisk = 0;
unsigned int i, j, hexlen, dlen, parts = 0;
int mindist = 0, maxdist = 0, error = 0;
ssize_t bytes_written;
hexlen = strlen(hexsig);
if ((wild = strchr(hexsig, '/'))) {
/* ^offset:trigger-logic/regex/options$ */
char *trigger, *regex, *regex_end, *cflags;
size_t tlen = wild - hexsig, rlen = 0, clen;
/* check for trigger */
if (!tlen) {
mprintf(LOGG_ERROR, "pcre without logical trigger\n");
return -1;
}
/* locate end of regex for options start, locate options length */
if ((regex_end = strchr(wild + 1, '/')) == NULL) {
mprintf(LOGG_ERROR, "missing regex expression terminator /\n");
return -1;
}
/* gotta make sure we treat escaped slashes */
for (i = tlen + 1; i < hexlen; i++) {
if (hexsig[i] == '/' && hexsig[i - 1] != '\\') {
rlen = i - tlen - 1;
break;
}
}
if (i == hexlen) {
mprintf(LOGG_ERROR, "missing regex expression terminator /\n");
return -1;
}
clen = hexlen - tlen - rlen - 2; /* 2 from regex boundaries '/' */
/* get the trigger statement */
trigger = calloc(tlen + 1, sizeof(char));
if (!trigger) {
mprintf(LOGG_ERROR, "cannot allocate memory for trigger string\n");
return -1;
}
strncpy(trigger, hexsig, tlen);
trigger[tlen] = '\0';
/* get the regex expression */
regex = calloc(rlen + 1, sizeof(char));
if (!regex) {
mprintf(LOGG_ERROR, "cannot allocate memory for regex expression\n");
free(trigger);
return -1;
}
strncpy(regex, hexsig + tlen + 1, rlen);
regex[rlen] = '\0';
/* get the compile flags */
if (clen) {
cflags = calloc(clen + 1, sizeof(char));
if (!cflags) {
mprintf(LOGG_ERROR, "cannot allocate memory for compile flags\n");
free(trigger);
free(regex);
return -1;
}
strncpy(cflags, hexsig + tlen + rlen + 2, clen);
cflags[clen] = '\0';
} else {
cflags = NULL;
}
/* print components of regex subsig */
mprintf(LOGG_INFO, " +-> TRIGGER: %s\n", trigger);
mprintf(LOGG_INFO, " +-> REGEX: %s\n", regex);
mprintf(LOGG_INFO, " +-> CFLAGS: %s\n", cflags != NULL ? cflags : "null");
free(trigger);
free(regex);
if (cflags)
free(cflags);
return 0;
} else if (strchr(hexsig, '{') || strchr(hexsig, '[')) {
if (!(hexcpy = strdup(hexsig)))
return -1;
for (i = 0; i < hexlen; i++)
if (hexsig[i] == '{' || hexsig[i] == '[' || hexsig[i] == '*')
parts++;
if (parts)
parts++;
start = pt = hexcpy;
for (i = 1; i <= parts; i++) {
if (i != parts) {
for (j = 0; j < strlen(start); j++) {
if (start[j] == '{' || start[j] == '[') {
asterisk = 0;
pt = start + j;
break;
}
if (start[j] == '*') {
asterisk = 1;
pt = start + j;
break;
}
}
*pt++ = 0;
}
if (mindist && maxdist) {
if (mindist == maxdist)
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING(LENGTH==%u)}", mindist);
else
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING(LENGTH>=%u&&<=%u)}", mindist, maxdist);
} else if (mindist)
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING(LENGTH>=%u)}", mindist);
else if (maxdist)
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING(LENGTH<=%u)}", maxdist);
if (!(decoded = decodehexspecial(start, &dlen))) {
mprintf(LOGG_ERROR, "Decoding failed\n");
free(hexcpy);
return -1;
}
bytes_written = write(1, decoded, dlen);
if (bytes_written != dlen) {
mprintf(LOGG_WARNING, "Failed to print all decoded bytes\n");
}
free(decoded);
if (i == parts)
break;
if (asterisk)
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING}");
mindist = maxdist = 0;
if (asterisk) {
start = pt;
continue;
}
if (!(start = strchr(pt, '}')) && !(start = strchr(pt, ']'))) {
error = 1;
break;
}
*start++ = 0;
if (!pt) {
error = 1;
break;
}
if (!strchr(pt, '-')) {
if (!cli_isnumber(pt) || (mindist = maxdist = atoi(pt)) < 0) {
error = 1;
break;
}
} else {
if ((n = cli_strtok(pt, 0, "-"))) {
if (!cli_isnumber(n) || (mindist = atoi(n)) < 0) {
error = 1;
free(n);
break;
}
free(n);
}
if ((n = cli_strtok(pt, 1, "-"))) {
if (!cli_isnumber(n) || (maxdist = atoi(n)) < 0) {
error = 1;
free(n);
break;
}
free(n);
}
if ((n = cli_strtok(pt, 2, "-"))) { /* strict check */
error = 1;
free(n);
break;
}
}
}
free(hexcpy);
if (error)
return -1;
} else if (strchr(hexsig, '*')) {
for (i = 0; i < hexlen; i++)
if (hexsig[i] == '*')
parts++;
if (parts)
parts++;
for (i = 1; i <= parts; i++) {
if ((pt = cli_strtok(hexsig, i - 1, "*")) == NULL) {
mprintf(LOGG_ERROR, "Can't extract part %u of partial signature\n", i);
return -1;
}
if (!(decoded = decodehexspecial(pt, &dlen))) {
mprintf(LOGG_ERROR, "Decoding failed\n");
free(pt);
return -1;
}
bytes_written = write(1, decoded, dlen);
if (bytes_written != dlen) {
mprintf(LOGG_WARNING, "Failed to print all decoded bytes\n");
}
free(decoded);
if (i < parts)
mprintf(LOGG_INFO, "{WILDCARD_ANY_STRING}");
free(pt);
}
} else {
if (!(decoded = decodehexspecial(hexsig, &dlen))) {
mprintf(LOGG_ERROR, "Decoding failed\n");
return -1;
}
bytes_written = write(1, decoded, dlen);
if (bytes_written != dlen) {
mprintf(LOGG_WARNING, "Failed to print all decoded bytes\n");
}
free(decoded);
}
mprintf(LOGG_INFO, "\n");
return 0;
}
static int decodesigmod(const char *sigmod)
{
size_t i;
for (i = 0; i < strlen(sigmod); i++) {
mprintf(LOGG_INFO, " ");
switch (sigmod[i]) {
case 'i':
mprintf(LOGG_INFO, "NOCASE");
break;
case 'f':
mprintf(LOGG_INFO, "FULLWORD");
break;
case 'w':
mprintf(LOGG_INFO, "WIDE");
break;
case 'a':
mprintf(LOGG_INFO, "ASCII");
break;
default:
mprintf(LOGG_INFO, "UNKNOWN");
return -1;
}
}
mprintf(LOGG_INFO, "\n");
return 0;
}
static int decodecdb(char **tokens)
{
int sz = 0;
char *range[2];
if (!tokens)
return -1;
mprintf(LOGG_INFO, "VIRUS NAME: %s\n", tokens[0]);
mprintf(LOGG_INFO, "CONTAINER TYPE: %s\n", (strcmp(tokens[1], "*") ? tokens[1] : "ANY"));
mprintf(LOGG_INFO, "CONTAINER SIZE: ");
if (!cli_isnumber(tokens[2])) {
if (!strcmp(tokens[2], "*")) {
mprintf(LOGG_INFO, "ANY\n");
} else if (strchr(tokens[2], '-')) {
sz = cli_strtokenize(tokens[2], '-', 2, (const char **)range);
if (sz != 2 || !cli_isnumber(range[0]) || !cli_isnumber(range[1])) {
mprintf(LOGG_ERROR, "decodesig: Invalid container size range\n");
return -1;
}
mprintf(LOGG_INFO, "WITHIN RANGE %s to %s\n", range[0], range[1]);
} else {
mprintf(LOGG_ERROR, "decodesig: Invalid container size\n");
return -1;
}
} else {
mprintf(LOGG_INFO, "%s\n", tokens[2]);
}
mprintf(LOGG_INFO, "FILENAME REGEX: %s\n", tokens[3]);
mprintf(LOGG_INFO, "COMPRESSED FILESIZE: ");
if (!cli_isnumber(tokens[4])) {
if (!strcmp(tokens[4], "*")) {
mprintf(LOGG_INFO, "ANY\n");
} else if (strchr(tokens[4], '-')) {
sz = cli_strtokenize(tokens[4], '-', 2, (const char **)range);
if (sz != 2 || !cli_isnumber(range[0]) || !cli_isnumber(range[1])) {
mprintf(LOGG_ERROR, "decodesig: Invalid container size range\n");
return -1;
}
mprintf(LOGG_INFO, "WITHIN RANGE %s to %s\n", range[0], range[1]);
} else {
mprintf(LOGG_ERROR, "decodesig: Invalid compressed filesize\n");
return -1;
}
} else {
mprintf(LOGG_INFO, "%s\n", tokens[4]);
}
mprintf(LOGG_INFO, "UNCOMPRESSED FILESIZE: ");
if (!cli_isnumber(tokens[5])) {
if (!strcmp(tokens[5], "*")) {
mprintf(LOGG_INFO, "ANY\n");
} else if (strchr(tokens[5], '-')) {
sz = cli_strtokenize(tokens[5], '-', 2, (const char **)range);
if (sz != 2 || !cli_isnumber(range[0]) || !cli_isnumber(range[1])) {
mprintf(LOGG_ERROR, "decodesig: Invalid container size range\n");
return -1;
}
mprintf(LOGG_INFO, "WITHIN RANGE %s to %s\n", range[0], range[1]);
} else {
mprintf(LOGG_ERROR, "decodesig: Invalid uncompressed filesize\n");
return -1;
}
} else {
mprintf(LOGG_INFO, "%s\n", tokens[5]);
}
mprintf(LOGG_INFO, "ENCRYPTION: ");
if (!cli_isnumber(tokens[6])) {
if (!strcmp(tokens[6], "*")) {
mprintf(LOGG_INFO, "IGNORED\n");
} else {
mprintf(LOGG_ERROR, "decodesig: Invalid encryption flag\n");
return -1;
}
} else {
mprintf(LOGG_INFO, "%s\n", (atoi(tokens[6]) ? "YES" : "NO"));
}
mprintf(LOGG_INFO, "FILE POSITION: ");
if (!cli_isnumber(tokens[7])) {
if (!strcmp(tokens[7], "*")) {
mprintf(LOGG_INFO, "ANY\n");
} else if (strchr(tokens[7], '-')) {
sz = cli_strtokenize(tokens[7], '-', 2, (const char **)range);
if (sz != 2 || !cli_isnumber(range[0]) || !cli_isnumber(range[1])) {
mprintf(LOGG_ERROR, "decodesig: Invalid container size range\n");
return -1;
}
mprintf(LOGG_INFO, "WITHIN RANGE %s to %s\n", range[0], range[1]);
} else {
mprintf(LOGG_ERROR, "decodesig: Invalid file position\n");
return -1;
}
} else {
mprintf(LOGG_INFO, "%s\n", tokens[7]);
}
if (!strcmp(tokens[1], "CL_TYPE_ZIP") || !strcmp(tokens[1], "CL_TYPE_RAR")) {
if (!strcmp(tokens[8], "*")) {
mprintf(LOGG_INFO, "CRC SUM: ANY\n");
} else {
errno = 0;
sz = (int)strtol(tokens[8], NULL, 16);
if (!sz && errno) {
mprintf(LOGG_ERROR, "decodesig: Invalid cyclic redundancy check sum\n");
return -1;
} else {
mprintf(LOGG_INFO, "CRC SUM: %d\n", sz);
}
}
}
return 0;
}
static int decodeftm(char **tokens, int tokens_count)
{
mprintf(LOGG_INFO, "FILE TYPE NAME: %s\n", tokens[3]);
mprintf(LOGG_INFO, "FILE SIGNATURE TYPE: %s\n", tokens[0]);
mprintf(LOGG_INFO, "FILE MAGIC OFFSET: %s\n", tokens[1]);
mprintf(LOGG_INFO, "FILE MAGIC HEX: %s\n", tokens[2]);
mprintf(LOGG_INFO, "FILE MAGIC DECODED:\n");
decodehex(tokens[2]);
mprintf(LOGG_INFO, "FILE TYPE REQUIRED: %s\n", tokens[4]);
mprintf(LOGG_INFO, "FILE TYPE DETECTED: %s\n", tokens[5]);
if (tokens_count == 7)
mprintf(LOGG_INFO, "FTM FLEVEL: >=%s\n", tokens[6]);
else if (tokens_count == 8)
mprintf(LOGG_INFO, "FTM FLEVEL: %s..%s\n", tokens[6], tokens[7]);
return 0;
}
static int decodesig(char *sig, int fd)
{
char *pt;
char *tokens[68], *subtokens[4], *subhex;
int tokens_count, subtokens_count, subsigs, i, bc = 0;
if (*sig == '[') {
if (!(pt = strchr(sig, ']'))) {
mprintf(LOGG_ERROR, "decodesig: Invalid input\n");
return -1;
}
sig = &pt[2];
}
if (strchr(sig, ';')) { /* lsig */
tokens_count = cli_ldbtokenize(sig, ';', 67 + 1, (const char **)tokens, 2);
if (tokens_count < 4) {
mprintf(LOGG_ERROR, "decodesig: Invalid or not supported signature format\n");
return -1;
}
mprintf(LOGG_INFO, "VIRUS NAME: %s\n", tokens[0]);
if (strlen(tokens[0]) && strstr(tokens[0], ".{") && tokens[0][strlen(tokens[0]) - 1] == '}')
bc = 1;
mprintf(LOGG_INFO, "TDB: %s\n", tokens[1]);
mprintf(LOGG_INFO, "LOGICAL EXPRESSION: %s\n", tokens[2]);
subsigs = cli_ac_chklsig(tokens[2], tokens[2] + strlen(tokens[2]), NULL, NULL, NULL, 1);
if (subsigs == -1) {
mprintf(LOGG_ERROR, "decodesig: Broken logical expression\n");
return -1;
}
subsigs++;
if (subsigs > 64) {
mprintf(LOGG_ERROR, "decodesig: Too many subsignatures\n");
return -1;
}
if (!bc && subsigs != tokens_count - 3) {
mprintf(LOGG_ERROR, "decodesig: The number of subsignatures (==%u) doesn't match the IDs in the logical expression (==%u)\n", tokens_count - 3, subsigs);
return -1;
}
for (i = 0; i < tokens_count - 3; i++) {
if (i >= subsigs)
mprintf(LOGG_INFO, " * BYTECODE SUBSIG\n");
else
mprintf(LOGG_INFO, " * SUBSIG ID %d\n", i);
subtokens_count = cli_ldbtokenize(tokens[3 + i], ':', 4, (const char **)subtokens, 0);
if (!subtokens_count) {
mprintf(LOGG_ERROR, "decodesig: Invalid or not supported subsignature format\n");
return -1;
}
if ((subtokens_count % 2) == 0)
mprintf(LOGG_INFO, " +-> OFFSET: %s\n", subtokens[0]);
else
mprintf(LOGG_INFO, " +-> OFFSET: ANY\n");
if (subtokens_count == 3) {
mprintf(LOGG_INFO, " +-> SIGMOD:");
decodesigmod(subtokens[2]);
} else if (subtokens_count == 4) {
mprintf(LOGG_INFO, " +-> SIGMOD:");
decodesigmod(subtokens[3]);
} else {
mprintf(LOGG_INFO, " +-> SIGMOD: NONE\n");
}
subhex = (subtokens_count % 2) ? subtokens[0] : subtokens[1];
if (fd == -1) {
mprintf(LOGG_INFO, " +-> DECODED SUBSIGNATURE:\n");
decodehex(subhex);
} else {
mprintf(LOGG_INFO, " +-> ");
matchsig(subhex, subhex, fd);
}
}
} else if (strchr(sig, ':')) { /* ndb or cdb or ftm*/
tokens_count = cli_strtokenize(sig, ':', 12 + 1, (const char **)tokens);
if (tokens_count > 9 && tokens_count < 13) { /* cdb*/
return decodecdb(tokens);
}
if (tokens_count > 5 && tokens_count < 9) { /* ftm */
long ftmsigtype;
char *end;
ftmsigtype = strtol(tokens[0], &end, 10);
if (end == tokens[0] + 1 && (ftmsigtype == 0 || ftmsigtype == 1 || ftmsigtype == 4))
return decodeftm(tokens, tokens_count);
}
if (tokens_count < 4 || tokens_count > 6) {
mprintf(LOGG_ERROR, "decodesig: Invalid or not supported signature format\n");
mprintf(LOGG_INFO, "TOKENS COUNT: %u\n", tokens_count);
return -1;
}
mprintf(LOGG_INFO, "VIRUS NAME: %s\n", tokens[0]);
if (tokens_count == 5)
mprintf(LOGG_INFO, "FUNCTIONALITY LEVEL: >=%s\n", tokens[4]);
else if (tokens_count == 6)
mprintf(LOGG_INFO, "FUNCTIONALITY LEVEL: %s..%s\n", tokens[4], tokens[5]);
if (!cli_isnumber(tokens[1])) {
mprintf(LOGG_ERROR, "decodesig: Invalid target type\n");
return -1;
}
mprintf(LOGG_INFO, "TARGET TYPE: ");
switch (atoi(tokens[1])) {
case 0:
mprintf(LOGG_INFO, "ANY FILE\n");
break;
case 1:
mprintf(LOGG_INFO, "PE\n");
break;
case 2:
mprintf(LOGG_INFO, "OLE2\n");
break;
case 3:
mprintf(LOGG_INFO, "HTML\n");
break;
case 4:
mprintf(LOGG_INFO, "MAIL\n");
break;
case 5:
mprintf(LOGG_INFO, "GRAPHICS\n");
break;
case 6:
mprintf(LOGG_INFO, "ELF\n");
break;
case 7:
mprintf(LOGG_INFO, "NORMALIZED ASCII TEXT\n");
break;
case 8:
mprintf(LOGG_INFO, "DISASM DATA\n");
break;
case 9:
mprintf(LOGG_INFO, "MACHO\n");
break;
case 10:
mprintf(LOGG_INFO, "PDF\n");
break;
case 11:
mprintf(LOGG_INFO, "FLASH\n");
break;
case 12:
mprintf(LOGG_INFO, "JAVA CLASS\n");
break;
default:
mprintf(LOGG_ERROR, "decodesig: Invalid target type\n");
return -1;
}
mprintf(LOGG_INFO, "OFFSET: %s\n", tokens[2]);
if (fd == -1) {
mprintf(LOGG_INFO, "DECODED SIGNATURE:\n");
decodehex(tokens[3]);
} else {
matchsig(tokens[3], strcmp(tokens[2], "*") ? tokens[2] : NULL, fd);
}
} else if ((pt = strchr(sig, '='))) {
*pt++ = 0;
mprintf(LOGG_INFO, "VIRUS NAME: %s\n", sig);
if (fd == -1) {
mprintf(LOGG_INFO, "DECODED SIGNATURE:\n");
decodehex(pt);
} else {
matchsig(pt, NULL, fd);
}
} else {
mprintf(LOGG_INFO, "decodesig: Not supported signature format\n");
return -1;
}
return 0;
}
static int decodesigs(void)
{
char buffer[32769];
fflush(stdin);
while (fgets(buffer, sizeof(buffer), stdin)) {
cli_chomp(buffer);
if (!strlen(buffer))
break;
if (decodesig(buffer, -1) == -1)
return -1;
}
return 0;
}
static int testsigs(const struct optstruct *opts)
{
char buffer[32769];
FILE *sigs;
int ret = 0, fd;
if (!opts->filename) {
mprintf(LOGG_ERROR, "--test-sigs requires two arguments\n");
return -1;
}
sigs = fopen(optget(opts, "test-sigs")->strarg, "rb");
if (!sigs) {
mprintf(LOGG_ERROR, "testsigs: Can't open file %s\n", optget(opts, "test-sigs")->strarg);
return -1;
}
fd = open(opts->filename[0], O_RDONLY | O_BINARY);
if (fd == -1) {
mprintf(LOGG_ERROR, "testsigs: Can't open file %s\n", optget(opts, "test-sigs")->strarg);
fclose(sigs);
return -1;
}
while (fgets(buffer, sizeof(buffer), sigs)) {
cli_chomp(buffer);
if (!strlen(buffer))
break;
if (decodesig(buffer, fd) == -1) {
ret = -1;
break;
}
}
close(fd);
fclose(sigs);
return ret;
}
static int diffdirs(const char *old, const char *new, const char *patch)
{
FILE *diff;
DIR *dd;
struct dirent *dent;
char cwd[512], path[1024];
if (!getcwd(cwd, sizeof(cwd))) {
mprintf(LOGG_ERROR, "diffdirs: getcwd() failed\n");
return -1;
}
if (!(diff = fopen(patch, "wb"))) {
mprintf(LOGG_ERROR, "diffdirs: Can't open %s for writing\n", patch);
return -1;
}
if (chdir(new) == -1) {
mprintf(LOGG_ERROR, "diffdirs: Can't chdir to %s\n", new);
fclose(diff);
return -1;
}
if ((dd = opendir(new)) == NULL) {
mprintf(LOGG_ERROR, "diffdirs: Can't open directory %s\n", new);
fclose(diff);
return -1;
}
while ((dent = readdir(dd))) {
if (dent->d_ino) {
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
continue;
snprintf(path, sizeof(path), "%s" PATHSEP "%s", old, dent->d_name);
if (compare(path, dent->d_name, diff) == -1) {
if (chdir(cwd) == -1)
mprintf(LOGG_WARNING, "diffdirs: Can't chdir to %s\n", cwd);
fclose(diff);
unlink(patch);
closedir(dd);
return -1;
}
}
}
closedir(dd);
/* check for removed files */
if ((dd = opendir(old)) == NULL) {
mprintf(LOGG_ERROR, "diffdirs: Can't open directory %s\n", old);
fclose(diff);
return -1;
}
while ((dent = readdir(dd))) {
if (dent->d_ino) {
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
continue;
snprintf(path, sizeof(path), "%s" PATHSEP "%s", new, dent->d_name);
if (access(path, R_OK))
fprintf(diff, "UNLINK %s\n", dent->d_name);
}
}
closedir(dd);
fclose(diff);
mprintf(LOGG_INFO, "Generated diff file %s\n", patch);
if (chdir(cwd) == -1)
mprintf(LOGG_WARNING, "diffdirs: Can't chdir to %s\n", cwd);
return 0;
}
static int makediff(const struct optstruct *opts)
{
char *odir, *ndir, name[PATH_MAX], broken[PATH_MAX + 8], dbname[PATH_MAX];
struct cl_cvd *cvd;
unsigned int oldver, newver;
int ret;
if (!opts->filename) {
mprintf(LOGG_ERROR, "makediff: --diff requires two arguments\n");
return -1;
}
if (!(cvd = cl_cvdhead(opts->filename[0]))) {
mprintf(LOGG_ERROR, "makediff: Can't read CVD header from %s\n", opts->filename[0]);
return -1;
}
newver = cvd->version;
cl_cvdfree(cvd);
if (!(cvd = cl_cvdhead(optget(opts, "diff")->strarg))) {
mprintf(LOGG_ERROR, "makediff: Can't read CVD header from %s\n", optget(opts, "diff")->strarg);
return -1;
}
oldver = cvd->version;
cl_cvdfree(cvd);
if (oldver + 1 != newver) {
mprintf(LOGG_ERROR, "makediff: The old CVD must be %u\n", newver - 1);
return -1;
}
if (!(odir = createTempDir(opts))) {
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(optget(opts, "diff")->strarg, odir, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "makediff: Can't unpack CVD file %s\n", optget(opts, "diff")->strarg);
removeTempDir(opts, odir);
free(odir);
return -1;
}
if (!(ndir = createTempDir(opts))) {
return -1;
}
if (CL_SUCCESS != cl_cvdunpack_ex(opts->filename[0], ndir, NULL, CL_DB_UNSIGNED)) {
mprintf(LOGG_ERROR, "makediff: Can't unpack CVD file %s\n", opts->filename[0]);
removeTempDir(opts, odir);
removeTempDir(opts, ndir);
free(odir);
free(ndir);
return -1;
}
snprintf(name, sizeof(name), "%s-%u.script", getdbname(opts->filename[0], dbname, sizeof(dbname)), newver);
ret = diffdirs(odir, ndir, name);
removeTempDir(opts, odir);
removeTempDir(opts, ndir);
free(odir);
free(ndir);
if (ret == -1)
return -1;
if (verifydiff(opts, name, optget(opts, "diff")->strarg, NULL) == -1) {
snprintf(broken, sizeof(broken), "%s.broken", name);
if (rename(name, broken)) {
unlink(name);
mprintf(LOGG_ERROR, "Generated file is incorrect, removed");
} else {
mprintf(LOGG_ERROR, "Generated file is incorrect, renamed to %s\n", broken);
}
return -1;
}
return 0;
}
static int dumpcerts(const struct optstruct *opts)
{
int status = -1;
char *filename = NULL;
STATBUF sb;
struct cl_engine *engine = NULL;
cli_ctx ctx = {0};
struct cl_scan_options options = {0};
int fd = -1;
cl_fmap_t *new_map = NULL;
cl_error_t ret;
logg_file = NULL;
filename = optget(opts, "print-certs")->strarg;
if (!filename) {
mprintf(LOGG_ERROR, "dumpcerts: No filename!\n");
goto done;
}
/* Prepare file */
fd = open(filename, O_RDONLY | O_BINARY);
if (fd < 0) {
mprintf(LOGG_ERROR, "dumpcerts: Can't open file %s!\n", filename);
goto done;
}
lseek(fd, 0, SEEK_SET);
FSTAT(fd, &sb);
new_map = fmap_new(fd, 0, sb.st_size, filename, filename);
if (NULL == new_map) {
mprintf(LOGG_ERROR, "dumpcerts: Can't create fmap for open file\n");
goto done;
}
/* build engine */
if (!(engine = cl_engine_new())) {
mprintf(LOGG_ERROR, "dumpcerts: Can't create new engine\n");
goto done;
}
cl_engine_set_num(engine, CL_ENGINE_AC_ONLY, 1);
if (cli_initroots(engine, 0) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "dumpcerts: cli_initroots() failed\n");
goto done;
}
if (cli_add_content_match_pattern(engine->root[0], "test", "deadbeef", 0, 0, 0, "*", NULL, 0) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "dumpcerts: Can't parse signature\n");
goto done;
}
if (cl_engine_compile(engine) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "dumpcerts: Can't compile engine\n");
goto done;
}
cl_engine_set_num(engine, CL_ENGINE_PE_DUMPCERTS, 1);
cl_debug();
/* prepare context */
ctx.engine = engine;
ctx.options = &options;
ctx.options->parse = ~0;
ctx.dconf = (struct cli_dconf *)engine->dconf;
ctx.recursion_stack_size = ctx.engine->max_recursion_level;
ctx.recursion_stack = calloc(sizeof(cli_scan_layer_t), ctx.recursion_stack_size);
if (!ctx.recursion_stack) {
goto done;
}
// ctx was memset, so recursion_level starts at 0.
ctx.recursion_stack[ctx.recursion_level].fmap = new_map;
ctx.recursion_stack[ctx.recursion_level].type = CL_TYPE_ANY; // ANY for the top level, because we don't yet know the type.
ctx.recursion_stack[ctx.recursion_level].size = new_map->len;
ctx.fmap = ctx.recursion_stack[ctx.recursion_level].fmap;
ret = cli_check_auth_header(&ctx, NULL);
switch (ret) {
case CL_VERIFIED:
case CL_VIRUS:
// These shouldn't happen, since sigtool doesn't load in any sigs
break;
case CL_EVERIFY:
// The Authenticode header was parsed successfully but there were
// no applicable trust/block rules
break;
case CL_BREAK:
mprintf(LOGG_DEBUG, "dumpcerts: No Authenticode signature detected\n");
break;
case CL_EFORMAT:
mprintf(LOGG_ERROR, "dumpcerts: An error occurred when parsing the file\n");
break;
default:
mprintf(LOGG_ERROR, "dumpcerts: Other error %d inside cli_check_auth_header.\n", ret);
break;
}
status = 0;
done:
/* Cleanup */
if (NULL != new_map) {
fmap_free(new_map);
}
if (ctx.recursion_stack[ctx.recursion_level].evidence) {
evidence_free(ctx.recursion_stack[ctx.recursion_level].evidence);
}
if (NULL != ctx.recursion_stack) {
free(ctx.recursion_stack);
}
if (NULL != engine) {
cl_engine_free(engine);
}
if (-1 != fd) {
close(fd);
}
return status;
}
static void help(void)
{
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Clam AntiVirus: Signature Tool %s\n", get_version());
mprintf(LOGG_INFO, " By The ClamAV Team: https://www.clamav.net/about.html#credits\n");
mprintf(LOGG_INFO, " (C) 2025 Cisco Systems, Inc.\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " sigtool [options]\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --help -h Show this help\n");
mprintf(LOGG_INFO, " --version -V Print version number and exit\n");
mprintf(LOGG_INFO, " --quiet Be quiet, output only error messages\n");
mprintf(LOGG_INFO, " --debug Enable debug messages\n");
mprintf(LOGG_INFO, " --stdout Write to stdout instead of stderr. Does not affect 'debug' messages.\n");
mprintf(LOGG_INFO, " --tempdir=DIRECTORY Create temporary files in DIRECTORY\n");
mprintf(LOGG_INFO, " --leave-temps[=yes/no(*)] Do not remove temporary files\n");
mprintf(LOGG_INFO, " --datadir=DIR Use DIR as default database directory\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands for working with signatures:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --list-sigs[=FILE] -l[FILE] List signature names\n");
mprintf(LOGG_INFO, " --find-sigs=REGEX -fREGEX Find signatures matching REGEX\n");
mprintf(LOGG_INFO, " --decode-sigs Decode signatures from stdin\n");
mprintf(LOGG_INFO, " --test-sigs=DATABASE TARGET_FILE Test signatures from DATABASE against \n");
mprintf(LOGG_INFO, " TARGET_FILE\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands to generate signatures:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --md5 [FILES] Generate MD5 hash from stdin\n");
mprintf(LOGG_INFO, " or MD5 sigs for FILES\n");
mprintf(LOGG_INFO, " --sha1 [FILES] Generate SHA1 hash from stdin\n");
mprintf(LOGG_INFO, " or SHA1 sigs for FILES\n");
mprintf(LOGG_INFO, " --sha2-256 [FILES] Generate SHA2-256 hash from stdin\n");
mprintf(LOGG_INFO, " or SHA2-256 sigs for FILES\n");
mprintf(LOGG_INFO, " --mdb [FILES] Generate .mdb (section hash) sigs\n");
mprintf(LOGG_INFO, " --imp [FILES] Generate .imp (import table hash) sigs\n");
mprintf(LOGG_INFO, " --fuzzy-img FILE(S) Generate image fuzzy hash for each file\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands to normalize files, so you can write more generic signatures:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --html-normalise=FILE Create normalised parts of HTML file\n");
mprintf(LOGG_INFO, " --ascii-normalise=FILE Create normalised text file from ascii source\n");
mprintf(LOGG_INFO, " --utf16-decode=FILE Decode UTF16 encoded files\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Assorted commands to aid file analysis:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --vba=FILE Extract VBA/Word6 macro code\n");
mprintf(LOGG_INFO, " --vba-hex=FILE Extract Word6 macro code with hex values\n");
mprintf(LOGG_INFO, " --print-certs=FILE Print Authenticode details from a PE\n");
mprintf(LOGG_INFO, " --hex-dump Convert data from stdin to a hex\n");
mprintf(LOGG_INFO, " string and print it on stdout\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands for working with CVD signature database archives:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --info=FILE -i FILE Print CVD database archive information\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --build=NAME [cvd] -b NAME Build a CVD file.\n");
mprintf(LOGG_INFO, " The following options augment --build.\n");
mprintf(LOGG_INFO, " --max-bad-sigs=NUMBER Maximum number of mismatched signatures\n");
mprintf(LOGG_INFO, " When building a CVD. Default: 3000\n");
mprintf(LOGG_INFO, " --flevel=FLEVEL Specify a custom flevel.\n");
mprintf(LOGG_INFO, " Default: %u\n", cl_retflevel());
mprintf(LOGG_INFO, " --cvd-version=NUMBER Specify the version number to use for\n");
mprintf(LOGG_INFO, " the build. Default is to use the value+1\n");
mprintf(LOGG_INFO, " from the current CVD in --datadir.\n");
mprintf(LOGG_INFO, " If no datafile is found the default\n");
mprintf(LOGG_INFO, " behaviour is to prompt for a version\n");
mprintf(LOGG_INFO, " number, this switch will prevent the\n");
mprintf(LOGG_INFO, " prompt. NOTE: If a CVD is found in the\n");
mprintf(LOGG_INFO, " --datadir its version+1 is used and\n");
mprintf(LOGG_INFO, " this value is ignored.\n");
mprintf(LOGG_INFO, " --no-cdiff Don't generate .cdiff file.\n");
mprintf(LOGG_INFO, " If not specified, --build will try to\n");
mprintf(LOGG_INFO, " create a cdiff by comparing with the\n");
mprintf(LOGG_INFO, " current CVD in --datadir\n");
mprintf(LOGG_INFO, " If no datafile is found the default\n");
mprintf(LOGG_INFO, " behaviour is to skip making a CDIFF.\n");
mprintf(LOGG_INFO, " --hybrid Create a hybrid (standard and bytecode)\n");
mprintf(LOGG_INFO, " database file\n");
mprintf(LOGG_INFO, " --unsigned Create unsigned database file (.cud)\n");
mprintf(LOGG_INFO, " --server=ADDR ClamAV Signing Service address\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --unpack=FILE -u FILE Unpack a CVD/CLD file\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --unpack-current=SHORTNAME Unpack local CVD/CLD into cwd\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --fips-limits Enforce FIPS-like limits on using hash algorithms for\n");
mprintf(LOGG_INFO, " cryptographic purposes. Will disable MD5 & SHA1\n");
mprintf(LOGG_INFO, " FP sigs and will require '.sign' files to verify CVD\n");
mprintf(LOGG_INFO, " authenticity.\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands for working with CDIFF patch files:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --diff=OLD NEW -d OLD NEW Create diff for OLD and NEW CVDs\n");
mprintf(LOGG_INFO, " --compare=OLD NEW -c OLD NEW Show diff between OLD and NEW files in\n");
mprintf(LOGG_INFO, " cdiff format\n");
mprintf(LOGG_INFO, " --run-cdiff=FILE -r FILE Execute update script FILE in cwd\n");
mprintf(LOGG_INFO, " --verify-cdiff=DIFF CVD/CLD Verify DIFF against CVD/CLD\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " Commands creating and verifying .sign detached digital signatures:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --sign FILE Sign a file.\n");
mprintf(LOGG_INFO, " The resulting .sign file name will\n");
mprintf(LOGG_INFO, " be in the form: dbname-version.cvd.sign\n");
mprintf(LOGG_INFO, " or FILE.sign for non-CVD targets.\n");
mprintf(LOGG_INFO, " It will be created next to the target file.\n");
mprintf(LOGG_INFO, " If a .sign file already exists, then the\n");
mprintf(LOGG_INFO, " new signature will be appended to file.\n");
mprintf(LOGG_INFO, " --key /path/to/private.key Specify a signing key.\n");
mprintf(LOGG_INFO, " --cert /path/to/private.key Specify a signing cert.\n");
mprintf(LOGG_INFO, " May be used more than once to add\n");
mprintf(LOGG_INFO, " intermediate and root certificates.\n");
mprintf(LOGG_INFO, " --append Use to add a signature line to an existing .sign file.\n");
mprintf(LOGG_INFO, " Otherwise an existing .sign file will be overwritten.\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " --verify FILE Find and verify a detached digital\n");
mprintf(LOGG_INFO, " signature for the given file.\n");
mprintf(LOGG_INFO, " The digital signature file name must\n");
mprintf(LOGG_INFO, " be in the form: dbname-version.cvd.sign\n");
mprintf(LOGG_INFO, " or FILE.sign for non-CVD targets.\n");
mprintf(LOGG_INFO, " It must be found next to the target file.\n");
mprintf(LOGG_INFO, " --cvdcertsdir DIRECTORY Specify a directory containing the root\n");
mprintf(LOGG_INFO, " CA cert needed to verify the signature.\n");
mprintf(LOGG_INFO, " If not provided, then sigtool will look in the default directory.n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, "Environment Variables:\n");
mprintf(LOGG_INFO, "\n");
mprintf(LOGG_INFO, " SIGNDUSER The username to authenticate with the signing server when building a\n");
mprintf(LOGG_INFO, " signed CVD database.\n");
mprintf(LOGG_INFO, " SIGNDPASS The password to authenticate with the signing server when building a\n");
mprintf(LOGG_INFO, " signed CVD database.\n");
mprintf(LOGG_INFO, " CVD_CERTS_DIR Specify a directory containing the root CA cert needed\n");
mprintf(LOGG_INFO, " to verify detached CVD digital signatures.\n");
mprintf(LOGG_INFO, " If not provided, then sigtool will look in the default directory.\n");
mprintf(LOGG_INFO, "\n");
return;
}
int main(int argc, char **argv)
{
int ret;
struct optstruct *opts;
STATBUF sb;
const char *cvdcertsdir = NULL;
STATBUF statbuf;
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
if (check_flevel())
exit(1);
if ((ret = cl_init(CL_INIT_DEFAULT)) != CL_SUCCESS) {
mprintf(LOGG_ERROR, "Can't initialize libclamav: %s\n", cl_strerror(ret));
return -1;
}
ret = 1;
/* Rust logging initialization */
if (!clrs_log_init()) {
cli_dbgmsg("Unexpected problem occurred while setting up rust logging... continuing without rust logging. \
Please submit an issue to https://github.com/Cisco-Talos/clamav");
}
opts = optparse(NULL, argc, argv, 1, OPT_SIGTOOL, 0, NULL);
if (!opts) {
mprintf(LOGG_ERROR, "Can't parse command line options\n");
return 1;
}
if (optget(opts, "quiet")->enabled)
mprintf_quiet = 1;
if (optget(opts, "stdout")->enabled)
mprintf_stdout = 1;
if (optget(opts, "debug")->enabled)
cl_debug();
if (optget(opts, "version")->enabled) {
print_version(NULL);
optfree(opts);
return 0;
}
if (optget(opts, "help")->enabled) {
optfree(opts);
help();
return 0;
}
// Evaluate the absolute path for cvdcertsdir in case we change directories later.
cvdcertsdir = optget(opts, "cvdcertsdir")->strarg;
if (NULL == cvdcertsdir) {
// If not set, check the environment variable.
cvdcertsdir = getenv("CVD_CERTS_DIR");
if (NULL == cvdcertsdir) {
// If not set, use the default path.
cvdcertsdir = OPT_CERTSDIR;
}
}
// Command line option must override the engine defaults
// (which would've used the env var or hardcoded path)
if (LSTAT(cvdcertsdir, &statbuf) == -1) {
logg(LOGG_ERROR,
"ClamAV CA certificates directory is missing: %s"
" - It should have been provided as a part of installation.\n",
cvdcertsdir);
return -1;
}
// Convert certs dir to real path.
ret = cli_realpath((const char *)cvdcertsdir, &g_cvdcertsdir);
if (CL_SUCCESS != ret) {
logg(LOGG_ERROR, "Failed to determine absolute path of '%s' for the CVD certs directory.\n", cvdcertsdir);
return -1;
}
if (optget(opts, "hex-dump")->enabled)
ret = hexdump();
else if (optget(opts, "md5")->enabled)
ret = hashsig(opts, 0, CLI_HASH_MD5);
else if (optget(opts, "sha1")->enabled)
ret = hashsig(opts, 0, CLI_HASH_SHA1);
else if (optget(opts, "sha2-256")->enabled || optget(opts, "sha256")->enabled)
ret = hashsig(opts, 0, CLI_HASH_SHA2_256);
else if (optget(opts, "mdb")->enabled)
ret = hashsig(opts, 1, CLI_HASH_MD5);
else if (optget(opts, "imp")->enabled)
ret = hashsig(opts, 2, CLI_HASH_MD5);
else if (optget(opts, "fuzzy-img")->enabled)
ret = fuzzy_img(opts);
else if (optget(opts, "html-normalise")->enabled)
ret = htmlnorm(opts);
else if (optget(opts, "ascii-normalise")->enabled)
ret = asciinorm(opts);
else if (optget(opts, "utf16-decode")->enabled)
ret = utf16decode(opts);
else if (optget(opts, "build")->enabled) {
ret = build(opts);
if (ret == CL_ELAST_ERROR) {
// build() returns CL_ELAST_ERROR the hash starts with 00. This will fail to verify with ClamAV 1.1 -> 1.4.
// Retry the build again to get new hashes.
mprintf(LOGG_WARNING, "Retrying the build for a chance at a better hash.\n");
ret = build(opts);
}
} else if (optget(opts, "sign")->enabled)
ret = sign(opts);
else if (optget(opts, "verify")->enabled)
ret = verify(opts);
else if (optget(opts, "unpack")->enabled)
ret = unpack(opts);
else if (optget(opts, "unpack-current")->enabled)
ret = unpack(opts);
else if (optget(opts, "info")->enabled)
ret = cvdinfo(opts);
else if (optget(opts, "list-sigs")->active)
ret = listsigs(opts, 0);
else if (optget(opts, "find-sigs")->active)
ret = listsigs(opts, 1);
else if (optget(opts, "decode-sigs")->active)
ret = decodesigs();
else if (optget(opts, "test-sigs")->enabled)
ret = testsigs(opts);
else if (optget(opts, "vba")->enabled || optget(opts, "vba-hex")->enabled)
ret = vbadump(opts);
else if (optget(opts, "diff")->enabled)
ret = makediff(opts);
else if (optget(opts, "compare")->enabled)
ret = compareone(opts);
else if (optget(opts, "print-certs")->enabled)
ret = dumpcerts(opts);
else if (optget(opts, "run-cdiff")->enabled)
ret = rundiff(opts);
else if (optget(opts, "verify-cdiff")->enabled) {
if (!opts->filename) {
mprintf(LOGG_ERROR, "--verify-cdiff requires two arguments\n");
ret = -1;
} else {
if (CLAMSTAT(opts->filename[0], &sb) == -1) {
mprintf(LOGG_INFO, "--verify-cdiff: Can't get status of %s\n", opts->filename[0]);
ret = -1;
} else {
if (S_ISDIR(sb.st_mode))
ret = verifydiff(opts, optget(opts, "verify-cdiff")->strarg, NULL, opts->filename[0]);
else
ret = verifydiff(opts, optget(opts, "verify-cdiff")->strarg, opts->filename[0], NULL);
}
}
} else
help();
optfree(opts);
return ret ? 1 : 0;
}