ai_root_shield 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +56 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +88 -0
- data/LICENSE +21 -0
- data/README.md +310 -0
- data/Rakefile +36 -0
- data/examples/device_logs/clean_device.json +74 -0
- data/examples/device_logs/rooted_android.json +93 -0
- data/exe/ai_root_shield +155 -0
- data/lib/ai_root_shield/analyzers/emulator_detector.rb +331 -0
- data/lib/ai_root_shield/analyzers/hooking_detector.rb +375 -0
- data/lib/ai_root_shield/analyzers/integrity_checker.rb +407 -0
- data/lib/ai_root_shield/analyzers/network_analyzer.rb +352 -0
- data/lib/ai_root_shield/analyzers/root_detector.rb +292 -0
- data/lib/ai_root_shield/detector.rb +78 -0
- data/lib/ai_root_shield/device_log_parser.rb +118 -0
- data/lib/ai_root_shield/risk_calculator.rb +161 -0
- data/lib/ai_root_shield/version.rb +5 -0
- data/lib/ai_root_shield.rb +36 -0
- metadata +179 -0
@@ -0,0 +1,407 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "openssl"
|
5
|
+
|
6
|
+
module AiRootShield
|
7
|
+
module Analyzers
|
8
|
+
# Checks application integrity and detects repackaging/tampering
|
9
|
+
class IntegrityChecker
|
10
|
+
# Known repackaging indicators
|
11
|
+
REPACKAGING_INDICATORS = %w[
|
12
|
+
test-keys
|
13
|
+
platform.pk8
|
14
|
+
shared.pk8
|
15
|
+
media.pk8
|
16
|
+
debug.keystore
|
17
|
+
androiddebugkey
|
18
|
+
unsigned
|
19
|
+
CERT.RSA
|
20
|
+
CERT.DSA
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
# Suspicious certificate authorities
|
24
|
+
SUSPICIOUS_CAS = %w[
|
25
|
+
CN=Android Debug
|
26
|
+
CN=Test
|
27
|
+
CN=Debug
|
28
|
+
O=Android
|
29
|
+
OU=Android
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
def analyze(device_data)
|
33
|
+
factors = []
|
34
|
+
risk_score = 0
|
35
|
+
|
36
|
+
# Check application signatures
|
37
|
+
signature_result = check_app_signatures(device_data)
|
38
|
+
factors.concat(signature_result[:factors])
|
39
|
+
risk_score += signature_result[:risk_score]
|
40
|
+
|
41
|
+
# Check for repackaging indicators
|
42
|
+
repackaging_result = check_repackaging_indicators(device_data)
|
43
|
+
factors.concat(repackaging_result[:factors])
|
44
|
+
risk_score += repackaging_result[:risk_score]
|
45
|
+
|
46
|
+
# Check file integrity
|
47
|
+
integrity_result = check_file_integrity(device_data)
|
48
|
+
factors.concat(integrity_result[:factors])
|
49
|
+
risk_score += integrity_result[:risk_score]
|
50
|
+
|
51
|
+
# Check for code injection
|
52
|
+
injection_result = check_code_injection(device_data)
|
53
|
+
factors.concat(injection_result[:factors])
|
54
|
+
risk_score += injection_result[:risk_score]
|
55
|
+
|
56
|
+
{
|
57
|
+
factors: factors,
|
58
|
+
risk_score: [risk_score, 100].min
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def check_app_signatures(device_data)
|
65
|
+
factors = []
|
66
|
+
risk_score = 0
|
67
|
+
|
68
|
+
certificates = device_data[:certificates] || []
|
69
|
+
|
70
|
+
certificates.each do |cert|
|
71
|
+
next unless cert.is_a?(Hash)
|
72
|
+
|
73
|
+
# Check for debug certificates
|
74
|
+
if debug_certificate?(cert)
|
75
|
+
factors << "DEBUG_CERTIFICATE_DETECTED"
|
76
|
+
risk_score += 15
|
77
|
+
end
|
78
|
+
|
79
|
+
# Check for suspicious issuers
|
80
|
+
if suspicious_issuer?(cert)
|
81
|
+
factors << "SUSPICIOUS_CERTIFICATE_ISSUER"
|
82
|
+
risk_score += 12
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check certificate validity
|
86
|
+
if expired_certificate?(cert)
|
87
|
+
factors << "EXPIRED_CERTIFICATE"
|
88
|
+
risk_score += 8
|
89
|
+
end
|
90
|
+
|
91
|
+
# Check for self-signed certificates
|
92
|
+
if self_signed_certificate?(cert)
|
93
|
+
factors << "SELF_SIGNED_CERTIFICATE"
|
94
|
+
risk_score += 10
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Check for missing signatures
|
99
|
+
if certificates.empty?
|
100
|
+
factors << "MISSING_SIGNATURES"
|
101
|
+
risk_score += 20
|
102
|
+
end
|
103
|
+
|
104
|
+
{
|
105
|
+
factors: factors,
|
106
|
+
risk_score: risk_score
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_repackaging_indicators(device_data)
|
111
|
+
factors = []
|
112
|
+
risk_score = 0
|
113
|
+
|
114
|
+
# Check system info for repackaging signs
|
115
|
+
system_info = device_data[:system_info] || {}
|
116
|
+
|
117
|
+
# Check build fingerprint
|
118
|
+
fingerprint = system_info[:build_fingerprint].to_s
|
119
|
+
REPACKAGING_INDICATORS.each do |indicator|
|
120
|
+
if fingerprint.downcase.include?(indicator.downcase)
|
121
|
+
factors << "REPACKAGED_APP"
|
122
|
+
risk_score += 18
|
123
|
+
break
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check installed packages for suspicious signatures
|
128
|
+
packages = device_data[:installed_packages] || []
|
129
|
+
|
130
|
+
packages.each do |package|
|
131
|
+
next unless package.is_a?(Hash)
|
132
|
+
|
133
|
+
signature = package["signature"] || package["cert"]
|
134
|
+
next unless signature
|
135
|
+
|
136
|
+
REPACKAGING_INDICATORS.each do |indicator|
|
137
|
+
if signature.to_s.downcase.include?(indicator.downcase)
|
138
|
+
factors << "PACKAGE_REPACKAGED"
|
139
|
+
risk_score += 15
|
140
|
+
break
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Check for multiple signatures on same package
|
146
|
+
signature_counts = {}
|
147
|
+
packages.each do |package|
|
148
|
+
next unless package.is_a?(Hash)
|
149
|
+
|
150
|
+
pkg_name = package["name"]
|
151
|
+
signature = package["signature"]
|
152
|
+
|
153
|
+
next unless pkg_name && signature
|
154
|
+
|
155
|
+
signature_counts[pkg_name] ||= []
|
156
|
+
signature_counts[pkg_name] << signature
|
157
|
+
end
|
158
|
+
|
159
|
+
signature_counts.each do |pkg_name, signatures|
|
160
|
+
if signatures.uniq.length > 1
|
161
|
+
factors << "MULTIPLE_SIGNATURES_DETECTED"
|
162
|
+
risk_score += 12
|
163
|
+
break
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
{
|
168
|
+
factors: factors,
|
169
|
+
risk_score: risk_score
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
def check_file_integrity(device_data)
|
174
|
+
factors = []
|
175
|
+
risk_score = 0
|
176
|
+
|
177
|
+
file_system = device_data[:file_system] || {}
|
178
|
+
|
179
|
+
# Check for modified system files
|
180
|
+
system_binaries = file_system[:system_binaries] || []
|
181
|
+
|
182
|
+
# Look for unexpected modifications in system directories
|
183
|
+
writable_system_dirs = file_system[:writable_system_dirs] || []
|
184
|
+
|
185
|
+
if writable_system_dirs.any?
|
186
|
+
factors << "SYSTEM_PARTITION_WRITABLE"
|
187
|
+
risk_score += 15
|
188
|
+
end
|
189
|
+
|
190
|
+
# Check for suspicious file permissions
|
191
|
+
suspicious_files = file_system[:suspicious_files] || []
|
192
|
+
|
193
|
+
suspicious_files.each do |file_info|
|
194
|
+
next unless file_info.is_a?(Hash)
|
195
|
+
|
196
|
+
permissions = file_info["permissions"]
|
197
|
+
path = file_info["path"]
|
198
|
+
|
199
|
+
# Check for world-writable system files
|
200
|
+
if permissions && permissions.include?("777") && path&.start_with?("/system")
|
201
|
+
factors << "SUSPICIOUS_FILE_PERMISSIONS"
|
202
|
+
risk_score += 10
|
203
|
+
break
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Check for DEX file modifications (Android)
|
208
|
+
if device_data[:platform] == "android"
|
209
|
+
dex_result = check_dex_integrity(device_data)
|
210
|
+
factors.concat(dex_result[:factors])
|
211
|
+
risk_score += dex_result[:risk_score]
|
212
|
+
end
|
213
|
+
|
214
|
+
# Check for bundle modifications (iOS)
|
215
|
+
if device_data[:platform] == "ios"
|
216
|
+
bundle_result = check_bundle_integrity(device_data)
|
217
|
+
factors.concat(bundle_result[:factors])
|
218
|
+
risk_score += bundle_result[:risk_score]
|
219
|
+
end
|
220
|
+
|
221
|
+
{
|
222
|
+
factors: factors,
|
223
|
+
risk_score: risk_score
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def check_code_injection(device_data)
|
228
|
+
factors = []
|
229
|
+
risk_score = 0
|
230
|
+
|
231
|
+
processes = device_data[:processes] || []
|
232
|
+
|
233
|
+
processes.each do |process|
|
234
|
+
next unless process.is_a?(Hash)
|
235
|
+
|
236
|
+
# Check for unexpected loaded libraries
|
237
|
+
libraries = process["loaded_libraries"] || []
|
238
|
+
memory_maps = process["memory_maps"] || []
|
239
|
+
|
240
|
+
# Look for libraries loaded from suspicious locations
|
241
|
+
suspicious_locations = %w[/data/local/tmp /sdcard /cache /data/data]
|
242
|
+
|
243
|
+
libraries.each do |lib|
|
244
|
+
suspicious_locations.each do |location|
|
245
|
+
if lib.to_s.start_with?(location)
|
246
|
+
factors << "SUSPICIOUS_LIBRARY_INJECTION"
|
247
|
+
risk_score += 15
|
248
|
+
break
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Check for executable memory in data segments
|
254
|
+
memory_maps.each do |map|
|
255
|
+
next unless map.is_a?(Hash)
|
256
|
+
|
257
|
+
permissions = map["permissions"]
|
258
|
+
path = map["path"]
|
259
|
+
|
260
|
+
if permissions&.include?("x") && path&.start_with?("/data")
|
261
|
+
factors << "EXECUTABLE_DATA_SEGMENT"
|
262
|
+
risk_score += 12
|
263
|
+
break
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Check system logs for injection indicators
|
269
|
+
logs = device_data[:logs] || []
|
270
|
+
|
271
|
+
injection_keywords = %w[
|
272
|
+
"code injection"
|
273
|
+
"library injection"
|
274
|
+
"process injection"
|
275
|
+
"dll injection"
|
276
|
+
"dylib injection"
|
277
|
+
]
|
278
|
+
|
279
|
+
injection_keywords.each do |keyword|
|
280
|
+
if logs.any? { |log| log.to_s.downcase.include?(keyword) }
|
281
|
+
factors << "CODE_INJECTION_DETECTED"
|
282
|
+
risk_score += 18
|
283
|
+
break
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
{
|
288
|
+
factors: factors,
|
289
|
+
risk_score: risk_score
|
290
|
+
}
|
291
|
+
end
|
292
|
+
|
293
|
+
def check_dex_integrity(device_data)
|
294
|
+
factors = []
|
295
|
+
risk_score = 0
|
296
|
+
|
297
|
+
packages = device_data[:installed_packages] || []
|
298
|
+
|
299
|
+
packages.each do |package|
|
300
|
+
next unless package.is_a?(Hash)
|
301
|
+
|
302
|
+
dex_hash = package["dex_hash"]
|
303
|
+
original_hash = package["original_dex_hash"]
|
304
|
+
|
305
|
+
if dex_hash && original_hash && dex_hash != original_hash
|
306
|
+
factors << "DEX_TAMPERED"
|
307
|
+
risk_score += 12
|
308
|
+
break
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Check for multiple DEX files (could indicate repackaging)
|
313
|
+
file_system = device_data[:file_system] || {}
|
314
|
+
suspicious_files = file_system[:suspicious_files] || []
|
315
|
+
|
316
|
+
dex_files = suspicious_files.select { |file| file.to_s.end_with?(".dex") }
|
317
|
+
|
318
|
+
if dex_files.length > 2 # Most apps have 1-2 DEX files normally
|
319
|
+
factors << "MULTIPLE_DEX_FILES"
|
320
|
+
risk_score += 8
|
321
|
+
end
|
322
|
+
|
323
|
+
{
|
324
|
+
factors: factors,
|
325
|
+
risk_score: risk_score
|
326
|
+
}
|
327
|
+
end
|
328
|
+
|
329
|
+
def check_bundle_integrity(device_data)
|
330
|
+
factors = []
|
331
|
+
risk_score = 0
|
332
|
+
|
333
|
+
packages = device_data[:installed_packages] || []
|
334
|
+
|
335
|
+
packages.each do |package|
|
336
|
+
next unless package.is_a?(Hash)
|
337
|
+
|
338
|
+
bundle_hash = package["bundle_hash"]
|
339
|
+
original_hash = package["original_bundle_hash"]
|
340
|
+
|
341
|
+
if bundle_hash && original_hash && bundle_hash != original_hash
|
342
|
+
factors << "BUNDLE_MODIFIED"
|
343
|
+
risk_score += 10
|
344
|
+
break
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Check for suspicious Info.plist modifications
|
349
|
+
file_system = device_data[:file_system] || {}
|
350
|
+
suspicious_files = file_system[:suspicious_files] || []
|
351
|
+
|
352
|
+
info_plist_files = suspicious_files.select { |file| file.to_s.include?("Info.plist") }
|
353
|
+
|
354
|
+
info_plist_files.each do |plist_file|
|
355
|
+
# This would typically check for modifications to critical plist values
|
356
|
+
# For now, we'll flag if there are multiple Info.plist files
|
357
|
+
if info_plist_files.length > 1
|
358
|
+
factors << "MULTIPLE_INFO_PLIST"
|
359
|
+
risk_score += 6
|
360
|
+
break
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
{
|
365
|
+
factors: factors,
|
366
|
+
risk_score: risk_score
|
367
|
+
}
|
368
|
+
end
|
369
|
+
|
370
|
+
def debug_certificate?(cert)
|
371
|
+
subject = cert["subject"].to_s
|
372
|
+
issuer = cert["issuer"].to_s
|
373
|
+
|
374
|
+
debug_indicators = ["debug", "test", "android debug", "development"]
|
375
|
+
|
376
|
+
debug_indicators.any? do |indicator|
|
377
|
+
subject.downcase.include?(indicator) || issuer.downcase.include?(indicator)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def suspicious_issuer?(cert)
|
382
|
+
issuer = cert["issuer"].to_s
|
383
|
+
|
384
|
+
SUSPICIOUS_CAS.any? { |ca| issuer.include?(ca) }
|
385
|
+
end
|
386
|
+
|
387
|
+
def expired_certificate?(cert)
|
388
|
+
not_after = cert["not_after"]
|
389
|
+
return false unless not_after
|
390
|
+
|
391
|
+
begin
|
392
|
+
expiry_date = Time.parse(not_after)
|
393
|
+
expiry_date < Time.now
|
394
|
+
rescue
|
395
|
+
false
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def self_signed_certificate?(cert)
|
400
|
+
subject = cert["subject"].to_s
|
401
|
+
issuer = cert["issuer"].to_s
|
402
|
+
|
403
|
+
subject == issuer
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|