denyhosts/clamscan/shared/actions.c

718 lines
23 KiB
C
Raw Normal View History

2022-10-22 18:41:00 +08:00
/*
* Copyright (C) 2013-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
* Copyright (C) 2009-2013 Sourcefire, Inc.
*
* Author: aCaB, Micah Snyder
*
* These functions are actions that may be taken when a sample alerts.
* The user may wish to:
* - move file to destination directory.
* - copy file to destination directory.
* - remove (delete) the file.
*
* 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.
*/
#ifdef _WIN32
#include <windows.h>
#include <winternl.h>
#endif
#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdbool.h>
#include <fcntl.h>
#include <errno.h>
#include <libgen.h>
// libclamav
#include "clamav.h"
#include "str.h"
#include "others.h"
#include "optparser.h"
#include "output.h"
#include "misc.h"
#include "actions.h"
void (*action)(const char *) = NULL;
unsigned int notmoved = 0, notremoved = 0;
static char *actarget;
static int targlen;
static int getdest(const char *fullpath, char **newname)
{
char *tmps, *filename;
int fd, i;
tmps = strdup(fullpath);
if (!tmps) {
*newname = NULL;
return -1;
}
filename = basename(tmps);
if (!(*newname = (char *)malloc(targlen + strlen(filename) + 6))) {
free(tmps);
return -1;
}
sprintf(*newname, "%s" PATHSEP "%s", actarget, filename);
for (i = 1; i < 1000; i++) {
fd = open(*newname, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd >= 0) {
free(tmps);
return fd;
}
if (errno != EEXIST) break;
sprintf(*newname, "%s" PATHSEP "%s.%03u", actarget, filename, i);
}
free(tmps);
free(*newname);
*newname = NULL;
return -1;
}
#ifdef _WIN32
typedef LONG (*PNTCF)(
PHANDLE FileHandle, // OUT
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock, // OUT
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);
typedef void (*PRIUS)(
PUNICODE_STRING DestinationString,
PCWSTR SourceString);
/**
* @brief A openat equivalent for Win32 with a check to NOFOLLOW soft-links.
*
* The caller is resposible for closing the HANDLE.
*
* For the desiredAccess, fileAttributes, createOptions, and shareAccess parameters
* see https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile
*
* @param current_handle The current handle. If set to NULL, then filename should be a drive letter.
* @param filename The directory to open. If current_handle is valid, should be a directory found in the current directory.
* @param pNtCreateFile A function pointer to the NtCreateFile Win32 Native API.
* @param pRtlInitUnicodeString A function pointer to the RtlInitUnicodeString Win32 Native API.
* @param desiredAccess The DesiredAccess option for NtCreateFile
* @param fileAttributes The FileAttributes option for NtCreateFile
* @param createOptions The CreateOptions option for NtCreateFile
* @param shareAccess The ShareAccess option for NtCreateFile
* @return HANDLE A handle on success, NULL on failure.
*/
static HANDLE win32_openat(
HANDLE current_handle,
const char *filename,
PNTCF pNtCreateFile,
PRIUS pRtlInitUnicodeString,
ACCESS_MASK desiredAccess,
ULONG fileAttributes,
ULONG createOptions,
ULONG shareAccess)
{
HANDLE next_handle = NULL;
LONG ntStatus;
WCHAR *filenameW = NULL;
UNICODE_STRING filenameU;
int cchNextDirectoryName = 0;
IO_STATUS_BLOCK ioStatusBlock = {0};
OBJECT_ATTRIBUTES objAttributes = {0};
FILE_ATTRIBUTE_TAG_INFO tagInfo = {0};
/* Convert filename to a UNICODE_STRING, required by the native API NtCreateFile() */
cchNextDirectoryName = MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0);
filenameW = malloc(cchNextDirectoryName * sizeof(WCHAR));
if (NULL == filenameW) {
logg("win32_openat: failed to allocate memory for next directory name UTF16LE string\n");
goto done;
}
if (0 == MultiByteToWideChar(CP_UTF8, 0, filename, -1, filenameW, cchNextDirectoryName)) {
logg("win32_openat: failed to allocate buffer for unicode version of intermediate directory name.\n");
goto done;
}
pRtlInitUnicodeString(&filenameU, filenameW);
InitializeObjectAttributes(
&objAttributes, // ObjectAttributes
&filenameU, // ObjectName
OBJ_CASE_INSENSITIVE, // Attributes
current_handle, // Root directory
NULL); // SecurityDescriptor
ntStatus = pNtCreateFile(
&next_handle, // FileHandle
desiredAccess, // DesiredAccess
&objAttributes, // ObjectAttributes
&ioStatusBlock, // [out] status
0, // AllocationSize
fileAttributes, // FileAttributes
shareAccess, // ShareAccess
FILE_OPEN, // CreateDisposition
createOptions, // CreateOptions
NULL, // EaBuffer
0); // EaLength
if (!NT_SUCCESS(ntStatus) || (NULL == next_handle)) {
logg("win32_openat: Failed to open file '%s'. \nError: 0x%x \nioStatusBlock: 0x%x\n", filename, ntStatus, ioStatusBlock.Information);
goto done;
}
logg("*win32_openat: Opened file \"%s\"\n", filename);
if (0 == GetFileInformationByHandleEx(
next_handle, // hFile,
FileAttributeTagInfo, // FileInformationClass
&tagInfo, // lpFileInformation
sizeof(FILE_ATTRIBUTE_TAG_INFO))) { // dwBufferSize
logg("win32_openat: Failed to get file information by handle '%s'. Error: %d.\n", filename, GetLastError());
CloseHandle(next_handle);
next_handle = NULL;
goto done;
}
logg("*win32_openat: tagInfo.FileAttributes: 0x%0x\n", tagInfo.FileAttributes);
logg("*win32_openat: tagInfo.ReparseTag: 0x%0x\n", tagInfo.ReparseTag);
if (0 != (tagInfo.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
logg("win32_openat: File is a soft link: '%s' Aborting path traversal.\n\n", filename);
CloseHandle(next_handle);
next_handle = NULL;
goto done;
}
logg("*win32_openat: File or directory is not a soft link.\n\n");
done:
if (NULL != filenameW) {
free(filenameW);
}
return next_handle;
}
#endif
/**
* @brief Traverse from root to the specified directory without following symlinks.
*
* The intention is so you can use `unlinkat` or `rename_at` to safely move or
* delete the target directory.
*
* The caller is responsible for closing the output file descriptor if the
* traversal succeeded.
*
* @param directory The directory to traverse to (must be NULL terminated).
* @param want_directory_handle Set to true to get the directory handle containing the file, false to get the file handle.
* @param[out] out_handle An open file descriptor or HANDLE (win32) for the directory.
* @return 0 Traverse succeeded.
* @return -1 Traverse failed.
*/
#ifndef _WIN32
static int traverse_to(const char *directory, bool want_directory_handle, int *out_handle)
#else
static int traverse_to(const char *directory, bool want_directory_handle, HANDLE *out_handle)
#endif
{
int status = -1;
size_t tokens_count;
const char *tokens[PATH_MAX / 2];
size_t i;
char *tokenized_directory = NULL;
#ifndef _WIN32
int current_handle = -1;
int next_handle = -1;
#else
bool bNeedDeleteFileAccess = false;
HMODULE ntdll = NULL;
PNTCF pNtCreateFile = NULL;
PRIUS pRtlInitUnicodeString = NULL;
PHANDLE current_handle = NULL;
PHANDLE next_handle = NULL;
ACCESS_MASK desiredAccess = STANDARD_RIGHTS_READ | STANDARD_RIGHTS_WRITE | SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_READ_EA;
ULONG fileAttributes = FILE_ATTRIBUTE_DIRECTORY;
ULONG createOptions = FILE_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT;
ULONG shareAccess = FILE_SHARE_READ;
#endif
if (NULL == directory || NULL == out_handle) {
logg("traverse_to: Invalid arguments!\n");
goto done;
}
#ifdef _WIN32
ntdll = LoadLibraryA("ntdll.dll");
if (NULL == ntdll) {
logg("traverse_to: failed to load ntdll!\n");
goto done;
}
pNtCreateFile = (PNTCF)GetProcAddress(ntdll, "NtCreateFile");
if (NULL == pNtCreateFile) {
logg("traverse_to: failed to get NtCreateFile proc address!\n");
goto done;
}
pRtlInitUnicodeString = (PRIUS)GetProcAddress(ntdll, "RtlInitUnicodeString");
if (NULL == pRtlInitUnicodeString) {
logg("traverse_to: failed to get pRtlInitUnicodeString proc address!\n");
goto done;
}
#endif
tokenized_directory = strdup(directory);
if (NULL == tokenized_directory) {
logg("traverse_to: Failed to get copy of directory path to be tokenized!\n");
goto done;
}
tokens_count = cli_strtokenize(tokenized_directory, *PATHSEP, PATH_MAX / 2, tokens);
if (0 == tokens_count) {
logg("traverse_to: tokenize of target directory returned 0 tokens!\n");
goto done;
}
#ifndef _WIN32
/*
* Open the root(/) directory, because it won't be the first token like a
* drive letter (i.e. "C:") would be on Windows.
*/
current_handle = open("/", O_RDONLY | O_NOFOLLOW);
if (-1 == current_handle) {
logg("traverse_to: Failed to open file descriptor for '/' directory.\n");
goto done;
}
#endif
if (true == want_directory_handle) {
tokens_count -= 1;
}
if (0 == tokens_count) {
logg("traverse_to: Failed to get copy of directory path to be tokenized!\n");
goto done;
}
for (i = 0; i < tokens_count; i++) {
if (0 == strlen(tokens[i])) {
/* Empty token, likely first / or double // */
continue;
}
#ifndef _WIN32
next_handle = openat(current_handle, tokens[i], O_RDONLY | O_NOFOLLOW);
if (-1 == next_handle) {
logg("traverse_to: Failed open %s\n", tokens[i]);
goto done;
}
close(current_handle);
current_handle = next_handle;
next_handle = -1;
#else
if (true != want_directory_handle) {
if (i == tokens_count - 1) {
/* Change createfile options for our target file instead of an intermediate directory. */
desiredAccess = FILE_GENERIC_READ | DELETE;
fileAttributes = FILE_ATTRIBUTE_NORMAL;
createOptions = FILE_NON_DIRECTORY_FILE;
shareAccess = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
}
}
if (i == 0) {
/* NtCreateFile requires the \???\ prefix on drive letters. Eg: \???\C:\ */
size_t driveroot_len = strlen("\\??\\\\") + strlen(tokens[0]) + 1;
char *driveroot = malloc(driveroot_len);
snprintf(driveroot, driveroot_len + 1, "\\??\\%s\\", tokens[0]);
next_handle = win32_openat(current_handle,
driveroot,
pNtCreateFile,
pRtlInitUnicodeString,
desiredAccess,
fileAttributes,
createOptions,
shareAccess);
free(driveroot);
} else {
next_handle = win32_openat(current_handle,
tokens[i],
pNtCreateFile,
pRtlInitUnicodeString,
desiredAccess,
fileAttributes,
createOptions,
shareAccess);
}
if (NULL == next_handle) {
logg("traverse_to: Failed open %s\n", tokens[i]);
goto done;
}
CloseHandle(current_handle);
current_handle = next_handle;
next_handle = NULL;
#endif
logg("*traverse_to: Handle opened for '%s' directory.\n", tokens[i]);
}
status = 0;
*out_handle = current_handle;
done:
#ifndef _WIN32
if ((-1 == status) && (-1 != current_handle)) {
close(current_handle);
}
#else
if ((-1 == status) && (NULL != current_handle)) {
CloseHandle(current_handle);
}
#endif
if (NULL != tokenized_directory) {
free(tokenized_directory);
}
return status;
}
/**
* @brief Rename (move) a file from Source to Destination without following symlinks.
*
* This approach mitigates the possibility that one of the directories
* in the path has been replaces with a malicious symlink.
*
* @param source Source pathname.
* @param destination Destination pathname (including file name)
* @return 0 Rename succeeded.
* @return -1 Rename failed.
*/
static int traverse_rename(const char *source, const char *destination)
{
int status = -1;
#ifndef _WIN32
cl_error_t ret;
int source_directory_fd = -1;
char *source_basename = NULL;
#else
FILE_RENAME_INFO *fileInfo = NULL;
HANDLE source_file_handle = NULL;
HANDLE destination_dir_handle = NULL;
WCHAR *destFilepathW = NULL;
int cchDestFilepath = 0;
#endif
if (NULL == source || NULL == destination) {
logg("traverse_rename: Invalid arguments!\n");
goto done;
}
#ifndef _WIN32
if (0 != traverse_to(source, true, &source_directory_fd)) {
logg("traverse_rename: Failed to open file descriptor for source directory!\n");
goto done;
}
#else
if (0 != traverse_to(source, false, &source_file_handle)) {
logg("traverse_rename: Failed to open file descriptor for source file!\n");
goto done;
}
if (0 != traverse_to(destination, true, &destination_dir_handle)) {
logg("traverse_rename: Failed to open file descriptor for destination directory!\n");
goto done;
}
#endif
#ifndef _WIN32
ret = cli_basename(source, strlen(source), &source_basename);
if (CL_SUCCESS != ret) {
logg("traverse_rename: Failed to get basename of source path:%s\n\tError: %d\n", source, (int)ret);
goto done;
}
if (0 != renameat(source_directory_fd, source_basename, -1, destination)) {
logg("traverse_rename: Failed to rename: %s\n\tto: %s\nError:%s\n", source, destination, strerror(errno));
goto done;
}
#else
/* Convert destination filepath to a PWCHAR */
cchDestFilepath = MultiByteToWideChar(CP_UTF8, 0, destination, strlen(destination), NULL, 0);
destFilepathW = calloc(cchDestFilepath * sizeof(WCHAR), 1);
if (NULL == destFilepathW) {
logg("traverse_rename: failed to allocate memory for destination basename UTF16LE string\n");
goto done;
}
if (0 == MultiByteToWideChar(CP_UTF8, 0, destination, strlen(destination), destFilepathW, cchDestFilepath)) {
logg("traverse_rename: failed to allocate buffer for UTF16LE version of destination file basename.\n");
goto done;
}
fileInfo = calloc(1, sizeof(FILE_RENAME_INFO) + cchDestFilepath * sizeof(WCHAR));
if (NULL == fileInfo) {
logg("traverse_rename: failed to allocate memory for fileInfo struct\n");
goto done;
}
fileInfo->ReplaceIfExists = TRUE;
fileInfo->RootDirectory = NULL;
memcpy(fileInfo->FileName, destFilepathW, cchDestFilepath * sizeof(WCHAR));
fileInfo->FileNameLength = cchDestFilepath;
if (FALSE == SetFileInformationByHandle(
source_file_handle, // FileHandle
FileRenameInfo, // FileInformationClass
fileInfo, // FileInformation
sizeof(FILE_RENAME_INFO) + cchDestFilepath * sizeof(WCHAR))) { // Length
logg("traverse_rename: Failed to set file rename info for '%s' to '%s'.\nError: %d\n", source, destination, GetLastError());
goto done;
}
#endif
status = 0;
done:
#ifndef _WIN32
if (NULL != source_basename) {
free(source_basename);
}
if (-1 != source_directory_fd) {
close(source_directory_fd);
}
#else
if (NULL != fileInfo) {
free(fileInfo);
}
if (NULL != destFilepathW) {
free(destFilepathW);
}
if (NULL != source_file_handle) {
CloseHandle(source_file_handle);
}
if (NULL != destination_dir_handle) {
CloseHandle(destination_dir_handle);
}
#endif
return status;
}
/**
* @brief Unlink (delete) a target file without following symlinks.
*
* This approach mitigates the possibility that one of the directories
* in the path has been replaces with a malicious symlink.
*
* @param target A file to be deleted.
* @return 0 Unlink succeeded.
* @return -1 Unlink failed.
*/
static int traverse_unlink(const char *target)
{
int status = -1;
cl_error_t ret;
#ifndef _WIN32
int target_directory_fd = -1;
#else
FILE_DISPOSITION_INFO fileInfo = {0};
HANDLE target_file_handle = NULL;
#endif
char *target_basename = NULL;
if (NULL == target) {
logg("traverse_unlink: Invalid arguments!\n");
goto done;
}
#ifndef _WIN32
/* On posix, we want a file descriptor for the directory */
if (0 != traverse_to(target, true, &target_directory_fd)) {
#else
/* On Windows, we want a handle to the file, not the directory */
if (0 != traverse_to(target, false, &target_file_handle)) {
#endif
logg("traverse_unlink: Failed to open file descriptor for target directory!\n");
goto done;
}
ret = cli_basename(target, strlen(target), &target_basename);
if (CL_SUCCESS != ret) {
logg("traverse_unlink: Failed to get basename of target path: %s\n\tError: %d\n", target, (int)ret);
goto done;
}
#ifndef _WIN32
if (0 != unlinkat(target_directory_fd, target_basename, 0)) {
logg("traverse_unlink: Failed to unlink: %s\nError:%s\n", target, strerror(errno));
goto done;
}
#else
fileInfo.DeleteFileA = TRUE;
if (FALSE == SetFileInformationByHandle(
target_file_handle, // FileHandle
FileDispositionInfo, // FileInformationClass
&fileInfo, // FileInformation
sizeof(FILE_DISPOSITION_INFO))) { // Length
logg("traverse_unlink: Failed to set file disposition to 'DELETE' for '%s'.\n", target);
goto done;
}
if (FALSE == CloseHandle(target_file_handle)) {
logg("traverse_unlink: Failed to set close & delete file '%s'.\n", target);
goto done;
}
target_file_handle = NULL;
#endif
status = 0;
done:
if (NULL != target_basename) {
free(target_basename);
}
#ifndef _WIN32
if (-1 != target_directory_fd) {
close(target_directory_fd);
}
#else
if (NULL != target_file_handle) {
CloseHandle(target_file_handle);
}
#endif
return status;
}
static void action_move(const char *filename)
{
char *nuname = NULL;
char *real_filename = NULL;
int fd = -1;
int copied = 0;
if (NULL == filename) {
goto done;
}
fd = getdest(filename, &nuname);
#ifndef _WIN32
if (fd < 0 || (0 != traverse_rename(filename, nuname) && ((copied = 1)) && filecopy(filename, nuname))) {
#else
if (fd < 0 || (((copied = 1)) && filecopy(filename, nuname))) {
#endif
logg("!Can't move file %s to %s\n", filename, nuname);
notmoved++;
if (nuname) traverse_unlink(nuname);
} else {
if (copied && (0 != traverse_unlink(filename)))
logg("!Can't unlink '%s' after copy: %s\n", filename, strerror(errno));
else
logg("~%s: moved to '%s'\n", filename, nuname);
}
done:
if (NULL != real_filename) free(real_filename);
if (fd >= 0) close(fd);
if (NULL != nuname) free(nuname);
return;
}
static void action_copy(const char *filename)
{
char *nuname;
int fd = getdest(filename, &nuname);
if (fd < 0 || filecopy(filename, nuname)) {
logg("!Can't copy file '%s'\n", filename);
notmoved++;
if (nuname) traverse_unlink(nuname);
} else
logg("~%s: copied to '%s'\n", filename, nuname);
if (fd >= 0) close(fd);
if (nuname) free(nuname);
}
static void action_remove(const char *filename)
{
char *real_filename = NULL;
if (NULL == filename) {
goto done;
}
if (0 != traverse_unlink(filename)) {
logg("!Can't remove file '%s'\n", filename);
notremoved++;
} else {
logg("~%s: Removed.\n", filename);
}
done:
if (NULL != real_filename) free(real_filename);
return;
}
static int isdir(void)
{
STATBUF sb;
if (CLAMSTAT(actarget, &sb) || !S_ISDIR(sb.st_mode)) {
logg("!'%s' doesn't exist or is not a directory\n", actarget);
return 0;
}
return 1;
}
/*
* Call this function at the beginning to configure the user preference.
* Later, call the "action" callback function to perform the selection action.
*/
int actsetup(const struct optstruct *opts)
{
int move = optget(opts, "move")->enabled;
if (move || optget(opts, "copy")->enabled) {
#ifndef _WIN32
cl_error_t ret;
#endif
actarget = optget(opts, move ? "move" : "copy")->strarg;
#ifndef _WIN32
ret = cli_realpath((const char *)actarget, &actarget);
if (CL_SUCCESS != ret || NULL == actarget) {
logg("action_setup: Failed to get realpath of %s\n", actarget);
return 0;
}
#endif
if (!isdir()) return 1;
action = move ? action_move : action_copy;
targlen = strlen(actarget);
} else if (optget(opts, "remove")->enabled)
action = action_remove;
return 0;
}