#!/usr/bin/env python3
import unittest
import logging
import os
import stat
import sys
import re
import subprocess
import platform
import tempfile
import shutil

from subprocess import Popen

import lief

from unittest import TestCase
from utils import get_sample

LIBADD_C = """\
#include <stdlib.h>
#include <stdio.h>

int add(int a, int b);

int add(int a, int b) {
  printf("%d + %d = %d\\n", a, b, a+b);
  return a + b;
}
"""

BINADD_C = """\
#include <stdio.h>
#include <stdlib.h>

int add(int a, int b);

int main(int argc, char **argv) {
  if (argc != 3) {
    printf("Usage: %s <a> <b>\\n", argv[0]);
    exit(-1);
  }

  int res = add(atoi(argv[1]), atoi(argv[2]));
  printf("From myLIb, a + b = %d\\n", res);
  return 0;
}
"""

class TestHash(TestCase):

    def setUp(self):
        self.logger  = logging.getLogger(__name__)
        self.tmp_dir = tempfile.mkdtemp(suffix='_lief_testhash')
        self.logger.debug("temp dir: {}".format(self.tmp_dir))

        self.binadd_path = os.path.join(self.tmp_dir, "binadd.c")
        self.libadd_path = os.path.join(self.tmp_dir, "libadd.c")

        self.libadd_so  = os.path.join(self.tmp_dir, "libadd.so")
        self.binadd_bin = os.path.join(self.tmp_dir, "binadd.bin")

        self.compiler = shutil.which(os.environ.get('CC', 'cc'))
        if self.compiler is None:
            self.logger.error("Unable to find a compiler")
            sys.exit(0)

        self.logger.debug("Compiler: {}".format(self.compiler))

        with open(self.binadd_path, 'w') as f:
            f.write(BINADD_C)

        with open(self.libadd_path, 'w') as f:
            f.write(LIBADD_C)

    def compile_libadd(self, input, extra_flags=[]):
        if os.path.isfile(self.libadd_so):
            os.remove(self.libadd_so)
        CC_FLAGS = ['-fPIC', '-shared'] + extra_flags
        cmd = [self.compiler, '-o', self.libadd_so] + CC_FLAGS + [input]
        self.logger.debug("Compile 'libadd' with: {}".format(" ".join(cmd)))
        p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        stdout, _ = p.communicate()
        self.logger.debug(stdout)


    def compile_binadd(self, input, extra_flags=[]):

        if os.path.isfile(self.binadd_bin):
            os.remove(self.binadd_bin)

        CC_FLAGS = ['-L', self.tmp_dir] + extra_flags
        cmd = [self.compiler, '-o', self.binadd_bin] + CC_FLAGS + [input, '-ladd']
        self.logger.debug("Compile 'binadd' with: {}".format(" ".join(cmd)))
        p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        stdout, _ = p.communicate()
        self.logger.debug(stdout)

    def obfuscate(self):

        libadd = lief.parse(self.libadd_so)
        binadd = lief.parse(self.binadd_bin)

        libadd_dynsym = libadd.dynamic_symbols
        binadd_dynsym = binadd.dynamic_symbols

        # Change add in the libary
        for sym in libadd_dynsym:
            if sym.name == "add":
                sym.name = "abc"

        # Change "add" in the binary
        for sym in binadd_dynsym:
            if sym.name == "add":
                sym.name = "abc"


        # change library name in the binary
        for entry in binadd.dynamic_entries:
            if entry.tag == lief.ELF.DYNAMIC_TAGS.NEEDED and entry.name == "libadd.so":
                entry.name = "libabc.so"

        libadd_modified = os.path.join(self.tmp_dir, "libabc.so")
        binadd_modified = os.path.join(self.tmp_dir, "binadd_obf.bin")

        libadd.write(libadd_modified);
        binadd.write(binadd_modified)

        st = os.stat(libadd_modified)
        os.chmod(libadd_modified, st.st_mode | stat.S_IEXEC)

        st = os.stat(binadd_modified)
        os.chmod(binadd_modified, st.st_mode | stat.S_IEXEC)

        p = Popen([binadd_modified, '1', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env={"LD_LIBRARY_PATH": self.tmp_dir})
        stdout, _ = p.communicate()

        self.assertIn('From myLIb, a + b = 3', stdout.decode("utf8"))


    @unittest.skipUnless(sys.platform.startswith("linux") and sys.version_info >= (3, 5), "requires Linux")
    def test_gnuhash(self):
        self.compile_libadd(self.libadd_path, ['-Wl,--hash-style=gnu'])
        self.compile_binadd(self.binadd_path, ['-Wl,--hash-style=gnu'])
        self.obfuscate()


    @unittest.skipUnless(sys.platform.startswith("linux") and sys.version_info >= (3, 5), "requires Linux")
    def test_sysv(self):
        self.compile_libadd(self.libadd_path, ['-Wl,--hash-style=sysv'])
        self.compile_binadd(self.binadd_path, ['-Wl,--hash-style=sysv'])
        self.obfuscate()



    def tearDown(self):
        # Delete it
        if os.path.isdir(self.tmp_dir):
            shutil.rmtree(self.tmp_dir)

if __name__ == '__main__':

    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)

    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    root_logger.addHandler(ch)

    unittest.main(verbosity=2)

