/* vim: set ts=4 sw=4 noet: */
/*
	pev - the PE file analyzer toolkit

	pesec.c - Checks for security features in PE files.

	Copyright (C) 2012 - 2020 pev authors

	This program 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 2 of the License, or
	(at your option) any later version.

	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, see <http://www.gnu.org/licenses/>.

	In addition, as a special exception, the copyright holders give
	permission to link the code of portions of this program with the
	OpenSSL library under certain conditions as described in each
	individual source file, and distribute linked combinations
	including the two.
	
	You must obey the GNU General Public License in all respects
	for all of the code used other than OpenSSL.  If you modify
	file(s) with this exception, you may extend this exception to your
	version of the file(s), but you are not obligated to do so.  If you
	do not wish to do so, delete this exception statement from your
	version.  If you delete this exception statement from all source
	files in the program, then also delete it here.
*/

#include "common.h"
#include <string.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include "compat/strlcat.h"
#include "plugins.h"

#define PROGRAM "pesec"

typedef enum {
	CERT_FORMAT_TEXT = 1,
	CERT_FORMAT_PEM = 2,
	CERT_FORMAT_DER = 3
} cert_format_e;

typedef struct {
	cert_format_e certoutform;
	BIO *certout;
} options_t;

static void usage(void)
{
	static char formats[255];
	output_available_formats(formats, sizeof(formats), '|');
	printf("Usage: %s [OPTIONS] FILE\n"
		"Check for security features in PE files\n"
		"\nExample: %s wordpad.exe\n"
		"\nOptions:\n"
		" -f, --format <%s>  Change output format (default: text)\n"
		" -c, --certoutform <text|pem>			 Specifies the certificate output format (default: text).\n"
		" -o, --certout <filename>				 Specifies the output filename to write certificates to (default: stdout).\n"
		" -V, --version							 Show version.\n"
		" --help								 Show this help.\n",
		PROGRAM, PROGRAM, formats);
}

static cert_format_e parse_certoutform(const char *optarg)
{
	cert_format_e result;

	if (strcmp(optarg, "text") == 0)
		result = CERT_FORMAT_TEXT;
	else if (strcmp(optarg, "pem") == 0)
		result = CERT_FORMAT_PEM;
	else if (strcmp(optarg, "der") == 0)
		result = CERT_FORMAT_DER;
	else
		EXIT_ERROR("invalid cert_format option");

	return result;
}

static BIO *parse_certout(const char *optarg)
{
	BIO *bio = BIO_new(BIO_s_file());
	if (bio == NULL) {
		EXIT_ERROR("could not allocate BIO");
	}

	if (strcmp(optarg, "stdout") == 0) {
		BIO_set_fp(bio, stdout, BIO_NOCLOSE);
	} else if (strcmp(optarg, "stderr") == 0) {
		BIO_set_fp(bio, stderr, BIO_NOCLOSE);
	} else {
		int ret = BIO_write_filename(bio, (char *)optarg);
		if (ret == 0) {
			BIO_free(bio);
			EXIT_ERROR("failed to open file");
		}
	}

	return bio;
}

static void free_options(options_t *options)
{
	if (options && options->certout)
		BIO_free(options->certout);

	free(options);
}

static options_t *parse_options(int argc, char *argv[])
{
	options_t *options = calloc_s(1, sizeof(options_t));

	/* Parameters for getopt_long() function */
	static const char short_options[] = "f:c:o:V";

	static const struct option long_options[] = {
		{ "format",			required_argument,	NULL,	'f' },
		{ "certoutform",	required_argument,	NULL,	'c' },
		{ "certout",		required_argument,	NULL,	'o' },
		{ "help",			no_argument,		NULL,	 1	},
		{ "version",		no_argument,		NULL,	'V' },
		{ NULL,				0,					NULL,	 0	}
	};

	int c, ind;

	while ((c = getopt_long(argc, argv, short_options, long_options, &ind)))
	{
		if (c < 0)
			break;

		switch (c)
		{
			case 1: // --help option
				usage();
				exit(EXIT_SUCCESS);
			case 'f':
				if (output_set_format_by_name(optarg) < 0)
					EXIT_ERROR("invalid format option");
				break;
			case 'v':
				printf("%s %s\n%s\n", PROGRAM, TOOLKIT, COPY);
				exit(EXIT_SUCCESS);
			case 'c':
				options->certoutform = parse_certoutform(optarg);
				break;
			case 'o':
				options->certout = parse_certout(optarg);
				break;
			case 'V':
				printf("%s %s\n%s\n", PROGRAM, TOOLKIT, COPY);
				exit(EXIT_SUCCESS);
			default:
				fprintf(stderr, "%s: try '--help' for more information\n", PROGRAM);
				exit(EXIT_FAILURE);
		}
	}

	return options;
}

/*
find stack cookies, a.k.a canary, buffer security check
option in MVS 2010
*/
// FIXME: What about other versions?
static bool stack_cookies(pe_ctx_t *ctx)
{
	static const unsigned char mvs2010[] = {
		0x55, 0x8b, 0xec, 0x83,
		0x33, 0xc5, 0x33, 0xcd,
		0xe8, 0xc3
	};

	if (ctx == NULL)
		return false;

	size_t found = 0;
	const uint8_t *file_bytes = LIBPE_PTR_ADD(ctx->map_addr, 0);
	const uint64_t filesize = pe_filesize(ctx);

	// FIXME: Is this right?! Seems like partial matches will be
	//		  Accumulated. Example: If all these bytes are found,
	//		  separatelly in the file, this function will return true.
	for (uint64_t ofs=0; ofs < filesize; ofs++) {
		for (size_t i=0; i < sizeof(mvs2010); i++) {
			if (file_bytes[ofs] == mvs2010[i] && found == i)
				found++;
		}
	}

	return found == sizeof(mvs2010);
}

static void print_certificate(BIO *out, cert_format_e format, X509 *cert)
{
	if (out == NULL)
		return;
	switch (format) {
		default:
		case CERT_FORMAT_TEXT:
			X509_print(out, cert);
			break;
		case CERT_FORMAT_PEM:
			PEM_write_bio_X509(out, cert);
			break;
		case CERT_FORMAT_DER:
			LIBPE_WARNING("DER format is not yet supported for output");
			break;
	}
}

static unsigned int roundBy8( unsigned int n )
{
  unsigned int t = n & ~7U;
  if ( n & 7U ) t += 8;
  return t;
}

static int parse_pkcs7_data(const options_t *options, const CRYPT_DATA_BLOB *blob)
{
	int result = 0;
	const cert_format_e input_fmt = CERT_FORMAT_DER;
	PKCS7 *p7 = NULL;	/* Need to be initialized! */
	BIO *in;

#if OPENSSL_VERSION_NUMBER < 0x10100000L
	CRYPTO_malloc_init();
#endif
	ERR_load_crypto_strings();
	OpenSSL_add_all_algorithms();

	in = BIO_new_mem_buf(blob->pbData, blob->cbData);
	if (in == NULL) {
		result = -2;
		goto error;
	}

	// FIXME: input_fmt never changed!
	switch (input_fmt) {
		default:
			LIBPE_WARNING("unhandled input format for certificate");
			break;
		case CERT_FORMAT_DER:
			p7 = d2i_PKCS7_bio(in, NULL);
			break;
		case CERT_FORMAT_PEM:
			p7 = PEM_read_bio_PKCS7(in, NULL, NULL, NULL);
			break;
	}

	if (p7 == NULL) {
		ERR_print_errors_fp(stderr);
		result = -3;
		goto error;
	}

	STACK_OF(X509) *certs = NULL;

	int type = OBJ_obj2nid(p7->type);
	switch (type) {
		default: break;
		case NID_pkcs7_signed: // PKCS7_type_is_signed(p7)
			certs = p7->d.sign->cert;
			break;
		case NID_pkcs7_signedAndEnveloped: // PKCS7_type_is_signedAndEnveloped(p7)
			certs = p7->d.signed_and_enveloped->cert;
			break;
	}

	const int numcerts = certs != NULL ? sk_X509_num(certs) : 0;
	for (int i = 0; i < numcerts; i++) {
		X509 *cert = sk_X509_value(certs, i);
		print_certificate(options->certout, options->certoutform, cert);
		// NOTE: Calling X509_free(cert) is unnecessary.
	}

	// Print whether certificate signature is valid
	if (numcerts > 0) {
		X509 *subject = sk_X509_value(certs, 0);
		X509 *issuer = sk_X509_value(certs, numcerts - 1);
		EVP_PKEY *issuer_pubkey = X509_get_pubkey(issuer);
		int valid_sig = X509_verify(subject, issuer_pubkey);
		EVP_PKEY_free(issuer_pubkey);
		output("Signature", valid_sig == 1 ? "valid" : "invalid");
	}

	// Print signers
	if (numcerts > 0) {
		output_open_scope("signers", OUTPUT_SCOPE_TYPE_ARRAY);
		for (int i = 0; i < numcerts; i++) {
			X509 *cert = sk_X509_value(certs, i);
			X509_NAME *name = X509_get_subject_name(cert);

			int issuer_name_len = X509_NAME_get_text_by_NID(name, NID_commonName, NULL, 0);
			if (issuer_name_len > 0) {
				output_open_scope("signer", OUTPUT_SCOPE_TYPE_OBJECT);
				char issuer_name[issuer_name_len + 1];
				X509_NAME_get_text_by_NID(name, NID_commonName, issuer_name, issuer_name_len + 1);
				output("Issuer", issuer_name);
				output_close_scope(); // signer
			}
		}
		output_close_scope(); // signers
	}

error:
	if (p7 != NULL)
		PKCS7_free(p7);
	if (in != NULL)
		BIO_free(in);

	// Deallocate everything from OpenSSL_add_all_algorithms
	EVP_cleanup();
	// Deallocate everything from ERR_load_crypto_strings
	ERR_free_strings();

	return result;
}

static void parse_certificates(const options_t *options, pe_ctx_t *ctx)
{
	const IMAGE_DATA_DIRECTORY * const directory = pe_directory_by_entry(ctx, IMAGE_DIRECTORY_ENTRY_SECURITY);
	if (directory == NULL)
		return;

	if (directory->VirtualAddress == 0 || directory->Size == 0)
		return;

	uint32_t fileOffset = directory->VirtualAddress; // This a file pointer rather than a common RVA.

	// TODO(jweyrich): We should count how many certificates the file has, and based on this
	//				   decide whether to proceed and open the certificates scope.
	output_open_scope("certificates", OUTPUT_SCOPE_TYPE_ARRAY);
	while (fileOffset - directory->VirtualAddress < directory->Size)
	{
		// Read the size of this WIN_CERTIFICATE
		uint32_t *dwLength_ptr = LIBPE_PTR_ADD(ctx->map_addr, fileOffset);
		if (!pe_can_read(ctx, dwLength_ptr, sizeof(uint32_t))) {
			output_close_scope(); // certificates
			// TODO: Should we report something?
			return;
		}
		// Type punning
		uint32_t dwLength = *(uint32_t *)dwLength_ptr;

		WIN_CERTIFICATE *cert = LIBPE_PTR_ADD(ctx->map_addr, fileOffset);
		if (!pe_can_read(ctx, cert, dwLength)) {
			output_close_scope(); // certificates
			// TODO: Should we report something?
			return;
		}

		output_open_scope("certificate", OUTPUT_SCOPE_TYPE_OBJECT);

		static char value[MAX_MSG];

		snprintf(value, MAX_MSG, "%u bytes", cert->dwLength);
		output("Length", value);

		snprintf(value, MAX_MSG, "0x%x (%s)", cert->wRevision,
			cert->wRevision == WIN_CERT_REVISION_1_0 ? "1" :
			cert->wRevision == WIN_CERT_REVISION_2_0 ? "2" : "unknown");
		output("Revision", value);

		snprintf(value, MAX_MSG, "0x%x", cert->wCertificateType);
		switch (cert->wCertificateType) {
			default: bsd_strlcat(value, " (UNKNOWN)", MAX_MSG); break;
			case WIN_CERT_TYPE_X509: bsd_strlcat(value, " (X509)", MAX_MSG); break;
			case WIN_CERT_TYPE_PKCS_SIGNED_DATA: bsd_strlcat(value, " (PKCS_SIGNED_DATA)", MAX_MSG); break;
			case WIN_CERT_TYPE_TS_STACK_SIGNED: bsd_strlcat(value, " (TS_STACK_SIGNED)", MAX_MSG); break;
		}
		output("Type", value);

		fileOffset += roundBy8(cert->dwLength); // Offset to the next certificate.

		if (fileOffset - directory->VirtualAddress > directory->Size) {
			LIBPE_WARNING("either the attribute certificate table or the Size field is corrupted");
			output_close_scope(); // certificate
			break; // Exit the while-loop.
		}

		switch (cert->wRevision) {
			default:
				LIBPE_WARNING("unknown wRevision");
				break;
			case WIN_CERT_REVISION_1_0:
				LIBPE_WARNING("WIN_CERT_REVISION_1_0 is not supported");
				break;
			case WIN_CERT_REVISION_2_0:
				break;
		}

		switch (cert->wCertificateType) {
			default:
				LIBPE_WARNING("unknown wCertificateType");
				break;
			case WIN_CERT_TYPE_X509:
				LIBPE_WARNING("WIN_CERT_TYPE_X509 is not supported");
				break;
			case WIN_CERT_TYPE_PKCS_SIGNED_DATA:
			{
				CRYPT_DATA_BLOB p7data;
				p7data.cbData = cert->dwLength - offsetof(WIN_CERTIFICATE, bCertificate);
				p7data.pbData = cert->bCertificate;
				parse_pkcs7_data(options, &p7data);
				break;
			}
			case WIN_CERT_TYPE_TS_STACK_SIGNED:
				LIBPE_WARNING("WIN_CERT_TYPE_TS_STACK_SIGNED is not supported");
				break;
			case WIN_CERT_TYPE_EFI_PKCS115:
				LIBPE_WARNING("WIN_CERT_TYPE_EFI_PKCS115 is not supported");
				break;
			case WIN_CERT_TYPE_EFI_GUID:
				LIBPE_WARNING("WIN_CERT_TYPE_EFI_GUID is not supported");
				break;
		}
		output_close_scope(); // certificate
	}
	output_close_scope(); // certificates
}

int main(int argc, char *argv[])
{
	pev_config_t config;
	PEV_INITIALIZE(&config);

	if (argc < 2) {
		usage();
		exit(EXIT_FAILURE);
	}

	output_set_cmdline(argc, argv);

	options_t *options = parse_options(argc, argv); // opcoes

	const char *path = argv[argc-1];
	pe_ctx_t ctx;

	pe_err_e err = pe_load_file(&ctx, path);
	if (err != LIBPE_E_OK) {
		pe_error_print(stderr, err);
		return EXIT_FAILURE;
	}

	err = pe_parse(&ctx);
	if (err != LIBPE_E_OK) {
		pe_error_print(stderr, err);
		return EXIT_FAILURE;
	}

	if (!pe_is_pe(&ctx))
		EXIT_ERROR("not a valid PE file");

	IMAGE_OPTIONAL_HEADER *optional = pe_optional(&ctx);
	if (optional == NULL)
		return EXIT_FAILURE;

	uint16_t dllchar = 0;
	switch (optional->type) {
		default:
			return EXIT_FAILURE;
		case MAGIC_ROM:
			break;
		case MAGIC_PE32:
			dllchar = optional->_32->DllCharacteristics;
			break;
		case MAGIC_PE64:
			dllchar = optional->_64->DllCharacteristics;
			break;
	}

	output_open_document();

	static char field[MAX_MSG];

	// aslr
	snprintf(field, MAX_MSG, "ASLR");
	output(field, (dllchar & 0x40) ? "yes" : "no");

	// dep/nx
	snprintf(field, MAX_MSG, "DEP/NX");
	output(field, (dllchar & 0x100) ? "yes" : "no");

	// seh
	snprintf(field, MAX_MSG, "SEH");
	output(field, (dllchar & 0x400) ? "no" : "yes");

	// cfg
	snprintf(field, MAX_MSG, "CFG");
	output(field, (dllchar & 0x4000) ? "yes" : "no");

	// stack cookies
	snprintf(field, MAX_MSG, "Stack cookies (EXPERIMENTAL)");
	output(field, stack_cookies(&ctx) ? "yes" : "no");

	// certificados
	parse_certificates(options, &ctx);

	output_close_document();

	// libera a memoria
	free_options(options);

	// free
	err = pe_unload(&ctx);
	if (err != LIBPE_E_OK) {
		pe_error_print(stderr, err);
		return EXIT_FAILURE;
	}

	PEV_FINALIZE(&config);

	return EXIT_SUCCESS;
}
