├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── lumina ├── database.py ├── lumina_server.py └── lumina_structs.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # OpenSSL keys 132 | keys/* 133 | 134 | # VS Code 135 | .vscode 136 | 137 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | # Directory for the app 4 | # Sets the Working dir and location in one command 5 | WORKDIR /opt/lumina 6 | 7 | # Copy the contents of server 8 | # to the container 9 | COPY lumina . 10 | COPY requirements.txt . 11 | 12 | # Install dependencies 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | # Expose this port number 16 | EXPOSE 8443 17 | 18 | # Run this ComManD 19 | CMD ["python", "./lumina/lumina_server.py"] 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | About 3 | ===== 4 | 5 | POC of an offline server for IDA Lumina feature. 6 | 7 | More details on https://www.synacktiv.com/publications/investigating-ida-lumina-feature.html 8 | 9 | Instalation 10 | =========== 11 | 12 | Python package installation 13 | --------------------------- 14 | 15 | Download project and run `python lumina/setup.py` (or `pip install .`). 16 | 17 | Server can also be used as a standalone script. The command `lumina_server` won't be registered in the PATH though. You will have to run manually using `python3 lumina/lumina_server.py`. 18 | 19 | Generate certificates 20 | ---------------------- 21 | 22 | This step is optionnal if you don't need using TLS. You will then have to modify the `LUMINA_TLS = NO` in `ida.cfg`. 23 | 24 | Generate a new ROOT CA certificate and key using one of these lines 25 | (you can remove the `-nodes` option to set a passphrase but keep in mind you will need to pass passphrase argument to server script): 26 | 27 | ```bash 28 | # sha256WithRSAEncryption 29 | openssl req -nodes -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -sha256 -keyout luminaRootCAKey.pem -out luminaRootCAK.pem -days 365 -subj '/CN=www.fakerays.com/O=Fake Hexrays/C=XX' 30 | 31 | # ecdsa-with-SHA256 (prime256v1) 32 | openssl req -nodes -x509 -newkey rsa:4096 -sha512 -keyout luminaRootCAKey.pem -out luminaRootCA.pem -days 365 -subj '/CN=www.fakerays.com/O=Fake Hexrays/C=XX' 33 | ``` 34 | 35 | Client setup 36 | ------------ 37 | 38 | Copy the CA certificate (`luminaRootCA.pem`) to IDA config directory as `hexrays.crt`: 39 | - Windows: ``%APPDATA%\Hex-Rays\IDA Pro\hexrays.crt`` 40 | - Linux/OSX: ``$HOME/.idapro/hexrays.crt`` 41 | 42 | e.g (linux): `cp luminaRootCA.pem $HOME/.idapro/hexrays.crt` 43 | 44 | modify the IDA configuration file (``cfg/ida.cfg``), either in installation directory or (recommanded) user directory: 45 | - Windows: ``%APPDATA%\Hex-Rays\IDA Pro\cfg\ida.cfg`` 46 | - Linux/OSX: ``$HOME/.idapro/hexrays.crt`` 47 | 48 | ```c 49 | // Lumina related parameters 50 | LUMINA_HOST = "localhost"; // Lumina server url (default : "lumina.hex-rays.com") 51 | // warning: keep the the semicolon 52 | LUMINA_MIN_FUNC_SIZE = 32 // default function size : 32 53 | LUMINA_PORT = 4443 // default port : 443 54 | LUMINA_TLS = YES // enable TLS (default : YES) 55 | ``` 56 | 57 | First run 58 | ========= 59 | 60 | Start the server 61 | ---------------- 62 | 63 | Usage: 64 | ``` 65 | usage: lumina_server [-h] [-i IP] [-p PORT] [-c CERT] [-k CERT_KEY] 66 | [-l {NOTSET,DEBUG,INFO,WARNING}] 67 | db 68 | 69 | positional arguments: 70 | db database file 71 | 72 | optional arguments: 73 | -h, --help show this help message and exit 74 | -i IP, --ip IP listening ip address (default: 127.0.0.1 75 | -p PORT, --port PORT listening port (default: 4443 76 | -c CERT, --cert CERT proxy certfile (no cert means TLS OFF). 77 | -k CERT_KEY, --key CERT_KEY 78 | certificate private key 79 | -l {NOTSET,DEBUG,INFO,WARNING}, --log {NOTSET,DEBUG,INFO,WARNING} 80 | log level bases on python logging value (default:info) 81 | 82 | ``` 83 | 84 | example: 85 | 86 | ```bash 87 | lumina_server db.json --cert luminaRootCA.pem --key luminaRootCAKey.pem --ip 127.0.0.1 --port 4443 --log DEBUG 88 | ``` 89 | 90 | Start server, (re)start IDA with an idb database and push your first function using Lumina. 91 | Hit `ctrl-c` to terminate server and save database. 92 | 93 | **Important**: keep in mind that the database is only saved or updated on server exit (`ctrl-c`). 94 | 95 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo-server: 4 | image: mongo 5 | container_name: mongo-db 6 | environment: 7 | - MONGO_INITDB_ROOT_USERNAME=mongoadmin 8 | - MONGO_INITDB_ROOT_PASSWORD=secret 9 | ports: 10 | - 27017:27017 11 | volumes: 12 | - mongo-data:/data/db 13 | networks: 14 | - lumina-net 15 | 16 | lumina: 17 | container_name: lumina-server 18 | build: . # replaces image for dev 19 | restart: on-failure 20 | command: python ./lumina_server.py 21 | depends_on: 22 | - mongo-server 23 | ports: 24 | - 8443:8443 25 | volumes: 26 | - ./lumina:/opt/lumina 27 | networks: 28 | - lumina-net 29 | 30 | volumes: 31 | mongo-data: 32 | 33 | networks: 34 | lumina-net: 35 | 36 | -------------------------------------------------------------------------------- /lumina/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from base64 import b64encode, b64decode 5 | from pymongo import MongoClient 6 | 7 | 8 | 9 | class LuminaDatabase(object): 10 | def __init__(self, logger, db_name="test"): 11 | self.logger = logger 12 | self.client = None 13 | self.db = None 14 | self.collection = None 15 | 16 | self.load(db_name) 17 | 18 | 19 | def load(self, db_name): 20 | self.client = MongoClient('mongodb://mongoadmin:secret@mongo-server:27017/') 21 | 22 | try: 23 | self.db = self.client[db_name] 24 | self.collection = self.db['lumina_data'] 25 | except Exception as e: 26 | self.logger.exception(e) 27 | self.client.close() 28 | raise 29 | 30 | def close(self, save=False): 31 | self.client.close() 32 | 33 | def push(self, info): 34 | """Return True on new insertion, else False""" 35 | 36 | # Signature and metadata contains non string data that need to be encoded: 37 | sig_version = info.signature.version 38 | signature = b64encode(info.signature.signature).decode("ascii") 39 | 40 | metadata = { 41 | "func_name" : info.metadata.func_name, 42 | "func_size" : info.metadata.func_size, 43 | "serialized_data" : b64encode(info.metadata.serialized_data).decode("ascii"), 44 | } 45 | 46 | if sig_version != 1: 47 | self.logger.warning("Signature version {sig_version} not supported. Results might be inconsistent") 48 | 49 | # Insert into database 50 | new_sig = False 51 | db_entry = self.collection.find_one({"sig": signature}) 52 | 53 | if db_entry is None: 54 | # The entry does NOT exist in the collection. Create a new one. 55 | # TODO: Collision/merge not implemented yet. Just keep every push queries. 56 | new_entry = { 57 | "sig": signature, 58 | "metadata": list(), 59 | "popularity" : 0 60 | } 61 | 62 | # Actually insert this data into the collection :) 63 | try: 64 | self.collection.insert_one(new_entry) 65 | new_sig = True 66 | except Exception as e: 67 | self.logger.error(e) 68 | 69 | else: 70 | try: 71 | # Existing entry. Update it. 72 | db_entry["metadata"].append(metadata) 73 | db_entry["popularity"] += 1 74 | self.collection.update_one( 75 | {"sig": signature}, 76 | {"$set": { 77 | "metadata": db_entry["metadata"], 78 | "popularity": db_entry["popularity"] 79 | }}) 80 | except Exception as e: 81 | self.logger.error(e) 82 | 83 | return new_sig 84 | 85 | 86 | def pull(self, signature): 87 | """Return function metadata or None if not found""" 88 | 89 | sig_version = signature.version 90 | signature = b64encode(signature.signature).decode("ascii") 91 | 92 | if sig_version != 1: 93 | self.logger.warning("Signature version {sig_version} not supported. Results might be inconsistent") 94 | 95 | # Query database 96 | db_entry = self.collection.find_one({"sig": signature}) 97 | 98 | if db_entry: 99 | # Take last signature (arbitrary choice) 100 | _metadata_list = db_entry["metadata"] 101 | if _metadata_list: 102 | metadata = _metadata_list[-1] 103 | else: 104 | return None 105 | 106 | # Decode signature (take that last match for a result) 107 | _metadata = { 108 | "func_name" : metadata["func_name"], 109 | "func_size" : metadata["func_size"], 110 | "serialized_data" : b64decode(metadata["serialized_data"]), 111 | } 112 | 113 | result = { 114 | "metadata" : _metadata, 115 | "popularity" : db_entry["popularity"] 116 | } 117 | 118 | return result 119 | 120 | return None 121 | 122 | -------------------------------------------------------------------------------- /lumina/lumina_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, sys, argparse, logging, signal, threading 4 | 5 | import socket, ssl 6 | from socketserver import ThreadingMixIn, TCPServer, BaseRequestHandler 7 | 8 | 9 | # local import for standalone use 10 | from lumina_structs import rpc_message_parse, rpc_message_build, RPC_TYPE 11 | from database import LuminaDatabase 12 | 13 | 14 | ################################################################################ 15 | # 16 | # Protocole 17 | # 18 | ################################################################################ 19 | 20 | class LuminaRequestHandler(BaseRequestHandler): 21 | def __init__(self, request, client_address, server): 22 | self.logger = server.logger 23 | self.database = server.database 24 | super().__init__(request, client_address, server) 25 | 26 | 27 | def sendMessage(self, code, **kwargs): 28 | self.logger.debug(f"sending RPC Packet (code = {code}, data={kwargs}") 29 | 30 | data = rpc_message_build(code, **kwargs) 31 | self.request.send(data) 32 | 33 | def recvMessage(self): 34 | packet, message = rpc_message_parse(self.request) 35 | self.logger.debug(f"got new RPC Packet (code = {packet.code}, data={message}") 36 | return packet, message 37 | 38 | def handle(self): 39 | 40 | # 41 | # Get first RPC packet (RPC_HELO) 42 | # 43 | 44 | packet, message = self.recvMessage() 45 | 46 | if packet.code != RPC_TYPE.RPC_HELO: 47 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = 'Expected helo') 48 | return 49 | 50 | if self.server.check_client(message): 51 | self.sendMessage(RPC_TYPE.RPC_OK) 52 | else: 53 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = 'Invalid license') 54 | return 55 | 56 | # 57 | # Receive and handle request command: 58 | # 59 | packet, message = self.recvMessage() 60 | 61 | if packet.code == RPC_TYPE.PUSH_MD: 62 | results = list() 63 | for _, info in enumerate(message.funcInfos): 64 | results.append(self.database.push(info)) 65 | 66 | self.sendMessage(RPC_TYPE.PUSH_MD_RESULT, resultsFlags = results) 67 | 68 | elif packet.code == RPC_TYPE.PULL_MD: 69 | found = list() 70 | results = list() 71 | 72 | for sig in message.funcInfos: 73 | metadata = self.database.pull(sig) 74 | if metadata: 75 | found.append(0) 76 | results.append(metadata) 77 | else: 78 | found.append(1) 79 | 80 | self.sendMessage(RPC_TYPE.PULL_MD_RESULT, found = found, results =results) 81 | 82 | else: 83 | self.logger("[-] ERROR: message handler not implemented") 84 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = "Unknown command") 85 | #self.sendMessage(RPC_TYPE.RPC_FAIL, status = -1, message = "not implemented") 86 | 87 | return 88 | 89 | class LuminaServer(ThreadingMixIn, TCPServer): 90 | def __init__(self, database, config, logger, bind_and_activate=True): 91 | super().__init__((config.ip, config.port), LuminaRequestHandler, bind_and_activate) 92 | self.config = config 93 | self.database = database 94 | self.logger = logger 95 | self.useTLS = False 96 | 97 | if self.config.cert: 98 | if self.config.cert_key is None: 99 | raise ValueError("Missing certificate key argument") 100 | 101 | self.useTLS = True 102 | 103 | def get_request(self): 104 | client_socket, fromaddr = self.socket.accept() 105 | 106 | self.logger.debug(f"new client {fromaddr[0]}:{fromaddr[1]}") 107 | if not self.useTLS: 108 | self.logger.debug("Starting plaintext session") 109 | # extra check: make sure client does no try to initiate a TLS session (or parsing would hang) 110 | data = client_socket.recv(3, socket.MSG_PEEK) 111 | if data == b'\x16\x03\x01': 112 | self.logger.error("TLS client HELLO detected on plaintext mode. Check IDA configuration and cert. Aborting") 113 | client_socket.close() 114 | raise OSError("NO TLS") 115 | 116 | if self.useTLS: 117 | self.logger.debug("Starting TLS session") 118 | try: 119 | client_socket = ssl.wrap_socket(client_socket, 120 | ssl_version = ssl.PROTOCOL_TLSv1_2, 121 | server_side = True, 122 | certfile=self.config.cert.name, 123 | keyfile=self.config.cert_key.name) 124 | 125 | except Exception: 126 | self.logger.exception("TLS connection failed. Check IDA configuration and cert") 127 | raise 128 | 129 | return client_socket, fromaddr 130 | 131 | def shutdown(self, save=True): 132 | self.logger.info("Server stopped") 133 | super().shutdown() 134 | self.database.close(save=save) 135 | 136 | def serve_forever(self): 137 | self.logger.info(f"Server started. Listening on {self.server_address[0]}:{self.server_address[1]} (TLS={'ON' if self.useTLS else 'OFF'})") 138 | super().serve_forever() 139 | 140 | def check_client(self, message): 141 | """ 142 | Return True if user is authozied, else False 143 | """ 144 | # check (message.hexrays_licence, message.hexrays_id, message.watermak, message.field_0x36) 145 | self.logger.debug("RPC client accepted") 146 | 147 | return True 148 | 149 | 150 | def signal_handler(sig, frame, server): 151 | print('Ctrl+C caught. Exiting') 152 | server.shutdown(save=True) 153 | sys.exit(0) 154 | 155 | 156 | def main(): 157 | # default log handler is stdout. You can add a FileHandler or any handler you want 158 | log_handler = logging.StreamHandler(sys.stdout) 159 | log_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")) 160 | logger = logging.getLogger("lumina") 161 | logger.addHandler(log_handler) 162 | logger.setLevel(logging.DEBUG) 163 | 164 | # Parse command line 165 | parser = argparse.ArgumentParser() 166 | parser.add_argument("-i", "--ip", dest="ip", type=str, default="0.0.0.0", help="listening ip address (default: 0.0.0.0") 167 | parser.add_argument("-p", "--port", dest="port", type=int, default=8443, help="listening port (default: 8443") 168 | parser.add_argument("-c", "--cert", dest="cert", type=argparse.FileType('r'), default = None, help="proxy certfile (no cert means TLS OFF).") 169 | parser.add_argument("-k", "--key", dest="cert_key",type=argparse.FileType('r'), default = None, help="certificate private key") 170 | parser.add_argument("-l", "--log", dest="log_level", type=str, choices=["NOTSET", "DEBUG", "INFO", "WARNING"], default="INFO", help="log level bases on python logging value (default:info)") 171 | config = parser.parse_args() 172 | 173 | 174 | logger.setLevel(config.log_level) 175 | 176 | # create db & server 177 | database = LuminaDatabase(logger, "test") 178 | TCPServer.allow_reuse_address = True 179 | server = LuminaServer(database, config, logger) 180 | 181 | # set ctrl-c handler 182 | signal.signal(signal.SIGINT, lambda sig,frame:signal_handler(sig, frame, server)) 183 | 184 | # start server 185 | server_thread = threading.Thread(target=server.serve_forever) 186 | server_thread.daemon = False 187 | server_thread.start() 188 | server_thread.join() 189 | 190 | server.database.close(save=True) 191 | 192 | 193 | if __name__ == "__main__": 194 | main() 195 | 196 | -------------------------------------------------------------------------------- /lumina/lumina_structs.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import construct as con 3 | from construct import ( 4 | Byte, Bytes, Int8ub, Int16ub, Int16ul, Int16sb, Int32ub, Int32ul, Int64ub, 5 | CString, Hex, 6 | Struct, Array, Const, Rebuild, len_, this, FormatField, 7 | byte2int, int2byte, stream_read, stream_write, Construct, singleton, IntegerError, integertypes, 8 | Container, 9 | ) 10 | 11 | IDA_PROTOCOLE_VERSION = 2 12 | 13 | ####################################### 14 | # 15 | # Construct adapters 16 | # 17 | # Each adapter handles (de)serialization of variable length integer 18 | ####################################### 19 | 20 | @singleton 21 | class IdaVarInt16(Construct): 22 | r""" 23 | construct adapter that handles (de)serialization of variable length int16 (see pack_dw/unpack_dw in IDA API) 24 | """ 25 | 26 | def _parse(self, stream, context, path): 27 | b = byte2int(stream_read(stream, 1, path)) 28 | extrabytes, mask = [ 29 | # lookup table 30 | [0, 0xff], # (0b0xxxxxxx) 31 | [0, 0xff], # (0b0xxxxxxx) 32 | [1, 0x7f], # 0x80 (0b10xxxxxx) 33 | [2, 0x00] # 0xC0 (0b11xxxxxx) 34 | ][b >> 6] 35 | 36 | num = b & mask 37 | for _ in range(extrabytes): 38 | num = (num << 8) + byte2int(stream_read(stream, 1, path)) 39 | 40 | return num 41 | 42 | def _build(self, obj, stream, context, path): 43 | if not isinstance(obj, integertypes): 44 | raise IntegerError("value is not an integer", path=path) 45 | if obj < 0: 46 | raise IntegerError("cannot build from negative number: %r" % (obj,), path=path) 47 | if obj > 0xFFFF: 48 | raise IntegerError("cannot build from number above short range: %r" % (obj,), path=path) 49 | 50 | x = obj 51 | 52 | if (x > 0x3FFF): 53 | x |= 0xFF0000 54 | nbytes = 3 55 | elif (x > 0x7F): 56 | x |= 0x8000 57 | nbytes = 2 58 | else: 59 | nbytes = 1 60 | 61 | for i in range(nbytes, 0, -1): 62 | stream_write(stream, int2byte((x >> (8*(i-1))) & 0xFF), 1, path) 63 | 64 | return obj 65 | 66 | @singleton 67 | class IdaVarInt32(Construct): 68 | r""" 69 | construct adapter that handles (de)serialization of variable length int32 (see pack_dd/unpack_dd in IDA API) 70 | """ 71 | 72 | def _parse(self, stream, context, path): 73 | b = byte2int(stream_read(stream, 1, path)) 74 | extrabytes, mask = [ 75 | [0, 0xff], [0, 0xff], [0, 0xff], [0, 0xff], # (0b0..xxxxx) 76 | [1, 0x7f], [1, 0x7f], # 0x80 (0b10.xxxxx) 77 | [3, 0x3f], # 0xC0 (0b110xxxxx) 78 | [4, 0x00] # 0xE0 (0b111xxxxx) 79 | ][b>>5] 80 | 81 | num = b & mask 82 | for _ in range(extrabytes): 83 | num = (num << 8) + byte2int(stream_read(stream, 1, path)) 84 | 85 | return num 86 | 87 | 88 | def _build(self, obj, stream, context, path): 89 | if not isinstance(obj, integertypes): 90 | raise IntegerError("value is not an integer", path=path) 91 | if obj < 0: 92 | raise IntegerError("cannot build from negative number: %r" % (obj,), path=path) 93 | if obj > 0xFFFFFFFF: 94 | raise IntegerError("cannot build from number above integer range: %r" % (obj,), path=path) 95 | x = obj 96 | 97 | if (x > 0x1FFFFFFF): 98 | x |= 0xFF00000000 99 | nbytes = 5 100 | elif (x > 0x3FFF): 101 | x |= 0xC0000000 102 | nbytes = 4 103 | elif (x > 0x7F): 104 | x |= 0x8000 105 | nbytes = 2 106 | else: 107 | nbytes = 1 108 | 109 | for i in range(nbytes, 0, -1): 110 | stream_write(stream, int2byte((x >> (8*(i-1))) & 0xFF), 1, path) 111 | 112 | return obj 113 | 114 | @singleton 115 | class IdaVarInt64(Construct): 116 | """ 117 | construct adapter that handles (de)serialization of variable length int64 (see pack_dq/unpack_dq in IDA API) 118 | """ 119 | 120 | def _parse(self, stream, context, path): 121 | low = IdaVarInt32._parse(stream, context, path) 122 | high = IdaVarInt32._parse(stream, context, path) 123 | num = (high << 32) | low 124 | return num 125 | 126 | def _build(self, obj, stream, context, path): 127 | if not isinstance(obj, integertypes): 128 | raise IntegerError("value is not an integer", path=path) 129 | if obj < 0: 130 | raise IntegerError("cannot build from negative number: %r" % (obj,), path=path) 131 | if obj > 0xFFFFFFFFFFFFFFFF: 132 | raise IntegerError("cannot build from number above short range: %r" % (obj,), path=path) 133 | 134 | low = obj & 0xFFFFFFFF 135 | IdaVarInt32._build(low, stream, context, path) 136 | high = obj >> 32 137 | IdaVarInt32._build(high, stream, context, path) 138 | 139 | return obj 140 | 141 | 142 | 143 | 144 | ####################################### 145 | # 146 | # Basic types & helpers 147 | # 148 | ####################################### 149 | 150 | # String prefixed with a variable int size 151 | VarString = con.PascalString(IdaVarInt32, "utf8") 152 | # Bytes buffer prefixed with a variable int size 153 | VarBuff = con.Prefixed(IdaVarInt32, con.GreedyBytes) 154 | # IDA typedefs 155 | ea_t = asize_t = adiff_t = con.ExprAdapter(IdaVarInt64, con.obj_-1, con.obj_+1) 156 | 157 | # "template" for defining object list, prefixed with a variable int size 158 | def ObjectList(obj): 159 | return con.PrefixedArray(IdaVarInt32, obj) 160 | 161 | 162 | ####################################### 163 | # 164 | # Lumina types 165 | # 166 | ####################################### 167 | 168 | # function signature 169 | func_sig_t = con.Struct( 170 | "version" / Const(1, IdaVarInt32), # protocol version (con.Default: 1) 171 | "signature" / VarBuff # signature buffer 172 | ) 173 | 174 | # a.k.a func_info_t 175 | func_metadata = con.Struct( 176 | "func_name" / CString("utf8"), # function name 177 | "func_size" / IdaVarInt32, # function size in bytes 178 | "serialized_data" / VarBuff # metadata 179 | ) 180 | 181 | # extended func_metadata 182 | func_info_t = con.Struct( 183 | "metadata" / func_metadata, # 184 | "popularity" / con.Default(IdaVarInt32, 0), # unknown 185 | ) 186 | 187 | func_md_t = con.Struct( 188 | "metadata" / func_metadata, 189 | "signature" / func_sig_t 190 | ) 191 | 192 | # same as func_md_t with extra (unknown) field 193 | func_md2_t = con.Struct( 194 | "metadata" / func_metadata, 195 | "signature" / func_sig_t, 196 | "field_0x58" / Hex(Const(0, IdaVarInt32)), 197 | ) 198 | 199 | ####################################### 200 | # 201 | # Lumina message types 202 | # 203 | ####################################### 204 | 205 | RPC_TYPE = con.Enum(Byte, 206 | RPC_OK = 0xa, 207 | RPC_FAIL = 0xb, 208 | RPC_NOTIFY = 0xc, 209 | RPC_HELO = 0xd, 210 | PULL_MD = 0xe, 211 | PULL_MD_RESULT = 0xf, 212 | PUSH_MD = 0x10, 213 | PUSH_MD_RESULT = 0x11, 214 | # below messages are not implemented or not used by Lumina. Enjoy yourselves ;) 215 | GET_POP = 0x12, 216 | GET_POP_RESULT = 0x13, 217 | LIST_PEERS = 0x14, 218 | LIST_PEERS_RESULT = 0x15, 219 | KILL_SESSIONS = 0x16, 220 | KILL_SESSIONS_RESULT = 0x17, 221 | DEL_ENTRIES = 0x18, 222 | DEL_ENTRIES_RESULT = 0x19, 223 | SHOW_ENTRIES = 0x1a, 224 | SHOW_ENTRIES_RESULT = 0x1b, 225 | DUMP_MD = 0x1c, 226 | DUMP_MD_RESULT = 0x1d, 227 | CLEAN_DB = 0x1e, 228 | DEBUGCTL = 0x1f 229 | ) 230 | 231 | RpcMessage_FAIL = con.Struct( 232 | "status" / IdaVarInt32, 233 | "message" / CString("utf-8"), # null terminated string 234 | ) 235 | 236 | RpcMessage_HELO = con.Struct( 237 | "protocole" / con.Default(IdaVarInt32, IDA_PROTOCOLE_VERSION), 238 | "hexrays_licence" / VarBuff, # ida.key file content 239 | "hexrays_id" / Hex(Int32ul), # internal licence_info 240 | "watermak" / Hex(Int16ul), # internal licence_info 241 | "field_0x36" / IdaVarInt32, # always zero ? 242 | ) 243 | 244 | RpcMessage_NOTIFY = con.Struct( 245 | "protocole" / con.Default(IdaVarInt32, IDA_PROTOCOLE_VERSION), 246 | "message" / CString("utf-8"), # null terminated string 247 | ) 248 | 249 | RpcMessage_PULL_MD = con.Struct( 250 | "flags" / IdaVarInt32, 251 | "ukn_list" / ObjectList(IdaVarInt32), # list of IdaVarInt32 252 | "funcInfos" / ObjectList(func_sig_t) # list of func_sig_t 253 | ) 254 | 255 | RpcMessage_PULL_MD_RESULT = con.Struct( 256 | "found" / ObjectList(IdaVarInt32), # list of boolean for each request in PULL_MD (1 if matching/found) 257 | "results" / ObjectList(func_info_t) # list of func_info_t for each matching result 258 | ) 259 | 260 | RpcMessage_PUSH_MD = con.Struct( 261 | "field_0x10" / IdaVarInt32, 262 | "idb_filepath" / CString("utf-8"), # absolute file path of current idb 263 | "input_filepath" / CString("utf-8"), # absolute file path of input file 264 | "input_md5" / Bytes(16), # input file md5 265 | "hostname" / CString("utf-8"), # machine name 266 | "funcInfos" / ObjectList(func_md_t), # list of func_md_t to push 267 | "funcEas" / ObjectList(IdaVarInt64), # absolute (!?) address of each pushed function 268 | ) 269 | 270 | 271 | RpcMessage_PUSH_MD_RESULT = con.Struct( 272 | "resultsFlags" / ObjectList(IdaVarInt32), # status for each function pushed 273 | ) 274 | 275 | 276 | 277 | # Generic RPC message 'union' 278 | RpcMessage = con.Switch(this.code, 279 | { 280 | RPC_TYPE.RPC_OK : con.Pass, 281 | RPC_TYPE.RPC_FAIL : RpcMessage_FAIL, 282 | RPC_TYPE.RPC_NOTIFY : RpcMessage_NOTIFY, 283 | RPC_TYPE.RPC_HELO : RpcMessage_HELO, 284 | RPC_TYPE.PULL_MD : RpcMessage_PULL_MD, 285 | RPC_TYPE.PULL_MD_RESULT : RpcMessage_PULL_MD_RESULT, 286 | RPC_TYPE.PUSH_MD : RpcMessage_PUSH_MD, 287 | RPC_TYPE.PUSH_MD_RESULT : RpcMessage_PUSH_MD_RESULT, 288 | #RPC_TYPE.GET_POP : RpcMessage_GET_POP, 289 | #RPC_TYPE.GET_POP_RESULT : RpcMessage_GET_POP_RESULT, 290 | #RPC_TYPE.LIST_PEERS : RpcMessage_LIST_PEERS, 291 | #RPC_TYPE.LIST_PEERS_RESULT : RpcMessage_LIST_PEERS_RESULT, 292 | #RPC_TYPE.KILL_SESSIONS : RpcMessage_KILL_SESSIONS, 293 | #RPC_TYPE.KILL_SESSIONS_RESULT : RpcMessage_KILL_SESSIONS_RESULT, 294 | #RPC_TYPE.DEL_ENTRIES : RpcMessage_DEL_ENTRIES, 295 | #RPC_TYPE.DEL_ENTRIES_RESULT : RpcMessage_DEL_ENTRIES_RESULT, 296 | #RPC_TYPE.SHOW_ENTRIES : RpcMessage_SHOW_ENTRIES, 297 | #RPC_TYPE.SHOW_ENTRIES_RESULT : RpcMessage_SHOW_ENTRIES_RESULT, 298 | #RPC_TYPE.DUMP_MD : RpcMessage_DUMP_MD, 299 | #RPC_TYPE.DUMP_MD_RESULT : RpcMessage_DUMP_MD_RESULT, 300 | #RPC_TYPE.CLEAN_DB : RpcMessage_CLEAN_DB, 301 | #RPC_TYPE.DEBUGCTL : RpcMessage_DEBUGCTL, 302 | }, 303 | default = None 304 | ) 305 | 306 | # RPC packet common header 307 | rpc_packet_t = con.Struct( 308 | "length" / Rebuild(Hex(Int32ub), len_(this.data)), 309 | "code" / RPC_TYPE, 310 | "data" / con.HexDump(con.Bytes(this.length)) 311 | ) 312 | 313 | def rpc_message_build(code, **kwargs): 314 | """ 315 | Build and serialize an RPC packet 316 | """ 317 | data = RpcMessage.build(kwargs, code = code) 318 | 319 | return rpc_packet_t.build(Container(code = code, 320 | data = data) 321 | ) 322 | 323 | def rpc_message_parse(source): 324 | """ 325 | Read and deserilize RPC message from a file-like object or socket) 326 | """ 327 | if isinstance(source, str): 328 | # parse source as filename 329 | packet = rpc_packet_t.parse_stream(source) 330 | elif isinstance(source, bytes): 331 | # parse source as bytes 332 | packet = rpc_packet_t.parse(source) 333 | else: 334 | # parse source as file-like object 335 | if isinstance(source, socket.socket): 336 | # construct requires a file-like object with read/write methods: 337 | source = source.makefile(mode='rb') 338 | 339 | packet = rpc_packet_t.parse_stream(source) 340 | 341 | message = RpcMessage.parse(packet.data , code = packet.code) 342 | # Warning: parsing return a Container object wich hold a io.BytesIO to the socket 343 | # see https://github.com/construct/construct/issues/852 344 | return packet, message 345 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | construct 2 | pymongo 3 | 4 | --------------------------------------------------------------------------------