android_parser 2.5.1 → 2.7.0.beta1
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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/create_release.yml +15 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +4 -5
- data/README.md +1 -1
- data/android_parser.gemspec +3 -6
- data/docs/resources-arsc-reference.png +0 -0
- data/lib/android/apk.rb +91 -30
- data/lib/android/configuration.rb +13 -0
- data/lib/android/dex/info.rb +23 -6
- data/lib/android/manifest.rb +105 -5
- data/lib/android/null_logger.rb +65 -0
- data/lib/android/resource.rb +100 -29
- data/lib/android/utils.rb +6 -7
- data/lib/android_parser.rb +6 -0
- metadata +8 -5
- data/lib/ruby_apk.rb +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fe3ead9fd9c4393e0cf3fb39fae3f4ab4a99e68e30cbceae79a8861d451f3266
|
4
|
+
data.tar.gz: 2d80687dc01a1541d4f0b049d499e448eeee4f7413834fd603b5c7560b40048b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98d6c6286ec3c02a7939e94bdeff0d0316db3997df828af79bb5b585e4f698ac36cb63c419d66da0da97ba48595e33e3d405e8296de95161cb3a9d8003931233
|
7
|
+
data.tar.gz: bd31cc2455cf30dcb001cc838fa48608910a20aaaa7b9e74f020c4c6fb3ea07f19cc6d0e598813bccfcfec30765f1775968cf4cba0e436a58bc6ddb2548eba00
|
data/.github/workflows/ci.yml
CHANGED
@@ -10,12 +10,12 @@ jobs:
|
|
10
10
|
strategy:
|
11
11
|
fail-fast: false
|
12
12
|
matrix:
|
13
|
-
ruby: [ ruby-
|
13
|
+
ruby: [ ruby-3.1, ruby-3.2, ruby-3.3 ]
|
14
14
|
os: [ ubuntu-latest, macos-latest ]
|
15
15
|
runs-on: ${{ matrix.os }}
|
16
16
|
|
17
17
|
steps:
|
18
|
-
- uses: actions/checkout@
|
18
|
+
- uses: actions/checkout@v4
|
19
19
|
|
20
20
|
- uses: ruby/setup-ruby@v1
|
21
21
|
with:
|
@@ -0,0 +1,15 @@
|
|
1
|
+
name: Create Release
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
tags:
|
5
|
+
- "v*"
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
create:
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
steps:
|
11
|
+
- name: Create Release
|
12
|
+
uses: softprops/action-gh-release@v2
|
13
|
+
if: startsWith(github.ref, 'refs/tags/')
|
14
|
+
env:
|
15
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# ChangeLog
|
2
2
|
|
3
|
+
## 2.7.0.beta1
|
4
|
+
|
5
|
+
* Dropped Ruby 2.5~3.0 support, now requires Ruby 3.1+.
|
6
|
+
* Bugfix: [#7] parse library type to fix unknown chunk type 0x0203.
|
7
|
+
* Bugfix: fix 3bits of lang and country in locales.
|
8
|
+
* add fetch locales support.
|
9
|
+
* add architectures support.
|
10
|
+
* add detect universal apk.
|
11
|
+
|
12
|
+
## 2.6.0
|
13
|
+
|
14
|
+
* [#2] add queries component support.
|
15
|
+
* [#3] add detect enable kotlin code.
|
16
|
+
|
3
17
|
## 2.5.1
|
4
18
|
|
5
19
|
* Bugfix: Handle string resources referencing other resources.
|
data/Gemfile
CHANGED
@@ -3,8 +3,7 @@ source 'http://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in android_parser.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# end
|
6
|
+
group :development do
|
7
|
+
gem 'awesome_print'
|
8
|
+
gem 'debug'
|
9
|
+
end
|
data/README.md
CHANGED
data/android_parser.gemspec
CHANGED
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'android_parser'
|
8
|
-
spec.version = '2.
|
8
|
+
spec.version = '2.7.0.beta1'
|
9
9
|
spec.authors = ['SecureBrain', 'icyleaf']
|
10
10
|
spec.email = ['info@securebrain.co.jp', 'icyleaf.cn@gmail.com']
|
11
11
|
spec.platform = Gem::Platform::RUBY
|
@@ -15,13 +15,10 @@ Gem::Specification.new do |spec|
|
|
15
15
|
spec.license = 'MIT'
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
17
|
spec.require_paths = ['lib']
|
18
|
-
spec.required_ruby_version = '>=
|
18
|
+
spec.required_ruby_version = '>= 3.1'
|
19
19
|
|
20
20
|
spec.add_dependency 'rubyzip', '>= 1.0', '< 3.0'
|
21
|
-
|
22
|
-
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') then
|
23
|
-
spec.add_dependency 'rexml', '> 3.0'
|
24
|
-
end
|
21
|
+
spec.add_dependency 'rexml', '> 3.0' # requires for Ruby 3.0+
|
25
22
|
|
26
23
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
27
24
|
spec.add_development_dependency 'rspec-its', '>= 1.2.0'
|
Binary file
|
data/lib/android/apk.rb
CHANGED
@@ -12,7 +12,6 @@ module Android
|
|
12
12
|
|
13
13
|
# apk object class
|
14
14
|
class Apk
|
15
|
-
|
16
15
|
# @return [String] apk file path
|
17
16
|
attr_reader :path
|
18
17
|
# @return [Android::Manifest] manifest instance
|
@@ -43,32 +42,30 @@ module Android
|
|
43
42
|
|
44
43
|
@path = filepath
|
45
44
|
raise NotFoundError, "'#{filepath}'" unless File.exist? @path
|
46
|
-
begin
|
47
|
-
@zip = Zip::File.open(@path)
|
48
|
-
rescue Zip::Error => e
|
49
|
-
raise NotApkFileError, e.message
|
50
|
-
end
|
51
45
|
|
52
46
|
@bindata = File.open(@path, 'rb') {|f| f.read }
|
53
47
|
@bindata.force_encoding(Encoding::ASCII_8BIT)
|
54
|
-
raise NotApkFileError,
|
48
|
+
raise NotApkFileError, 'manifest file is not found.' if zip.find_entry(MANIFEST).nil?
|
49
|
+
|
55
50
|
begin
|
56
51
|
@resource = Android::Resource.new(self.file(RESOURCE))
|
57
52
|
rescue => e
|
58
|
-
|
59
|
-
|
53
|
+
logger.error "failed to parse resource: #{e} with backtrace"
|
54
|
+
logger.error e.backtrace
|
60
55
|
end
|
56
|
+
|
61
57
|
begin
|
62
58
|
@manifest = Android::Manifest.new(self.file(MANIFEST), @resource)
|
63
59
|
rescue => e
|
64
|
-
|
65
|
-
|
60
|
+
logger.error "failed to parse manifest:#{e} with backtrace"
|
61
|
+
logger.error e.backtrace
|
66
62
|
end
|
63
|
+
|
67
64
|
begin
|
68
65
|
@dex = Android::Dex.new(self.file(DEX))
|
69
66
|
rescue => e
|
70
|
-
|
71
|
-
|
67
|
+
logger.error "failed to parse dex:#{e} with backtrace"
|
68
|
+
logger.error e.backtrace
|
72
69
|
end
|
73
70
|
end
|
74
71
|
|
@@ -105,9 +102,9 @@ module Android
|
|
105
102
|
# @yieldparam [String] name file name in apk
|
106
103
|
# @yieldparam [String] data file data in apk
|
107
104
|
def each_file
|
108
|
-
|
105
|
+
zip.each do |entry|
|
109
106
|
next unless entry.file?
|
110
|
-
yield entry.name,
|
107
|
+
yield entry.name, zip.read(entry)
|
111
108
|
end
|
112
109
|
end
|
113
110
|
|
@@ -116,13 +113,13 @@ module Android
|
|
116
113
|
# @return [String] binary data
|
117
114
|
# @raise [NotFoundError] when 'name' doesn't exist in the apk
|
118
115
|
def file(name) # get data by entry name(path)
|
119
|
-
|
116
|
+
zip.read(entry(name))
|
120
117
|
end
|
121
118
|
|
122
119
|
# @yield [entry]
|
123
120
|
# @yieldparam [Zip::Entry] entry zip entry
|
124
121
|
def each_entry
|
125
|
-
|
122
|
+
zip.each do |entry|
|
126
123
|
next unless entry.file?
|
127
124
|
yield entry
|
128
125
|
end
|
@@ -133,7 +130,7 @@ module Android
|
|
133
130
|
# @return [Zip::Entry] zip entry object
|
134
131
|
# @raise [NotFoundError] when 'name' doesn't exist in the apk
|
135
132
|
def entry(name)
|
136
|
-
entry =
|
133
|
+
entry = zip.find_entry(name)
|
137
134
|
raise NotFoundError, "'#{name}'" if entry.nil?
|
138
135
|
return entry
|
139
136
|
end
|
@@ -189,7 +186,7 @@ module Android
|
|
189
186
|
# @return [nil] when label is not found
|
190
187
|
# @deprecated move to {Android::Manifest#label}
|
191
188
|
# @since 0.6.0
|
192
|
-
def label(lang=nil)
|
189
|
+
def label(lang = nil)
|
193
190
|
@manifest.label
|
194
191
|
end
|
195
192
|
|
@@ -200,25 +197,89 @@ module Android
|
|
200
197
|
@layouts ||= Layout.collect_layouts(self) # lazy parse
|
201
198
|
end
|
202
199
|
|
203
|
-
# apk's signature information
|
200
|
+
# apk's v1 signature information
|
204
201
|
# @return [Hash{ String => OpenSSL::PKCS7 } ] key: sign file path, value: signature
|
205
202
|
# @since 0.7.0
|
206
203
|
def signs
|
207
|
-
signs
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
204
|
+
@signs ||= lambda {
|
205
|
+
signs = {}
|
206
|
+
self.each_file do |path, data|
|
207
|
+
# find META-INF/xxx.{RSA|DSA|EC}
|
208
|
+
next unless path =~ /^META-INF\// && data.unpack("CC") == [0x30, 0x82]
|
209
|
+
|
210
|
+
signs[path] = OpenSSL::PKCS7.new(data)
|
211
|
+
end
|
212
|
+
signs
|
213
|
+
}.call
|
214
214
|
end
|
215
215
|
|
216
|
-
# certificate info which is used for signing
|
216
|
+
# v1 certificate info which is used for signing
|
217
217
|
# @return [Hash{String => OpenSSL::X509::Certificate }] key: sign file path, value: first certficate in the sign file
|
218
218
|
# @since 0.7.0
|
219
219
|
def certificates
|
220
|
-
|
220
|
+
@certificates ||= Hash[self.signs.map{|path, sign| [path, sign.certificates.first] }]
|
221
|
+
end
|
222
|
+
|
223
|
+
# Return all architectures (all most for universal apk)
|
224
|
+
# @return [Array<String>]
|
225
|
+
# @since 2.7.0
|
226
|
+
def archs
|
227
|
+
@archs ||= zip.glob('lib/**/*').each_with_object([]) do |entry, obj|
|
228
|
+
arch = entry.name.split('/')[1]
|
229
|
+
obj << arch unless obj.include?(arch)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# detect if contains multi-platforms (native machine code)
|
234
|
+
# @return [Boolean]
|
235
|
+
# @since 2.7.0
|
236
|
+
def universal?
|
237
|
+
archs.size > 1
|
238
|
+
end
|
239
|
+
|
240
|
+
# detect if use kotlin language (may be third-party sdk or not)
|
241
|
+
# @return [Boolean]
|
242
|
+
# @since 2.6.0
|
243
|
+
def kotlin?
|
244
|
+
@kotlin ||= kotlin_file? || kotlin_classes?
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
def kotlin_file?
|
250
|
+
KOTLIN_FILES.any? do |file|
|
251
|
+
begin
|
252
|
+
entry(file)
|
253
|
+
rescue
|
254
|
+
next
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def kotlin_classes?
|
260
|
+
@dex.classes.any? do |class_info|
|
261
|
+
class_info.type.start_with?('kotlin.') ||
|
262
|
+
class_info.type.start_with?('kotlinx.')
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
KOTLIN_FILES = [
|
267
|
+
'kotlin-tooling-metadata.json',
|
268
|
+
'kotlin/kotlin.kotlin_builtins',
|
269
|
+
'META-INF/kotlinx_coroutines_android.version',
|
270
|
+
'META-INF/kotlinx_coroutines_core.version',
|
271
|
+
'META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler',
|
272
|
+
'META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory'
|
273
|
+
].freeze
|
274
|
+
|
275
|
+
def zip
|
276
|
+
@zip ||= Zip::File.open(@path)
|
277
|
+
rescue Zip::Error => e
|
278
|
+
raise NotApkFileError, e.message
|
279
|
+
end
|
280
|
+
|
281
|
+
def logger
|
282
|
+
Android.logger
|
221
283
|
end
|
222
284
|
end
|
223
285
|
end
|
224
|
-
|
data/lib/android/dex/info.rb
CHANGED
@@ -29,16 +29,32 @@ module Android
|
|
29
29
|
# @return [DexObject::ClassDefItem]
|
30
30
|
attr_reader :class_def
|
31
31
|
|
32
|
+
# return full namespace of class
|
33
|
+
#
|
34
|
+
# sample value like `Landroidx/activity/Cancellable;`
|
35
|
+
#
|
36
|
+
# @return [String] class name
|
32
37
|
def name
|
33
|
-
@dex.type_resolve(@class_def[:class_idx])
|
38
|
+
@name ||= @dex.type_resolve(@class_def[:class_idx])
|
34
39
|
end
|
40
|
+
|
35
41
|
def super_class
|
36
|
-
if @class_def[:superclass_idx] != NO_INDEX
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
42
|
+
@super_class ||= if @class_def[:superclass_idx] != NO_INDEX
|
43
|
+
@dex.type_resolve(@class_def[:superclass_idx])
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Convert name to human readable string
|
50
|
+
#
|
51
|
+
# From `Landroidx/activity/Cancellable;` to `androidx.activity.Cancellable`
|
52
|
+
#
|
53
|
+
# @return [String] human readable name
|
54
|
+
def type
|
55
|
+
@type ||= name.gsub('/', '.')[1..-2]
|
41
56
|
end
|
57
|
+
|
42
58
|
# @param [Dex::ClassDefItem] class_def
|
43
59
|
# @param [Dex] dex dex class instance
|
44
60
|
def initialize(class_def, dex)
|
@@ -62,6 +78,7 @@ module Android
|
|
62
78
|
end
|
63
79
|
|
64
80
|
private
|
81
|
+
|
65
82
|
def cls2info(arr, cls, idx_key)
|
66
83
|
idx = 0
|
67
84
|
ret = []
|
data/lib/android/manifest.rb
CHANGED
@@ -10,14 +10,14 @@ module Android
|
|
10
10
|
# <activity>, <service>, <receiver> or <provider> element in <application> element of the manifest file.
|
11
11
|
class Component
|
12
12
|
# component types
|
13
|
-
TYPES = ['activity', 'activity-alias', 'service', 'receiver', 'provider', 'application']
|
13
|
+
TYPES = ['activity', 'activity-alias', 'service', 'receiver', 'provider', 'application', 'queries']
|
14
14
|
|
15
15
|
# the element is valid Component element or not
|
16
16
|
# @param [REXML::Element] elem xml element
|
17
17
|
# @return [Boolean]
|
18
18
|
def self.valid?(elem)
|
19
19
|
TYPES.include?(elem.name.downcase)
|
20
|
-
rescue
|
20
|
+
rescue
|
21
21
|
false
|
22
22
|
end
|
23
23
|
|
@@ -83,7 +83,7 @@ module Android
|
|
83
83
|
# @return [Boolean]
|
84
84
|
def self.valid?(elem)
|
85
85
|
['activity', 'activity-alias'].include?(elem.name.downcase)
|
86
|
-
rescue
|
86
|
+
rescue
|
87
87
|
false
|
88
88
|
end
|
89
89
|
|
@@ -145,7 +145,7 @@ module Android
|
|
145
145
|
filter&.elements&.any? do |elem|
|
146
146
|
TYPES.include?(elem&.name&.downcase)
|
147
147
|
end
|
148
|
-
rescue
|
148
|
+
rescue
|
149
149
|
false
|
150
150
|
end
|
151
151
|
|
@@ -322,10 +322,103 @@ module Android
|
|
322
322
|
end
|
323
323
|
end
|
324
324
|
|
325
|
+
# <intent>, <service>, <receiver> or <provider> element in <application> element of the manifest file.
|
326
|
+
class QueriesComponent
|
327
|
+
# component types
|
328
|
+
TYPES = ['package', 'intent', 'provider']
|
329
|
+
|
330
|
+
# @return [String] type string in TYPES
|
331
|
+
attr_reader :type
|
332
|
+
# @return [Manifest::Queries::Package]
|
333
|
+
attr_reader :packages
|
334
|
+
# @return [Array<Manifest::Queries::Intent>]
|
335
|
+
attr_reader :intents
|
336
|
+
# @return [Array<Manifest::Queries::Provider>]
|
337
|
+
attr_reader :providers
|
338
|
+
# @return [REXML::Element]
|
339
|
+
attr_reader :elem
|
340
|
+
|
341
|
+
# @param [REXML::Element] elem target element
|
342
|
+
# @raise [ArgumentError] when elem is invalid.
|
343
|
+
def initialize(elem)
|
344
|
+
raise ArgumentError unless Component.valid?(elem)
|
345
|
+
|
346
|
+
@elem = elem
|
347
|
+
@type = elem.name
|
348
|
+
@packages = parse_packages
|
349
|
+
@intents = parse_intents
|
350
|
+
@providers = parse_providers
|
351
|
+
end
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def parse_packages
|
356
|
+
packages = []
|
357
|
+
return packages if @elem.elements['package'].nil?
|
358
|
+
|
359
|
+
@elem.each_element('package') do |package|
|
360
|
+
packages << Android::Manifest::Queries::Package.new(package)
|
361
|
+
end
|
362
|
+
|
363
|
+
packages
|
364
|
+
end
|
365
|
+
|
366
|
+
def parse_intents
|
367
|
+
intents = []
|
368
|
+
return intents if @elem.elements['intent'].nil?
|
369
|
+
|
370
|
+
@elem.each_element('intent') do |intent|
|
371
|
+
next if intent&.elements&.empty?
|
372
|
+
next unless Android::Manifest::Queries::Intent.valid?(intent)
|
373
|
+
|
374
|
+
intent = Android::Manifest::Queries::Intent.new(intent)
|
375
|
+
intents << intent unless intent.empty?
|
376
|
+
end
|
377
|
+
|
378
|
+
intents
|
379
|
+
end
|
380
|
+
|
381
|
+
def parse_providers
|
382
|
+
providers = []
|
383
|
+
return providers if @elem.elements['provider'].nil?
|
384
|
+
|
385
|
+
@elem.each_element('provider') do |provider|
|
386
|
+
providers << Android::Manifest::Queries::Provider.new(provider)
|
387
|
+
end
|
388
|
+
|
389
|
+
providers
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
class Queries < QueriesComponent
|
394
|
+
class Intent < Android::Manifest::IntentFilter; end
|
395
|
+
class Provider < Android::Manifest::Provider
|
396
|
+
def authorities
|
397
|
+
@authorities ||= @elem.attributes['authorities']
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
class Package
|
402
|
+
# @return [String] action name of queries
|
403
|
+
attr_reader :name
|
404
|
+
# @return [String] action type of intent-filter
|
405
|
+
attr_reader :type
|
406
|
+
|
407
|
+
def initialize(elem)
|
408
|
+
@type = 'package'
|
409
|
+
@name = elem.attributes['name']
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
def self.valid?(elem)
|
414
|
+
elem&.name == 'queries'
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
325
418
|
#################################
|
326
419
|
# Manifest class definitions
|
327
420
|
#################################
|
328
|
-
|
421
|
+
|
329
422
|
# @return [REXML::Document] manifest xml
|
330
423
|
attr_reader :doc
|
331
424
|
|
@@ -370,6 +463,13 @@ module Android
|
|
370
463
|
components
|
371
464
|
end
|
372
465
|
|
466
|
+
# Returns the manifest's queries element or nil, if there isn't any.
|
467
|
+
# @return [Android::Manifest::Queries] the manifest's application element
|
468
|
+
def queries
|
469
|
+
element = @doc.elements['/manifest/queries']
|
470
|
+
Queries.new(element) if Queries.valid?(element)
|
471
|
+
end
|
472
|
+
|
373
473
|
# @return [Array<Android::Manifest::Activity&ActivityAlias>] all activities in the apk
|
374
474
|
# @note return empty array when the manifest include no activities
|
375
475
|
def activities
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
module Android
|
3
|
+
class NullLogger
|
4
|
+
# @param _args Anything that we want to ignore
|
5
|
+
# @return [nil]
|
6
|
+
def unknown(*_args)
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param _args Anything that we want to ignore
|
11
|
+
# @return [nil]
|
12
|
+
def fatal(*_args)
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [FALSE]
|
17
|
+
def fatal?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param _args Anything that we want to ignore
|
22
|
+
# @return [nil]
|
23
|
+
def error(*_args)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [FALSE]
|
28
|
+
def error?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
# @param _args Anything that we want to ignore
|
33
|
+
# @return [nil]
|
34
|
+
def warn(*_args)
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [FALSE]
|
39
|
+
def warn?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param _args Anything that we want to ignore
|
44
|
+
# @return [nil]
|
45
|
+
def info(*_args)
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [FALSE]
|
50
|
+
def info?
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param _args Anything that we want to ignore
|
55
|
+
# @return [nil]
|
56
|
+
def debug(*_args)
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [FALSE]
|
61
|
+
def debug?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/android/resource.rb
CHANGED
@@ -6,6 +6,8 @@ module Android
|
|
6
6
|
# /frameworks/base/include/utils/ResourceTypes.h
|
7
7
|
# @see http://justanapplication.wordpress.com/category/android/android-resources/
|
8
8
|
class Resource
|
9
|
+
class UnknownChunkType < StandardError; end
|
10
|
+
|
9
11
|
class Chunk
|
10
12
|
def initialize(data, offset)
|
11
13
|
data.force_encoding(Encoding::ASCII_8BIT)
|
@@ -198,7 +200,7 @@ module Android
|
|
198
200
|
end
|
199
201
|
|
200
202
|
class ResTablePackage < ChunkHeader
|
201
|
-
attr_reader :name
|
203
|
+
attr_reader :name, :locales
|
202
204
|
|
203
205
|
def global_string_pool=(pool)
|
204
206
|
@global_string_pool = pool
|
@@ -279,7 +281,7 @@ module Android
|
|
279
281
|
"@#{type(tid)}/#{key(keyid)}"
|
280
282
|
end
|
281
283
|
def res_hex_id(readable_id, opt={})
|
282
|
-
|
284
|
+
_dummy, typestr, keystr = readable_id.match(/^@?(\w+)\/(\w+)$/).to_a
|
283
285
|
tid = type_id(typestr)
|
284
286
|
raise NotFoundError unless @types.has_key?(tid)
|
285
287
|
keyid = @types[tid][0].keys[keystr]
|
@@ -324,21 +326,26 @@ module Android
|
|
324
326
|
|
325
327
|
@types = {}
|
326
328
|
@specs = {}
|
329
|
+
@libraries = []
|
327
330
|
while offset < (@offset + @size)
|
328
331
|
type = @data[offset, 2].unpack('v')[0]
|
329
332
|
case type
|
330
|
-
when
|
333
|
+
when TYPE_TYPE
|
331
334
|
type = ResTableType.new(@data, offset, self)
|
332
335
|
offset += type.size
|
333
336
|
@types[type.id] = [] if @types[type.id].nil?
|
334
337
|
@types[type.id] << type
|
335
|
-
when
|
338
|
+
when TYPE_SPEC_TYPE
|
336
339
|
spec = ResTableTypeSpec.new(@data, offset)
|
337
340
|
offset += spec.size
|
338
341
|
@specs[spec.id] = [] if @specs[spec.id].nil?
|
339
342
|
@specs[spec.id] << spec
|
343
|
+
when TYPE_LIBRARY
|
344
|
+
library = ResTableLibraryType.new(@data, offset)
|
345
|
+
offset += library.size
|
346
|
+
@libraries.concat(library.libraries)
|
340
347
|
else
|
341
|
-
raise "chunk type error: type:%#04x" % type
|
348
|
+
raise UnknownChunkType, "chunk type error: type:%#04x" % type
|
342
349
|
end
|
343
350
|
end
|
344
351
|
end
|
@@ -348,7 +355,7 @@ module Android
|
|
348
355
|
@res_strings_lang = {}
|
349
356
|
@res_strings_contry = {}
|
350
357
|
begin
|
351
|
-
|
358
|
+
_type = type_id('string')
|
352
359
|
rescue NotFoundError
|
353
360
|
return
|
354
361
|
end
|
@@ -367,6 +374,8 @@ module Android
|
|
367
374
|
@res_strings_contry[contry] = str_hash unless contry.nil?
|
368
375
|
end
|
369
376
|
end
|
377
|
+
|
378
|
+
@locales = @res_strings_lang.keys
|
370
379
|
end
|
371
380
|
private :extract_res_strings
|
372
381
|
|
@@ -409,8 +418,8 @@ module Android
|
|
409
418
|
def parse
|
410
419
|
super
|
411
420
|
@id = read_int8
|
412
|
-
|
413
|
-
|
421
|
+
_res0 = read_int8 # must be 0.(maybe 4byte align)
|
422
|
+
_res1 = read_int16 # must be 0.(maybe 4byte align)
|
414
423
|
@entry_count = read_int32
|
415
424
|
@entry_start = read_int32
|
416
425
|
@config = ResTableConfig.new(@data, current_position)
|
@@ -444,10 +453,8 @@ module Android
|
|
444
453
|
def parse
|
445
454
|
@size = read_int32
|
446
455
|
@imei = read_int32
|
447
|
-
|
448
|
-
@
|
449
|
-
cn = @data_io.read(2)
|
450
|
-
@locale_contry = cn unless cn == "\x00\x00"
|
456
|
+
@locale_lang = unpack_locale(@data_io.read(1), @data_io.read(1), 'a')
|
457
|
+
@locale_contry = unpack_locale(@data_io.read(1), @data_io.read(1), '0')
|
451
458
|
@screen_type = read_int32
|
452
459
|
@input = read_int32
|
453
460
|
@screen_input = read_int32
|
@@ -457,6 +464,27 @@ module Android
|
|
457
464
|
def inspect
|
458
465
|
"<ResTableConfig size:#{@size}, imei:#{@imei}, la:'#{@locale_lang}' cn:'#{@locale_contry}'"
|
459
466
|
end
|
467
|
+
|
468
|
+
private
|
469
|
+
|
470
|
+
# Convert from https://github.com/LoyieKing/Apktool/blob/e0e6cfd03f10d710e666c29456659688fb5aeea2/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java#L474
|
471
|
+
def unpack_locale(in0, in1, base)
|
472
|
+
return if in0 == "\x00" && in1 == "\x00"
|
473
|
+
|
474
|
+
in0_value = in0.ord
|
475
|
+
in1_value = in1.ord
|
476
|
+
# check high bit, if so we have a packed 3 letter code
|
477
|
+
return [(in0_value).chr, (in1_value).chr].join('') unless ((in0_value >> 7) & 1) == 1
|
478
|
+
|
479
|
+
first = in1_value & 0x1F
|
480
|
+
second = ((in1_value & 0xE0) >> 5) + ((in0_value & 0x03) << 3)
|
481
|
+
third = (in0_value & 0x7C) >> 2
|
482
|
+
|
483
|
+
base_value = base.ord
|
484
|
+
# since this function handles languages & regions, we add the value(s) to the base char
|
485
|
+
# which is usually 'a' or '0' depending on language or region.
|
486
|
+
[(first + base_value).chr, (second + base_value).chr, (third + base_value).chr].join('')
|
487
|
+
end
|
460
488
|
end
|
461
489
|
|
462
490
|
class ResTableTypeSpec < ChunkHeader
|
@@ -465,8 +493,8 @@ module Android
|
|
465
493
|
def parse
|
466
494
|
super
|
467
495
|
@id = read_int8
|
468
|
-
|
469
|
-
|
496
|
+
_res0 = read_int8 # must be 0.(maybe 4byte align)
|
497
|
+
_res1 = read_int16 # must be 0.(maybe 4byte align)
|
470
498
|
@entry_count = read_int32
|
471
499
|
end
|
472
500
|
private :parse
|
@@ -531,29 +559,54 @@ module Android
|
|
531
559
|
end
|
532
560
|
end
|
533
561
|
|
562
|
+
class ResTableLibraryType < ChunkHeader
|
563
|
+
attr_reader :libraries
|
564
|
+
|
565
|
+
def parse
|
566
|
+
super
|
567
|
+
@libraries = []
|
568
|
+
library_count = read_int32
|
569
|
+
library_count.times do
|
570
|
+
package_id = read_int32
|
571
|
+
package_name = @data_io.read(128).force_encoding(Encoding::UTF_16LE).strip
|
572
|
+
@libraries << ResTableLibraryValue.new(package_id, package_name)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
private :parse
|
576
|
+
|
577
|
+
def inspect
|
578
|
+
"<ResTableLibraryType library_count:#{@libraries.size}>"
|
579
|
+
end
|
580
|
+
|
581
|
+
ResTableLibraryValue = Struct.new(:package_id, :package_name)
|
582
|
+
end
|
583
|
+
|
534
584
|
class ResValue < Chunk
|
535
585
|
TYPE_REFERENCE = 0x01
|
536
586
|
|
537
587
|
attr_reader :size, :data_type, :data
|
538
588
|
def parse
|
539
589
|
@size = read_int16
|
540
|
-
|
590
|
+
_res0 = read_int8 # Always set 0.
|
541
591
|
@data_type = read_int8
|
542
592
|
@data = read_int32
|
543
593
|
end
|
544
594
|
private :parse
|
545
595
|
end
|
546
596
|
|
547
|
-
|
597
|
+
#################################
|
598
|
+
# Resource class definitions
|
599
|
+
#################################
|
600
|
+
|
548
601
|
# @returns [Hash] { name(String) => value(ResTablePackage) }
|
549
602
|
attr_reader :packages
|
550
603
|
|
551
604
|
def initialize(data)
|
552
605
|
data.force_encoding(Encoding::ASCII_8BIT)
|
553
606
|
@data = data
|
554
|
-
parse()
|
555
|
-
end
|
556
607
|
|
608
|
+
parse
|
609
|
+
end
|
557
610
|
|
558
611
|
# @return [Array<String>] all strings defined in arsc.
|
559
612
|
def strings
|
@@ -565,7 +618,7 @@ module Android
|
|
565
618
|
@res_table.package_count
|
566
619
|
end
|
567
620
|
|
568
|
-
#
|
621
|
+
# This method only support string resource for now.
|
569
622
|
# find resource by resource id
|
570
623
|
# @param [String] res_id (like '@0x7f010001' or '@string/key')
|
571
624
|
# @param [Hash] opts option
|
@@ -596,6 +649,13 @@ module Android
|
|
596
649
|
first_pkg.res_hex_id(readable_id)
|
597
650
|
end
|
598
651
|
|
652
|
+
# Return resources locales
|
653
|
+
# @return [Array<String>] all strings of locales.
|
654
|
+
# @since 2.7.0
|
655
|
+
def locales
|
656
|
+
first_pkg.locales
|
657
|
+
end
|
658
|
+
|
599
659
|
def first_pkg
|
600
660
|
@packages.first[1]
|
601
661
|
end
|
@@ -607,27 +667,38 @@ module Android
|
|
607
667
|
|
608
668
|
while offset < @data.size
|
609
669
|
type = @data[offset, 2].unpack('v')[0]
|
610
|
-
|
670
|
+
logger.debug "[%#08x] " % offset
|
611
671
|
@packages = {}
|
612
672
|
case type
|
613
|
-
when
|
614
|
-
@string_pool = ResStringPool.new(@data, offset)
|
615
|
-
offset += @string_pool.size
|
616
|
-
#puts "RES_STRING_POOL_TYPE %#x, %#x" % [@string_pool.size, offset]
|
617
|
-
when 0x0002 # RES_TABLE_TYPE
|
618
|
-
#puts "RES_TABLE_TYPE"
|
673
|
+
when TYPE_TABLE
|
619
674
|
@res_table = ResTableHeader.new(@data, offset)
|
620
675
|
offset += @res_table.header_size
|
621
|
-
|
622
|
-
|
676
|
+
logger.debug "RES_TABLE_TYPE"
|
677
|
+
when TYPE_STRING_POOL_TYPE
|
678
|
+
@string_pool = ResStringPool.new(@data, offset)
|
679
|
+
offset += @string_pool.size
|
680
|
+
logger.debug("RES_STRING_POOL_TYPE %#x, %#x" % [@string_pool.size, offset])
|
681
|
+
when TYPE_PACKAGE
|
623
682
|
pkg = ResTablePackage.new(@data, offset)
|
624
683
|
pkg.global_string_pool = @string_pool
|
625
684
|
offset += pkg.size
|
626
685
|
@packages[pkg.name] = pkg
|
686
|
+
logger.debug "RES_TABLE_PACKAGE_TYPE"
|
627
687
|
else
|
628
|
-
raise "chunk type error: type:%#04x" % type
|
688
|
+
raise UnknownChunkType, "chunk type error: type:%#04x" % type
|
629
689
|
end
|
630
690
|
end
|
631
691
|
end
|
692
|
+
|
693
|
+
def logger
|
694
|
+
Android.logger
|
695
|
+
end
|
696
|
+
|
697
|
+
TYPE_STRING_POOL_TYPE = 0x0001 # RES_STRING_POOL_TYPE
|
698
|
+
TYPE_TABLE = 0x0002 # RES_TABLE_TYPE
|
699
|
+
TYPE_PACKAGE = 0x0200 # RES_TABLE_PACKAGE_TYPE
|
700
|
+
TYPE_TYPE = 0x0201 # RES_TABLE_TYPE_TYPE
|
701
|
+
TYPE_SPEC_TYPE = 0x0202 # RES_TABLE_TYPE_SPEC_TYPE
|
702
|
+
TYPE_LIBRARY = 0x0203 # RES_TABLE_LIBRARY_TYPE
|
632
703
|
end
|
633
704
|
end
|
data/lib/android/utils.rb
CHANGED
@@ -8,9 +8,9 @@ module Android
|
|
8
8
|
# @return [Boolean]
|
9
9
|
def self.apk?(path)
|
10
10
|
begin
|
11
|
-
|
11
|
+
Apk.new(path)
|
12
12
|
return true
|
13
|
-
rescue
|
13
|
+
rescue
|
14
14
|
return false
|
15
15
|
end
|
16
16
|
end
|
@@ -20,7 +20,7 @@ module Android
|
|
20
20
|
# @return [Boolean]
|
21
21
|
def self.elf?(data)
|
22
22
|
data[0..3] == "\x7f\x45\x4c\x46"
|
23
|
-
rescue
|
23
|
+
rescue
|
24
24
|
false
|
25
25
|
end
|
26
26
|
|
@@ -29,7 +29,7 @@ module Android
|
|
29
29
|
# @return [Boolean]
|
30
30
|
def self.cert?(data)
|
31
31
|
data[0..1] == "\x30\x82"
|
32
|
-
rescue
|
32
|
+
rescue
|
33
33
|
false
|
34
34
|
end
|
35
35
|
|
@@ -38,7 +38,7 @@ module Android
|
|
38
38
|
# @return [Boolean]
|
39
39
|
def self.dex?(data)
|
40
40
|
data[0..7] == "\x64\x65\x78\x0a\x30\x33\x35\x00" # "dex\n035\0"
|
41
|
-
rescue
|
41
|
+
rescue
|
42
42
|
false
|
43
43
|
end
|
44
44
|
|
@@ -48,9 +48,8 @@ module Android
|
|
48
48
|
def self.valid_dex?(data)
|
49
49
|
Android::Dex.new(data)
|
50
50
|
true
|
51
|
-
rescue
|
51
|
+
rescue
|
52
52
|
false
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
56
|
-
|
data/lib/android_parser.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'android/null_logger'
|
4
|
+
require_relative 'android/configuration'
|
3
5
|
require_relative 'android/apk'
|
4
6
|
require_relative 'android/manifest'
|
5
7
|
require_relative 'android/axml_parser'
|
@@ -8,3 +10,7 @@ require_relative 'android/dex'
|
|
8
10
|
require_relative 'android/resource'
|
9
11
|
require_relative 'android/utils'
|
10
12
|
require_relative 'android/layout'
|
13
|
+
|
14
|
+
module Android
|
15
|
+
extend Configuration
|
16
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: android_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.7.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SecureBrain
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2024-06-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rubyzip
|
@@ -139,6 +139,7 @@ extra_rdoc_files: []
|
|
139
139
|
files:
|
140
140
|
- ".github/dependabot.yml"
|
141
141
|
- ".github/workflows/ci.yml"
|
142
|
+
- ".github/workflows/create_release.yml"
|
142
143
|
- ".gitignore"
|
143
144
|
- ".rspec"
|
144
145
|
- CHANGELOG.md
|
@@ -147,9 +148,11 @@ files:
|
|
147
148
|
- README.md
|
148
149
|
- Rakefile
|
149
150
|
- android_parser.gemspec
|
151
|
+
- docs/resources-arsc-reference.png
|
150
152
|
- lib/android/apk.rb
|
151
153
|
- lib/android/axml_parser.rb
|
152
154
|
- lib/android/axml_writer.rb
|
155
|
+
- lib/android/configuration.rb
|
153
156
|
- lib/android/dex.rb
|
154
157
|
- lib/android/dex/access_flag.rb
|
155
158
|
- lib/android/dex/dex_object.rb
|
@@ -157,10 +160,10 @@ files:
|
|
157
160
|
- lib/android/dex/utils.rb
|
158
161
|
- lib/android/layout.rb
|
159
162
|
- lib/android/manifest.rb
|
163
|
+
- lib/android/null_logger.rb
|
160
164
|
- lib/android/resource.rb
|
161
165
|
- lib/android/utils.rb
|
162
166
|
- lib/android_parser.rb
|
163
|
-
- lib/ruby_apk.rb
|
164
167
|
homepage: https://github.com/icyleaf/android_parser
|
165
168
|
licenses:
|
166
169
|
- MIT
|
@@ -173,14 +176,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
173
176
|
requirements:
|
174
177
|
- - ">="
|
175
178
|
- !ruby/object:Gem::Version
|
176
|
-
version: '
|
179
|
+
version: '3.1'
|
177
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
178
181
|
requirements:
|
179
182
|
- - ">="
|
180
183
|
- !ruby/object:Gem::Version
|
181
184
|
version: '0'
|
182
185
|
requirements: []
|
183
|
-
rubygems_version: 3.
|
186
|
+
rubygems_version: 3.5.9
|
184
187
|
signing_key:
|
185
188
|
specification_version: 4
|
186
189
|
summary: Static analysis tool for android apk since 2021
|
data/lib/ruby_apk.rb
DELETED