/*
 * KProcessHacker
 *
 * Copyright (C) 2016 wj32
 *
 * This file is part of Process Hacker.
 *
 * Process Hacker is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Process Hacker 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 Process Hacker.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <kph.h>

#define FILE_BUFFER_SIZE (2 * PAGE_SIZE)
#define FILE_MAX_SIZE (32 * 1024 * 1024) // 32 MB

VOID KphpBackoffKey(
    __in PKPH_CLIENT Client
    );

static UCHAR KphpTrustedPublicKey[] =
{
    0x45, 0x43, 0x53, 0x31, 0x20, 0x00, 0x00, 0x00, 0x5f, 0xe8, 0xab, 0xac, 0x01, 0xad, 0x6b, 0x48,
    0xfd, 0x84, 0x7f, 0x43, 0x70, 0xb6, 0x57, 0xb0, 0x76, 0xe3, 0x10, 0x07, 0x19, 0xbd, 0x0e, 0xd4,
    0x10, 0x5c, 0x1f, 0xfc, 0x40, 0x91, 0xb6, 0xed, 0x94, 0x37, 0x76, 0xb7, 0x86, 0x88, 0xf7, 0x34,
    0x12, 0x91, 0xf6, 0x65, 0x23, 0x58, 0xc9, 0xeb, 0x2f, 0xcb, 0x96, 0x13, 0x8f, 0xca, 0x57, 0x7a,
    0xd0, 0x7a, 0xbf, 0x22, 0xde, 0xd2, 0x15, 0xfc
};

#ifdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, KphHashFile)
#pragma alloc_text(PAGE, KphVerifyFile)
#pragma alloc_text(PAGE, KphVerifyClient)
#pragma alloc_text(PAGE, KpiVerifyClient)
#pragma alloc_text(PAGE, KphGenerateKeysClient)
#pragma alloc_text(PAGE, KphRetrieveKeyViaApc)
#pragma alloc_text(PAGE, KphValidateKey)
#pragma alloc_text(PAGE, KphpBackoffKey)
#endif

NTSTATUS KphHashFile(
    __in PUNICODE_STRING FileName,
    __out PVOID *Hash,
    __out PULONG HashSize
    )
{
    NTSTATUS status;
    BCRYPT_ALG_HANDLE hashAlgHandle = NULL;
    ULONG querySize;
    ULONG hashObjectSize;
    ULONG hashSize;
    PVOID hashObject = NULL;
    PVOID hash = NULL;
    BCRYPT_HASH_HANDLE hashHandle = NULL;
    OBJECT_ATTRIBUTES objectAttributes;
    IO_STATUS_BLOCK iosb;
    HANDLE fileHandle = NULL;
    FILE_STANDARD_INFORMATION standardInfo;
    ULONG remainingBytes;
    ULONG bytesToRead;
    PVOID buffer = NULL;

    PAGED_CODE();

    // Open the hash algorithm and allocate memory for the hash object.

    if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider(&hashAlgHandle, KPH_HASH_ALGORITHM, NULL, 0)))
        goto CleanupExit;
    if (!NT_SUCCESS(status = BCryptGetProperty(hashAlgHandle, BCRYPT_OBJECT_LENGTH,
        (PUCHAR)&hashObjectSize, sizeof(ULONG), &querySize, 0)))
    {
        goto CleanupExit;
    }
    if (!NT_SUCCESS(status = BCryptGetProperty(hashAlgHandle, BCRYPT_HASH_LENGTH, (PUCHAR)&hashSize,
        sizeof(ULONG), &querySize, 0)))
    {
        goto CleanupExit;
    }

    if (!(hashObject = ExAllocatePoolWithTag(PagedPool, hashObjectSize, 'vhpK')))
    {
        status = STATUS_INSUFFICIENT_RESOURCES;
        goto CleanupExit;
    }
    if (!(hash = ExAllocatePoolWithTag(PagedPool, hashSize, 'vhpK')))
    {
        status = STATUS_INSUFFICIENT_RESOURCES;
        goto CleanupExit;
    }

    if (!NT_SUCCESS(status = BCryptCreateHash(hashAlgHandle, &hashHandle, hashObject, hashObjectSize,
        NULL, 0, 0)))
    {
        goto CleanupExit;
    }

    // Open the file and compute the hash.

    InitializeObjectAttributes(&objectAttributes, FileName, OBJ_KERNEL_HANDLE, NULL, NULL);

    if (!NT_SUCCESS(status = ZwCreateFile(&fileHandle, FILE_GENERIC_READ, &objectAttributes,
        &iosb, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN,
        FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0)))
    {
        goto CleanupExit;
    }

    if (!NT_SUCCESS(status = ZwQueryInformationFile(fileHandle, &iosb, &standardInfo,
        sizeof(FILE_STANDARD_INFORMATION), FileStandardInformation)))
    {
        goto CleanupExit;
    }

    if (standardInfo.EndOfFile.QuadPart <= 0)
    {
        status = STATUS_UNSUCCESSFUL;
        goto CleanupExit;
    }
    if (standardInfo.EndOfFile.QuadPart > FILE_MAX_SIZE)
    {
        status = STATUS_FILE_TOO_LARGE;
        goto CleanupExit;
    }

    if (!(buffer = ExAllocatePoolWithTag(PagedPool, FILE_BUFFER_SIZE, 'vhpK')))
    {
        status = STATUS_INSUFFICIENT_RESOURCES;
        goto CleanupExit;
    }

    remainingBytes = (ULONG)standardInfo.EndOfFile.QuadPart;

    while (remainingBytes != 0)
    {
        bytesToRead = FILE_BUFFER_SIZE;
        if (bytesToRead > remainingBytes)
            bytesToRead = remainingBytes;

        if (!NT_SUCCESS(status = ZwReadFile(fileHandle, NULL, NULL, NULL, &iosb, buffer, bytesToRead,
            NULL, NULL)))
        {
            goto CleanupExit;
        }
        if ((ULONG)iosb.Information != bytesToRead)
        {
            status = STATUS_INTERNAL_ERROR;
            goto CleanupExit;
        }

        if (!NT_SUCCESS(status = BCryptHashData(hashHandle, buffer, bytesToRead, 0)))
            goto CleanupExit;

        remainingBytes -= bytesToRead;
    }

    if (!NT_SUCCESS(status = BCryptFinishHash(hashHandle, hash, hashSize, 0)))
        goto CleanupExit;

    if (NT_SUCCESS(status))
    {
        *Hash = hash;
        *HashSize = hashSize;

        hash = NULL; // Don't free this in the cleanup section
    }

CleanupExit:
    if (buffer)
        ExFreePoolWithTag(buffer, 'vhpK');
    if (fileHandle)
        ZwClose(fileHandle);
    if (hashHandle)
        BCryptDestroyHash(hashHandle);
    if (hash)
        ExFreePoolWithTag(hash, 'vhpK');
    if (hashObject)
        ExFreePoolWithTag(hashObject, 'vhpK');
    if (hashAlgHandle)
        BCryptCloseAlgorithmProvider(hashAlgHandle, 0);

    return status;
}

NTSTATUS KphVerifyFile(
    __in PUNICODE_STRING FileName,
    __in_bcount(SignatureSize) PUCHAR Signature,
    __in ULONG SignatureSize
    )
{
    NTSTATUS status;
    BCRYPT_ALG_HANDLE signAlgHandle = NULL;
    BCRYPT_KEY_HANDLE keyHandle = NULL;
    PVOID hash = NULL;
    ULONG hashSize;

    PAGED_CODE();

    // Import the trusted public key.

    if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider(&signAlgHandle, KPH_SIGN_ALGORITHM, NULL, 0)))
        goto CleanupExit;
    if (!NT_SUCCESS(status = BCryptImportKeyPair(signAlgHandle, NULL, KPH_BLOB_PUBLIC, &keyHandle,
        KphpTrustedPublicKey, sizeof(KphpTrustedPublicKey), 0)))
    {
        goto CleanupExit;
    }

    // Hash the file.

    if (!NT_SUCCESS(status = KphHashFile(FileName, &hash, &hashSize)))
        goto CleanupExit;

    // Verify the hash.

    if (!NT_SUCCESS(status = BCryptVerifySignature(keyHandle, NULL, hash, hashSize, Signature,
        SignatureSize, 0)))
    {
        goto CleanupExit;
    }

CleanupExit:
    if (hash)
        ExFreePoolWithTag(hash, 'vhpK');
    if (keyHandle)
        BCryptDestroyKey(keyHandle);
    if (signAlgHandle)
        BCryptCloseAlgorithmProvider(signAlgHandle, 0);

    return status;
}

VOID KphVerifyClient(
    __inout PKPH_CLIENT Client,
    __in PVOID CodeAddress,
    __in_bcount(SignatureSize) PUCHAR Signature,
    __in ULONG SignatureSize
    )
{
    NTSTATUS status;
    PUNICODE_STRING processFileName = NULL;
    MEMORY_BASIC_INFORMATION memoryBasicInfo;
    PUNICODE_STRING mappedFileName = NULL;

    PAGED_CODE();

    if (Client->VerificationPerformed)
        return;

    if ((ULONG_PTR)CodeAddress > (ULONG_PTR)MmHighestUserAddress)
    {
        status = STATUS_ACCESS_VIOLATION;
        goto CleanupExit;
    }
    if (!NT_SUCCESS(status = SeLocateProcessImageName(PsGetCurrentProcess(), &processFileName)))
        goto CleanupExit;
    if (!NT_SUCCESS(status = ZwQueryVirtualMemory(NtCurrentProcess(), CodeAddress,
        MemoryBasicInformation, &memoryBasicInfo, sizeof(MEMORY_BASIC_INFORMATION), NULL)))
    {
        goto CleanupExit;
    }
    if (memoryBasicInfo.Type != MEM_IMAGE || memoryBasicInfo.State != MEM_COMMIT)
    {
        status = STATUS_INVALID_PARAMETER;
        goto CleanupExit;
    }
    if (!NT_SUCCESS(status = KphGetProcessMappedFileName(NtCurrentProcess(), CodeAddress,
        &mappedFileName)))
    {
        goto CleanupExit;
    }
    if (!RtlEqualUnicodeString(processFileName, mappedFileName, TRUE))
    {
        status = STATUS_INVALID_PARAMETER;
        goto CleanupExit;
    }

    status = KphVerifyFile(processFileName, Signature, SignatureSize);

CleanupExit:
    if (mappedFileName)
        ExFreePoolWithTag(mappedFileName, 'ThpK');
    if (processFileName)
        ExFreePool(processFileName);

    ExAcquireFastMutex(&Client->StateMutex);

    if (NT_SUCCESS(status))
    {
        Client->VerifiedProcess = PsGetCurrentProcess();
        Client->VerifiedProcessId = PsGetCurrentProcessId();
        Client->VerifiedRangeBase = memoryBasicInfo.BaseAddress;
        Client->VerifiedRangeSize = memoryBasicInfo.RegionSize;
    }

    Client->VerificationStatus = status;
    MemoryBarrier();
    Client->VerificationSucceeded = NT_SUCCESS(status);
    Client->VerificationPerformed = TRUE;

    ExReleaseFastMutex(&Client->StateMutex);
}

NTSTATUS KpiVerifyClient(
    __in PVOID CodeAddress,
    __in_bcount(SignatureSize) PUCHAR Signature,
    __in ULONG SignatureSize,
    __in PKPH_CLIENT Client
    )
{
    PUCHAR signature;

    PAGED_CODE();

    __try
    {
        ProbeForRead(Signature, SignatureSize, 1);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        return GetExceptionCode();
    }

    if (SignatureSize > KPH_SIGNATURE_MAX_SIZE)
        return STATUS_INVALID_PARAMETER_3;

    signature = ExAllocatePoolWithTag(PagedPool, SignatureSize, 'ThpK');
    if (!signature)
        return STATUS_INSUFFICIENT_RESOURCES;

    __try
    {
        memcpy(signature, Signature, SignatureSize);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        ExFreePoolWithTag(signature, 'ThpK');
        return GetExceptionCode();
    }

    KphVerifyClient(Client, CodeAddress, Signature, SignatureSize);
    ExFreePoolWithTag(signature, 'ThpK');

    return Client->VerificationStatus;
}

VOID KphGenerateKeysClient(
    __inout PKPH_CLIENT Client
    )
{
    ULONGLONG interruptTime;
    ULONG seed;
    KPH_KEY l1Key;
    KPH_KEY l2Key;

    PAGED_CODE();

    if (Client->KeysGenerated)
        return;

    interruptTime = KeQueryInterruptTime();
    seed = (ULONG)(interruptTime >> 32) | (ULONG)interruptTime | PtrToUlong(Client);
    l1Key = RtlRandomEx(&seed) | 0x80000000; // Make sure the key is nonzero
    do
    {
        l2Key = RtlRandomEx(&seed) | 0x80000000;
    } while (l2Key == l1Key);

    ExAcquireFastMutex(&Client->StateMutex);

    if (!Client->KeysGenerated)
    {
        Client->L1Key = l1Key;
        Client->L2Key = l2Key;
        MemoryBarrier();
        Client->KeysGenerated = TRUE;
    }

    ExReleaseFastMutex(&Client->StateMutex);
}

NTSTATUS KphRetrieveKeyViaApc(
    __inout PKPH_CLIENT Client,
    __in KPH_KEY_LEVEL KeyLevel,
    __inout PIRP Irp
    )
{
    PIO_APC_ROUTINE userApcRoutine;
    KPH_KEY key;

    PAGED_CODE();

    if (!Client->VerificationSucceeded)
        return STATUS_ACCESS_DENIED;

    MemoryBarrier();

    if (PsGetCurrentProcess() != Client->VerifiedProcess ||
        PsGetCurrentProcessId() != Client->VerifiedProcessId)
    {
        return STATUS_ACCESS_DENIED;
    }

    userApcRoutine = Irp->Overlay.AsynchronousParameters.UserApcRoutine;

    if (!userApcRoutine)
        return STATUS_INVALID_PARAMETER;
    if ((ULONG_PTR)userApcRoutine < (ULONG_PTR)Client->VerifiedRangeBase ||
        (ULONG_PTR)userApcRoutine >= (ULONG_PTR)Client->VerifiedRangeBase + Client->VerifiedRangeSize)
    {
        return STATUS_ACCESS_DENIED;
    }

    KphGenerateKeysClient(Client);

    switch (KeyLevel)
    {
    case KphKeyLevel1:
        key = Client->L1Key;
        break;
    case KphKeyLevel2:
        key = Client->L2Key;
        break;
    default:
        return STATUS_INVALID_PARAMETER;
    }

    Irp->Overlay.AsynchronousParameters.UserApcContext = UlongToPtr(key);

    return STATUS_SUCCESS;
}

NTSTATUS KphValidateKey(
    __in KPH_KEY_LEVEL RequiredKeyLevel,
    __in_opt KPH_KEY Key,
    __in PKPH_CLIENT Client,
    __in KPROCESSOR_MODE AccessMode
    )
{
    PAGED_CODE();

    if (AccessMode == KernelMode)
        return STATUS_SUCCESS;

    if (Key && Client->VerificationSucceeded && Client->KeysGenerated)
    {
        MemoryBarrier();

        switch (RequiredKeyLevel)
        {
        case KphKeyLevel1:
            if (Key == Client->L1Key || Key == Client->L2Key)
                return STATUS_SUCCESS;
            else
                KphpBackoffKey(Client);
            break;
        case KphKeyLevel2:
            if (Key == Client->L2Key)
                return STATUS_SUCCESS;
            else
                KphpBackoffKey(Client);
            break;
        default:
            return STATUS_INVALID_PARAMETER;
        }
    }

    return STATUS_ACCESS_DENIED;
}

VOID KphpBackoffKey(
    __in PKPH_CLIENT Client
    )
{
    LARGE_INTEGER backoffTime;

    PAGED_CODE();

    // Serialize to make it impossible for a single client to bypass the backoff by creating
    // multiple threads.
    ExAcquireFastMutex(&Client->KeyBackoffMutex);

    backoffTime.QuadPart = -KPH_KEY_BACKOFF_TIME;
    KeDelayExecutionThread(KernelMode, FALSE, &backoffTime);

    ExReleaseFastMutex(&Client->KeyBackoffMutex);
}
