/* * Copyright (C) 2013-2019 Cisco Systems, Inc. and/or its affiliates. All rights reserved. * Copyright (C) 2007-2013 Sourcefire, Inc. * Copyright (C) 2002-2007 Tomasz Kojm * * 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 #include #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #include #include #ifndef _WIN32 #include #endif #include #include #ifdef HAVE_PWD_H #include #endif #ifdef HAVE_GRP_H #include #endif #if defined(USE_SYSLOG) && !defined(C_AIX) #include #endif #include #include "target.h" #include "clamav.h" #include "libfreshclam.h" #include "libfreshclam_internal.h" #include "dns.h" #include "shared/cert_util.h" #include "shared/output.h" #include "shared/misc.h" #include "libclamav/others.h" #include "libclamav/regex_list.h" #include "libclamav/str.h" /* * Private functions */ /* * libclamav API functions */ const char *fc_strerror(fc_error_t fcerror) { switch (fcerror) { case FC_SUCCESS: return "Success"; case FC_UPTODATE: return "Up-to-date"; case FC_EINIT: return "Failed to initalize"; case FC_EDIRECTORY: return "Invalid, nonexistant, or inaccessible directory"; case FC_EFILE: return "Invalid, nonexistant, or inaccessible file"; case FC_ECONNECTION: return "Connection failed"; case FC_EEMPTYFILE: return "Empty file"; case FC_EBADCVD: return "Invalid or corrupted CVD/CLD database"; case FC_ETESTFAIL: return "Test failed"; case FC_ECONFIG: return "Invalid configuration settings(s)"; case FC_EDBDIRACCESS: return "Failed to read/write file to database directory"; case FC_EFAILEDGET: return "HTTP GET failed"; case FC_EMIRRORNOTSYNC: return "Downloaded database had lower version than advertised"; case FC_ELOGGING: return "Failed to write to log"; case FC_EFAILEDUPDATE: return "Failed to update database"; case FC_EMEM: return "Memory allocation error"; case FC_EARG: return "Invalid argument(s)"; default: return "Unknown libfreshclam error code!"; } } fc_error_t fc_initialize(fc_config *fcConfig) { fc_error_t status = FC_EARG; STATBUF statbuf; if (NULL == fcConfig) { printf("fc_initialize: Invalid arguments.\n"); return status; } /* Initilize libcurl */ curl_global_init(CURL_GLOBAL_ALL); /* Initialize mprintf options */ if (fcConfig->msgFlags & FC_CONFIG_MSG_DEBUG) cl_debug(); mprintf_verbose = (fcConfig->msgFlags & FC_CONFIG_MSG_VERBOSE) ? 1 : 0; mprintf_quiet = (fcConfig->msgFlags & FC_CONFIG_MSG_QUIET) ? 1 : 0; mprintf_nowarn = (fcConfig->msgFlags & FC_CONFIG_MSG_NOWARN) ? 1 : 0; mprintf_stdout = (fcConfig->msgFlags & FC_CONFIG_MSG_STDOUT) ? 1 : 0; mprintf_progress = (fcConfig->msgFlags & FC_CONFIG_MSG_SHOWPROGRESS) ? 1 : 0; /* Initialize logger */ logg_verbose = (fcConfig->logFlags & FC_CONFIG_LOG_VERBOSE) ? 1 : 0; logg_nowarn = (fcConfig->logFlags & FC_CONFIG_LOG_NOWARN) ? 1 : 0; logg_time = (fcConfig->logFlags & FC_CONFIG_LOG_TIME) ? 1 : 0; logg_rotate = (fcConfig->logFlags & FC_CONFIG_LOG_ROTATE) ? 1 : 0; logg_size = fcConfig->maxLogSize; /* Set a log file if requested, and is not already set */ if ((NULL == logg_file) && (NULL != fcConfig->logFile)) { logg_file = cli_strdup(fcConfig->logFile); if (0 != logg("#--------------------------------------\n")) { mprintf("!Problem with internal logger (UpdateLogFile = %s).\n", logg_file); status = FC_ELOGGING; goto done; } } #if defined(USE_SYSLOG) && !defined(C_AIX) /* Initialize syslog if available and requested, and is not already set */ if (fcConfig->logFlags & FC_CONFIG_LOG_SYSLOG) { int logFacility = LOG_LOCAL6; if ((0 == logg_syslog) && (NULL != fcConfig->logFacility) && (-1 == (logFacility = logg_facility(fcConfig->logFacility)))) { mprintf("!LogFacility: %s: No such facility.\n", fcConfig->logFacility); status = FC_ELOGGING; goto done; } openlog("freshclam", LOG_PID, logFacility); logg_syslog = 1; } #endif /* Optional connection settings. */ if (NULL != fcConfig->localIP) { #if !defined(CURLOPT_DNS_LOCAL_IP4) || !defined(CURLOPT_DNS_LOCAL_IP6) mprintf("!The LocalIP feature was requested but this local IP support is not presently available.\n"); mprintf("!Your installation may require a newer version of libcurl or your libcurl build lacks the c-ares optional dependency.\n"); #else g_localIP = cli_strdup(fcConfig->localIP); #endif } if (NULL != fcConfig->userAgent) { g_userAgent = cli_strdup(fcConfig->userAgent); } if (NULL != fcConfig->proxyServer) { g_proxyServer = cli_strdup(fcConfig->proxyServer); if (0 != fcConfig->proxyPort) { g_proxyPort = fcConfig->proxyPort; } else { /* * Proxy port not provided. Look up the default port for * webcache in /etc/services. * Default to 8080 if not provided. */ const struct servent *webcache = getservbyname("webcache", "TCP"); if (webcache) g_proxyPort = ntohs(webcache->s_port); else g_proxyPort = 8080; endservent(); } } if (NULL != fcConfig->proxyUsername) { g_proxyUsername = cli_strdup(fcConfig->proxyUsername); } if (NULL != fcConfig->proxyPassword) { g_proxyPassword = cli_strdup(fcConfig->proxyPassword); } #ifdef _WIN32 if ((fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '/') && ((fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '\\'))) { #else if (fcConfig->databaseDirectory[strlen(fcConfig->databaseDirectory) - 1] != '/') { #endif g_databaseDirectory = cli_malloc(strlen(fcConfig->databaseDirectory) + strlen(PATHSEP) + 1); snprintf( g_databaseDirectory, strlen(fcConfig->databaseDirectory) + strlen(PATHSEP) + 1, "%s" PATHSEP, fcConfig->databaseDirectory); } else { g_databaseDirectory = cli_strdup(fcConfig->databaseDirectory); } /* Validate that the database directory exists, and store it. */ if (LSTAT(g_databaseDirectory, &statbuf) == -1) { logg("!Database directory does not exist: %s\n", g_databaseDirectory); status = FC_EDIRECTORY; goto done; } if (!S_ISDIR(statbuf.st_mode)) { logg("!Database directory is not a directory: %s\n", g_databaseDirectory); status = FC_EDIRECTORY; goto done; } /* Validate that the temp directory exists, and store it. */ if (LSTAT(fcConfig->tempDirectory, &statbuf) == -1) { logg("!Temp directory does not exist: %s\n", fcConfig->tempDirectory); status = FC_EDIRECTORY; goto done; } if (!S_ISDIR(statbuf.st_mode)) { logg("!Temp directory is not a directory: %s\n", fcConfig->tempDirectory); status = FC_EDIRECTORY; goto done; } g_tempDirectory = cli_strdup(fcConfig->tempDirectory); g_maxAttempts = fcConfig->maxAttempts; g_connectTimeout = fcConfig->connectTimeout; g_requestTimeout = fcConfig->requestTimeout; g_bCompressLocalDatabase = fcConfig->bCompressLocalDatabase; status = FC_SUCCESS; done: if (FC_SUCCESS != status) { fc_cleanup(); } return status; } void fc_cleanup(void) { /* Cleanup libcurl */ curl_global_cleanup(); if (NULL != logg_file) { free((void *)logg_file); logg_file = NULL; } if (NULL != g_localIP) { free(g_localIP); g_localIP = NULL; } if (NULL != g_userAgent) { free(g_userAgent); g_userAgent = NULL; } if (NULL != g_proxyServer) { free(g_proxyServer); g_proxyServer = NULL; } if (NULL != g_proxyUsername) { free(g_proxyUsername); g_proxyUsername = NULL; } if (NULL != g_proxyPassword) { free(g_proxyPassword); g_proxyPassword = NULL; } if (NULL != g_databaseDirectory) { free(g_databaseDirectory); g_databaseDirectory = NULL; } if (NULL != g_tempDirectory) { free(g_tempDirectory); g_tempDirectory = NULL; } } fc_error_t fc_prune_database_directory(char **databaseList, uint32_t nDatabases) { fc_error_t status = FC_EARG; DIR *dir = NULL; struct dirent *dent; char *extension = NULL; char currDir[PATH_MAX]; /* Store CWD */ if (!getcwd(currDir, PATH_MAX)) { logg("!getcwd() failed\n"); status = FC_EDIRECTORY; goto done; } /* Change directory to database directory */ if (chdir(g_databaseDirectory)) { logg("!Can't change dir to %s\n", g_databaseDirectory); status = FC_EDIRECTORY; goto done; } logg("*Current working dir is %s\n", g_databaseDirectory); if (!(dir = opendir(g_databaseDirectory))) { logg("!checkdbdir: Can't open directory %s\n", g_databaseDirectory); status = FC_EDBDIRACCESS; goto done; } while ((dent = readdir(dir))) { if (dent->d_ino) { if ((NULL != (extension = strstr(dent->d_name, ".cld"))) || (NULL != (extension = strstr(dent->d_name, ".cvd")))) { uint32_t i; int bFound = 0; for (i = 0; i < nDatabases; i++) { if (0 == strncmp(databaseList[i], dent->d_name, extension - dent->d_name)) { bFound = 1; } } if (!bFound) { /* Prune CVD/CLD */ mprintf("Pruning unwanted or deprecated database file %s.\n", dent->d_name); if (unlink(dent->d_name)) { mprintf("!Failed to prune unwanted database file %s, consider removing it manually.\n", dent->d_name); status = FC_EDBDIRACCESS; goto done; } } } } } status = FC_SUCCESS; done: if (NULL != dir) { closedir(dir); } if (currDir[0] != '\0') { /* Restore CWD */ if (chdir(currDir)) { logg("!Failed to change back to original directory %s\n", currDir); status = FC_EDIRECTORY; goto done; } logg("*Current working dir restored to %s\n", currDir); } return status; } /** * @brief Compare two version strings. * * @param v1 Version string 1 * @param v2 Version string 2 * @return int 1 if v1 is greater, 0 if equal, -1 if smaller. */ int version_string_compare(char *v1, size_t v1_len, char *v2, size_t v2_len) { size_t i, j; int vnum1 = 0, vnum2 = 0; for (i = 0, j = 0; (i < v1_len || j < v2_len);) { while (i < v1_len && v1[i] != '.') { vnum1 = vnum1 * 10 + (v1[i] - '0'); i++; } while (j < v2_len && v2[j] != '.') { vnum2 = vnum2 * 10 + (v2[j] - '0'); j++; } if (vnum1 > vnum2) return 1; if (vnum2 > vnum1) return -1; vnum1 = vnum2 = 0; i++; j++; } return 0; } fc_error_t fc_test_database(const char *dbFilename, int bBytecodeEnabled) { fc_error_t status = FC_EARG; struct cl_engine *engine = NULL; unsigned newsigs = 0; cl_error_t cl_ret; if ((NULL == dbFilename)) { logg("^fc_test_database: Invalid arguments.\n"); goto done; } logg("*Loading signatures from %s\n", dbFilename); if (NULL == (engine = cl_engine_new())) { status = FC_ETESTFAIL; goto done; } cl_engine_set_clcb_stats_submit(engine, NULL); if (CL_SUCCESS != (cl_ret = cl_load( dbFilename, engine, &newsigs, CL_DB_PHISHING | CL_DB_PHISHING_URLS | CL_DB_BYTECODE | CL_DB_PUA | CL_DB_ENHANCED))) { logg("!Failed to load new database: %s\n", cl_strerror(cl_ret)); status = FC_ETESTFAIL; goto done; } if (bBytecodeEnabled && (CL_SUCCESS != (cl_ret = cli_bytecode_prepare2( engine, &engine->bcs, engine->dconf->bytecode /*FIXME: dconf has no sense here */)))) { logg("!Failed to compile/load bytecode: %s\n", cl_strerror(cl_ret)); status = FC_ETESTFAIL; goto done; } logg("*Properly loaded %u signatures from %s\n", newsigs, dbFilename); status = FC_SUCCESS; done: if (NULL != engine) { if (engine->domainlist_matcher && engine->domainlist_matcher->sha256_pfx_set.keys) cli_hashset_destroy(&engine->domainlist_matcher->sha256_pfx_set); cl_engine_free(engine); } return status; } fc_error_t fc_dns_query_update_info( const char *dnsUpdateInfoServer, char **dnsUpdateInfo, char **newVersion) { fc_error_t status = FC_EFAILEDGET; char *dnsReply = NULL; #ifdef HAVE_RESOLV_H unsigned int ttl; char *reply_token = NULL; int recordTime; time_t currentTime; int vwarning = 1; char version_string[32]; #endif /* HAVE_RESOLV_H */ if ((NULL == dnsUpdateInfo) || (NULL == newVersion)) { logg("^dns_query_update_info: Invalid arguments.\n"); status = FC_EARG; goto done; } *dnsUpdateInfo = NULL; *newVersion = NULL; #ifdef HAVE_RESOLV_H if (dnsUpdateInfoServer == NULL) { logg("^DNS Update Info disabled. Falling back to HTTP mode.\n"); goto done; } if (NULL == (dnsReply = dnsquery(dnsUpdateInfoServer, T_TXT, &ttl))) { logg("^Invalid DNS reply. Falling back to HTTP mode.\n"); goto done; } logg("*TTL: %d\n", ttl); /* * Check Record Time. */ if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_RECORDTIME, ":"))) { logg("^Failed to find Record Time field in DNS Update Info.\n"); goto done; } recordTime = atoi(reply_token); free(reply_token); reply_token = NULL; time(¤tTime); if ((int)currentTime - recordTime > 10800) { logg("^DNS record is older than 3 hours.\n"); goto done; } /* * Check Version Warning Flag. */ if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_VERSIONWARNING, ":"))) { logg("^Failed to find Version Warning Flag in DNS Update Info.\n"); goto done; } if (*reply_token == '0') vwarning = 0; free(reply_token); reply_token = NULL; /* * Check the latest available ClamAV software version. */ if (NULL == (reply_token = cli_strtok(dnsReply, DNS_UPDATEINFO_NEWVERSION, ":"))) { logg("^Failed to find New Version field in DNS Update Info.\n"); goto done; } logg("*fc_dns_query_update_info: Software version from DNS: %s\n", reply_token); /* * Compare the latest available ClamAV version with this ClamAV version. * Only throw a warning if the Version Warning Flag was set, * and this is not a beta, release candidate, or development version. */ strncpy(version_string, get_version(), sizeof(version_string)); version_string[31] = 0; if (vwarning) { if (!strstr(version_string, "devel") && !strstr(version_string, "beta") && !strstr(version_string, "rc")) { char *suffix = strchr(version_string, '-'); if ((suffix && (0 > version_string_compare(version_string, suffix - version_string, reply_token, strlen(reply_token)))) || (!suffix && (0 > version_string_compare(version_string, strlen(version_string), reply_token, strlen(reply_token))))) { logg("^Your ClamAV installation is OUTDATED!\n"); logg("^Local version: %s Recommended version: %s\n", version_string, reply_token); logg("DON'T PANIC! Read https://www.clamav.net/documents/upgrading-clamav\n"); *newVersion = cli_strdup(reply_token); } } } free(reply_token); reply_token = NULL; *dnsUpdateInfo = dnsReply; status = FC_SUCCESS; #endif /* HAVE_RESOLV_H */ done: if (FC_SUCCESS != status) { free(dnsReply); } return status; } fc_error_t fc_update_database( const char *database, char **serverList, uint32_t nServers, int bPrivateMirror, const char *dnsUpdateInfo, int bScriptedUpdates, void *context, int *bUpdated) { fc_error_t ret; fc_error_t status = FC_EARG; char *dbFilename = NULL; char currDir[PATH_MAX]; int signo = 0; long attempt = 1; uint32_t i; currDir[0] = '\0'; if ((NULL == database) || (NULL == serverList) || (NULL == bUpdated)) { logg("^fc_update_database: Invalid arguments.\n"); goto done; } *bUpdated = 0; /* Store CWD */ if (!getcwd(currDir, PATH_MAX)) { logg("!getcwd() failed\n"); status = FC_EDIRECTORY; goto done; } /* Change directory to database directory */ if (chdir(g_databaseDirectory)) { logg("!Can't change dir to %s\n", g_databaseDirectory); status = FC_EDIRECTORY; goto done; } logg("*Current working dir is %s\n", g_databaseDirectory); /* * Attempt to update official database using DatabaseMirrors or PrivateMirrors. */ for (i = 0; i < nServers; i++) { for (attempt = 1; attempt <= g_maxAttempts; attempt++) { ret = updatedb( database, dnsUpdateInfo, serverList[i], bPrivateMirror, context, bScriptedUpdates, attempt == g_maxAttempts ? 1 : 0, &signo, &dbFilename, bUpdated); switch (ret) { case FC_SUCCESS: { if (*bUpdated) { logg("*fc_update_database: %s updated.\n", dbFilename); } else { logg("*fc_update_database: %s already up-to-date.\n", dbFilename); } goto success; } case FC_ECONNECTION: case FC_EBADCVD: case FC_EFAILEDGET: case FC_EMIRRORNOTSYNC: { if (attempt < g_maxAttempts) { logg("Trying again in 5 secs...\n"); sleep(5); } else { logg("Giving up on %s...\n", serverList[i]); if (i == nServers - 1) { logg("!Update failed for database: %s\n", database); status = ret; goto done; } } break; } default: { logg("!Unexpected error when attempting to update database: %s\n", database); status = ret; goto done; } } } } success: status = FC_SUCCESS; done: if (NULL != dbFilename) { free(dbFilename); } if (currDir[0] != '\0') { /* Restore CWD */ if (chdir(currDir)) { logg("!Failed to change back to original directory %s\n", currDir); status = FC_EDIRECTORY; goto done; } logg("*Current working dir restored to %s\n", currDir); } return status; } fc_error_t fc_update_databases( char **databaseList, uint32_t nDatabases, char **serverList, uint32_t nServers, int bPrivateMirror, const char *dnsUpdateInfo, int bScriptedUpdates, void *context, uint32_t *nUpdated) { fc_error_t ret; fc_error_t status = FC_EARG; uint32_t i; int bUpdated = 0; uint32_t numUpdated = 0; if ((NULL == databaseList) || (0 == nDatabases) || (NULL == serverList) || (NULL == nUpdated)) { logg("^fc_update_databases: Invalid arguments.\n"); goto done; } *nUpdated = 0; for (i = 0; i < nDatabases; i++) { if (FC_SUCCESS != (ret = fc_update_database( databaseList[i], serverList, nServers, bPrivateMirror, dnsUpdateInfo, bScriptedUpdates, context, &bUpdated))) { logg("^fc_update_databases: fc_update_database failed: %s (%d)\n", fc_strerror(ret), ret); status = ret; goto done; } if (bUpdated) { numUpdated++; } } *nUpdated = numUpdated; status = FC_SUCCESS; done: return status; } fc_error_t fc_download_url_database( const char *urlDatabase, void *context, int *bUpdated) { fc_error_t ret; fc_error_t status = FC_EARG; char currDir[PATH_MAX]; long attempt = 1; char *dbFilename = NULL; currDir[0] = '\0'; if ((NULL == urlDatabase) || (NULL == bUpdated)) { logg("^fc_download_url_database: Invalid arguments.\n"); goto done; } *bUpdated = 0; /* Store CWD */ if (!getcwd(currDir, PATH_MAX)) { logg("!getcwd() failed\n"); status = FC_EDIRECTORY; goto done; } /* Change directory to database directory */ if (chdir(g_databaseDirectory)) { logg("!Can't change dir to %s\n", g_databaseDirectory); status = FC_EDIRECTORY; goto done; } logg("*Current working dir is %s\n", g_databaseDirectory); /* * Attempt to update official database using DatabaseMirrors or PrivateMirrors. */ for (attempt = 1; attempt <= g_maxAttempts; attempt++) { int signo = 0; ret = updatecustomdb( urlDatabase, context, attempt == g_maxAttempts ? 1 : 0, &signo, &dbFilename, bUpdated); switch (ret) { case FC_SUCCESS: { if (*bUpdated) { logg("*fc_download_url_database: %s updated.\n", dbFilename); } else { logg("*fc_download_url_database: %s already up-to-date.\n", dbFilename); } goto success; } case FC_ECONNECTION: case FC_EBADCVD: case FC_EFAILEDGET: { if (attempt < g_maxAttempts) { logg("Trying again in 5 secs...\n"); sleep(5); } else { logg("Update failed for custom database URL: %s\n", urlDatabase); status = ret; goto done; } break; } default: { logg("Unexpected error when attempting to update from custom database URL: %s\n", urlDatabase); status = ret; goto done; } } } success: status = FC_SUCCESS; done: if (NULL != dbFilename) { free(dbFilename); } if (currDir[0] != '\0') { /* Restore CWD */ if (chdir(currDir)) { logg("!Failed to change back to original directory %s\n", currDir); status = FC_EDIRECTORY; goto done; } logg("*Current working dir restored to %s\n", currDir); } return status; } fc_error_t fc_download_url_databases( char **urlDatabaseList, uint32_t nUrlDatabases, void *context, uint32_t *nUpdated) { fc_error_t ret; fc_error_t status = FC_EARG; int bUpdated = 0; uint32_t numUpdated = 0; uint32_t i; if ((NULL == urlDatabaseList) || (0 == nUrlDatabases) || (NULL == nUpdated)) { logg("^fc_download_url_databases: Invalid arguments.\n"); goto done; } *nUpdated = 0; for (i = 0; i < nUrlDatabases; i++) { if (FC_SUCCESS != (ret = fc_download_url_database( urlDatabaseList[i], context, &bUpdated))) { logg("^fc_download_url_databases: fc_download_url_database failed: %s (%d)\n", fc_strerror(ret), ret); status = ret; goto done; } if (bUpdated) { numUpdated++; } } *nUpdated = numUpdated; status = FC_SUCCESS; done: return status; } void fc_set_fccb_download_complete(fccb_download_complete callback) { g_cb_download_complete = callback; }