android_parser 2.5.1 → 2.7.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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