Skip to content

Commit ad47185

Browse files
authored
Add files via upload
1 parent 472ace2 commit ad47185

File tree

1 file changed

+336
-0
lines changed

1 file changed

+336
-0
lines changed

crypto/findaes.py

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
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

Comments
 (0)