├── README.md ├── lumina ├── __init__.py ├── database.py ├── lumina_server.py └── lumina_structs.py └── setup.py /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 | exemple: 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 | -------------------------------------------------------------------------------- /lumina/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synacktiv/lumina_server/869a176f178f0fe0f720d0e4fe0cf4672517b6fc/lumina/__init__.py -------------------------------------------------------------------------------- /lumina/database.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | from base64 import b64encode, b64decode 3 | 4 | class LuminaDatabase(object): 5 | def __init__(self, logger, db_file): 6 | self.logger = logger 7 | self.logger.info(f"loading database {os.path.abspath(db_file.name)}") 8 | self.load(db_file) 9 | 10 | 11 | def load(self, db_file): 12 | self.db_file = db_file 13 | self.db_file.seek(0, os.SEEK_SET) 14 | 15 | if os.stat(self.db_file.name).st_size == 0: 16 | # create new db 17 | self.db = dict() 18 | else: 19 | try: 20 | self.db = json.load(self.db_file) 21 | except Exception as e: 22 | self.logger.exception(e) 23 | self.db_file.close() 24 | self.db = None 25 | raise 26 | 27 | def save(self): 28 | try: 29 | self.logger.info(f"saving database to {self.db_file.name}") 30 | self.db_file.seek(0, os.SEEK_SET) 31 | json.dump(self.db, self.db_file) 32 | except Exception as e: 33 | self.logger.exception(e) 34 | raise 35 | return True 36 | 37 | def close(self, save=False): 38 | if save: 39 | self.save() 40 | self.db_file.close() 41 | self.db = None 42 | 43 | def push(self, info): 44 | """ 45 | return True on new insertion, else False 46 | """ 47 | 48 | # Signature and metadata contains non string data that need to be encoded: 49 | sig_version = info.signature.version 50 | signature = b64encode(info.signature.signature).decode("ascii") 51 | metadata = { 52 | "func_name" : info.metadata.func_name, 53 | "func_size" : info.metadata.func_size, 54 | "serialized_data" : b64encode(info.metadata.serialized_data).decode("ascii"), 55 | } 56 | 57 | if sig_version != 1: 58 | self.logger.warning("Signature version {sig_version} not supported. Results might be inconsistent") 59 | 60 | 61 | # insert into database 62 | new_sig = False 63 | db_entry = self.db.get(signature, None) 64 | 65 | if db_entry is None: 66 | db_entry = { 67 | "metadata": list(), # collision/merge not implemented yet. just keep every push queries 68 | "popularity" : 0 69 | } 70 | self.db[signature] = db_entry 71 | new_sig = True 72 | 73 | db_entry["metadata"].append(metadata) 74 | db_entry["popularity"] += 1 75 | 76 | return new_sig 77 | 78 | def pull(self,signature): 79 | """ 80 | return function metadata or None if not found 81 | """ 82 | 83 | sig_version = signature.version 84 | signature = b64encode(signature.signature).decode("ascii") 85 | 86 | if sig_version != 1: 87 | self.logger.warning("Signature version {sig_version} not supported. Results might be inconsistent") 88 | 89 | # query database 90 | db_entry = self.db.get(signature, None) 91 | 92 | if db_entry: 93 | # take last signature (arbitrary choice) 94 | metadata = db_entry["metadata"][-1] 95 | 96 | # Decode signature (take that last match for a result) 97 | metadata = { 98 | "func_name" : metadata["func_name"], 99 | "func_size" : metadata["func_size"], 100 | "serialized_data" : b64decode(metadata["serialized_data"]), 101 | } 102 | 103 | result = { 104 | "metadata" : metadata, 105 | "popularity" : db_entry["popularity"] 106 | } 107 | 108 | return result 109 | return None -------------------------------------------------------------------------------- /lumina/lumina_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os, sys, argparse, logging, signal, threading 4 | 5 | from socketserver import ThreadingMixIn, TCPServer, BaseRequestHandler 6 | import socket, ssl 7 | 8 | try: 9 | from lumina.lumina_structs import rpc_message_parse, rpc_message_build, RPC_TYPE 10 | from lumina.database import LuminaDatabase 11 | except ImportError: 12 | # local import for standalone use 13 | from lumina_structs import rpc_message_parse, rpc_message_build, RPC_TYPE 14 | from database import LuminaDatabase 15 | 16 | 17 | ################################################################################ 18 | # 19 | # Protocole 20 | # 21 | ################################################################################ 22 | 23 | class LuminaRequestHandler(BaseRequestHandler): 24 | def __init__(self, request, client_address, server): 25 | self.logger = server.logger 26 | self.database = server.database 27 | super().__init__(request, client_address, server) 28 | 29 | 30 | def sendMessage(self, code, **kwargs): 31 | self.logger.debug(f"sending RPC Packet (code = {code}, data={kwargs}") 32 | 33 | data = rpc_message_build(code, **kwargs) 34 | self.request.send(data) 35 | 36 | def recvMessage(self): 37 | packet, message = rpc_message_parse(self.request) 38 | self.logger.debug(f"got new RPC Packet (code = {packet.code}, data={message}") 39 | return packet, message 40 | 41 | def handle(self): 42 | 43 | # 44 | # Get first RPC packet (RPC_HELO) 45 | # 46 | 47 | packet, message = self.recvMessage() 48 | 49 | if packet.code != RPC_TYPE.RPC_HELO: 50 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = 'Expected helo') 51 | return 52 | 53 | if self.server.check_client(message): 54 | self.sendMessage(RPC_TYPE.RPC_OK) 55 | else: 56 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = 'Invalid license') 57 | return 58 | 59 | # 60 | # Receive and handle request command: 61 | # 62 | packet, message = self.recvMessage() 63 | 64 | if packet.code == RPC_TYPE.PUSH_MD: 65 | results = list() 66 | for _, info in enumerate(message.funcInfos): 67 | results.append(self.database.push(info)) 68 | 69 | self.sendMessage(RPC_TYPE.PUSH_MD_RESULT, resultsFlags = results) 70 | 71 | elif packet.code == RPC_TYPE.PULL_MD: 72 | found = list() 73 | results = list() 74 | 75 | for sig in message.funcInfos: 76 | metadata = self.database.pull(sig) 77 | if metadata: 78 | found.append(1) 79 | results.append(metadata) 80 | else: 81 | found.append(0) 82 | 83 | self.sendMessage(RPC_TYPE.PULL_MD_RESULT, found = found, results =results) 84 | 85 | else: 86 | self.logger("[-] ERROR: message handler not implemented") 87 | self.sendMessage(RPC_TYPE.RPC_NOTIFY, message = "Unknown command") 88 | #self.sendMessage(RPC_TYPE.RPC_FAIL, status = -1, message = "not implemented") 89 | 90 | return 91 | 92 | class LuminaServer(ThreadingMixIn, TCPServer): 93 | def __init__(self, database, config, logger, bind_and_activate=True): 94 | super().__init__((config.ip, config.port), LuminaRequestHandler, bind_and_activate) 95 | self.config = config 96 | self.database = database 97 | self.logger = logger 98 | self.useTLS = False 99 | 100 | if self.config.cert: 101 | if self.config.cert_key is None: 102 | raise ValueError("Missing certificate key argument") 103 | 104 | self.useTLS = True 105 | 106 | def get_request(self): 107 | client_socket, fromaddr = self.socket.accept() 108 | 109 | self.logger.debug(f"new client {fromaddr[0]}:{fromaddr[1]}") 110 | if not self.useTLS: 111 | self.logger.debug("Starting plaintext session") 112 | # extra check: make sure client does no try to initiate a TLS session (or parsing would hang) 113 | data = client_socket.recv(3, socket.MSG_PEEK) 114 | if data == b'\x16\x03\x01': 115 | self.logger.error("TLS client HELLO detected on plaintext mode. Check IDA configuration and cert. Aborting") 116 | client_socket.close() 117 | raise OSError("NO TLS") 118 | 119 | if self.useTLS: 120 | self.logger.debug("Starting TLS session") 121 | try: 122 | client_socket = ssl.wrap_socket(client_socket, 123 | ssl_version = ssl.PROTOCOL_TLSv1_2, 124 | server_side = True, 125 | certfile=self.config.cert.name, 126 | keyfile=self.config.cert_key.name) 127 | 128 | except Exception: 129 | self.logger.exception("TLS connection failed. Check IDA configuration and cert") 130 | raise 131 | 132 | return client_socket, fromaddr 133 | 134 | def shutdown(self, save=True): 135 | self.logger.info("Server stopped") 136 | super().shutdown() 137 | self.database.close(save=save) 138 | 139 | def serve_forever(self): 140 | self.logger.info(f"Server started. Listening on {self.server_address[0]}:{self.server_address[1]} (TLS={'ON' if self.useTLS else 'OFF'})") 141 | super().serve_forever() 142 | 143 | def check_client(self, message): 144 | """ 145 | Return True if user is authozied, else False 146 | """ 147 | # check (message.hexrays_licence, message.hexrays_id, message.watermak, message.field_0x36) 148 | self.logger.debug("RPC client accepted") 149 | return True 150 | 151 | def signal_handler(sig, frame, server): 152 | print('Ctrl+C caught. Exiting') 153 | server.shutdown(save=True) 154 | sys.exit(0) 155 | 156 | 157 | def main(): 158 | # default log handler is stdout. You can add a FileHandler or any handler you want 159 | log_handler = logging.StreamHandler(sys.stdout) 160 | log_handler.setFormatter(logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")) 161 | logger = logging.getLogger("lumina") 162 | logger.addHandler(log_handler) 163 | logger.setLevel(logging.DEBUG) 164 | 165 | # Parse command line 166 | parser = argparse.ArgumentParser() 167 | parser.add_argument("db", type=argparse.FileType('a+'), default="", help="database file") 168 | parser.add_argument("-i", "--ip", dest="ip", type=str, default="127.0.0.1", help="listening ip address (default: 127.0.0.1") 169 | parser.add_argument("-p", "--port", dest="port", type=int, default=4443, help="listening port (default: 4443") 170 | parser.add_argument("-c", "--cert", dest="cert", type=argparse.FileType('r'), default = None, help="proxy certfile (no cert means TLS OFF).") 171 | parser.add_argument("-k", "--key", dest="cert_key",type=argparse.FileType('r'), default = None, help="certificate private key") 172 | 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)") 173 | config = parser.parse_args() 174 | 175 | 176 | logger.setLevel(config.log_level) 177 | 178 | # create db & server 179 | database = LuminaDatabase(logger, config.db) 180 | TCPServer.allow_reuse_address = True 181 | server = LuminaServer(database, config, logger) 182 | 183 | # set ctrl-c handler 184 | signal.signal(signal.SIGINT, lambda sig,frame:signal_handler(sig, frame, server)) 185 | 186 | # start server 187 | server_thread = threading.Thread(target=server.serve_forever) 188 | server_thread.daemon = False 189 | server_thread.start() 190 | server_thread.join() 191 | 192 | server.database.close(save=True) 193 | 194 | if __name__ == "__main__": 195 | main() -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os, sys 3 | from setuptools import setup, find_packages 4 | 5 | setup(name='lumina', 6 | version='0.1', 7 | description='IDA lumina offline server', 8 | author='Synacktiv', 9 | author_email='johan.bonvicini@synacktiv.com', 10 | packages=find_packages(exclude=["tests"]), 11 | package_data={"lumina": [""]}, 12 | test_suite="tests", 13 | scripts=[], 14 | install_requires=["construct"], 15 | entry_points={ 16 | 'console_scripts' : ['lumina_server=lumina.lumina_server:main'] 17 | }) --------------------------------------------------------------------------------