android_parser 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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