|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +FindAES - Python rewrite of FindAES v1.2 by Jesse Kornblum |
| 4 | +Original C code: http://jessekornblum.com/tools/findaes/ |
| 5 | +This code is public domain. |
| 6 | +
|
| 7 | +Searches binary files for AES-128, AES-192, and AES-256 key schedules. |
| 8 | +""" |
| 9 | + |
| 10 | +import sys |
| 11 | +import struct |
| 12 | + |
| 13 | +# Key sizes in bytes |
| 14 | +AES128_KEY_SIZE = 16 |
| 15 | +AES192_KEY_SIZE = 24 |
| 16 | +AES256_KEY_SIZE = 32 |
| 17 | + |
| 18 | +# Key schedule sizes in bytes |
| 19 | +AES128_KEY_SCHEDULE_SIZE = 176 |
| 20 | +AES192_KEY_SCHEDULE_SIZE = 208 |
| 21 | +AES256_KEY_SCHEDULE_SIZE = 240 |
| 22 | + |
| 23 | +BUFFER_SIZE = 10 * 1024 * 1024 # 10 MB |
| 24 | +WINDOW_SIZE = AES256_KEY_SCHEDULE_SIZE |
| 25 | + |
| 26 | +# --------------------------------------------------------------------------- |
| 27 | +# AES math helpers |
| 28 | +# --------------------------------------------------------------------------- |
| 29 | + |
| 30 | +def _gmul(a: int, b: int) -> int: |
| 31 | + """Galois field GF(2^8) multiplication.""" |
| 32 | + p = 0 |
| 33 | + for _ in range(8): |
| 34 | + if b & 1: |
| 35 | + p ^= a |
| 36 | + hi = a & 0x80 |
| 37 | + a = (a << 1) & 0xFF |
| 38 | + if hi: |
| 39 | + a ^= 0x1B |
| 40 | + b >>= 1 |
| 41 | + return p |
| 42 | + |
| 43 | + |
| 44 | +def _rcon(n: int) -> int: |
| 45 | + """AES round constant.""" |
| 46 | + if n == 0: |
| 47 | + return 0 |
| 48 | + c = 1 |
| 49 | + for _ in range(n - 1): |
| 50 | + c = _gmul(c, 2) |
| 51 | + return c |
| 52 | + |
| 53 | + |
| 54 | +# Log / anti-log tables (generator 0xe5 = 229) |
| 55 | +_LTABLE = bytes([ |
| 56 | + 0x00, 0xff, 0xc8, 0x08, 0x91, 0x10, 0xd0, 0x36, |
| 57 | + 0x5a, 0x3e, 0xd8, 0x43, 0x99, 0x77, 0xfe, 0x18, |
| 58 | + 0x23, 0x20, 0x07, 0x70, 0xa1, 0x6c, 0x0c, 0x7f, |
| 59 | + 0x62, 0x8b, 0x40, 0x46, 0xc7, 0x4b, 0xe0, 0x0e, |
| 60 | + 0xeb, 0x16, 0xe8, 0xad, 0xcf, 0xcd, 0x39, 0x53, |
| 61 | + 0x6a, 0x27, 0x35, 0x93, 0xd4, 0x4e, 0x48, 0xc3, |
| 62 | + 0x2b, 0x79, 0x54, 0x28, 0x09, 0x78, 0x0f, 0x21, |
| 63 | + 0x90, 0x87, 0x14, 0x2a, 0xa9, 0x9c, 0xd6, 0x74, |
| 64 | + 0xb4, 0x7c, 0xde, 0xed, 0xb1, 0x86, 0x76, 0xa4, |
| 65 | + 0x98, 0xe2, 0x96, 0x8f, 0x02, 0x32, 0x1c, 0xc1, |
| 66 | + 0x33, 0xee, 0xef, 0x81, 0xfd, 0x30, 0x5c, 0x13, |
| 67 | + 0x9d, 0x29, 0x17, 0xc4, 0x11, 0x44, 0x8c, 0x80, |
| 68 | + 0xf3, 0x73, 0x42, 0x1e, 0x1d, 0xb5, 0xf0, 0x12, |
| 69 | + 0xd1, 0x5b, 0x41, 0xa2, 0xd7, 0x2c, 0xe9, 0xd5, |
| 70 | + 0x59, 0xcb, 0x50, 0xa8, 0xdc, 0xfc, 0xf2, 0x56, |
| 71 | + 0x72, 0xa6, 0x65, 0x2f, 0x9f, 0x9b, 0x3d, 0xba, |
| 72 | + 0x7d, 0xc2, 0x45, 0x82, 0xa7, 0x57, 0xb6, 0xa3, |
| 73 | + 0x7a, 0x75, 0x4f, 0xae, 0x3f, 0x37, 0x6d, 0x47, |
| 74 | + 0x61, 0xbe, 0xab, 0xd3, 0x5f, 0xb0, 0x58, 0xaf, |
| 75 | + 0xca, 0x5e, 0xfa, 0x85, 0xe4, 0x4d, 0x8a, 0x05, |
| 76 | + 0xfb, 0x60, 0xb7, 0x7b, 0xb8, 0x26, 0x4a, 0x67, |
| 77 | + 0xc6, 0x1a, 0xf8, 0x69, 0x25, 0xb3, 0xdb, 0xbd, |
| 78 | + 0x66, 0xdd, 0xf1, 0xd2, 0xdf, 0x03, 0x8d, 0x34, |
| 79 | + 0xd9, 0x92, 0x0d, 0x63, 0x55, 0xaa, 0x49, 0xec, |
| 80 | + 0xbc, 0x95, 0x3c, 0x84, 0x0b, 0xf5, 0xe6, 0xe7, |
| 81 | + 0xe5, 0xac, 0x7e, 0x6e, 0xb9, 0xf9, 0xda, 0x8e, |
| 82 | + 0x9a, 0xc9, 0x24, 0xe1, 0x0a, 0x15, 0x6b, 0x3a, |
| 83 | + 0xa0, 0x51, 0xf4, 0xea, 0xb2, 0x97, 0x9e, 0x5d, |
| 84 | + 0x22, 0x88, 0x94, 0xce, 0x19, 0x01, 0x71, 0x4c, |
| 85 | + 0xa5, 0xe3, 0xc5, 0x31, 0xbb, 0xcc, 0x1f, 0x2d, |
| 86 | + 0x3b, 0x52, 0x6f, 0xf6, 0x2e, 0x89, 0xf7, 0xc0, |
| 87 | + 0x68, 0x1b, 0x64, 0x04, 0x06, 0xbf, 0x83, 0x38, |
| 88 | +]) |
| 89 | + |
| 90 | +_ATABLE = bytes([ |
| 91 | + 0x01, 0xe5, 0x4c, 0xb5, 0xfb, 0x9f, 0xfc, 0x12, |
| 92 | + 0x03, 0x34, 0xd4, 0xc4, 0x16, 0xba, 0x1f, 0x36, |
| 93 | + 0x05, 0x5c, 0x67, 0x57, 0x3a, 0xd5, 0x21, 0x5a, |
| 94 | + 0x0f, 0xe4, 0xa9, 0xf9, 0x4e, 0x64, 0x63, 0xee, |
| 95 | + 0x11, 0x37, 0xe0, 0x10, 0xd2, 0xac, 0xa5, 0x29, |
| 96 | + 0x33, 0x59, 0x3b, 0x30, 0x6d, 0xef, 0xf4, 0x7b, |
| 97 | + 0x55, 0xeb, 0x4d, 0x50, 0xb7, 0x2a, 0x07, 0x8d, |
| 98 | + 0xff, 0x26, 0xd7, 0xf0, 0xc2, 0x7e, 0x09, 0x8c, |
| 99 | + 0x1a, 0x6a, 0x62, 0x0b, 0x5d, 0x82, 0x1b, 0x8f, |
| 100 | + 0x2e, 0xbe, 0xa6, 0x1d, 0xe7, 0x9d, 0x2d, 0x8a, |
| 101 | + 0x72, 0xd9, 0xf1, 0x27, 0x32, 0xbc, 0x77, 0x85, |
| 102 | + 0x96, 0x70, 0x08, 0x69, 0x56, 0xdf, 0x99, 0x94, |
| 103 | + 0xa1, 0x90, 0x18, 0xbb, 0xfa, 0x7a, 0xb0, 0xa7, |
| 104 | + 0xf8, 0xab, 0x28, 0xd6, 0x15, 0x8e, 0xcb, 0xf2, |
| 105 | + 0x13, 0xe6, 0x78, 0x61, 0x3f, 0x89, 0x46, 0x0d, |
| 106 | + 0x35, 0x31, 0x88, 0xa3, 0x41, 0x80, 0xca, 0x17, |
| 107 | + 0x5f, 0x53, 0x83, 0xfe, 0xc3, 0x9b, 0x45, 0x39, |
| 108 | + 0xe1, 0xf5, 0x9e, 0x19, 0x5e, 0xb6, 0xcf, 0x4b, |
| 109 | + 0x38, 0x04, 0xb9, 0x2b, 0xe2, 0xc1, 0x4a, 0xdd, |
| 110 | + 0x48, 0x0c, 0xd0, 0x7d, 0x3d, 0x58, 0xde, 0x7c, |
| 111 | + 0xd8, 0x14, 0x6b, 0x87, 0x47, 0xe8, 0x79, 0x84, |
| 112 | + 0x73, 0x3c, 0xbd, 0x92, 0xc9, 0x23, 0x8b, 0x97, |
| 113 | + 0x95, 0x44, 0xdc, 0xad, 0x40, 0x65, 0x86, 0xa2, |
| 114 | + 0xa4, 0xcc, 0x7f, 0xec, 0xc0, 0xaf, 0x91, 0xfd, |
| 115 | + 0xf7, 0x4f, 0x81, 0x2f, 0x5b, 0xea, 0xa8, 0x1c, |
| 116 | + 0x02, 0xd1, 0x98, 0x71, 0xed, 0x25, 0xe3, 0x24, |
| 117 | + 0x06, 0x68, 0xb3, 0x93, 0x2c, 0x6f, 0x3e, 0x6c, |
| 118 | + 0x0a, 0xb8, 0xce, 0xae, 0x74, 0xb1, 0x42, 0xb4, |
| 119 | + 0x1e, 0xd3, 0x49, 0xe9, 0x9c, 0xc8, 0xc6, 0xc7, |
| 120 | + 0x22, 0x6e, 0xdb, 0x20, 0xbf, 0x43, 0x51, 0x52, |
| 121 | + 0x66, 0xb2, 0x76, 0x60, 0xda, 0xc5, 0xf3, 0xf6, |
| 122 | + 0xaa, 0xcd, 0x9a, 0xa0, 0x75, 0x54, 0x0e, 0x01, |
| 123 | +]) |
| 124 | + |
| 125 | + |
| 126 | +def _gmul_inverse(v: int) -> int: |
| 127 | + if v == 0: |
| 128 | + return 0 |
| 129 | + return _ATABLE[255 - _LTABLE[v]] |
| 130 | + |
| 131 | + |
| 132 | +def _sbox(v: int) -> int: |
| 133 | + s = x = _gmul_inverse(v) |
| 134 | + for _ in range(4): |
| 135 | + s = ((s << 1) | (s >> 7)) & 0xFF |
| 136 | + x ^= s |
| 137 | + return x ^ 0x63 |
| 138 | + |
| 139 | + |
| 140 | +# Pre-compute the full S-box for speed |
| 141 | +_SBOX = bytes(_sbox(i) for i in range(256)) |
| 142 | + |
| 143 | + |
| 144 | +def _schedule_core(word: bytearray, i: int) -> bytearray: |
| 145 | + """Rotate left by 1 byte, apply S-box, XOR first byte with rcon(i).""" |
| 146 | + word = bytearray([word[1], word[2], word[3], word[0]]) |
| 147 | + word = bytearray(_SBOX[b] for b in word) |
| 148 | + word[0] ^= _rcon(i) |
| 149 | + return word |
| 150 | + |
| 151 | + |
| 152 | +# --------------------------------------------------------------------------- |
| 153 | +# Key schedule validators |
| 154 | +# --------------------------------------------------------------------------- |
| 155 | + |
| 156 | +def valid_aes128_schedule(data: bytes | bytearray) -> bool: |
| 157 | + """Return True if data[0:176] is a valid AES-128 key schedule.""" |
| 158 | + if len(data) < AES128_KEY_SCHEDULE_SIZE: |
| 159 | + return False |
| 160 | + computed = bytearray(data[:AES128_KEY_SIZE]) |
| 161 | + pos = AES128_KEY_SIZE |
| 162 | + i = 1 |
| 163 | + while pos < AES128_KEY_SCHEDULE_SIZE: |
| 164 | + t = bytearray(computed[pos - 4:pos]) |
| 165 | + if pos % AES128_KEY_SIZE == 0: |
| 166 | + t = _schedule_core(t, i) |
| 167 | + i += 1 |
| 168 | + for a in range(4): |
| 169 | + val = computed[pos - AES128_KEY_SIZE] ^ t[a] |
| 170 | + if val != data[pos]: |
| 171 | + return False |
| 172 | + computed.append(val) |
| 173 | + pos += 1 |
| 174 | + return True |
| 175 | + |
| 176 | + |
| 177 | +def valid_aes192_schedule(data: bytes | bytearray) -> bool: |
| 178 | + """Return True if data[0:208] is a valid AES-192 key schedule.""" |
| 179 | + if len(data) < AES192_KEY_SCHEDULE_SIZE: |
| 180 | + return False |
| 181 | + computed = bytearray(data[:AES192_KEY_SIZE]) |
| 182 | + pos = AES192_KEY_SIZE |
| 183 | + i = 0 |
| 184 | + while pos < AES192_KEY_SCHEDULE_SIZE: |
| 185 | + t = bytearray(computed[pos - 4:pos]) |
| 186 | + if pos % AES192_KEY_SIZE == 0: |
| 187 | + t = _schedule_core(t, i) |
| 188 | + i += 1 |
| 189 | + for a in range(4): |
| 190 | + val = computed[pos - AES192_KEY_SIZE] ^ t[a] |
| 191 | + if val != data[pos]: |
| 192 | + return False |
| 193 | + computed.append(val) |
| 194 | + pos += 1 |
| 195 | + return True |
| 196 | + |
| 197 | + |
| 198 | +def valid_aes256_schedule(data: bytes | bytearray) -> bool: |
| 199 | + """Return True if data[0:240] is a valid AES-256 key schedule.""" |
| 200 | + if len(data) < AES256_KEY_SCHEDULE_SIZE: |
| 201 | + return False |
| 202 | + computed = bytearray(data[:AES256_KEY_SIZE]) |
| 203 | + pos = AES256_KEY_SIZE |
| 204 | + i = 1 |
| 205 | + while pos < AES256_KEY_SCHEDULE_SIZE: |
| 206 | + t = bytearray(computed[pos - 4:pos]) |
| 207 | + if pos % AES256_KEY_SIZE == 0: |
| 208 | + t = _schedule_core(t, i) |
| 209 | + i += 1 |
| 210 | + elif pos % AES256_KEY_SIZE == 16: |
| 211 | + t = bytearray(_SBOX[b] for b in t) |
| 212 | + for a in range(4): |
| 213 | + val = computed[pos - AES256_KEY_SIZE] ^ t[a] |
| 214 | + if val != data[pos]: |
| 215 | + return False |
| 216 | + computed.append(val) |
| 217 | + pos += 1 |
| 218 | + return True |
| 219 | + |
| 220 | + |
| 221 | +# --------------------------------------------------------------------------- |
| 222 | +# Entropy / frequency filter |
| 223 | +# --------------------------------------------------------------------------- |
| 224 | + |
| 225 | +class EntropyChecker: |
| 226 | + """ |
| 227 | + Sliding-window entropy check. Returns True (high entropy / skip) when |
| 228 | + any single byte value appears more than 8 times in the window. |
| 229 | + This mirrors the C implementation's logic exactly. |
| 230 | + """ |
| 231 | + |
| 232 | + def __init__(self): |
| 233 | + self._count = [0] * 256 |
| 234 | + self._initialized = False |
| 235 | + |
| 236 | + def check(self, buf: bytes | bytearray, pos: int, window: int, first: bool) -> bool: |
| 237 | + """ |
| 238 | + Check the window buf[pos : pos+window]. |
| 239 | + `first` signals that this is the very first call (resets state). |
| 240 | + Returns True if entropy is too low (any byte repeats > 8 times). |
| 241 | + """ |
| 242 | + if first: |
| 243 | + self._initialized = False |
| 244 | + |
| 245 | + if not self._initialized: |
| 246 | + self._initialized = True |
| 247 | + self._count = [0] * 256 |
| 248 | + for b in buf[pos:pos + window]: |
| 249 | + self._count[b] += 1 |
| 250 | + |
| 251 | + result = any(c > 8 for c in self._count) |
| 252 | + |
| 253 | + # Slide the window: remove buf[pos], add buf[pos+window] |
| 254 | + self._count[buf[pos]] -= 1 |
| 255 | + if pos + window < len(buf): |
| 256 | + self._count[buf[pos + window]] += 1 |
| 257 | + |
| 258 | + return result |
| 259 | + |
| 260 | + |
| 261 | +# --------------------------------------------------------------------------- |
| 262 | +# Buffer / file scanning |
| 263 | +# --------------------------------------------------------------------------- |
| 264 | + |
| 265 | +def display_key(key: bytes | bytearray, size: int) -> str: |
| 266 | + return " ".join(f"{b:02x}" for b in key[:size]) |
| 267 | + |
| 268 | + |
| 269 | +def scan_buffer(buf: bytes | bytearray, size: int, offset: int) -> None: |
| 270 | + checker = EntropyChecker() |
| 271 | + for pos in range(size): |
| 272 | + first = (offset + pos) == 0 |
| 273 | + if checker.check(buf, pos, AES128_KEY_SCHEDULE_SIZE, first): |
| 274 | + continue |
| 275 | + window = buf[pos:] |
| 276 | + if valid_aes128_schedule(window): |
| 277 | + print(f"Found AES-128 key schedule at offset 0x{offset + pos:x}:") |
| 278 | + print(display_key(window, AES128_KEY_SIZE)) |
| 279 | + if valid_aes192_schedule(window): |
| 280 | + print(f"Found AES-192 key schedule at offset 0x{offset + pos:x}:") |
| 281 | + print(display_key(window, AES192_KEY_SIZE)) |
| 282 | + if valid_aes256_schedule(window): |
| 283 | + print(f"Found AES-256 key schedule at offset 0x{offset + pos:x}:") |
| 284 | + print(display_key(window, AES256_KEY_SIZE)) |
| 285 | + |
| 286 | + |
| 287 | +def scan_file(path: str) -> bool: |
| 288 | + """Scan a file for AES key schedules using a sliding-window approach.""" |
| 289 | + try: |
| 290 | + handle = open(path, "rb") |
| 291 | + except OSError as e: |
| 292 | + print(e, file=sys.stderr) |
| 293 | + return True |
| 294 | + |
| 295 | + print(f"Searching {path}") |
| 296 | + offset = 0 |
| 297 | + carry = bytearray(WINDOW_SIZE) # overlap from previous chunk |
| 298 | + |
| 299 | + with handle: |
| 300 | + while True: |
| 301 | + raw = handle.read(BUFFER_SIZE) |
| 302 | + if not raw: |
| 303 | + break |
| 304 | + buf = bytes(carry) + raw |
| 305 | + bytes_read = len(raw) |
| 306 | + |
| 307 | + if offset == 0: |
| 308 | + size = bytes_read if bytes_read < BUFFER_SIZE else bytes_read - WINDOW_SIZE |
| 309 | + scan_buffer(buf[WINDOW_SIZE:], size, 0) |
| 310 | + else: |
| 311 | + scan_buffer(buf, bytes_read, offset - WINDOW_SIZE) |
| 312 | + |
| 313 | + offset += bytes_read |
| 314 | + carry = bytearray(buf[-WINDOW_SIZE:]) |
| 315 | + |
| 316 | + return False |
| 317 | + |
| 318 | + |
| 319 | +# --------------------------------------------------------------------------- |
| 320 | +# Entry point |
| 321 | +# --------------------------------------------------------------------------- |
| 322 | + |
| 323 | +def main() -> int: |
| 324 | + if len(sys.argv) < 2: |
| 325 | + print("FindAES (Python) - Searches for AES-128, AES-192, and AES-256 keys\n") |
| 326 | + print("Usage: findaes.py [FILES]") |
| 327 | + return 1 |
| 328 | + |
| 329 | + for path in sys.argv[1:]: |
| 330 | + scan_file(path) |
| 331 | + |
| 332 | + return 0 |
| 333 | + |
| 334 | + |
| 335 | +if __name__ == "__main__": |
| 336 | + sys.exit(main()) |
0 commit comments