|
| 1 | +#!/usr/bin/env python |
| 2 | +''' |
| 3 | +Parse and display license information from an IDA Pro database. |
| 4 | +
|
| 5 | +author: Willi Ballenthin |
| 6 | +email: willi.ballenthin@gmail.com |
| 7 | +''' |
| 8 | +import sys |
| 9 | +import struct |
| 10 | +import logging |
| 11 | +import datetime |
| 12 | + |
| 13 | +import argparse |
| 14 | + |
| 15 | +import idb |
| 16 | +import idb.netnode |
| 17 | + |
| 18 | + |
| 19 | +logger = logging.getLogger(__name__) |
| 20 | + |
| 21 | + |
| 22 | +def is_encrypted(buf): |
| 23 | + return buf.find(b'\x00' * 4) >= 0x80 |
| 24 | + |
| 25 | + |
| 26 | +HEXRAYS_PUBKEY = 0x93AF7A8E3A6EB93D1B4D1FB7EC29299D2BC8F3CE5F84BFE88E47DDBDD5550C3CE3D2B16A2E2FBD0FBD919E8038BB05752EC92DD1498CB283AA087A93184F1DD9DD5D5DF7857322DFCD70890F814B58448071BBABB0FC8A7868B62EB29CC2664C8FE61DFBC5DB0EE8BF6ECF0B65250514576C4384582211896E5478F95C42FDED |
| 27 | + |
| 28 | + |
| 29 | +def decrypt(buf): |
| 30 | + ''' |
| 31 | + decrypt the given 1024-bit blob using Hex-Ray's public key. |
| 32 | +
|
| 33 | + i'm not sure from where this public key originally came. |
| 34 | + the algorithm is derived from here: |
| 35 | + https://github.com/nlitsme/pyidbutil/blob/87cb3235a462774eedfafca00f67c3ce01eeb326/idbtool.py#L43 |
| 36 | +
|
| 37 | + Args: |
| 38 | + buf (bytes): at least 0x80 bytes, of which the first 1024 bits will be decrypted. |
| 39 | +
|
| 40 | + Returns: |
| 41 | + bytes: 0x80 bytes of decrypted data. |
| 42 | + ''' |
| 43 | + enc = int.from_bytes(buf[:0x80], 'little') |
| 44 | + dec = pow(enc, 0x13, HEXRAYS_PUBKEY) |
| 45 | + return dec.to_bytes(0x80, 'big') |
| 46 | + |
| 47 | + |
| 48 | +def parse_user_data(buf): |
| 49 | + ''' |
| 50 | + parse a decrypted user blob into a structured dictionary. |
| 51 | +
|
| 52 | + Args: |
| 53 | + buf (bytes): exactly 0x80 bytes of plaintext data. |
| 54 | +
|
| 55 | + Returns: |
| 56 | + Dict[str, Any]: a dictionary with the following values: |
| 57 | + - ts1 (datetime.datetime): timestamp in UTC of something. database creation? |
| 58 | + - ts2 (datetime.datetime): timestamp in UTC of something. sometimes zero. |
| 59 | + - id (str): the ID of the license. |
| 60 | + - name (str): the name of the user and organization that owns the license. |
| 61 | + ''' |
| 62 | + if len(buf) != 0x80: |
| 63 | + raise ValueError('invalid user blob.') |
| 64 | + |
| 65 | + version = struct.unpack_from('<H', buf, 0x3) |
| 66 | + if version == 0: |
| 67 | + raise NotImplementedError('user blob version not supported.') |
| 68 | + |
| 69 | + ts1, _, ts2 = struct.unpack_from('<III', buf, 0x11) |
| 70 | + id = "%02X-%02X%02X-%02X%02X-%02X" % struct.unpack_from("6B", buf, 0x1D) |
| 71 | + name = buf[0x23:buf.find(b'\x00', 0x23)].decode('utf-8') |
| 72 | + |
| 73 | + return { |
| 74 | + # unknown if these are in UTC or not. right now, assuming so. |
| 75 | + 'ts1': datetime.datetime.utcfromtimestamp(ts1), |
| 76 | + 'ts2': datetime.datetime.utcfromtimestamp(ts2), |
| 77 | + 'id': id, |
| 78 | + 'name': name, |
| 79 | + } |
| 80 | + |
| 81 | + |
| 82 | +def get_userdata(netnode): |
| 83 | + ''' |
| 84 | + fetch, decrypt, and parse the user data from the given netnode. |
| 85 | +
|
| 86 | + Args: |
| 87 | + netnode (ida_netnode.Netnode): the netnode containing the user data. |
| 88 | +
|
| 89 | + Returns: |
| 90 | + dict[str, Any]: see `parse_user_data`. |
| 91 | + ''' |
| 92 | + userdata = netnode.supval(0x0) |
| 93 | + |
| 94 | + if is_encrypted(userdata): |
| 95 | + userdata = decrypt(userdata) |
| 96 | + else: |
| 97 | + userdata = userdata[:0x80] |
| 98 | + |
| 99 | + return parse_user_data(userdata) |
| 100 | + |
| 101 | + |
| 102 | +def main(argv=None): |
| 103 | + if argv is None: |
| 104 | + argv = sys.argv[1:] |
| 105 | + |
| 106 | + parser = argparse.ArgumentParser(description="Parse and display license information from an IDA Pro database.") |
| 107 | + parser.add_argument("idbpath", type=str, |
| 108 | + help="Path to input idb file") |
| 109 | + parser.add_argument("-v", "--verbose", action="store_true", |
| 110 | + help="Enable debug logging") |
| 111 | + parser.add_argument("-q", "--quiet", action="store_true", |
| 112 | + help="Disable all output but errors") |
| 113 | + args = parser.parse_args(args=argv) |
| 114 | + |
| 115 | + if args.verbose: |
| 116 | + logging.basicConfig(level=logging.DEBUG) |
| 117 | + logging.getLogger().setLevel(logging.DEBUG) |
| 118 | + elif args.quiet: |
| 119 | + logging.basicConfig(level=logging.ERROR) |
| 120 | + logging.getLogger().setLevel(logging.ERROR) |
| 121 | + else: |
| 122 | + logging.basicConfig(level=logging.INFO) |
| 123 | + logging.getLogger().setLevel(logging.INFO) |
| 124 | + |
| 125 | + with idb.from_file(args.idbpath) as db: |
| 126 | + api = idb.IDAPython(db) |
| 127 | + data = get_userdata(api.ida_netnode.netnode('$ original user')) |
| 128 | + |
| 129 | + print('user: %s' % data['name']) |
| 130 | + print('id: %s' % data['id']) |
| 131 | + print('ts1: %s' % data['ts1'].isoformat(' ') + 'Z') |
| 132 | + print('ts2: %s' % data['ts2'].isoformat(' ') + 'Z') |
| 133 | + |
| 134 | + return 0 |
| 135 | + |
| 136 | + |
| 137 | +if __name__ == "__main__": |
| 138 | + sys.exit(main()) |
0 commit comments