android_parser 2.4.1

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.
@@ -0,0 +1,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: ruby_apk 2.3.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "android_parser".freeze
9
+ s.version = "2.4.1"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["SecureBrain".freeze]
14
+ s.date = "2021-08-04"
15
+ s.description = "static analysis tool for android apk".freeze
16
+ s.email = "info@securebrain.co.jp".freeze
17
+ s.extra_rdoc_files = [
18
+ "CHANGELOG.md",
19
+ "LICENSE.txt",
20
+ "README.md"
21
+ ]
22
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ s.homepage = "https://github.com/icyleaf/ruby_apk".freeze
24
+ s.licenses = ["MIT".freeze]
25
+ s.rubygems_version = "3.0.3".freeze
26
+ s.summary = "static analysis tool for android apk".freeze
27
+
28
+ if s.respond_to? :specification_version then
29
+ s.specification_version = 4
30
+
31
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
32
+ s.add_runtime_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
33
+ s.add_development_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
34
+ s.add_development_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
35
+ s.add_development_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
36
+ s.add_development_dependency(%q<bundler>.freeze, [">= 1.1.5"])
37
+ s.add_development_dependency(%q<jeweler>.freeze, [">= 0"])
38
+ s.add_development_dependency(%q<yard>.freeze, [">= 0"])
39
+ s.add_development_dependency(%q<redcarpet>.freeze, [">= 0"])
40
+ s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
41
+ else
42
+ s.add_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
43
+ s.add_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
44
+ s.add_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
45
+ s.add_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
46
+ s.add_dependency(%q<bundler>.freeze, [">= 1.1.5"])
47
+ s.add_dependency(%q<jeweler>.freeze, [">= 0"])
48
+ s.add_dependency(%q<yard>.freeze, [">= 0"])
49
+ s.add_dependency(%q<redcarpet>.freeze, [">= 0"])
50
+ s.add_dependency(%q<simplecov>.freeze, [">= 0"])
51
+ end
52
+ else
53
+ s.add_dependency(%q<rubyzip>.freeze, [">= 1.0.0"])
54
+ s.add_dependency(%q<rspec-its>.freeze, [">= 1.2.0"])
55
+ s.add_dependency(%q<rspec-collection_matchers>.freeze, [">= 1.1.0"])
56
+ s.add_dependency(%q<rspec-mocks>.freeze, [">= 3.6.0"])
57
+ s.add_dependency(%q<bundler>.freeze, [">= 1.1.5"])
58
+ s.add_dependency(%q<jeweler>.freeze, [">= 0"])
59
+ s.add_dependency(%q<yard>.freeze, [">= 0"])
60
+ s.add_dependency(%q<redcarpet>.freeze, [">= 0"])
61
+ s.add_dependency(%q<simplecov>.freeze, [">= 0"])
62
+ end
63
+ end
64
+
@@ -0,0 +1,220 @@
1
+ require 'zip' # need rubyzip gem -> doc: http://rubyzip.sourceforge.net/
2
+ require 'digest/md5'
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+ require 'openssl'
6
+
7
+ module Android
8
+ class NotApkFileError < StandardError; end
9
+ class NotFoundError < StandardError; end
10
+
11
+ # apk object class
12
+ class Apk
13
+
14
+ # @return [String] apk file path
15
+ attr_reader :path
16
+ # @return [Android::Manifest] manifest instance
17
+ # @return [nil] when parsing manifest is failed.
18
+ attr_reader :manifest
19
+ # @return [Android::Dex] dex instance
20
+ # @return [nil] when parsing dex is failed.
21
+ attr_reader :dex
22
+ # @return [String] binary data of apk
23
+ attr_reader :bindata
24
+ # @return [Resource] resouce data
25
+ # @return [nil] when parsing resource is failed.
26
+ attr_reader :resource
27
+
28
+ # AndroidManifest file name
29
+ MANIFEST = 'AndroidManifest.xml'
30
+ # dex file name
31
+ DEX = 'classes.dex'
32
+ # resource file name
33
+ RESOURCE = 'resources.arsc'
34
+
35
+ # create new apk object
36
+ # @param [String] filepath apk file path
37
+ # @raise [Android::NotFoundError] path file does'nt exist
38
+ # @raise [Android::NotApkFileError] path file is not Apk file.
39
+ def initialize(filepath)
40
+ @path = filepath
41
+ raise NotFoundError, "'#{filepath}'" unless File.exist? @path
42
+ begin
43
+ @zip = Zip::File.open(@path)
44
+ rescue Zip::Error => e
45
+ raise NotApkFileError, e.message
46
+ end
47
+
48
+ @bindata = File.open(@path, 'rb') {|f| f.read }
49
+ @bindata.force_encoding(Encoding::ASCII_8BIT)
50
+ raise NotApkFileError, "manifest file is not found." if @zip.find_entry(MANIFEST).nil?
51
+ begin
52
+ @resource = Android::Resource.new(self.file(RESOURCE))
53
+ rescue => e
54
+ $stderr.puts "failed to parse resource:#{e}"
55
+ #$stderr.puts e.backtrace
56
+ end
57
+ begin
58
+ @manifest = Android::Manifest.new(self.file(MANIFEST), @resource)
59
+ rescue => e
60
+ $stderr.puts "failed to parse manifest:#{e}"
61
+ #$stderr.puts e.backtrace
62
+ end
63
+ begin
64
+ @dex = Android::Dex.new(self.file(DEX))
65
+ rescue => e
66
+ $stderr.puts "failed to parse dex:#{e}"
67
+ #$stderr.puts e.backtrace
68
+ end
69
+ end
70
+
71
+ # return apk file size
72
+ # @return [Integer] bytes
73
+ def size
74
+ @bindata.size
75
+ end
76
+
77
+ # return hex digest string of apk file
78
+ # @param [Symbol] type hash digest type(:sha1, sha256, :md5)
79
+ # @return [String] hex digest string
80
+ # @raise [ArgumentError] type is knknown type
81
+ def digest(type = :sha1)
82
+ case type
83
+ when :sha1
84
+ Digest::SHA1.hexdigest(@bindata)
85
+ when :sha256
86
+ Digest::SHA256.hexdigest(@bindata)
87
+ when :md5
88
+ Digest::MD5.hexdigest(@bindata)
89
+ else
90
+ raise ArgumentError
91
+ end
92
+ end
93
+
94
+ # returns date of AndroidManifest.xml as Apk date
95
+ # @return [Time]
96
+ def time
97
+ entry(MANIFEST).time
98
+ end
99
+
100
+ # @yield [name, data]
101
+ # @yieldparam [String] name file name in apk
102
+ # @yieldparam [String] data file data in apk
103
+ def each_file
104
+ @zip.each do |entry|
105
+ next unless entry.file?
106
+ yield entry.name, @zip.read(entry)
107
+ end
108
+ end
109
+
110
+ # find and return binary data with name
111
+ # @param [String] name file name in apk(fullpath)
112
+ # @return [String] binary data
113
+ # @raise [NotFoundError] when 'name' doesn't exist in the apk
114
+ def file(name) # get data by entry name(path)
115
+ @zip.read(entry(name))
116
+ end
117
+
118
+ # @yield [entry]
119
+ # @yieldparam [Zip::Entry] entry zip entry
120
+ def each_entry
121
+ @zip.each do |entry|
122
+ next unless entry.file?
123
+ yield entry
124
+ end
125
+ end
126
+
127
+ # find and return zip entry with name
128
+ # @param [String] name file name in apk(fullpath)
129
+ # @return [Zip::Entry] zip entry object
130
+ # @raise [NotFoundError] when 'name' doesn't exist in the apk
131
+ def entry(name)
132
+ entry = @zip.find_entry(name)
133
+ raise NotFoundError, "'#{name}'" if entry.nil?
134
+ return entry
135
+ end
136
+
137
+ # find files which is matched with block condition
138
+ # @yield [name, data] find condition
139
+ # @yieldparam [String] name file name in apk
140
+ # @yieldparam [String] data file data in apk
141
+ # @yieldreturn [Array] Array of matched entry name
142
+ # @return [Array] Array of matched entry name
143
+ # @example
144
+ # apk = Apk.new(path)
145
+ # elf_files = apk.find { |name, data| data[0..3] == [0x7f, 0x45, 0x4c, 0x46] } # ELF magic number
146
+ def find(&block)
147
+ found = []
148
+ self.each_file do |name, data|
149
+ ret = block.call(name, data)
150
+ found << name if ret
151
+ end
152
+ found
153
+ end
154
+
155
+ # extract application icon data from AndroidManifest and resource.
156
+ # @return [Hash{ String => String }] hash key is icon filename. value is image data
157
+ # @raise [NotFoundError]
158
+ # @since 0.6.0
159
+ def icon
160
+ icon_id = @manifest.doc.elements['/manifest/application'].attributes['icon']
161
+ icon_by_id(icon_id)
162
+ end
163
+
164
+ # extract icon data from AndroidManifest and resource by a given icon id.
165
+ # @param [String] icon_id to be searched in the resource.
166
+ # @return [Hash{ String => String }] hash key is icon filename. value is image data
167
+ # @raise [NotFoundError]
168
+ # @since 0.6.0
169
+ def icon_by_id(icon_id)
170
+ if /^@(\w+\/\w+)|(0x[0-9a-fA-F]{8})$/ =~ icon_id
171
+ drawables = @resource.find(icon_id)
172
+ Hash[drawables.map {|name| [name, file(name)] }]
173
+ else
174
+ begin
175
+ { icon_id => file(icon_id) } # ugh!: not tested!!
176
+ rescue NotFoundError
177
+ {}
178
+ end
179
+ end
180
+ end
181
+
182
+ # get application label from AndroidManifest and resources.
183
+ # @param [String] lang language code like 'ja', 'cn', ...
184
+ # @return [String] application label string
185
+ # @return [nil] when label is not found
186
+ # @deprecated move to {Android::Manifest#label}
187
+ # @since 0.6.0
188
+ def label(lang=nil)
189
+ @manifest.label
190
+ end
191
+
192
+ # get screen layout xml datas
193
+ # @return [Hash{ String => Android::Layout }] key: laytout file path, value: layout object
194
+ # @since 0.6.0
195
+ def layouts
196
+ @layouts ||= Layout.collect_layouts(self) # lazy parse
197
+ end
198
+
199
+ # apk's signature information
200
+ # @return [Hash{ String => OpenSSL::PKCS7 } ] key: sign file path, value: signature
201
+ # @since 0.7.0
202
+ def signs
203
+ signs = {}
204
+ self.each_file do |path, data|
205
+ # find META-INF/xxx.{RSA|DSA}
206
+ next unless path =~ /^META-INF\// && data.unpack("CC") == [0x30, 0x82]
207
+ signs[path] = OpenSSL::PKCS7.new(data)
208
+ end
209
+ signs
210
+ end
211
+
212
+ # certificate info which is used for signing
213
+ # @return [Hash{String => OpenSSL::X509::Certificate }] key: sign file path, value: first certficate in the sign file
214
+ # @since 0.7.0
215
+ def certificates
216
+ return Hash[self.signs.map{|path, sign| [path, sign.certificates.first] }]
217
+ end
218
+ end
219
+ end
220
+
@@ -0,0 +1,239 @@
1
+ require 'rexml/document'
2
+ require 'stringio'
3
+
4
+
5
+ module Android
6
+ # binary AXML parser
7
+ # @see https://android.googlesource.com/platform/frameworks/base.git Android OS frameworks source
8
+ # @note
9
+ # refer to Android OS framework code:
10
+ #
11
+ # /frameworks/base/include/androidfw/ResourceTypes.h,
12
+ #
13
+ # /frameworks/base/libs/androidfw/ResourceTypes.cpp
14
+ class AXMLParser
15
+ def self.axml?(data)
16
+ (data[0..3] == "\x03\x00\x08\x00")
17
+ end
18
+
19
+ # axml parse error
20
+ class ReadError < StandardError; end
21
+
22
+ TAG_START_NAMESPACE = 0x00100100
23
+ TAG_END_NAMESPACE = 0x00100101
24
+ TAG_START = 0x00100102
25
+ TAG_END = 0x00100103
26
+ TAG_TEXT = 0x00100104
27
+ TAG_CDSECT = 0x00100105
28
+ TAG_ENTITY_REF = 0x00100106
29
+
30
+ VAL_TYPE_NULL =0
31
+ VAL_TYPE_REFERENCE =1
32
+ VAL_TYPE_ATTRIBUTE =2
33
+ VAL_TYPE_STRING =3
34
+ VAL_TYPE_FLOAT =4
35
+ VAL_TYPE_DIMENSION =5
36
+ VAL_TYPE_FRACTION =6
37
+ VAL_TYPE_INT_DEC =16
38
+ VAL_TYPE_INT_HEX =17
39
+ VAL_TYPE_INT_BOOLEAN =18
40
+ VAL_TYPE_INT_COLOR_ARGB8 =28
41
+ VAL_TYPE_INT_COLOR_RGB8 =29
42
+ VAL_TYPE_INT_COLOR_ARGB4 =30
43
+ VAL_TYPE_INT_COLOR_RGB4 =31
44
+
45
+ # @return [Array<String>] strings defined in axml
46
+ attr_reader :strings, :metadata
47
+
48
+ # @param [String] axml binary xml data
49
+ def initialize(axml)
50
+ @io = StringIO.new(axml, "rb")
51
+ @strings = []
52
+ end
53
+
54
+ # parse binary xml
55
+ # @return [REXML::Document]
56
+ def parse
57
+ @doc = REXML::Document.new
58
+ @doc << REXML::XMLDecl.new
59
+
60
+ @num_str = word(4*4)
61
+ @xml_offset = word(3*4)
62
+
63
+ @parents = [@doc]
64
+ @namespaces = []
65
+ @metadata = []
66
+ parse_strings
67
+ parse_tags
68
+ @doc
69
+ end
70
+
71
+
72
+ # read one word(4byte) as integer
73
+ # @param [Integer] offset offset from top position. current position is used if ofset is nil
74
+ # @return [Integer] little endian word value
75
+ def word(offset=nil)
76
+ @io.pos = offset unless offset.nil?
77
+ @io.read(4).unpack("V")[0]
78
+ end
79
+
80
+ # read 2byte as short integer
81
+ # @param [Integer] offset offset from top position. current position is used if ofset is nil
82
+ # @return [Integer] little endian unsign short value
83
+ def short(offset)
84
+ @io.pos = offset unless offset.nil?
85
+ @io.read(2).unpack("v")[0]
86
+ end
87
+
88
+ # relace string table parser
89
+ def parse_strings
90
+ strpool = Resource::ResStringPool.new(@io.string, 8) # ugh!
91
+ @strings = strpool.strings
92
+ end
93
+
94
+ # parse tag
95
+ def parse_tags
96
+ # skip until first TAG_START_NAMESPACE
97
+ pos = @xml_offset
98
+ pos += 4 until (word(pos) == TAG_START_NAMESPACE)
99
+ @io.pos -= 4
100
+
101
+ # read tags
102
+ #puts "start tag parse: %d(%#x)" % [@io.pos, @io.pos]
103
+ until @io.eof?
104
+ last_pos = @io.pos
105
+ tag, tag1, line, tag3, ns_id, name_id = @io.read(4*6).unpack("V*")
106
+ case tag
107
+ when TAG_START
108
+ tag6, num_attrs, tag8 = @io.read(4*3).unpack("V*")
109
+
110
+ prefix = ''
111
+ if ns_id != 0xFFFFFFFF
112
+ namespace_uri = @strings[ns_id]
113
+ prefix = get_namespace_prefix(namespace_uri) + ':'
114
+ end
115
+ elem = REXML::Element.new(prefix + @strings[name_id])
116
+
117
+ meta_tag = if @strings[name_id] == 'meta-data'
118
+ @metadata << {}
119
+ true
120
+ end
121
+
122
+ # If this element is a direct descendent of a namespace declaration
123
+ # we add the namespace definition as an attribute.
124
+ if @namespaces.last[:nesting_level] == current_nesting_level
125
+ elem.add_namespace(@namespaces.last[:prefix], @namespaces.last[:uri])
126
+ end
127
+ #puts "start tag %d(%#x): #{@strings[name_id]} attrs:#{num_attrs}" % [last_pos, last_pos]
128
+ @parents.last.add_element elem
129
+ num_attrs.times do
130
+ key, val, type = parse_attribute
131
+
132
+ if meta_tag
133
+ @metadata.last[key] = {
134
+ value: val,
135
+ position: @io.pos - 4,
136
+ val_str_id: @io.pos - 12,
137
+ is_string: type == VAL_TYPE_STRING
138
+ }
139
+ end
140
+
141
+ if val.is_a?(String)
142
+ # drop invalid chars that would be rejected by REXML from string
143
+ val = val.scan(REXML::Text::VALID_XML_CHARS).join
144
+ end
145
+ elem.add_attribute(key, val)
146
+ end
147
+ @parents.push elem
148
+ when TAG_END
149
+ @parents.pop
150
+ when TAG_END_NAMESPACE
151
+ @namespaces.pop
152
+ break if @namespaces.empty? # if the topmost namespace (usually 'android:') has been closed, we‘re done.
153
+ when TAG_TEXT
154
+ text = REXML::Text.new(@strings[ns_id])
155
+ @parents.last.text = text
156
+ dummy = @io.read(4*1).unpack("V*") # skip 4bytes
157
+ when TAG_START_NAMESPACE
158
+ prefix = @strings[ns_id]
159
+ uri = @strings[name_id]
160
+ @namespaces.push({ prefix: prefix, uri: uri, nesting_level: current_nesting_level })
161
+ when TAG_CDSECT
162
+ raise ReadError, "TAG_CDSECT not implemented"
163
+ when TAG_ENTITY_REF
164
+ raise ReadError, "TAG_ENTITY_REF not implemented"
165
+ else
166
+ raise ReadError, "pos=%d(%#x)[tag:%#x]" % [last_pos, last_pos, tag]
167
+ end
168
+ end
169
+ end
170
+
171
+ # parse attribute of a element
172
+ def parse_attribute
173
+ ns_id, name_id, val_str_id, flags, val = @io.read(4*5).unpack("V*")
174
+ key = @strings[name_id]
175
+ unless ns_id == 0xFFFFFFFF
176
+ namespace_uri = @strings[ns_id]
177
+ prefix = get_namespace_prefix(namespace_uri)
178
+ key = "#{prefix}:#{key}"
179
+ end
180
+ value = convert_value(val_str_id, flags, val)
181
+ return key, value, (flags >> 24)
182
+ end
183
+
184
+ # find the first declared namespace prefix for a URI
185
+ def get_namespace_prefix(ns_uri)
186
+ # a namespace might be given as a URI or as a reference to a previously defined namespace.
187
+ # E.g. like this:
188
+ # <tag1 xmlns:android="http://schemas.android.com/apk/res/android">
189
+ # <tag2 xmlns:n0="android" />
190
+ # </tag1>
191
+
192
+ # Walk recursively through the namespaces to
193
+ # transitively resolve URIs that just pointed to previous namespace prefixes
194
+ current_uri = ns_uri
195
+ @namespaces.reverse.each do |ns|
196
+ if ns[:prefix] == current_uri
197
+ # we found a previous namespace declaration that was referenced to by
198
+ # the current_uri. Proceed with this namespace’s URI and try to see if this
199
+ # is also just a reference to a previous namespace
200
+ current_uri = ns[:uri]
201
+ end
202
+ end
203
+
204
+ # current_uri now contains the URI of the topmost namespace declaration.
205
+ # We’ll take the prefix of this and return it.
206
+ @namespaces.reverse.each do |ns|
207
+ return ns[:prefix] if ns[:uri] == current_uri
208
+ end
209
+ raise "Could not resolve URI #{ns_uri} to a namespace prefix"
210
+ end
211
+
212
+ def current_nesting_level
213
+ @parents.length
214
+ end
215
+
216
+ def convert_value(val_str_id, flags, val)
217
+ unless val_str_id == 0xFFFFFFFF
218
+ value = @strings[val_str_id]
219
+ else
220
+ type = flags >> 24
221
+ case type
222
+ when VAL_TYPE_NULL
223
+ value = nil
224
+ when VAL_TYPE_REFERENCE
225
+ value = "@%#x" % val # refered resource id.
226
+ when VAL_TYPE_INT_DEC
227
+ value = val
228
+ when VAL_TYPE_INT_HEX
229
+ value = "%#x" % val
230
+ when VAL_TYPE_INT_BOOLEAN
231
+ value = ((val == 0xFFFFFFFF) || (val==1)) ? true : false
232
+ else
233
+ value = "[%#x, flag=%#x]" % [val, flags]
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ end