/*
 *  Copyright (C) 2021-2022 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
 *  Copyright (C) 2005-2010 Gianluigi Tiesi <sherpya@netfarm.it>
 *
 *  Authors: Gianluigi Tiesi
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA 02110-1301, USA.
 */

#include <windows.h>
#include <tlhelp32.h>

#include <psapi.h>
#include <windns.h>

#include <clamav.h>
#include <others.h>

#include "actions.h"
#include "output.h"
#include "clamdcom.h"
#include "exescanner.h"
#include "scanmem.h"

typedef int (*proc_callback)(PROCESSENTRY32 ProcStruct, MODULEENTRY32 me32, void *data, struct mem_info *info);
int sock;
struct optstruct *clamdopts;

static inline int lookup_cache(filelist_t **list, const char *filename)
{
    filelist_t *current = *list;
    while (current) {
        /* Cache hit */
        if (!_stricmp(filename, current->filename)) {
            return current->res;
        }
        current = current->next;
    }

    return -1;
}

static inline void insert_cache(filelist_t **list, const char *filename,
                                int res)
{
    filelist_t *current = *list, *prev = NULL;

    if (!current) /* New */
        *list = current = malloc(sizeof(filelist_t));
    else {
        while (current->next)
            current = current->next;
        prev       = current;
        prev->next = current = malloc(sizeof(filelist_t));
    }

    current->next        = NULL;
    current->res         = res;
    current->filename[0] = 0;
    strncat(current->filename, filename,
            MAX_PATH - 1 - strlen(current->filename));
    current->filename[MAX_PATH - 1] = 0;
}

static inline void free_cache(filelist_t **list)
{
    filelist_t *current, *prev;
    current = prev = *list;

    if (!current)
        return;

    do {
        prev    = current;
        current = prev->next;
        free(prev);
    } while (current);
}

static inline char *wc2mb(const wchar_t *wc, DWORD flags)
{
    BOOL invalid = FALSE;
    DWORD len = 0, res = 0;
    char *mb = NULL;

    len = WideCharToMultiByte(CP_ACP, flags, wc, -1, NULL, 0, NULL, &invalid);
    if (!len && (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) {
        fprintf(stderr, "WideCharToMultiByte() failed with %d\n", GetLastError());
        return NULL;
    }

    mb = cli_malloc(len + 1);
    if (!mb) return NULL;

    res = WideCharToMultiByte(CP_ACP, flags, wc, -1, mb, len, NULL, &invalid);
    if (res && ((!invalid || (flags != WC_NO_BEST_FIT_CHARS)))) return mb;
    free(mb);
    return NULL;
}

/* Needed to Scan System Processes */
int EnablePrivilege(LPCSTR PrivilegeName, DWORD yesno)
{
    HANDLE hToken;
    TOKEN_PRIVILEGES tp;
    LUID luid;

    if (!LoadLibraryA("advapi32.dll")) {
        logg(LOGG_WARNING, "EnablePrivilege functions are missing\n");
        return 0;
    }

    if (!OpenProcessToken(
            GetCurrentProcess(),
            TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_READ, &hToken))
        return 0;

    if (!LookupPrivilegeValue(NULL, PrivilegeName, &luid))
        return 0;

    tp.PrivilegeCount           = 1;
    tp.Privileges[0].Luid       = luid;
    tp.Privileges[0].Attributes = yesno;

    AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL);

    CloseHandle(hToken);
    return (GetLastError() == ERROR_SUCCESS) ? 1 : 0;
}

static char *getaltpath(const wchar_t *filename)
{
    WIN32_FIND_DATAW wfdw;
    HANDLE hf                     = INVALID_HANDLE_VALUE;
    wchar_t *part                 = _wcsdup(filename);
    wchar_t comprev[MAX_PATH + 1] = L"", compose[MAX_PATH + 1];
    wchar_t *rev = comprev, *slash = part, *c = NULL;
    size_t l, la;
    size_t i;

    do {
        if (slash != part)
            *slash = 0;

        /* c: d: etc */
        if ((wcslen(part) == 2) && (part[1] == L':')) {
            *rev++ = L':';
            *rev++ = part[0];
            break;
        }

        hf = FindFirstFileW(part, &wfdw);
        if (hf == INVALID_HANDLE_VALUE) /* Network path */
        {
            for (i = wcslen(part); i > 0; i--)
                *rev++ = part[i - 1];
            break;
        }
        FindClose(hf);
        l  = wcslen(wfdw.cFileName);
        la = wcslen(wfdw.cAlternateFileName);

        if (la)
            for (i = la; i > 0; i--)
                *rev++ = *(wfdw.cAlternateFileName + i - 1);
        else
            for (i = l; i > 0; i--)
                *rev++ = *(wfdw.cFileName + i - 1);
        *rev++ = '\\';

    } while ((slash = wcsrchr(part, L'\\')));

    rev = comprev;
    c   = compose;
    for (i = wcslen(rev); i > 0; i--)
        *c++ = *(rev + i - 1);
    *c = 0;

    free(part);
    return wc2mb(compose, WC_NO_BEST_FIT_CHARS);
}

int walkmodules_th(proc_callback callback, void *data, struct mem_info *info)
{
    HANDLE hSnap = INVALID_HANDLE_VALUE, hModuleSnap = INVALID_HANDLE_VALUE;
    PROCESSENTRY32 ps;
    MODULEENTRY32 me32;

    logg(LOGG_INFO, " *** Memory Scan: using ToolHelp ***\n\n");

    hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap == INVALID_HANDLE_VALUE)
        return -1;

    ps.dwSize = sizeof(PROCESSENTRY32);

    if (!Process32First(hSnap, &ps)) {
        CloseHandle(hSnap);
        return -1;
    }

    do {
        /* system process */
        if (!ps.th32ProcessID)
            continue;
        hModuleSnap = CreateToolhelp32Snapshot(
            TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, ps.th32ProcessID);
        if (hModuleSnap == INVALID_HANDLE_VALUE)
            continue;

        me32.dwSize = sizeof(MODULEENTRY32);
        if (!Module32First(hModuleSnap, &me32)) {
            CloseHandle(hModuleSnap);
            continue;
        }

        /* Check and transform non ANSI filenames to ANSI using altnames */
        if (GetModuleFileNameEx) {
            HANDLE hFile = CreateFile(
                me32.szExePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, NULL);

            if (hFile == INVALID_HANDLE_VALUE) {
                DWORD err = GetLastError();
                wchar_t name[MAX_PATH + 1];
                char *converted = NULL;
                HANDLE p;

                if (err == ERROR_BAD_NETPATH) {
                    logg(LOGG_WARNING, "Warning scanning files on non-ansi network paths is not "
                                       "supported\n");
                    logg(LOGG_WARNING, "File: %s\n", me32.szExePath);
                    continue;
                }

                if ((err != ERROR_INVALID_NAME) && (err != ERROR_PATH_NOT_FOUND)) {
                    logg(LOGG_WARNING, "Expected ERROR_INVALID_NAME/ERROR_PATH_NOT_FOUND but got %d\n",
                         err);
                    continue;
                }

                p = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
                                ps.th32ProcessID);
                if (!GetModuleFileNameEx(p, NULL, name, MAX_PATH)) {
                    logg(LOGG_WARNING, "GetModuleFileNameExW() failed %d\n", GetLastError());
                    CloseHandle(p);
                    continue;
                }
                CloseHandle(p);

                if (!(converted = getaltpath(name))) {
                    logg(LOGG_WARNING, "Cannot map filename to ANSI codepage\n");
                    continue;
                }
                strcpy(me32.szExePath, converted);
                free(converted);
            } else
                CloseHandle(hFile);
        }

        do
            if (callback(ps, me32, data, info))
                break;
        while (Module32Next(hModuleSnap, &me32));

        CloseHandle(hModuleSnap);
    } while (Process32Next(hSnap, &ps));
    CloseHandle(hSnap);
    return 0;
}

int walkmodules_psapi(proc_callback callback, void *data, struct mem_info *info)
{
    DWORD procs[1024], needed, nprocs, mneeded;
    HANDLE hProc;
    HMODULE mods[1024];
    PROCESSENTRY32 ps;
    MODULEENTRY32 me32;
    MODULEINFO mi;
    int i, j;

    logg(LOGG_INFO, " *** Memory Scan: using PsApi ***\n\n");

    if (!EnumProcesses(procs, sizeof(procs), &needed))
        return -1;

    nprocs = needed / sizeof(DWORD);

    memset(&ps, 0, sizeof(PROCESSENTRY32));
    memset(&me32, 0, sizeof(MODULEENTRY32));
    ps.dwSize   = sizeof(PROCESSENTRY32);
    me32.dwSize = sizeof(MODULEENTRY32);

    for (i = 0; i < nprocs; i++) {
        if (!procs[i])
            continue; /* System process */

        hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE,
                            procs[i]);

        if (!hProc)
            continue;

        if (!EnumProcessModules(hProc, mods, sizeof(mods),
                                &mneeded)) {
            CloseHandle(hProc);
            continue;
        }

        if (!GetModuleBaseName(hProc, mods[0], ps.szExeFile,
                               MAX_PATH - 1)) {
            CloseHandle(hProc);
            continue;
        }

        ps.th32ProcessID = procs[i];

        for (j = 0; j < (mneeded / sizeof(HMODULE)); j++) {
            if (!GetModuleBaseNameA(hProc, mods[j], me32.szModule,
                                    MAX_PATH - 1))
                continue;

            if (!GetModuleFileNameExA(hProc, mods[j], me32.szExePath,
                                      MAX_PATH - 1))
                continue;

            if (!GetModuleInformation(hProc, mods[j], &mi,
                                      sizeof(mi)))
                continue;

            me32.hModule       = mods[j];
            me32.th32ProcessID = procs[i];
            me32.modBaseAddr   = mi.lpBaseOfDll;
            me32.modBaseSize   = mi.SizeOfImage;
            if (callback(ps, me32, data, info))
                break;
        }
        CloseHandle(hProc);
    }
    return 0;
}

int kill_process(DWORD pid)
{
    HANDLE hProc;
    if (GetCurrentProcessId() == pid) {
        logg(LOGG_WARNING, "Don't want to kill myself\n");
        return 1;
    }

    if ((hProc = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, pid))) {
        TerminateProcess(hProc, 0);
        if (WaitForSingleObject(hProc, TIMEOUT_MODULE) != WAIT_OBJECT_0)
            logg(LOGG_WARNING, "Unable to unload process from memory\n");
        CloseHandle(hProc);
    } else
        logg(LOGG_WARNING, "OpenProcess() failed %lu\n", GetLastError());
    return 1; /* Skip to next process anyway */
}

/* Not so safe ;) */
int unload_module(DWORD pid, HANDLE hModule)
{
    DWORD rc = 1;
    HANDLE ht;
    HANDLE hProc;

    if (GetCurrentProcessId() == pid) {
        logg(LOGG_WARNING, "Don't want to unload modules from myself\n");
        return 1;
    }

    hProc = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION |
                            PROCESS_VM_WRITE | PROCESS_VM_READ,
                        FALSE, pid);

    if (!hProc) {
        logg(LOGG_WARNING, "OpenProcess() failed %lu\n", GetLastError());
        return 1; /* Skip to next process */
    }

    if ((ht = CreateRemoteThread(
             hProc, 0, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, hModule, 0,
             &rc))) {
        if (WaitForSingleObject(ht, TIMEOUT_MODULE) == WAIT_TIMEOUT) {
            CloseHandle(ht);
            CloseHandle(hProc);
            logg(LOGG_INFO, "The module may trying to trick us, killing the process, please "
                            "rescan\n");
            return kill_process(pid);
        }
        CloseHandle(ht);
        rc = 0; /* Continue scanning this process */
    } else {
        DWORD res = GetLastError();
        if (res == ERROR_CALL_NOT_IMPLEMENTED) {
            logg(LOGG_WARNING, "Module unloading is not supported on this OS\n");
            rc = -1; /* Don't complain about removing/moving the file */
        } else {
            logg(LOGG_ERROR, "CreateRemoteThread() failed %lu\n", res);
            rc = 1; /* Skip to next process */
        }
    }

    CloseHandle(hProc);
    return rc;
}

#define FILLBYTES(dst)                                       \
    if (IsBadReadPtr(seek, sizeof(dst))) {                   \
        logg(LOGG_ERROR, "ScanMem Align: Bad pointer!!!\n"); \
        return 1;                                            \
    }                                                        \
    memcpy(&dst, seek, sizeof(dst))

/* PE Realignment - FIXME: a lot of code is copy/paste from exeScanner.c */
int align_pe(unsigned char *buffer, size_t size)
{
    int i = 0;
    uint16_t e_mz;
    uint32_t e_lfanew, e_magic;
    unsigned char *seek = buffer;
    PIMAGE_FILE_HEADER pehdr;
    PIMAGE_OPTIONAL_HEADER32 opthdr;
    PIMAGE_SECTION_HEADER sechdr;

    FILLBYTES(e_mz);
    if (e_mz != IMAGE_DOS_SIGNATURE) {
        /* cli_dbgmsg("ScanMem Align: DOS Signature not found\n"); */
        return 0;
    }

    seek += 0x3c;

    FILLBYTES(e_lfanew);
    if (!e_lfanew) {
        /* cli_dbgmsg("ScanMem Align: Invalid PE offset\n"); */
        return 0;
    }
    seek = buffer + e_lfanew;

    /* PE Signature 'PE' */
    FILLBYTES(e_magic);
    if (e_magic != IMAGE_NT_SIGNATURE) {
        /* cli_dbgmsg("ScanMem Align: PE Signature not found\n"); */
        return 0;
    }
    seek += sizeof(e_magic);

    if (IsBadReadPtr(seek, sizeof(IMAGE_FILE_HEADER)))
        return 0;
    pehdr = (PIMAGE_FILE_HEADER)seek;
    seek += sizeof(IMAGE_FILE_HEADER);

    if (IsBadReadPtr(seek, sizeof(IMAGE_OPTIONAL_HEADER32)))
        return 0;
    opthdr = (PIMAGE_OPTIONAL_HEADER32)seek;
    seek += sizeof(IMAGE_OPTIONAL_HEADER32);

    /* Invalid sections number */
    if ((pehdr->NumberOfSections < 1) || (pehdr->NumberOfSections > 32)) {
        /* cli_dbgmsg("ScanMem Align: Invalid sections number\n"); */
        return 0;
    }

    for (i = 0; i < pehdr->NumberOfSections; i++) {
        if (IsBadWritePtr(seek, sizeof(IMAGE_SECTION_HEADER)))
            return 0;
        sechdr = (PIMAGE_SECTION_HEADER)seek;
        seek += sizeof(IMAGE_SECTION_HEADER);
        sechdr->PointerToRawData = sechdr->VirtualAddress;
        sechdr->SizeOfRawData    = sechdr->Misc.VirtualSize;
    }
    return 1;
}

int dump_pe(const char *filename, PROCESSENTRY32 ProcStruct,
            MODULEENTRY32 me32)
{
#ifdef _WIN64 /* MinGW has a broken header for ReadProcessMemory() */
    size_t bytesread = 0;
#else
    DWORD bytesread = 0;
#endif
    DWORD byteswrite = 0;
    int ret          = -1;
    HANDLE hFile = INVALID_HANDLE_VALUE, hProc = NULL;
    unsigned char *buffer = NULL;

    if (!(hProc = OpenProcess(PROCESS_VM_READ, FALSE, ProcStruct.th32ProcessID)))
        return -1;

    buffer = malloc((size_t)me32.modBaseSize);
    if (!ReadProcessMemory(hProc, me32.modBaseAddr, buffer,
                           (size_t)me32.modBaseSize, &bytesread)) {
        free(buffer);
        CloseHandle(hProc);
        return ret;
    }

    CloseHandle(hProc);

    /* PE Realignment */
    align_pe(buffer, me32.modBaseSize);

    hFile = CreateFile(filename, GENERIC_READ | GENERIC_WRITE, 0, NULL,
                       CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        logg(LOGG_INFO, "Error creating %s\n", filename);
        free(buffer);
        return ret;
    }

    if (WriteFile(hFile, buffer, (DWORD)bytesread, &byteswrite, NULL))
        ret = _open_osfhandle((intptr_t)hFile, O_RDONLY | O_BINARY);
    free(buffer);
    return ret;
}

int scanfile(const char *filename, scanmem_data *scan_data, struct mem_info *info)
{
    int fd;
    int scantype;
    int ret             = CL_CLEAN;
    const char *virname = NULL;

    logg(LOGG_DEBUG, "Scanning %s\n", filename);

    if ((fd = safe_open(filename, O_RDONLY | O_BINARY)) == -1) {
        logg(LOGG_WARNING, "Can't open file %s, %s\n", filename, strerror(errno));
        return -1;
    }

    if (info->d) { // clamdscan
        if (optget(info->opts, "stream")->enabled)
            scantype = STREAM;
        else if (optget(info->opts, "multiscan")->enabled)
            scantype = MULTI;
        else if (optget(info->opts, "allmatch")->enabled)
            scantype = ALLMATCH;
        else
            scantype = CONT;

        if ((sock = dconnect(clamdopts)) < 0) {
            info->errors++;
            return -1;
        }
        if (dsresult(sock, scantype, filename, NULL, &info->errors, clamdopts) > 0) {
            info->ifiles++;
            ret = CL_VIRUS;
        }
    } else { // clamscan
        ret = cl_scandesc(fd, filename, &virname, &info->blocks, info->engine, info->options);
        if (ret == CL_VIRUS) {
            logg(LOGG_INFO, "%s: %s FOUND\n", filename, virname);
            info->ifiles++;
        } else if (scan_data->printclean) {
            logg(LOGG_INFO, "%s: OK    \n", filename);
        }
    }

    close(fd);
    return ret;
}

int scanmem_cb(PROCESSENTRY32 ProcStruct, MODULEENTRY32 me32, void *data, struct mem_info *info)
{
    scanmem_data *scan_data     = data;
    int rc                      = 0;
    int isprocess               = 0;
    char modulename[MAX_PATH]   = "";
    char expandmodule[MAX_PATH] = "";

    if (!scan_data)
        return 0;
    scan_data->res = CL_CLEAN;

    modulename[0] = 0;
    /* Special case, btw why I get \SystemRoot\ in process szExePath?
     There are also other cases? */
    if ((strlen(me32.szExePath) > 12) &&
        !strncmp(me32.szExePath, "\\SystemRoot\\", 12)) {
        expandmodule[0] = 0;
        strncat(expandmodule, me32.szExePath, MAX_PATH - 1 - strlen(expandmodule));
        expandmodule[MAX_PATH - 1] = 0;
        snprintf(expandmodule, MAX_PATH - 1, "%%SystemRoot%%\\%s",
                 &me32.szExePath[12]);
        expandmodule[MAX_PATH - 1] = 0;
        ExpandEnvironmentStrings(expandmodule, modulename, MAX_PATH - 1);
        modulename[MAX_PATH - 1] = 0;
    }

    if (!modulename[0]) {
        strncpy(modulename, me32.szExePath, MAX_PATH - 1);
        modulename[MAX_PATH - 1] = 0;
    }

    scan_data->res = lookup_cache(&scan_data->files, modulename);
    isprocess      = !_stricmp(ProcStruct.szExeFile, modulename) ||
                !_stricmp(ProcStruct.szExeFile, me32.szModule);

    if (scan_data->res == -1) {
        if (isprocess)
            scan_data->processes++;
        else
            scan_data->modules++;

        info->files++;

        /* check for module exclusion */
        scan_data->res = CL_CLEAN;
        if (!(scan_data->exclude && chkpath(modulename, clamdopts)))
            scan_data->res = scanfile(modulename, scan_data, info);

        if ((scan_data->res != CL_VIRUS) && is_packed(modulename)) {
            char *dumped = cli_gentemp(NULL);
            int fd       = -1;
            if ((fd = dump_pe(dumped, ProcStruct, me32)) > 0) {
                close(fd);
                scan_data->res = scanfile(dumped, scan_data, info);
                DeleteFile(dumped);
            }
            free(dumped);
        }
        insert_cache(&scan_data->files, modulename, scan_data->res);
    }

    if (scan_data->res == CL_VIRUS) {
        if (isprocess && scan_data->kill) {
            logg(LOGG_INFO, "Unloading program %s from memory\n", modulename);
            rc = kill_process(ProcStruct.th32ProcessID);
        } else if (scan_data->unload) {
            logg(LOGG_INFO, "Unloading module %s from %s\n", me32.szModule, modulename);
            if ((rc = unload_module(ProcStruct.th32ProcessID, me32.hModule)) == -1)
                /* CreateProcessThread() is not implemented */
                return 0;
        }

        if (action)
            action(modulename);
        return rc;
    }
    return rc;
}

int scanmem(struct mem_info *info)
{
    scanmem_data data;
    data.files      = NULL;
    data.printclean = 1;
    data.kill       = 0;
    data.unload     = 0;
    data.exclude    = 0;
    data.res        = CL_CLEAN;
    data.processes  = 0;
    data.modules    = 0;

    HMODULE psapi_ok = LoadLibrary("psapi.dll");
    HMODULE k32_ok   = LoadLibrary("kernel32.dll");

    if (!(psapi_ok || k32_ok)) {
        logg(LOGG_INFO, " *** Memory Scanning is not supported on this OS ***\n\n");
        return -1;
    }

    if (optget(info->opts, "infected")->enabled)
        data.printclean = 0;
    if (optget(info->opts, "kill")->enabled)
        data.kill = 1;
    if (optget(info->opts, "unload")->enabled)
        data.unload = 1;
    if (optget(info->opts, "exclude")->enabled)
        data.exclude = 1;

    if (info->d) {
        if ((sock = dconnect(clamdopts)) < 0) {
            info->errors++;
            return -1;
        }
    }

    logg(LOGG_INFO, " *** Scanning Programs in Computer Memory ***\n");

    if (!EnablePrivilege(SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED))
        logg(LOGG_INFO, "---Please login as an Administrator to scan System processes loaded "
                        "in computer memory---\n");

    if (k32_ok)
        walkmodules_th(scanmem_cb, (void *)&data, info);
    else
        walkmodules_psapi(scanmem_cb, (void *)&data, info);
    free_cache(&data.files);

    logg(LOGG_INFO, "\n *** Scanned %lu processes - %lu modules ***\n", data.processes,
         data.modules);
    logg(LOGG_INFO, " *** Computer Memory Scan Completed ***\n\n");
    return data.res;
}
