/*
* Copyright ( C ) 2013 - 2021 Cisco Systems , Inc . and / or its affiliates . All rights reserved .
* Copyright ( C ) 2007 - 2013 Sourcefire , Inc .
*
* Authors : Tomasz Kojm , Trog
*
* 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 <stdarg.h>
# include <string.h>
# include <stdlib.h>
# include <ctype.h>
# ifdef HAVE_UNISTD_H
# include <unistd.h>
# endif
# include <sys/types.h>
# include <sys/stat.h>
# include <dirent.h>
# ifndef _WIN32
# include <sys/wait.h>
# include <sys/time.h>
# endif
# include <time.h>
# include <fcntl.h>
# ifdef HAVE_PWD_H
# include <pwd.h>
# endif
# include <errno.h>
# include "target.h"
# ifdef HAVE_SYS_PARAM_H
# include <sys/param.h>
# endif
# ifdef HAVE_MALLOC_H
# include <malloc.h>
# endif
# include "clamav.h"
# include "others.h"
# include "str.h"
# include "platform.h"
# include "regex/regex.h"
# include "matcher-ac.h"
Add CMake build tooling
This patch adds experimental-quality CMake build tooling.
The libmspack build required a modification to use "" instead of <> for
header #includes. This will hopefully be included in the libmspack
upstream project when adding CMake build tooling to libmspack.
Removed use of libltdl when using CMake.
Flex & Bison are now required to build.
If -DMAINTAINER_MODE, then GPERF is also required, though it currently
doesn't actually do anything. TODO!
I found that the autotools build system was generating the lexer output
but not actually compiling it, instead using previously generated (and
manually renamed) lexer c source. As a consequence, changes to the .l
and .y files weren't making it into the build. To resolve this, I
removed generated flex/bison files and fixed the tooling to use the
freshly generated files. Flex and bison are now required build tools.
On Windows, this adds a dependency on the winflexbison package,
which can be obtained using Chocolatey or may be manually installed.
CMake tooling only has partial support for building with external LLVM
library, and no support for the internal LLVM (to be removed in the
future). I.e. The CMake build currently only supports the bytecode
interpreter.
Many files used include paths relative to the top source directory or
relative to the current project, rather than relative to each build
target. Modern CMake support requires including internal dependency
headers the same way you would external dependency headers (albeit
with "" instead of <>). This meant correcting all header includes to
be relative to the build targets and not relative to the workspace.
For example, ...
```c
include "../libclamav/clamav.h"
include "clamd/clamd_others.h"
```
... becomes:
```c
// libclamav
include "clamav.h"
// clamd
include "clamd_others.h"
```
Fixes header name conflicts by renaming a few of the files.
Converted the "shared" code into a static library, which depends on
libclamav. The ironically named "shared" static library provides
features common to the ClamAV apps which are not required in
libclamav itself and are not intended for use by downstream projects.
This change was required for correct modern CMake practices but was
also required to use the automake "subdir-objects" option.
This eliminates warnings when running autoreconf which, in the next
version of autoconf & automake are likely to break the build.
libclamav used to build in multiple stages where an earlier stage is
a static library containing utils required by the "shared" code.
Linking clamdscan and clamdtop with this libclamav utils static lib
allowed these two apps to function without libclamav. While this is
nice in theory, the practical gains are minimal and it complicates
the build system. As such, the autotools and CMake tooling was
simplified for improved maintainability and this feature was thrown
out. clamdtop and clamdscan now require libclamav to function.
Removed the nopthreads version of the autotools
libclamav_internal_utils static library and added pthread linking to
a couple apps that may have issues building on some platforms without
it, with the intention of removing needless complexity from the
source. Kept the regular version of libclamav_internal_utils.la
though it is no longer used anywhere but in libclamav.
Added an experimental doxygen build option which attempts to build
clamav.h and libfreshclam doxygen html docs.
The CMake build tooling also may build the example program(s), which
isn't a feature in the Autotools build system.
Changed C standard to C90+ due to inline linking issues with socket.h
when linking libfreshclam.so on Linux.
Generate common.rc for win32.
Fix tabs/spaces in shared Makefile.am, and remove vestigial ifndef
from misc.c.
Add CMake files to the automake dist, so users can try the new
CMake tooling w/out having to build from a git clone.
clamonacc changes:
- Renamed FANOTIFY macro to HAVE_SYS_FANOTIFY_H to better match other
similar macros.
- Added a new clamav-clamonacc.service systemd unit file, based on
the work of ChadDevOps & Aaron Brighton.
- Added missing clamonacc man page.
Updates to clamdscan man page, add missing options.
Remove vestigial CL_NOLIBCLAMAV definitions (all apps now use
libclamav).
Rename Windows mspack.dll to libmspack.dll so all ClamAV-built
libraries have the lib-prefix with Visual Studio as with CMake.
5 years ago
# include "str.h"
# include "entconv.h"
# define MSGBUFSIZ 8192
static unsigned char name_salt [ 16 ] = { 16 , 38 , 97 , 12 , 8 , 4 , 72 , 196 , 217 , 144 , 33 , 124 , 18 , 11 , 17 , 253 } ;
# ifdef CL_THREAD_SAFE
# include <pthread.h>
static pthread_mutex_t cli_gentemp_mutex = PTHREAD_MUTEX_INITIALIZER ;
# ifndef HAVE_CTIME_R
static pthread_mutex_t cli_ctime_mutex = PTHREAD_MUTEX_INITIALIZER ;
# endif
static pthread_mutex_t cli_strerror_mutex = PTHREAD_MUTEX_INITIALIZER ;
static pthread_key_t cli_ctx_tls_key ;
static pthread_once_t cli_ctx_tls_key_once = PTHREAD_ONCE_INIT ;
static void cli_ctx_tls_key_alloc ( void )
{
pthread_key_create ( & cli_ctx_tls_key , NULL ) ;
}
void cli_logg_setup ( const cli_ctx * ctx )
{
pthread_once ( & cli_ctx_tls_key_once , cli_ctx_tls_key_alloc ) ;
pthread_setspecific ( cli_ctx_tls_key , ctx ) ;
}
void cli_logg_unsetup ( void )
{
pthread_setspecific ( cli_ctx_tls_key , NULL ) ;
}
static inline void * cli_getctx ( void )
{
cli_ctx * ctx ;
pthread_once ( & cli_ctx_tls_key_once , cli_ctx_tls_key_alloc ) ;
ctx = pthread_getspecific ( cli_ctx_tls_key ) ;
return ctx ? ctx - > cb_ctx : NULL ;
}
# else
static const cli_ctx * current_ctx = NULL ;
void cli_logg_setup ( const cli_ctx * ctx )
{
current_ctx = ctx ;
}
static inline void * cli_getctx ( void )
{
return current_ctx ? current_ctx - > cb_ctx : NULL ;
}
void cli_logg_unsetup ( void )
{
}
# endif
uint8_t cli_debug_flag = 0 ;
uint8_t cli_always_gen_section_hash = 0 ;
static void fputs_callback ( enum cl_msg severity , const char * fullmsg , const char * msg , void * context )
{
UNUSEDPARAM ( severity ) ;
UNUSEDPARAM ( msg ) ;
UNUSEDPARAM ( context ) ;
fputs ( fullmsg , stderr ) ;
}
static clcb_msg msg_callback = fputs_callback ;
void cl_set_clcb_msg ( clcb_msg callback )
{
msg_callback = callback ;
}
# define MSGCODE(buff, len, x) \
va_list args ; \
size_t len = sizeof ( x ) - 1 ; \
char buff [ MSGBUFSIZ ] ; \
strncpy ( buff , x , len ) ; \
va_start ( args , str ) ; \
vsnprintf ( buff + len , sizeof ( buff ) - len , str , args ) ; \
buff [ sizeof ( buff ) - 1 ] = ' \0 ' ; \
va_end ( args )
void cli_warnmsg ( const char * str , . . . )
{
MSGCODE ( buff , len , " LibClamAV Warning: " ) ;
msg_callback ( CL_MSG_WARN , buff , buff + len , cli_getctx ( ) ) ;
}
void cli_errmsg ( const char * str , . . . )
{
MSGCODE ( buff , len , " LibClamAV Error: " ) ;
msg_callback ( CL_MSG_ERROR , buff , buff + len , cli_getctx ( ) ) ;
}
void cli_infomsg ( const cli_ctx * ctx , const char * str , . . . )
{
MSGCODE ( buff , len , " LibClamAV info: " ) ;
msg_callback ( CL_MSG_INFO_VERBOSE , buff , buff + len , ctx ? ctx - > cb_ctx : NULL ) ;
}
void cli_dbgmsg_internal ( const char * str , . . . )
{
MSGCODE ( buff , len , " LibClamAV debug: " ) ;
fputs ( buff , stderr ) ;
}
int cli_matchregex ( const char * str , const char * regex )
{
regex_t reg ;
int match , flags = REG_EXTENDED | REG_NOSUB ;
# ifdef _WIN32
flags | = REG_ICASE ;
# endif
if ( cli_regcomp ( & reg , regex , flags ) = = 0 ) {
match = ( cli_regexec ( & reg , str , 0 , NULL , 0 ) = = REG_NOMATCH ) ? 0 : 1 ;
cli_regfree ( & reg ) ;
return match ;
}
return 0 ;
}
void * cli_malloc ( size_t size )
{
void * alloc ;
if ( ! size | | size > CLI_MAX_ALLOCATION ) {
cli_errmsg ( " cli_malloc(): Attempt to allocate %lu bytes. Please report to https://github.com/Cisco-Talos/clamav/issues \n " , ( unsigned long int ) size ) ;
return NULL ;
}
alloc = malloc ( size ) ;
if ( ! alloc ) {
perror ( " malloc_problem " ) ;
cli_errmsg ( " cli_malloc(): Can't allocate memory (%lu bytes). \n " , ( unsigned long int ) size ) ;
return NULL ;
} else
return alloc ;
}
void * cli_calloc ( size_t nmemb , size_t size )
{
void * alloc ;
if ( ! nmemb | | ! size | | size > CLI_MAX_ALLOCATION | | nmemb > CLI_MAX_ALLOCATION | | ( nmemb * size > CLI_MAX_ALLOCATION ) ) {
cli_errmsg ( " cli_calloc(): Attempt to allocate %lu bytes. Please report to https://github.com/Cisco-Talos/clamav/issues \n " , ( unsigned long int ) nmemb * size ) ;
return NULL ;
}
alloc = calloc ( nmemb , size ) ;
if ( ! alloc ) {
perror ( " calloc_problem " ) ;
cli_errmsg ( " cli_calloc(): Can't allocate memory (%lu bytes). \n " , ( unsigned long int ) ( nmemb * size ) ) ;
return NULL ;
} else
return alloc ;
}
void * cli_realloc ( void * ptr , size_t size )
{
void * alloc ;
if ( ! size | | size > CLI_MAX_ALLOCATION ) {
cli_errmsg ( " cli_realloc(): Attempt to allocate %lu bytes. Please report to https://github.com/Cisco-Talos/clamav/issues \n " , ( unsigned long int ) size ) ;
return NULL ;
}
alloc = realloc ( ptr , size ) ;
if ( ! alloc ) {
perror ( " realloc_problem " ) ;
cli_errmsg ( " cli_realloc(): Can't re-allocate memory to %lu bytes. \n " , ( unsigned long int ) size ) ;
return NULL ;
} else
return alloc ;
}
void * cli_realloc2 ( void * ptr , size_t size )
{
void * alloc ;
if ( ! size | | size > CLI_MAX_ALLOCATION ) {
cli_errmsg ( " cli_realloc2(): Attempt to allocate %lu bytes. Please report to https://github.com/Cisco-Talos/clamav/issues \n " , ( unsigned long int ) size ) ;
return NULL ;
}
alloc = realloc ( ptr , size ) ;
if ( ! alloc ) {
perror ( " realloc_problem " ) ;
cli_errmsg ( " cli_realloc2(): Can't re-allocate memory to %lu bytes. \n " , ( unsigned long int ) size ) ;
if ( ptr )
free ( ptr ) ;
return NULL ;
} else
return alloc ;
}
char * cli_strdup ( const char * s )
{
char * alloc ;
if ( s = = NULL ) {
cli_errmsg ( " cli_strdup(): s == NULL. Please report to https://github.com/Cisco-Talos/clamav/issues \n " ) ;
return NULL ;
}
alloc = strdup ( s ) ;
if ( ! alloc ) {
perror ( " strdup_problem " ) ;
cli_errmsg ( " cli_strdup(): Can't allocate memory (%u bytes). \n " , ( unsigned int ) strlen ( s ) ) ;
return NULL ;
}
return alloc ;
}
/* returns converted timestamp, in case of error the returned string contains at least one character */
const char * cli_ctime ( const time_t * timep , char * buf , const size_t bufsize )
{
const char * ret ;
if ( bufsize < 26 ) {
/* standard says we must have at least 26 bytes buffer */
cli_warnmsg ( " buffer too small for ctime \n " ) ;
return " " ;
}
if ( ( uint32_t ) ( * timep ) > 0x7fffffff ) {
/* some systems can consider these timestamps invalid */
strncpy ( buf , " invalid timestamp " , bufsize - 1 ) ;
buf [ bufsize - 1 ] = ' \0 ' ;
return buf ;
}
# ifdef HAVE_CTIME_R
# ifdef HAVE_CTIME_R_2
ret = ctime_r ( timep , buf ) ;
# else
ret = ctime_r ( timep , buf , bufsize ) ;
# endif
# else /* no ctime_r */
# ifdef CL_THREAD_SAFE
pthread_mutex_lock ( & cli_ctime_mutex ) ;
# endif
ret = ctime ( timep ) ;
if ( ret ) {
strncpy ( buf , ret , bufsize - 1 ) ;
buf [ bufsize - 1 ] = ' \0 ' ;
ret = buf ;
}
# ifdef CL_THREAD_SAFE
pthread_mutex_unlock ( & cli_ctime_mutex ) ;
# endif
# endif
/* common */
if ( ! ret ) {
buf [ 0 ] = ' ' ;
buf [ 1 ] = ' \0 ' ;
return buf ;
}
return ret ;
}
/**
* @ brief Try hard to read the requested number of bytes
*
* @ param fd File desriptor to read from .
* @ param buff Buffer to read data into .
* @ param count # of bytes to read .
* @ return size_t # of bytes read .
* @ return size_t ( size_t ) - 1 if error .
*/
size_t cli_readn ( int fd , void * buff , size_t count )
{
ssize_t retval ;
size_t todo ;
unsigned char * current ;
todo = count ;
current = ( unsigned char * ) buff ;
do {
retval = read ( fd , current , todo ) ;
if ( retval = = 0 ) {
return ( count - todo ) ;
}
if ( retval < 0 ) {
char err [ 128 ] ;
if ( errno = = EINTR ) {
continue ;
}
cli_errmsg ( " cli_readn: read error: %s \n " , cli_strerror ( errno , err , sizeof ( err ) ) ) ;
return ( size_t ) - 1 ;
}
if ( ( size_t ) retval > todo ) {
break ;
} else {
todo - = retval ;
}
current + = retval ;
} while ( todo > 0 ) ;
return count ;
}
/**
* @ brief Try hard to write the specified number of bytes
*
* @ param fd File descriptor to write to .
* @ param buff Buffer to write from .
* @ param count # of bytes to write .
* @ return size_t # of bytes written
* @ return size_t ( size_t ) - 1 if error .
*/
size_t cli_writen ( int fd , const void * buff , size_t count )
{
ssize_t retval ;
size_t todo ;
const unsigned char * current ;
if ( ! buff ) {
cli_errmsg ( " cli_writen: invalid NULL buff argument \n " ) ;
return ( size_t ) - 1 ;
}
todo = count ;
current = ( const unsigned char * ) buff ;
do {
retval = write ( fd , current , todo ) ;
if ( retval < 0 ) {
char err [ 128 ] ;
if ( errno = = EINTR ) {
continue ;
}
cli_errmsg ( " cli_writen: write error: %s \n " , cli_strerror ( errno , err , sizeof ( err ) ) ) ;
return ( size_t ) - 1 ;
}
if ( ( size_t ) retval > todo ) {
break ;
} else {
todo - = retval ;
}
current + = retval ;
} while ( todo > 0 ) ;
return count ;
}
int cli_filecopy ( const char * src , const char * dest )
{
# ifdef _WIN32
return CopyFileA ( src , dest , 0 ) ? 0 : - 1 ;
# else
char * buffer ;
int s , d ;
size_t bytes ;
if ( ( s = open ( src , O_RDONLY | O_BINARY ) ) = = - 1 )
return - 1 ;
if ( ( d = open ( dest , O_CREAT | O_WRONLY | O_TRUNC | O_BINARY , S_IRUSR | S_IWUSR ) ) = = - 1 ) {
close ( s ) ;
return - 1 ;
}
if ( ! ( buffer = cli_malloc ( FILEBUFF ) ) ) {
close ( s ) ;
close ( d ) ;
return - 1 ;
}
bytes = cli_readn ( s , buffer , FILEBUFF ) ;
while ( ( bytes ! = ( size_t ) - 1 ) & & ( bytes ! = 0 ) ) {
cli_writen ( d , buffer , bytes ) ;
bytes = cli_readn ( s , buffer , FILEBUFF ) ;
}
free ( buffer ) ;
close ( s ) ;
return close ( d ) ;
# endif
}
# ifndef P_tmpdir
# ifdef _WIN32
# define P_tmpdir "C:\\"
# else
# define P_tmpdir " / tmp"
# endif /* _WIN32 */
# endif /* P_tmpdir */
const char * cli_gettmpdir ( void )
{
const char * tmpdir ;
unsigned int i ;
# ifdef _WIN32
char * envs [ ] = { " TEMP " , " TMP " , NULL } ;
# else
char * envs [ ] = { " TMPDIR " , NULL } ;
# endif
for ( i = 0 ; envs [ i ] ! = NULL ; i + + )
if ( ( tmpdir = getenv ( envs [ i ] ) ) )
return tmpdir ;
return P_tmpdir ;
}
struct dirent_data {
char * filename ;
const char * dirname ;
STATBUF * statbuf ;
long ino ; /* -1: inode not available */
int is_dir ; /* 0 - no, 1 - yes */
} ;
/* sort files before directories, and lower inodes before higher inodes */
static int ftw_compare ( const void * a , const void * b )
{
const struct dirent_data * da = a ;
const struct dirent_data * db = b ;
long diff = da - > is_dir - db - > is_dir ;
if ( ! diff ) {
diff = da - > ino - db - > ino ;
}
return diff ;
}
enum filetype {
ft_unknown ,
ft_link ,
ft_directory ,
ft_regular ,
ft_skipped_special ,
ft_skipped_link
} ;
static inline int ft_skipped ( enum filetype ft )
{
return ft ! = ft_regular & & ft ! = ft_directory ;
}
# define FOLLOW_SYMLINK_MASK (CLI_FTW_FOLLOW_FILE_SYMLINK | CLI_FTW_FOLLOW_DIR_SYMLINK)
static int get_filetype ( const char * fname , int flags , int need_stat ,
STATBUF * statbuf , enum filetype * ft )
{
int stated = 0 ;
if ( * ft = = ft_unknown | | * ft = = ft_link ) {
need_stat = 1 ;
if ( ( flags & FOLLOW_SYMLINK_MASK ) ! = FOLLOW_SYMLINK_MASK ) {
/* Following only one of directory/file symlinks, or none, may
* need to lstat .
* If we ' re following both file and directory symlinks , we don ' t need
* to lstat ( ) , we can just stat ( ) directly . */
if ( * ft ! = ft_link ) {
/* need to lstat to determine if it is a symlink */
if ( LSTAT ( fname , statbuf ) = = - 1 )
return - 1 ;
if ( S_ISLNK ( statbuf - > st_mode ) ) {
* ft = ft_link ;
} else {
/* It was not a symlink, stat() not needed */
need_stat = 0 ;
stated = 1 ;
}
}
if ( * ft = = ft_link & & ! ( flags & FOLLOW_SYMLINK_MASK ) ) {
/* This is a symlink, but we don't follow any symlinks */
* ft = ft_skipped_link ;
return 0 ;
}
}
}
if ( need_stat ) {
if ( CLAMSTAT ( fname , statbuf ) = = - 1 )
return - 1 ;
stated = 1 ;
}
if ( * ft = = ft_unknown | | * ft = = ft_link ) {
if ( S_ISDIR ( statbuf - > st_mode ) & &
( * ft ! = ft_link | | ( flags & CLI_FTW_FOLLOW_DIR_SYMLINK ) ) ) {
/* A directory, or (a symlink to a directory and we're following dir
* symlinks ) */
* ft = ft_directory ;
} else if ( S_ISREG ( statbuf - > st_mode ) & &
( * ft ! = ft_link | | ( flags & CLI_FTW_FOLLOW_FILE_SYMLINK ) ) ) {
/* A file, or (a symlink to a file and we're following file symlinks) */
* ft = ft_regular ;
} else {
/* default: skipped */
* ft = S_ISLNK ( statbuf - > st_mode ) ? ft_skipped_link : ft_skipped_special ;
}
}
return stated ;
}
/**
* @ brief Determine the file type and pass the metadata to the callback as the " reason " .
*
* The callback may end up doing something or doing nothing , depending on the reason .
*
* @ param fname The file path
* @ param flags CLI_FTW_ * bitflag field
* @ param [ out ] statbuf the stat metadata for the file .
* @ param [ out ] stated 1 if statbuf contains stat info , 0 if not . - 1 if there was a stat error .
* @ param [ out ] ft will indicate if the file was skipped based on the file type .
* @ param callback the callback ( E . g . function that may scan the file )
* @ param data callback data
* @ return cl_error_t
*/
static cl_error_t handle_filetype ( const char * fname , int flags ,
STATBUF * statbuf , int * stated , enum filetype * ft ,
cli_ftw_cb callback , struct cli_ftw_cbdata * data )
{
cl_error_t status = CL_EMEM ;
* stated = get_filetype ( fname , flags , flags & CLI_FTW_NEED_STAT , statbuf , ft ) ;
if ( * stated = = - 1 ) {
/* we failed a stat() or lstat() */
char * fname_copy = cli_strdup ( fname ) ;
if ( NULL = = fname_copy ) {
goto done ;
}
status = callback ( NULL , fname_copy , fname , error_stat , data ) ;
if ( status ! = CL_SUCCESS ) {
goto done ;
}
* ft = ft_unknown ;
} else if ( * ft = = ft_skipped_link | | * ft = = ft_skipped_special ) {
/* skipped filetype */
char * fname_copy = cli_strdup ( fname ) ;
if ( NULL = = fname_copy ) {
goto done ;
}
status = callback ( stated ? statbuf : NULL ,
fname_copy ,
fname ,
* ft = = ft_skipped_link ? warning_skipped_link : warning_skipped_special ,
data ) ;
if ( status ! = CL_SUCCESS )
goto done ;
}
status = CL_SUCCESS ;
done :
return status ;
}
static cl_error_t cli_ftw_dir ( const char * dirname , int flags , int maxdepth , cli_ftw_cb callback , struct cli_ftw_cbdata * data , cli_ftw_pathchk pathchk ) ;
static int handle_entry ( struct dirent_data * entry , int flags , int maxdepth , cli_ftw_cb callback , struct cli_ftw_cbdata * data , cli_ftw_pathchk pathchk )
{
if ( ! entry - > is_dir ) {
return callback ( entry - > statbuf , entry - > filename , entry - > filename , visit_file , data ) ;
} else {
return cli_ftw_dir ( entry - > dirname , flags , maxdepth , callback , data , pathchk ) ;
}
}
cl_error_t cli_ftw ( char * path , int flags , int maxdepth , cli_ftw_cb callback , struct cli_ftw_cbdata * data , cli_ftw_pathchk pathchk )
{
cl_error_t status = CL_EMEM ;
STATBUF statbuf ;
enum filetype ft = ft_unknown ;
struct dirent_data entry = { 0 } ;
int stated = 0 ;
char * filename_for_callback = NULL ;
char * filename_for_handleentry = NULL ;
if ( ( ( flags & CLI_FTW_TRIM_SLASHES ) | | pathchk ) & & path [ 0 ] & & path [ 1 ] ) {
char * pathend ;
/* trim slashes so that dir and dir/ behave the same when
* they are symlinks , and we are not following symlinks */
# ifndef _WIN32
while ( path [ 0 ] = = * PATHSEP & & path [ 1 ] = = * PATHSEP ) path + + ;
# endif
pathend = path + strlen ( path ) ;
while ( pathend > path & & pathend [ - 1 ] = = * PATHSEP ) - - pathend ;
* pathend = ' \0 ' ;
}
if ( pathchk & & pathchk ( path , data ) = = 1 ) {
status = CL_SUCCESS ;
goto done ;
}
/* Determine if the file should be skipped (special file or symlink).
This will also get the stat metadata . */
status = handle_filetype ( path , flags , & statbuf , & stated , & ft , callback , data ) ;
if ( status ! = CL_SUCCESS ) {
goto done ;
}
/* Bail out if the file should be skipped. */
if ( ft_skipped ( ft ) ) {
status = CL_SUCCESS ;
goto done ;
}
entry . statbuf = stated ? & statbuf : NULL ;
entry . is_dir = ft = = ft_directory ;
/*
* handle_entry ( ) doesn ' t call the callback for directories , so we ' ll call it now first .
*/
if ( entry . is_dir ) {
/* Allocate the filename for the callback function. TODO: this FTW code is spaghetti, refactor. */
filename_for_callback = cli_strdup ( path ) ;
if ( NULL = = filename_for_callback ) {
goto done ;
}
status = callback ( entry . statbuf , filename_for_callback , path , visit_directory_toplev , data ) ;
filename_for_callback = NULL ; // free'd by the callback
if ( status ! = CL_SUCCESS ) {
goto done ;
}
}
/*
* Now call handle_entry ( ) to either call the callback for files ,
* or recurse deeper into the file tree walk .
* TODO : Recursion is bad , this whole thing should be iterative
*/
if ( entry . is_dir ) {
entry . dirname = path ;
} else {
/* Allocate the filename for the callback function within the handle_entry function. TODO: this FTW code is spaghetti, refactor. */
filename_for_handleentry = cli_strdup ( path ) ;
if ( NULL = = filename_for_handleentry ) {
goto done ;
}
entry . filename = filename_for_handleentry ;
}
status = handle_entry ( & entry , flags , maxdepth , callback , data , pathchk ) ;
filename_for_handleentry = NULL ; // free'd by the callback call in handle_entry()
done :
if ( NULL ! = filename_for_callback ) {
/* Free-check just in case someone injects additional calls and error handling before callback(). */
free ( filename_for_callback ) ;
}
if ( NULL ! = filename_for_handleentry ) {
/* Free-check just in case someone injects additional calls and error handling before handle_entry(). */
free ( filename_for_handleentry ) ;
}
return status ;
}
static cl_error_t cli_ftw_dir ( const char * dirname , int flags , int maxdepth , cli_ftw_cb callback , struct cli_ftw_cbdata * data , cli_ftw_pathchk pathchk )
{
DIR * dd ;
struct dirent_data * entries = NULL ;
size_t i , entries_cnt = 0 ;
cl_error_t ret ;
if ( maxdepth < 0 ) {
/* exceeded recursion limit */
ret = callback ( NULL , NULL , dirname , warning_skipped_dir , data ) ;
return ret ;
}
if ( ( dd = opendir ( dirname ) ) ! = NULL ) {
struct dirent * dent ;
errno = 0 ;
ret = CL_SUCCESS ;
while ( ( dent = readdir ( dd ) ) ) {
int stated = 0 ;
enum filetype ft ;
char * fname ;
STATBUF statbuf ;
STATBUF * statbufp ;
if ( ! strcmp ( dent - > d_name , " . " ) | | ! strcmp ( dent - > d_name , " .. " ) )
continue ;
# ifdef _DIRENT_HAVE_D_TYPE
switch ( dent - > d_type ) {
case DT_DIR :
ft = ft_directory ;
break ;
case DT_LNK :
if ( ! ( flags & FOLLOW_SYMLINK_MASK ) ) {
/* we don't follow symlinks, don't bother
* stating it */
errno = 0 ;
continue ;
}
ft = ft_link ;
break ;
case DT_REG :
ft = ft_regular ;
break ;
case DT_UNKNOWN :
ft = ft_unknown ;
break ;
default :
ft = ft_skipped_special ;
break ;
}
# else
ft = ft_unknown ;
# endif
fname = ( char * ) cli_malloc ( strlen ( dirname ) + strlen ( dent - > d_name ) + 2 ) ;
if ( ! fname ) {
ret = callback ( NULL , NULL , dirname , error_mem , data ) ;
if ( ret ! = CL_SUCCESS )
break ;
continue ; /* have to skip this one if continuing after error */
}
if ( ! strcmp ( dirname , PATHSEP ) )
sprintf ( fname , PATHSEP " %s " , dent - > d_name ) ;
else
sprintf ( fname , " %s " PATHSEP " %s " , dirname , dent - > d_name ) ;
if ( pathchk & & pathchk ( fname , data ) = = 1 ) {
free ( fname ) ;
continue ;
}
ret = handle_filetype ( fname , flags , & statbuf , & stated , & ft , callback , data ) ;
if ( ret ! = CL_SUCCESS ) {
free ( fname ) ;
break ;
}
if ( ft_skipped ( ft ) ) { /* skip */
free ( fname ) ;
errno = 0 ;
continue ;
}
if ( stated & & ( flags & CLI_FTW_NEED_STAT ) ) {
statbufp = cli_malloc ( sizeof ( * statbufp ) ) ;
if ( ! statbufp ) {
ret = callback ( stated ? & statbuf : NULL , NULL , fname , error_mem , data ) ;
free ( fname ) ;
if ( ret ! = CL_SUCCESS )
break ;
else {
errno = 0 ;
continue ;
}
}
memcpy ( statbufp , & statbuf , sizeof ( statbuf ) ) ;
} else {
statbufp = 0 ;
}
entries_cnt + + ;
entries = cli_realloc ( entries , entries_cnt * sizeof ( * entries ) ) ;
if ( ! entries ) {
ret = callback ( stated ? & statbuf : NULL , NULL , fname , error_mem , data ) ;
free ( fname ) ;
if ( statbufp )
free ( statbufp ) ;
break ;
} else {
struct dirent_data * entry = & entries [ entries_cnt - 1 ] ;
entry - > filename = fname ;
entry - > statbuf = statbufp ;
entry - > is_dir = ft = = ft_directory ;
entry - > dirname = entry - > is_dir ? fname : NULL ;
# ifdef _XOPEN_UNIX
entry - > ino = dent - > d_ino ;
# else
entry - > ino = - 1 ;
# endif
}
errno = 0 ;
}
closedir ( dd ) ;
ret = CL_SUCCESS ;
if ( entries ) {
cli_qsort ( entries , entries_cnt , sizeof ( * entries ) , ftw_compare ) ;
for ( i = 0 ; i < entries_cnt ; i + + ) {
struct dirent_data * entry = & entries [ i ] ;
ret = handle_entry ( entry , flags , maxdepth - 1 , callback , data , pathchk ) ;
if ( entry - > is_dir )
free ( entry - > filename ) ;
if ( entry - > statbuf )
free ( entry - > statbuf ) ;
if ( ret ! = CL_SUCCESS ) {
/* Something went horribly wrong, Skip the rest of the files */
cli_errmsg ( " File tree walk aborted. \n " ) ;
break ;
}
}
for ( i + + ; i < entries_cnt ; i + + ) {
struct dirent_data * entry = & entries [ i ] ;
free ( entry - > filename ) ;
free ( entry - > statbuf ) ;
}
free ( entries ) ;
}
} else {
ret = callback ( NULL , NULL , dirname , error_stat , data ) ;
}
return ret ;
}
/* strerror_r is not available everywhere, (and when it is there are two variants,
* the XSI , and the GNU one , so provide a wrapper to make sure correct one is
* used */
const char * cli_strerror ( int errnum , char * buf , size_t len )
{
char * err ;
# ifdef CL_THREAD_SAFE
pthread_mutex_lock ( & cli_strerror_mutex ) ;
# endif
err = strerror ( errnum ) ;
strncpy ( buf , err , len ) ;
buf [ len - 1 ] = ' \0 ' ; /* just in case */
# ifdef CL_THREAD_SAFE
pthread_mutex_unlock ( & cli_strerror_mutex ) ;
# endif
return buf ;
}
static char * cli_md5buff ( const unsigned char * buffer , unsigned int len , unsigned char * dig )
{
unsigned char digest [ 16 ] ;
char * md5str , * pt ;
int i ;
cl_hash_data ( " md5 " , buffer , len , digest , NULL ) ;
if ( dig )
memcpy ( dig , digest , 16 ) ;
if ( ! ( md5str = ( char * ) cli_calloc ( 32 + 1 , sizeof ( char ) ) ) )
return NULL ;
pt = md5str ;
for ( i = 0 ; i < 16 ; i + + ) {
sprintf ( pt , " %02x " , digest [ i ] ) ;
pt + = 2 ;
}
return md5str ;
}
unsigned int cli_rndnum ( unsigned int max )
{
if ( name_salt [ 0 ] = = 16 ) { /* minimizes re-seeding after the first call to cli_gentemp() */
struct timeval tv ;
gettimeofday ( & tv , ( struct timezone * ) 0 ) ;
srand ( tv . tv_usec + clock ( ) + rand ( ) ) ;
}
return 1 + ( unsigned int ) ( max * ( rand ( ) / ( 1.0 + RAND_MAX ) ) ) ;
}
char * cli_sanitize_filepath ( const char * filepath , size_t filepath_len , char * * sanitized_filebase )
{
uint32_t depth = 0 ;
size_t index = 0 ;
size_t sanitized_index = 0 ;
char * sanitized_filepath = NULL ;
if ( ( NULL = = filepath ) | | ( 0 = = filepath_len ) | | ( PATH_MAX < filepath_len ) ) {
goto done ;
}
if ( NULL ! = sanitized_filebase ) {
* sanitized_filebase = NULL ;
}
sanitized_filepath = cli_calloc ( filepath_len + 1 , sizeof ( unsigned char ) ) ;
if ( NULL = = sanitized_filepath ) {
cli_dbgmsg ( " cli_sanitize_filepath: out of memory \n " ) ;
goto done ;
}
while ( index < filepath_len ) {
char * next_pathsep = NULL ;
if ( 0 = = strncmp ( filepath + index , PATHSEP , strlen ( PATHSEP ) ) ) {
/*
* Is " / " ( or " \\ " on Windows )
*/
/* Skip leading pathsep in absolute path, or extra pathsep) */
index + = strlen ( PATHSEP ) ;
continue ;
} else if ( 0 = = strncmp ( filepath + index , " . " PATHSEP , strlen ( " . " PATHSEP ) ) ) {
/*
* Is " ./ " ( or " . \\ " on Windows )
*/
/* Current directory indicator is meaningless and should not add to the depth. Skip it. */
index + = strlen ( " . " PATHSEP ) ;
continue ;
} else if ( 0 = = strncmp ( filepath + index , " .. " PATHSEP , strlen ( " .. " PATHSEP ) ) ) {
/*
* Is " ../ " ( or " .. \\ " on Windows )
*/
if ( depth = = 0 ) {
/* Relative path would traverse parent directory. Skip it. */
index + = strlen ( " .. " PATHSEP ) ;
continue ;
} else {
/* Relative path is safe. Allow it. */
strncpy ( sanitized_filepath + sanitized_index , filepath + index , strlen ( " .. " PATHSEP ) ) ;
sanitized_index + = strlen ( " .. " PATHSEP ) ;
index + = strlen ( " .. " PATHSEP ) ;
depth - - ;
}
# ifdef _WIN32
/*
* Windows ' POSIX style API ' s accept both " / " and " \\ " style path separators .
* The following checks using POSIX style path separators on Windows .
*/
} else if ( 0 = = strncmp ( filepath + index , " / " , strlen ( " / " ) ) ) {
/*
* Is " / " .
*/
/* Skip leading pathsep in absolute path, or extra pathsep) */
index + = strlen ( " / " ) ;
continue ;
} else if ( 0 = = strncmp ( filepath + index , " ./ " , strlen ( " ./ " ) ) ) {
/*
* Is " ./ "
*/
/* Current directory indicator is meaningless and should not add to the depth. Skip it. */
index + = strlen ( " ./ " ) ;
continue ;
} else if ( 0 = = strncmp ( filepath + index , " ../ " , strlen ( " ../ " ) ) ) {
/*
* Is " ../ "
*/
if ( depth = = 0 ) {
/* Relative path would traverse parent directory. Skip it. */
index + = strlen ( " ../ " ) ;
continue ;
} else {
/* Relative path is safe. Allow it. */
strncpy ( sanitized_filepath + sanitized_index , filepath + index , strlen ( " ../ " ) ) ;
sanitized_index + = strlen ( " ../ " ) ;
index + = strlen ( " ../ " ) ;
depth - - ;
/* Convert path separator to Windows separator */
sanitized_filepath [ sanitized_index - 1 ] = ' \\ ' ;
}
# endif
} else {
/*
* Is not " / " , " ./ " , or " ../ " .
*/
/* Find the next path separator. */
# ifdef _WIN32
char * next_windows_pathsep = NULL ;
# endif
next_pathsep = CLI_STRNSTR ( filepath + index , " / " , filepath_len - index ) ;
# ifdef _WIN32
/* Check for both types of separators. */
next_windows_pathsep = CLI_STRNSTR ( filepath + index , " \\ " , filepath_len - index ) ;
if ( NULL ! = next_windows_pathsep ) {
if ( ( NULL = = next_pathsep ) | | ( next_windows_pathsep < next_pathsep ) ) {
next_pathsep = next_windows_pathsep ;
}
}
# endif
if ( NULL = = next_pathsep ) {
/* No more path separators, copy the rest (filename) into the sanitized path */
strncpy ( sanitized_filepath + sanitized_index , filepath + index , filepath_len - index ) ;
if ( NULL ! = sanitized_filebase ) {
/* Set output variable to point to the file base name */
* sanitized_filebase = sanitized_filepath + sanitized_index ;
}
break ;
}
next_pathsep + = strlen ( PATHSEP ) ; /* Include the path separator in the copy */
/* Copy next directory name into the sanitized path */
strncpy ( sanitized_filepath + sanitized_index , filepath + index , next_pathsep - ( filepath + index ) ) ;
sanitized_index + = next_pathsep - ( filepath + index ) ;
index + = next_pathsep - ( filepath + index ) ;
depth + + ;
# ifdef _WIN32
/* Convert path separator to Windows separator */
sanitized_filepath [ sanitized_index - 1 ] = ' \\ ' ;
# endif
}
}
done :
if ( ( NULL ! = sanitized_filepath ) & & ( 0 = = strlen ( sanitized_filepath ) ) ) {
free ( sanitized_filepath ) ;
sanitized_filepath = NULL ;
if ( NULL ! = sanitized_filebase ) {
* sanitized_filebase = NULL ;
}
}
return sanitized_filepath ;
}
# define SHORT_HASH_LENGTH 10
char * cli_genfname ( const char * prefix )
{
char * sanitized_prefix = NULL ;
char * sanitized_prefix_base = NULL ;
char * fname = NULL ;
unsigned char salt [ 16 + 32 ] ;
char * tmp ;
int i ;
size_t len ;
if ( prefix & & ( strlen ( prefix ) > 0 ) ) {
sanitized_prefix = cli_sanitize_filepath ( prefix , strlen ( prefix ) , & sanitized_prefix_base ) ;
}
if ( NULL ! = sanitized_prefix_base ) {
len = strlen ( sanitized_prefix_base ) + strlen ( " . " ) + SHORT_HASH_LENGTH + 1 ; /* {prefix}.{SHORT_HASH_LENGTH}\0 */
} else {
len = strlen ( " clamav- " ) + 48 + strlen ( " .tmp " ) + 1 ; /* clamav-{48}.tmp\0 */
}
fname = ( char * ) cli_calloc ( len , sizeof ( char ) ) ;
if ( ! fname ) {
cli_dbgmsg ( " cli_genfname: no memory left for fname \n " ) ;
if ( NULL ! = sanitized_prefix ) {
free ( sanitized_prefix ) ;
}
return NULL ;
}
# ifdef CL_THREAD_SAFE
pthread_mutex_lock ( & cli_gentemp_mutex ) ;
# endif
memcpy ( salt , name_salt , 16 ) ;
for ( i = 16 ; i < 48 ; i + + )
salt [ i ] = cli_rndnum ( 255 ) ;
tmp = cli_md5buff ( salt , 48 , name_salt ) ;
# ifdef CL_THREAD_SAFE
pthread_mutex_unlock ( & cli_gentemp_mutex ) ;
# endif
if ( NULL = = tmp ) {
free ( fname ) ;
if ( NULL ! = sanitized_prefix ) {
free ( sanitized_prefix ) ;
}
cli_dbgmsg ( " cli_genfname: no memory left for cli_md5buff output \n " ) ;
return NULL ;
}
if ( NULL ! = sanitized_prefix_base ) {
snprintf ( fname , len , " %s.%.*s " , sanitized_prefix_base , SHORT_HASH_LENGTH , tmp ) ;
} else {
snprintf ( fname , len , " clamav-%s.tmp " , tmp ) ;
}
if ( NULL ! = sanitized_prefix ) {
free ( sanitized_prefix ) ;
}
free ( tmp ) ;
return ( fname ) ;
}
char * cli_newfilepath ( const char * dir , const char * fname )
{
char * fullpath ;
const char * mdir ;
size_t len ;
if ( NULL = = fname ) {
cli_dbgmsg ( " cli_newfilepath('%s'): fname argument must not be NULL \n " , mdir ) ;
return NULL ;
}
mdir = dir ? dir : cli_gettmpdir ( ) ;
len = strlen ( mdir ) + strlen ( PATHSEP ) + strlen ( fname ) + 1 ; /* mdir/fname\0 */
fullpath = ( char * ) cli_calloc ( len , sizeof ( char ) ) ;
if ( NULL = = fullpath ) {
cli_dbgmsg ( " cli_newfilepath('%s'): out of memory \n " , mdir ) ;
return NULL ;
}
snprintf ( fullpath , len , " %s " PATHSEP " %s " , mdir , fname ) ;
return ( fullpath ) ;
}
cl_error_t cli_newfilepathfd ( const char * dir , char * fname , char * * name , int * fd )
{
if ( NULL = = name | | NULL = = fname | | NULL = = fd ) {
cli_dbgmsg ( " cli_newfilepathfd('%s'): invalid NULL arguments \n " , dir ) ;
return CL_EARG ;
}
* name = cli_newfilepath ( dir , fname ) ;
if ( ! * name ) {
cli_dbgmsg ( " cli_newfilepathfd('%s'): out of memory \n " , dir ) ;
return CL_EMEM ;
}
* fd = open ( * name , O_RDWR | O_CREAT | O_TRUNC | O_BINARY | O_EXCL , S_IRUSR | S_IWUSR ) ;
/*
* EEXIST is almost impossible to occur , so we just treat it as other
* errors
*/
if ( * fd = = - 1 ) {
cli_errmsg ( " cli_newfilepathfd: Can't create file %s: %s \n " , * name , strerror ( errno ) ) ;
free ( * name ) ;
* name = NULL ;
return CL_ECREAT ;
}
return CL_SUCCESS ;
}
char * cli_gentemp_with_prefix ( const char * dir , const char * prefix )
{
char * fname ;
char * fullpath ;
const char * mdir ;
size_t len ;
mdir = dir ? dir : cli_gettmpdir ( ) ;
fname = cli_genfname ( prefix ) ;
if ( ! fname ) {
cli_dbgmsg ( " cli_gentemp_with_prefix('%s'): out of memory \n " , mdir ) ;
return NULL ;
}
len = strlen ( mdir ) + strlen ( PATHSEP ) + strlen ( fname ) + 1 ; /* mdir/fname\0 */
fullpath = ( char * ) cli_calloc ( len , sizeof ( char ) ) ;
if ( ! fullpath ) {
free ( fname ) ;
cli_dbgmsg ( " cli_gentemp_with_prefix('%s'): out of memory \n " , mdir ) ;
return NULL ;
}
snprintf ( fullpath , len , " %s " PATHSEP " %s " , mdir , fname ) ;
free ( fname ) ;
return ( fullpath ) ;
}
char * cli_gentemp ( const char * dir )
{
return cli_gentemp_with_prefix ( dir , NULL ) ;
}
cl_error_t cli_gentempfd ( const char * dir , char * * name , int * fd )
{
return cli_gentempfd_with_prefix ( dir , NULL , name , fd ) ;
}
cl_error_t cli_gentempfd_with_prefix ( const char * dir , const char * prefix , char * * name , int * fd )
{
* name = cli_gentemp_with_prefix ( dir , prefix ) ;
if ( ! * name )
return CL_EMEM ;
* fd = open ( * name , O_RDWR | O_CREAT | O_TRUNC | O_BINARY | O_EXCL , S_IRUSR | S_IWUSR ) ;
/*
* EEXIST is almost impossible to occur , so we just treat it as other
* errors
*/
if ( * fd = = - 1 ) {
if ( ( EILSEQ = = errno ) | | ( EINVAL = = errno ) | | ( ENAMETOOLONG = = errno ) ) {
cli_dbgmsg ( " cli_gentempfd_with_prefix: Can't create temp file using prefix. Using a randomly generated name instead. \n " ) ;
free ( * name ) ;
* name = cli_gentemp ( dir ) ;
if ( ! * name )
return CL_EMEM ;
* fd = open ( * name , O_RDWR | O_CREAT | O_TRUNC | O_BINARY | O_EXCL , S_IRUSR | S_IWUSR ) ;
if ( * fd = = - 1 ) {
cli_errmsg ( " cli_gentempfd_with_prefix: Can't create temporary file %s: %s \n " , * name , strerror ( errno ) ) ;
free ( * name ) ;
* name = NULL ;
return CL_ECREAT ;
}
} else {
cli_errmsg ( " cli_gentempfd_with_prefix: Can't create temporary file %s: %s \n " , * name , strerror ( errno ) ) ;
free ( * name ) ;
* name = NULL ;
return CL_ECREAT ;
}
}
return CL_SUCCESS ;
}
int cli_regcomp ( regex_t * preg , const char * pattern , int cflags )
{
if ( ! strncmp ( pattern , " (?i) " , 4 ) ) {
pattern + = 4 ;
cflags | = REG_ICASE ;
}
return cli_regcomp_real ( preg , pattern , cflags ) ;
}
cl_error_t cli_get_filepath_from_filedesc ( int desc , char * * filepath )
{
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
cl_error_t status = CL_EARG ;
char * evaluated_filepath = NULL ;
# ifdef __linux__
char fname [ PATH_MAX ] ;
char link [ 32 ] ;
ssize_t linksz ;
memset ( & fname , 0 , PATH_MAX ) ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
if ( NULL = = filepath ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Invalid args. \n " ) ;
goto done ;
}
snprintf ( link , sizeof ( link ) , " /proc/self/fd/%u " , desc ) ;
link [ sizeof ( link ) - 1 ] = ' \0 ' ;
if ( - 1 = = ( linksz = readlink ( link , fname , PATH_MAX - 1 ) ) ) {
cli_dbgmsg ( " cli_get_filepath_from_filedesc: Failed to resolve filename for descriptor %d (%s) \n " , desc , link ) ;
status = CL_EOPEN ;
goto done ;
}
/* Success. Add null terminator */
fname [ linksz ] = ' \0 ' ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
evaluated_filepath = CLI_STRNDUP ( fname , CLI_STRNLEN ( fname , PATH_MAX ) ) ;
if ( NULL = = evaluated_filepath ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Failed to allocate memory to store filename \n " ) ;
status = CL_EMEM ;
goto done ;
}
# elif __APPLE__
char fname [ PATH_MAX ] ;
memset ( & fname , 0 , PATH_MAX ) ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
if ( NULL = = filepath ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Invalid args. \n " ) ;
goto done ;
}
if ( fcntl ( desc , F_GETPATH , & fname ) < 0 ) {
cli_dbgmsg ( " cli_get_filepath_from_filedesc: Failed to resolve filename for descriptor %d \n " , desc ) ;
status = CL_EOPEN ;
goto done ;
}
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
evaluated_filepath = CLI_STRNDUP ( fname , CLI_STRNLEN ( fname , PATH_MAX ) ) ;
if ( NULL = = evaluated_filepath ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Failed to allocate memory to store filename \n " ) ;
status = CL_EMEM ;
goto done ;
}
# elif _WIN32
DWORD dwRet = 0 ;
intptr_t hFile = _get_osfhandle ( desc ) ;
WCHAR * long_evaluated_filepathW = NULL ;
char * long_evaluated_filepathA = NULL ;
size_t evaluated_filepath_len = 0 ;
cl_error_t conv_result ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
if ( NULL = = filepath ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Invalid args. \n " ) ;
goto done ;
}
dwRet = GetFinalPathNameByHandleW ( ( HANDLE ) hFile , NULL , 0 , VOLUME_NAME_DOS ) ;
if ( dwRet = = 0 ) {
cli_dbgmsg ( " cli_get_filepath_from_filedesc: Failed to resolve filename for descriptor %d \n " , desc ) ;
status = CL_EOPEN ;
goto done ;
}
long_evaluated_filepathW = calloc ( dwRet + 1 , sizeof ( WCHAR ) ) ;
if ( NULL = = long_evaluated_filepathW ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Failed to allocate %u bytes to store filename \n " , dwRet + 1 ) ;
status = CL_EMEM ;
goto done ;
}
dwRet = GetFinalPathNameByHandleW ( ( HANDLE ) hFile , long_evaluated_filepathW , dwRet + 1 , VOLUME_NAME_DOS ) ;
if ( dwRet = = 0 ) {
cli_dbgmsg ( " cli_get_filepath_from_filedesc: Failed to resolve filename for descriptor %d \n " , desc ) ;
status = CL_EOPEN ;
goto done ;
}
if ( 0 = = wcsncmp ( L " \\ \\ ? \\ UNC " , long_evaluated_filepathW , wcslen ( L " \\ \\ ? \\ UNC " ) ) ) {
conv_result = cli_codepage_to_utf8 (
long_evaluated_filepathW ,
( wcslen ( long_evaluated_filepathW ) ) * sizeof ( WCHAR ) ,
CODEPAGE_UTF16_LE ,
& evaluated_filepath ,
& evaluated_filepath_len ) ;
if ( CL_SUCCESS ! = conv_result ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Failed to convert UTF16_LE filename to UTF8 \n " , dwRet + 1 ) ;
status = CL_EOPEN ;
goto done ;
}
} else {
conv_result = cli_codepage_to_utf8 (
long_evaluated_filepathW + wcslen ( L " \\ \\ ? \\ " ) ,
( wcslen ( long_evaluated_filepathW ) - wcslen ( L " \\ \\ ? \\ " ) ) * sizeof ( WCHAR ) ,
CODEPAGE_UTF16_LE ,
& evaluated_filepath ,
& evaluated_filepath_len ) ;
if ( CL_SUCCESS ! = conv_result ) {
cli_errmsg ( " cli_get_filepath_from_filedesc: Failed to convert UTF16_LE filename to UTF8 \n " , dwRet + 1 ) ;
status = CL_EOPEN ;
goto done ;
}
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
}
# else
cli_dbgmsg ( " cli_get_filepath_from_filedesc: No mechanism implemented to determine filename from file descriptor. \n " ) ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
status = CL_BREAK ;
goto done ;
# endif
cli_dbgmsg ( " cli_get_filepath_from_filedesc: File path for fd [%d] is: %s \n " , desc , evaluated_filepath ) ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
status = CL_SUCCESS ;
* filepath = evaluated_filepath ;
done :
# ifdef _WIN32
if ( NULL ! = long_evaluated_filepathW ) {
free ( long_evaluated_filepathW ) ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
}
# endif
return status ;
}
cl_error_t cli_realpath ( const char * file_name , char * * real_filename )
{
char * real_file_path = NULL ;
cl_error_t status = CL_EARG ;
# ifdef _WIN32
int desc = - 1 ;
# endif
cli_dbgmsg ( " Checking realpath of %s \n " , file_name ) ;
if ( NULL = = file_name | | NULL = = real_filename ) {
cli_warnmsg ( " cli_realpath: Invalid arguments. \n " ) ;
goto done ;
}
# ifndef _WIN32
real_file_path = realpath ( file_name , NULL ) ;
if ( NULL = = real_file_path ) {
status = CL_EMEM ;
goto done ;
}
status = CL_SUCCESS ;
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
# else
if ( ( desc = safe_open ( file_name , O_RDONLY | O_BINARY ) ) = = - 1 ) {
cli_warnmsg ( " Can't open file %s: %s \n " , file_name , strerror ( errno ) ) ;
status = CL_EOPEN ;
goto done ;
}
status = cli_get_filepath_from_filedesc ( desc , & real_file_path ) ;
# endif
* real_filename = real_file_path ;
done :
clamd clients: Mitigate move/remove symlink attack
A malicious user could replace a scan target's directory with a symlink
to another path to trick clamscan, clamdscan, or clamonacc into removing
or moving a different file (eg. a critical system file). The issue would
affect users that use the `--move` or `--remove` options for clamscan,
clamdscan, and clamonacc.
This patch gets the real path for the scan target before the scan,
and if the file alerts and the --move or --remove quarantine features
are used, it mitigates the symlink attack by traversing the path one
directory at a time until reaching the leaf directory where the scan
target file resides before unlinking (or renaming) the file directly.
This commit applies a similar tactic used in the previous commit for
Windows builds, using the Win32 Native API to traverse a path and delete
or move files by handle rather than by file path.
I had some trouble using SetFileInformationByHandle to rename a file by
handle, so for Windows instead it will copy the file to the new location
and then use the safe unlink technique to remove the old file. If the
symlink attack occurs, the unlink will fail, and the system will not be
damaged.
For more information about AV quarantine attacks using links, see the
[RACK911 Lab's report](https://www.rack911labs.com/research/exploiting-almost-every-antivirus-software)
5 years ago
# ifdef _WIN32
if ( - 1 ! = desc ) {
close ( desc ) ;
}
# endif
return status ;
}