| @@ -1,6 +1,6 @@ | |||
| # Python ELMO | |||
| # Python-ELMO | |||
| _Python ELMO_ is a Python library which proposes an encapsulation of the project _ELMO_. | |||
| _Python-ELMO_ is a Python library which proposes an encapsulation of the project _ELMO_. | |||
| [MOW17] **Towards Practical Tools for Side | |||
| Channel Aware Software Engineering : ’Grey Box’ Modelling for Instruction Leakages** | |||
| @@ -9,11 +9,13 @@ https://www.usenix.org/conference/usenixsecurity17/technical-sessions/presentati | |||
| **ELMO GitHub**: https://github.com/sca-research/ELMO | |||
| **Python-ELMO Contributors**: Thibauld Feneuil | |||
| ## Requirements | |||
| To use _Python ELMO_, you need at least Python3.5 and ```numpy```. | |||
| To use _Python-ELMO_, you need at least Python3.5 and ```numpy```. | |||
| The library will install and compile ELMO. So, you need the GCC compiler collection and the command/utility 'make' (for more details, see the documentation of ELMO). On Ubuntu/Debian, | |||
| The library will install and compile ELMO. So, you need the GCC compiler collection and the command/utility ```make``` (for more details, see the documentation of ELMO). On Ubuntu/Debian, | |||
| ```bash | |||
| sudo apt install build-essential | |||
| @@ -23,13 +25,13 @@ To use ELMO on a leaking binary program, you need to compile the C implementatio | |||
| ## Installation | |||
| First, download _Python ELMO_. | |||
| First, download _Python-ELMO_. | |||
| ```bash | |||
| git clone https://git.aprilas.fr/tfeneuil/python-elmo | |||
| ``` | |||
| And then, install ELMO thanks to the script of installation. | |||
| And then, install ELMO thanks to the installation script. It will use Internet to download the [ELMO project](https://github.com/sca-research/ELMO). | |||
| ```bash | |||
| python setup.py install | |||
| @@ -47,7 +49,7 @@ What is a _simulation project_ ? It is a project to simulate the traces of _one_ | |||
| To start a new project, you can use the following function. | |||
| ```python | |||
| from elmo.manage import create_simulation | |||
| from elmo import create_simulation | |||
| create_simulation( | |||
| 'dilithium', # The (relative) path of the project | |||
| 'DilithiumSimulation' # The classname of the simulation | |||
| @@ -62,32 +64,29 @@ This function will create a repository _dilithium_ with all the complete squelet | |||
| ### List all the available simulation | |||
| ```python | |||
| from elmo.manage import search_simulations | |||
| from elmo import search_simulations | |||
| search_simulations('.') | |||
| ``` | |||
| ```python | |||
| ```text | |||
| {'DilithiumSimulation': <class 'DilithiumSimulation'>, | |||
| 'KyberNTTSimulation': <class 'KyberNTTSimulation'>} | |||
| ``` | |||
| _Python ELMO_ offers a example project to you in the repository _projects/Examples_ of the module. This example is a project to generate traces of the execution of the NTT implemented in the cryptosystem [Kyber](https://pq-crystals.org/kyber/). | |||
| _Python-ELMO_ offers a example project to you in the repository _projects/Examples_ of the module. This example is a project to generate traces of the execution of the NTT implemented in the cryptosystem [Kyber](https://pq-crystals.org/kyber/). | |||
| ### Use a simulation project | |||
| Warning! Before using it, you have to compile your project thanks to the provided Makefile. | |||
| ```python | |||
| from elmo.manage import get_simulation | |||
| KyberNTTSimulation = get_simulation_via_classname('KyberNTTSimulation') | |||
| from elmo import get_simulation | |||
| KyberNTTSimulation = get_simulation('KyberNTTSimulation') | |||
| import numpy as np | |||
| Kyber512 = {'k': 2, 'n': 256} | |||
| challenges = [ | |||
| np.ones((Kyber512['k'], Kyber512['n']), dtype=int), | |||
| ] | |||
| simulation = KyberNTTSimulation() | |||
| challenges = simulation.get_random_challenges(10) | |||
| simulation.set_challenges(challenges) | |||
| simulation = KyberNTTSimulation(challenges) | |||
| simulation.run() # Launch the simulation | |||
| traces = simulation.get_traces() | |||
| # And now, I can draw and analyse the traces | |||
| @@ -95,33 +94,30 @@ traces = simulation.get_traces() | |||
| ### Use a simulation project thanks to a server | |||
| Sometimes, it is impossible to run the simulation thanks the simple method _run_ of the project class. Indeed, sometimes the Python script is executed in the environment where _Python ELMO_ cannot launch the ELMO tool. For example, it is the case where _Python ELMO_ is used in SageMath on Windows. On Windows, SageMath installation relies on the Cygwin POSIX emulation system and it can be a problem. | |||
| Sometimes, it is impossible to run the simulation thanks the simple method ```run``` of the project class. Indeed, sometimes the Python script is executed in the environment where _Python-ELMO_ cannot launch the ELMO tool. For example, it is the case where _Python-ELMO_ is used in SageMath on Windows. On Windows, SageMath installation relies on the Cygwin POSIX emulation system and it can be a problem. | |||
| To offer a solution, _Online ELMO_ can be used thanks to a link client-server. The idea is you must launch the script _run\_server.py_ which will listen (by default) at port 5000 in localhost. | |||
| To offer a solution, _Python-ELMO_ can be used thanks to a client-server link. The idea is you must launch the script ```run_server.py``` which will listen (by default) at port 5000 in localhost. | |||
| ```bash | |||
| python -m elmo run-server | |||
| ``` | |||
| And after, you can manipulate the projects as described in the previous section by replacing _run_ to _run\_online_. | |||
| And after, you can manipulate the projects as described in the previous section by replacing ```run``` to ```run_online```. | |||
| ```python | |||
| from elmo.manage import get_simulation | |||
| KyberNTTSimulation = get_simulation('KyberNTTSimulation') | |||
| import numpy as np | |||
| Kyber512 = {'k': 2, 'n': 256} | |||
| challenges = [ | |||
| np.ones((Kyber512['k'], Kyber512['n']), dtype=int), | |||
| ] | |||
| simulation = KyberNTTSimulation() | |||
| challenges = simulation.get_random_challenges(10) | |||
| simulation.set_challenges(challenges) | |||
| simulation = KyberNTTSimulation(challenges) | |||
| simulation.run_online() # Launch the simulation THANKS TO A SERVER | |||
| traces = simulation.get_traces() | |||
| # And now, I can draw and analyse the traces | |||
| ``` | |||
| Warning! Using the _run\_online_ method doesn't exempt you from compiling the project with the provided Makefile. | |||
| Warning! Using the ```run_online``` method doesn't exempt you from compiling the project with the provided Makefile. | |||
| ### Use the ELMO Engine | |||
| @@ -131,12 +127,12 @@ The engine exploits the model of ELMO to directly give the power consumption of | |||
| - the type of the next assembler instruction | |||
| The type of the instructions are: | |||
| - "_**EOR**_" for ADD(1-4), AND, CMP, CPY, EOR, MOV, ORR, ROR, SUB; | |||
| - "_**LSL**_" for LSL(2), LSR(2); | |||
| - "_**STR**_" for STR, STRB, STRH; | |||
| - "_**LDR**_" for LDR, LDRB, LDRH; | |||
| - "_**MUL**_" for MUL; | |||
| - "_**OTHER**_" for the other instructions. | |||
| - ```EOR``` for ADD(1-4), AND, CMP, CPY, EOR, MOV, ORR, ROR, SUB; | |||
| - ```LSL``` for LSL(2), LSR(2); | |||
| - ```STR``` for STR, STRB, STRH; | |||
| - ```LDR``` for LDR, LDRB, LDRH; | |||
| - ```MUL``` for MUL; | |||
| - ```OTHER``` for the other instructions. | |||
| ```python | |||
| from elmo.engine import ELMOEngine, Instr | |||
| @@ -152,6 +148,10 @@ power = engine.power # Numpy 1D array with an entry for each previous point | |||
| engine.reset_points() # Reset the engine to study other points | |||
| ``` | |||
| ## Limitations | |||
| Since the [ELMO project](https://github.com/sca-research/ELMO) takes its inputs and outputs from files, _Python-ELMO_ **can not** manage simultaneous runs. | |||
| ## Licences | |||
| [MIT](LICENCE.txt) | |||
| @@ -51,39 +51,13 @@ if command == 'create-simulation': | |||
| exit() | |||
| if command == 'run-server': | |||
| from .server.servicethread import ListeningThread | |||
| from .executorthread import ExecutorThread | |||
| def do_main_program(): | |||
| global thread, stop | |||
| thread = ListeningThread('localhost', 5000, ExecutorThread) | |||
| thread.start() | |||
| def program_cleanup(signum, frame): | |||
| global thread, stop | |||
| thread.stop() | |||
| stop = True | |||
| thread = None | |||
| stop = False | |||
| # Execute | |||
| print("Executing...") | |||
| do_main_program() | |||
| print("Done ! And now, listening...") | |||
| import signal | |||
| signal.signal(signal.SIGINT, program_cleanup) | |||
| signal.signal(signal.SIGTERM, program_cleanup) | |||
| from .executor import launch_executor | |||
| host = sys.argv[2] if len(sys.argv) >= 3 else 'localhost' | |||
| port = int(sys.argv[3]) if len(sys.argv) >= 4 else 5000 | |||
| # Wait | |||
| import time | |||
| while not stop: | |||
| time.sleep(1) | |||
| launch_executor(host, port) | |||
| exit() | |||
| print(Color.FAIL + 'Unknown Command.' + Color.ENDC) | |||
| exit() | |||
| @@ -0,0 +1,110 @@ | |||
| import shutil | |||
| import os, re | |||
| import sys | |||
| from .server.servicethread import OneShotServiceThread | |||
| from .config import MODULE_PATH, ELMO_TOOL_REPOSITORY, ELMO_INPUT_FILE_NAME | |||
| from .project_base import SimulationProject | |||
| from .manage import execute_simulation | |||
| from .utils import Color | |||
| class Executor(OneShotServiceThread): | |||
| def execute(self): | |||
| data = self.protocol.get_data() | |||
| self.protocol.please_assert(data) | |||
| elmo_path = os.path.join(MODULE_PATH, ELMO_TOOL_REPOSITORY) | |||
| # Set the input of ELMO | |||
| self.protocol.please_assert('input' in data) | |||
| with open(os.path.join(elmo_path, ELMO_INPUT_FILE_NAME), 'w') as _input_file: | |||
| _input_file.write(data['input']) | |||
| self.protocol.send_ack() | |||
| # Get the binary | |||
| binary_content = self.protocol.get_file() | |||
| binary_path = os.path.join(elmo_path, 'project.bin') | |||
| with open(binary_path, 'wb') as _binary_file: | |||
| _binary_file.write(binary_content) | |||
| self.protocol.send_ack() | |||
| ### Generate the traces by launching ELMO | |||
| print(Color.OKGREEN + ' - Simulation accepted...' + Color.ENDC) | |||
| simulation = SimulationProject() | |||
| simulation.get_binary_path = lambda: os.path.abspath(binary_path) | |||
| data = execute_simulation(simulation) | |||
| if data['error']: | |||
| print(Color.FAIL + ' - Simulation failed.' + Color.ENDC) | |||
| self.protocol.send_data(results) | |||
| self.protocol.close() | |||
| return | |||
| print(Color.OKGREEN + ' - Simulation finished: {} traces, {} instructions'.format( | |||
| data['nb_traces'], | |||
| data['nb_instructions'], | |||
| ) + Color.ENDC) | |||
| output_path = os.path.join(elmo_path, 'output') | |||
| ### Get the trace | |||
| data['results'] = [] | |||
| for i in range(data['nb_traces']): | |||
| filename = os.path.join(output_path, 'traces', 'trace%05d.trc' % (i+1)) | |||
| with open(filename, 'r') as _file: | |||
| data['results'].append( | |||
| list(map(float, _file.readlines())) | |||
| ) | |||
| ### Get asmtrace and printed data | |||
| asmtrace = None | |||
| if ('asmtrace' not in data) or data['asmtrace']: | |||
| with open(os.path.join(output_path, 'asmoutput', 'asmtrace00001.txt'), 'r') as _file: | |||
| data['asmtrace'] = _file.read() | |||
| printed_data = None | |||
| if ('printdata' not in data) or data['printdata']: | |||
| with open(os.path.join(output_path, 'printdata.txt'), 'r') as _file: | |||
| data['printed_data'] = list(map(lambda x: int(x, 16), _file.readlines())) | |||
| ### Send results | |||
| print(Color.OKCYAN + ' - Sending results...' + Color.ENDC, end='') | |||
| sys.stdout.flush() | |||
| self.protocol.send_data(data) | |||
| print(Color.OKGREEN + ' Sent!' + Color.ENDC) | |||
| self.protocol.close() | |||
| def launch_executor(host, port, waiting_function=True): | |||
| from .server.servicethread import ListeningThread | |||
| def do_main_program(): | |||
| thread = ListeningThread(host, port, Executor, debug=True) | |||
| thread.start() | |||
| return thread | |||
| def program_cleanup(signum, frame): | |||
| thread.stop() | |||
| thread = do_main_program() | |||
| import signal | |||
| signal.signal(signal.SIGINT, program_cleanup) | |||
| signal.signal(signal.SIGTERM, program_cleanup) | |||
| if waiting_function is True: | |||
| import time | |||
| while thread.is_running(): | |||
| time.sleep(1) | |||
| return | |||
| return_value = None | |||
| if waiting_function: | |||
| return_value = waiting_function() | |||
| if thread.is_running(): | |||
| program_cleanup(None, None) | |||
| return return_value | |||
| @@ -194,7 +194,9 @@ def execute_simulation(project): | |||
| if not isinstance(project, SimulationProject): | |||
| raise TypeError('The project is not an instance of \'SimulationProject\' class.') | |||
| leaking_binary_path = pjoin(project.get_project_directory(), project.get_binary_path()) | |||
| leaking_binary_path = project.get_binary_path() | |||
| if not os.path.isabs(leaking_binary_path): | |||
| leaking_binary_path = pjoin(project.get_project_directory(), project.get_binary_path()) | |||
| if not os.path.isfile(leaking_binary_path): | |||
| raise BinaryNotFoundError('Binary not found. Did you compile your project?') | |||
| @@ -129,21 +129,28 @@ class SimulationProject: | |||
| 'input': input.get_string(), | |||
| }) | |||
| if not SocketTool.get_ack(s): | |||
| raise RuntimeError("NACK received: The request has been refused !") | |||
| else: | |||
| SocketTool.send_file(s, '{}/{}'.format(self.get_project_directory(), self.get_binary())) | |||
| data = SocketTool.get_data(s) | |||
| if data['error']: | |||
| raise Exception("The simulation returned an error: {}".format(data['error'])) | |||
| raise RuntimeError("NACK received: The request has been refused!") | |||
| SocketTool.send_file(s, '{}/{}'.format(self.get_project_directory(), self.get_binary_path())) | |||
| if not SocketTool.get_ack(s): | |||
| raise RuntimeError("NACK received: The binary file has been refused!") | |||
| data = SocketTool.get_data(s) | |||
| if data['error']: | |||
| raise Exception("The simulation returned an error: {}".format(data['error'])) | |||
| s.close() | |||
| except IOError as err: | |||
| raise RuntimeError("The connection refused. Has the ELMO server been switch on ?") from err | |||
| raise RuntimeError("The connection refused. Has the ELMO server been switch on?") from err | |||
| self.is_executed = True | |||
| self.has_been_online = True | |||
| self._complete_asmtrace = data['asmtrace'] | |||
| self._complete_results = data['results'] | |||
| self._complete_printed_data = data['printed_data'] | |||
| return { key: value | |||
| for key, value in data.items() | |||
| if key not in ['results', 'asmtrace', 'printed_data'] | |||
| } | |||
| ### Manipulate the ASM trace | |||
| def get_asmtrace_filename(self): | |||
| @@ -1,87 +0,0 @@ | |||
| from .server.servicethread import OneShotServiceThread | |||
| import subprocess | |||
| import shutil | |||
| import os, re | |||
| class ExecutorThread(OneShotServiceThread): | |||
| def __init__(self, ip, port, clientsocket, **kwargs): | |||
| super().__init__(ip, port, clientsocket) | |||
| def execute(self): | |||
| data = self.protocol.get_data() | |||
| self.protocol.please_assert(data) | |||
| # Set the input of ELMO | |||
| self.protocol.please_assert('input' in data) | |||
| with open('elmo/input.txt', 'w') as _input_file: | |||
| _input_file.write(data['input']) | |||
| self.protocol.send_ack() | |||
| # Get the binary | |||
| binary_content = self.protocol.get_file() | |||
| with open('elmo/project.bin', 'wb') as _binary_file: | |||
| _binary_file.write(binary_content) | |||
| ### Generate the traces by launching ELMO | |||
| command = './elmo ./project.bin' | |||
| cwd = './elmo/' | |||
| process = subprocess.Popen( | |||
| command, shell=True, cwd=cwd, | |||
| executable='/bin/bash', | |||
| stdout=subprocess.PIPE, | |||
| stderr=subprocess.PIPE, | |||
| ) | |||
| output, error = process.communicate() | |||
| output = output.decode('latin-1') if output else None | |||
| error = error.decode('latin-1') if error else None | |||
| if error: | |||
| self.protocol.send_data({ | |||
| 'output': output, | |||
| 'error': error, | |||
| }) | |||
| self.protocol.close() | |||
| return | |||
| ### Get traces | |||
| nb_traces = output.count('TRACE NO') | |||
| trace_filenames = [] | |||
| for filename in os.listdir('elmo/output/traces/'): | |||
| if len(trace_filenames) < nb_traces: | |||
| if re.search(r'^trace\d+\.trc$', filename): | |||
| trace_filenames.append('elmo/output/traces/{}'.format(filename)) | |||
| else: | |||
| break | |||
| assert len(trace_filenames) == nb_traces | |||
| results = trace_filenames | |||
| for i in range(len(results)): | |||
| with open(results[i], 'r') as _file: | |||
| results[i] = list(map(float, _file.readlines())) | |||
| ### Get asmtrace and printed data | |||
| asmtrace = None | |||
| if not ('asmtrace' in data and not data['asmtrace']): | |||
| with open('elmo/output/asmoutput/asmtrace00001.txt', 'r') as _file: | |||
| asmtrace = ''.join(_file.readlines()) | |||
| printed_data = None | |||
| if not ('printdata' in data and not data['printdata']): | |||
| with open('elmo/output/printdata.txt', 'r') as _file: | |||
| printed_data = list(map(lambda x: int(x, 16), _file.readlines())) | |||
| ### Send results | |||
| self.protocol.send_data({ | |||
| 'output': output, | |||
| 'error': error, | |||
| 'nb_traces': nb_traces, | |||
| 'results': results, | |||
| 'asmtrace': asmtrace, | |||
| 'printed_data': printed_data, | |||
| }) | |||
| self.protocol.close() | |||
| @@ -1,5 +1,5 @@ | |||
| import threading | |||
| from .server.protocol import Protocol, ClosureException | |||
| from .protocol import Protocol, ClosureException | |||
| import socket | |||
| @@ -44,12 +44,13 @@ class PermanentServiceThread(ServiceThread): | |||
| class ListeningThread(PermanentServiceThread): | |||
| def __init__(self, host, port, threadclass, **kwargs): | |||
| def __init__(self, host, port, threadclass, debug=False, **kwargs): | |||
| super().__init__() | |||
| self.hostname = host | |||
| self.port = port | |||
| self.threadclass = threadclass | |||
| self.kwargs = kwargs | |||
| self.debug = debug | |||
| def execute(self): | |||
| self.tcpsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| @@ -58,24 +59,31 @@ class ListeningThread(PermanentServiceThread): | |||
| # self.tcpsock.setsockopt(socket.SOL_SOCKET, socket.SO_ATTACH_REUSEPORT_CBPF, 1) | |||
| self.tcpsock.bind((self.hostname, self.port)) | |||
| self.tcpsock.listen(5) | |||
| print('[port][%s] Listening' % self.port) | |||
| if self.debug: | |||
| print('[port][%s] Listening' % self.port) | |||
| while self.is_running(): | |||
| try: | |||
| (clientsocket, (ip, port)) = self.tcpsock.accept() | |||
| print('[port][{}] Accepted: {} <=> {}'.format( | |||
| self.port, | |||
| clientsocket.getsockname(), | |||
| clientsocket.getpeername(), | |||
| )) | |||
| newthread = self.threadclass(ip, port, clientsocket, **self.kwargs) | |||
| newthread.start() | |||
| if self.is_running(): | |||
| if self.debug: | |||
| print('[port][{}] Accepted: {} <=> {}'.format( | |||
| self.port, | |||
| clientsocket.getsockname(), | |||
| clientsocket.getpeername(), | |||
| )) | |||
| newthread = self.threadclass(ip, port, clientsocket, **self.kwargs) | |||
| newthread.start() | |||
| else: | |||
| break | |||
| except socket.timeout: | |||
| pass | |||
| print('[port][%s] Stop listening' % self.port) | |||
| def stop(self): | |||
| super().stop() | |||
| clientsocker = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| clientsocker.connect( (self.hostname, self.port) ) | |||
| clientsocker.connect((self.hostname, self.port)) | |||
| self.tcpsock.close() | |||
| print('[port][%s] Stop listening' % self.port) | |||
| @@ -79,5 +79,26 @@ assert res['nb_instructions'] | |||
| print_success(' - Test 3 "Use A Real Simulation": Success!') | |||
| ######################################################### | |||
| # TEST 4 : USE ELMO BY RUNNING ONLINE # | |||
| ######################################################### | |||
| def test_online_server(): | |||
| from elmo import get_simulation | |||
| KyberNTTSimulation = get_simulation('KyberNTTSimulation') | |||
| simulation = KyberNTTSimulation() | |||
| simulation.set_challenges(simulation.get_random_challenges(10)) | |||
| return simulation.run_online() | |||
| from elmo.executor import launch_executor | |||
| # Launch the server and realize the test | |||
| res = launch_executor('localhost', 5000, waiting_function=test_online_server) | |||
| assert not res['error'] | |||
| assert res['nb_traces'] == 10 | |||
| assert res['nb_instructions'] | |||
| print_success(' - Test 4 "Use ELMO By Running Online": Success!') | |||
| ######################################################### | |||
| print_success('All seems fine!') | |||