ruby_apk 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ require_relative 'dex/dex_object'
2
+ require_relative 'dex/info'
3
+ require_relative 'dex/access_flag'
4
+ require_relative 'dex/utils'
5
+
6
+ module Android
7
+ # parsed dex object
8
+ # @see http://source.android.com/tech/dalvik/dex-format.html
9
+ # @attr_reader strings [Array<String>] strings in dex file.
10
+ class Dex
11
+ # @return [Dex::Header] dex header information
12
+ attr_reader :header
13
+ alias :h :header
14
+
15
+ # @return [String] dex binary data
16
+ attr_reader :data
17
+ # @return [Array<Dex::ClassInfo>] array of class information
18
+ attr_reader :classes
19
+
20
+ attr_reader :field_ids, :method_ids, :proto_ids
21
+ # @param [String] data dex binary data
22
+ def initialize(data)
23
+ @data = data
24
+ @data.force_encoding(Encoding::ASCII_8BIT)
25
+ @classes = []
26
+ parse()
27
+ end
28
+
29
+ def strings
30
+ @strings ||= @string_data_items.map{|item| item.to_s }
31
+ end
32
+
33
+ def inspect
34
+ "<Android::Dex @classes => #{@classes.size}, datasize => #{@data.size}>"
35
+ end
36
+
37
+
38
+ # @private
39
+ TYPE_DESCRIPTOR = {
40
+ 'V' => 'void',
41
+ 'Z' => 'boolean',
42
+ 'B' => 'byte',
43
+ 'S' => 'short',
44
+ 'C' => 'short',
45
+ 'I' => 'int',
46
+ 'J' => 'long',
47
+ 'F' => 'float',
48
+ 'D' => 'double'
49
+ }
50
+
51
+
52
+ def type_resolve(typeid)
53
+ type = strings[@type_ids[typeid]]
54
+ if type.start_with? '['
55
+ type = type[1..type.size]
56
+ return TYPE_DESCRIPTOR.fetch(type, type) + "[]" # TODO: recursive
57
+ else
58
+ return TYPE_DESCRIPTOR.fetch(type, type)
59
+ end
60
+ end
61
+
62
+
63
+ private
64
+ def parse
65
+ @header = DexObject::Header.new(@data)
66
+ @map_list = DexObject::MapList.new(@data, h[:map_off])
67
+
68
+ # parse strings
69
+ @string_ids = DexObject::StringIdItem.new(@data, h[:string_ids_off], h[:string_ids_size])
70
+ @string_data_items = []
71
+ @string_ids[:string_data_off].each { |off| @string_data_items << DexObject::StringDataItem.new(@data, off) }
72
+
73
+ @type_ids = DexObject::TypeIdItem.new(@data, h[:type_ids_off], h[:type_ids_size])
74
+ @proto_ids = ids_list_array(DexObject::ProtoIdItem, h[:proto_ids_off], h[:proto_ids_size])
75
+ @field_ids = ids_list_array(DexObject::FieldIdItem, h[:field_ids_off], h[:field_ids_size])
76
+ @method_ids = ids_list_array(DexObject::MethodIdItem, h[:method_ids_off], h[:method_ids_size])
77
+ @class_defs = ids_list_array(DexObject::ClassDefItem, h[:class_defs_off], h[:class_defs_size])
78
+
79
+ @classes = []
80
+ @class_defs.each do |cls_def|
81
+ @classes << ClassInfo.new(cls_def, self)
82
+ end
83
+ end
84
+
85
+ def ids_list_array(cls, offset, size)
86
+ ret_array = []
87
+ size.times { |i| ret_array << cls.new(@data, offset + cls.size * i) }
88
+ ret_array
89
+ end
90
+ end
91
+ end
92
+
@@ -0,0 +1,209 @@
1
+ require 'rexml/document'
2
+
3
+ module Android
4
+ # parsed AndroidManifest.xml class
5
+ # @see http://developer.android.com/guide/topics/manifest/manifest-intro.html
6
+ class Manifest
7
+ APPLICATION_TAG = '/manifest/application'
8
+
9
+ # <activity>, <service>, <receiver> or <provider> element in <application> element of the manifest file.
10
+ class Component
11
+ # component types
12
+ TYPES = ['service', 'activity', 'receiver', 'provider']
13
+
14
+ # the element is valid Component element or not
15
+ # @param [REXML::Element] elem xml element
16
+ # @return [Boolean]
17
+ def self.valid?(elem)
18
+ TYPES.include?(elem.name.downcase)
19
+ rescue => e
20
+ false
21
+ end
22
+
23
+ # @return [String] type string in TYPES
24
+ attr_reader :type
25
+ # @return [String] component name
26
+ attr_reader :name
27
+ # @return [Array<Manifest::IntentFilter>]
28
+ attr_reader :intent_filters
29
+ # @return [Array<Manifest::Meta>]
30
+ attr_reader :metas
31
+
32
+ # @param [REXML::Element] elem target element
33
+ # @raise [ArgumentError] when elem is invalid.
34
+ def initialize(elem)
35
+ raise ArgumentError unless Component.valid?(elem)
36
+ @type = elem.name
37
+ @name = elem.attributes['name']
38
+ @intent_filters = []
39
+ unless elem.elements['intent-filter'].nil?
40
+ elem.elements['intent-filter'].each do |e|
41
+ @intent_filters << IntentFilter.parse(e)
42
+ end
43
+ end
44
+ @metas = []
45
+ elem.each_element('meta-data') do |e|
46
+ @metas << Meta.new(e)
47
+ end
48
+ end
49
+ end
50
+
51
+ # intent-filter element in components
52
+ module IntentFilter
53
+ # parse inside of intent-filter element
54
+ # @param [REXML::Element] elem target element
55
+ # @return [IntentFilter::Action, IntentFilter::Category, IntentFilter::Data]
56
+ # intent-filter element
57
+ def self.parse(elem)
58
+ case elem.name
59
+ when 'action'
60
+ Action.new(elem)
61
+ when 'category'
62
+ Category.new(elem)
63
+ when 'data'
64
+ Data.new(elem)
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ # intent-filter action class
71
+ class Action
72
+ # @return [String] action name of intent-filter
73
+ attr_reader :name
74
+ # @return [String] action type of intent-filter
75
+ attr_reader :type
76
+
77
+ def initialize(elem)
78
+ @type = 'action'
79
+ @name = elem.attributes['name']
80
+ end
81
+ end
82
+
83
+ # intent-filter category class
84
+ class Category
85
+ # @return [String] category name of intent-filter
86
+ attr_reader :name
87
+ # @return [String] category type of intent-filter
88
+ attr_reader :type
89
+
90
+ def initialize(elem)
91
+ @type = 'category'
92
+ @name = elem.attributes['name']
93
+ end
94
+ end
95
+
96
+ # intent-filter data class
97
+ class Data
98
+ # @return [String]
99
+ attr_reader :type
100
+ # @return [String]
101
+ attr_reader :host
102
+ # @return [String]
103
+ attr_reader :mime_type
104
+ # @return [String]
105
+ attr_reader :path
106
+ # @return [String]
107
+ attr_reader :path_pattern
108
+ # @return [String]
109
+ attr_reader :path_prefix
110
+ # @return [String]
111
+ attr_reader :port
112
+ # @return [String]
113
+ attr_reader :scheme
114
+
115
+ def initialize(elem)
116
+ @type = 'data'
117
+ @host = elem.attributes['host']
118
+ @mime_type = elem.attributes['mimeType']
119
+ @path = elem.attributes['path']
120
+ @path_pattern = elem.attributes['pathPattern']
121
+ @path_prefix = elem.attributes['pathPrefix']
122
+ @port = elem.attributes['port']
123
+ @scheme = elem.attributes['scheme']
124
+ end
125
+ end
126
+ end
127
+
128
+ # meta information class
129
+ class Meta
130
+ # @return [String]
131
+ attr_reader :name
132
+ # @return [String]
133
+ attr_reader :resource
134
+ # @return [String]
135
+ attr_reader :value
136
+ def initialize(elem)
137
+ @name = elem.attributes['name']
138
+ @resource = elem.attributes['resource']
139
+ @value = elem.attributes['value']
140
+ end
141
+ end
142
+
143
+ #################################
144
+ # Manifest class definitions
145
+ #################################
146
+
147
+ # @param [String] data binary data of AndroidManifest.xml
148
+ def initialize(data)
149
+ parser = AXMLParser.new(data)
150
+ @doc = parser.parse
151
+ end
152
+
153
+ # used permission array
154
+ # @return [Array<String>] permission names
155
+ # @note return empty array when the manifest includes no use-parmission element
156
+ def use_permissions
157
+ perms = []
158
+ @doc.each_element('/manifest/uses-permission') do |elem|
159
+ perms << elem.attributes['name']
160
+ end
161
+ perms.uniq
162
+ end
163
+
164
+ # @return [Array<Android::Manifest::Component>] all components in apk
165
+ # @note return empty array when the manifest include no components
166
+ def components
167
+ components = []
168
+ unless @doc.elements['/manifest/application'].nil?
169
+ @doc.elements['/manifest/application'].each do |elem|
170
+ components << Component.new(elem) if Component.valid?(elem)
171
+ end
172
+ end
173
+ components
174
+ end
175
+
176
+ # application package name
177
+ # @return [String]
178
+ def package_name
179
+ @doc.root.attributes['package']
180
+ end
181
+
182
+ # application version code
183
+ # @return [Integer]
184
+ def version_code
185
+ @doc.root.attributes['versionCode'].to_i
186
+ end
187
+
188
+ # application version name
189
+ # @return [String]
190
+ def version_name
191
+ @doc.root.attributes['versionName']
192
+ end
193
+
194
+ # @return [Integer] minSdkVersion in uses element
195
+ def min_sdk_ver
196
+ @doc.elements['/manifest/uses-sdk'].attributes['minSdkVersion'].to_i
197
+ end
198
+
199
+ # return xml as string format
200
+ # @param [Integer] indent size(bytes)
201
+ # @return [String] raw xml string
202
+ def to_xml(indent=4)
203
+ xml =''
204
+ formatter = REXML::Formatters::Pretty.new(indent)
205
+ formatter.write(@doc.root, xml)
206
+ xml
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+ require 'stringio'
3
+ require 'csv'
4
+ require 'ruby_apk'
5
+
6
+ module Android
7
+ # based on Android OS source code
8
+ # /frameworks/base/include/utils/ResourceTypes.h
9
+ class Resource
10
+ class ChunkHeader
11
+ attr_reader :type, :header_size, :size
12
+ def initialize(data, offset)
13
+ @data = data
14
+ @offset = offset
15
+ @data_io = StringIO.new(@data, 'rb')
16
+ @data_io.seek(offset)
17
+ parse
18
+ end
19
+ private
20
+ def parse
21
+ @type = read_int16
22
+ @header_size = read_int16
23
+ @size = read_int32
24
+ end
25
+ def read_int32
26
+ @data_io.read(4).unpack('V')[0]
27
+ end
28
+ def read_int16
29
+ @data_io.read(2).unpack('v')[0]
30
+ end
31
+ end
32
+
33
+ class ResTableHeader < ChunkHeader
34
+ attr_reader :package_count
35
+ def parse
36
+ super
37
+ @package_count = read_int32
38
+ end
39
+ end
40
+ class ResStringPool < ChunkHeader
41
+ SORTED_FLAG = 1 << 0
42
+ UTF8_FLAG = 1 << 8
43
+
44
+ attr_reader :strings
45
+ private
46
+ def parse
47
+ super
48
+ @string_count = read_int32
49
+ @style_count = read_int32
50
+ @flags = read_int32
51
+ @string_start = read_int32
52
+ @style_start = read_int32
53
+ @strings = []
54
+ @string_count.times do
55
+ offset = @offset + @string_start + read_int32
56
+ if (@flags & UTF8_FLAG != 0)
57
+ # read length twice(utf16 length and utf8 length)
58
+ # const uint16_t* ResStringPool::stringAt(size_t idx, size_t* u16len) const
59
+ u16len, o16 = ResStringPool.utf8_len(@data[offset, 2])
60
+ u8len, o8 = ResStringPool.utf8_len(@data[offset+o16, 2])
61
+ str = @data[offset+o16+o8, u8len]
62
+ @strings << str.force_encoding(Encoding::UTF_8)
63
+ else
64
+ u16len, o16 = ResStringPool.utf16_len(@data[offset, 4])
65
+ str = @data[offset+o16, u16len*2]
66
+ str.force_encoding(Encoding::UTF_16LE)
67
+ @strings << str.encode(Encoding::UTF_8)
68
+ end
69
+ end
70
+ end
71
+
72
+ # @note refer to /frameworks/base/libs/androidfw/ResourceTypes.cpp
73
+ # static inline size_t decodeLength(const uint8_t** str)
74
+ # @param [String] data parse target
75
+ # @return[Integer, Integer] string length and parsed length
76
+ def self.utf8_len(data)
77
+ first, second = data.unpack('CC')
78
+ if (first & 0x80) != 0
79
+ return (((first & 0x7F) << 8) + second), 2
80
+ else
81
+ return first, 1
82
+ end
83
+ end
84
+ # @note refer to /frameworks/base/libs/androidfw/ResourceTypes.cpp
85
+ # static inline size_t decodeLength(const char16_t** str)
86
+ # @param [String] data parse target
87
+ # @return[Integer, Integer] string length and parsed length
88
+ def self.utf16_len(data)
89
+ first, second = data.unpack('vv')
90
+ if (first & 0x8000) != 0
91
+ return (((first & 0x7FFF) << 16) + second), 4
92
+ else
93
+ return first, 2
94
+ end
95
+ end
96
+ end
97
+
98
+ ######################################################################
99
+ def initialize(data)
100
+ data.force_encoding(Encoding::ASCII_8BIT)
101
+ @data = data
102
+ parse()
103
+ end
104
+
105
+ def strings
106
+ @string_pool.strings
107
+ end
108
+ def package_count
109
+ @res_table.package_count
110
+ end
111
+
112
+ private
113
+ def parse
114
+ offset = 0
115
+
116
+ while offset < @data.size
117
+ type = @data[offset, 2].unpack('v')[0]
118
+ case type
119
+ when 0x0001 # RES_STRING_POOL_TYPE
120
+ @string_pool = ResStringPool.new(@data, offset)
121
+ offset += @string_pool.size
122
+ when 0x0002 # RES_TABLE_TYPE
123
+ @res_table = ResTableHeader.new(@data, offset)
124
+ offset += @res_table.header_size
125
+ when 0x0200, 0x0201, 0x0202
126
+ # not implemented yet.
127
+ chunk = ChunkHeader.new(@data, offset)
128
+ offset += chunk.size
129
+ else
130
+ raise "chunk type error: type:%#04x" % type
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,55 @@
1
+
2
+ module Android
3
+ # Utility methods
4
+ module Utils
5
+ # path is apk file or not.
6
+ # @param [String] path target file path
7
+ # @return [Boolean]
8
+ def self.apk?(path)
9
+ begin
10
+ apk = Apk.new(path)
11
+ return true
12
+ rescue => e
13
+ return false
14
+ end
15
+ end
16
+
17
+ # data is elf file or not.
18
+ # @param [String] data target data
19
+ # @return [Boolean]
20
+ def self.elf?(data)
21
+ data[0..3] == "\x7f\x45\x4c\x46"
22
+ rescue => e
23
+ false
24
+ end
25
+
26
+ # data is cert file or not.
27
+ # @param [String] data target data
28
+ # @return [Boolean]
29
+ def self.cert?(data)
30
+ data[0..1] == "\x30\x82"
31
+ rescue => e
32
+ false
33
+ end
34
+
35
+ # data is dex file or not.
36
+ # @param [String] data target data
37
+ # @return [Boolean]
38
+ def self.dex?(data)
39
+ data[0..7] == "\x64\x65\x78\x0a\x30\x33\x35\x00" # "dex\n035\0"
40
+ rescue => e
41
+ false
42
+ end
43
+
44
+ # data is valid dex file or not.
45
+ # @param [String] data target data
46
+ # @return [Boolean]
47
+ def self.valid_dex?(data)
48
+ Android::Dex.new(data)
49
+ true
50
+ rescue => e
51
+ false
52
+ end
53
+ end
54
+ end
55
+
data/lib/ruby_apk.rb ADDED
@@ -0,0 +1,6 @@
1
+ require_relative 'android/apk'
2
+ require_relative 'android/manifest'
3
+ require_relative 'android/axml_parser'
4
+ require_relative 'android/dex'
5
+ require_relative 'android/resource'
6
+ require_relative 'android/utils'
data/ruby_apk.gemspec ADDED
@@ -0,0 +1,93 @@
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
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "ruby_apk"
8
+ s.version = "0.4.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["SecureBrain"]
12
+ s.date = "2012-10-28"
13
+ s.description = "static analysis tool for android apk"
14
+ s.email = "info@securebrain.co.jp"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".rspec",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "lib/android/apk.rb",
28
+ "lib/android/axml_parser.rb",
29
+ "lib/android/dex.rb",
30
+ "lib/android/dex/access_flag.rb",
31
+ "lib/android/dex/dex_object.rb",
32
+ "lib/android/dex/info.rb",
33
+ "lib/android/dex/utils.rb",
34
+ "lib/android/manifest.rb",
35
+ "lib/android/resource.rb",
36
+ "lib/android/utils.rb",
37
+ "lib/ruby_apk.rb",
38
+ "ruby_apk.gemspec",
39
+ "spec/apk_spec.rb",
40
+ "spec/axml_parser_spec.rb",
41
+ "spec/data/sample.apk",
42
+ "spec/data/sample_AndroidManifest.xml",
43
+ "spec/data/sample_classes.dex",
44
+ "spec/data/sample_resources.arsc",
45
+ "spec/data/sample_resources_utf16.arsc",
46
+ "spec/dex/access_flag_spec.rb",
47
+ "spec/dex/dex_object_spec.rb",
48
+ "spec/dex/info_spec.rb",
49
+ "spec/dex/utils_spec.rb",
50
+ "spec/dex_spec.rb",
51
+ "spec/manifest_spec.rb",
52
+ "spec/resource_spec.rb",
53
+ "spec/ruby_apk_spec.rb",
54
+ "spec/spec_helper.rb",
55
+ "spec/utils_spec.rb"
56
+ ]
57
+ s.homepage = "http://www.securebrain.co.jp/"
58
+ s.licenses = ["MIT"]
59
+ s.require_paths = ["lib"]
60
+ s.rubygems_version = "1.8.24"
61
+ s.summary = "static analysis tool for android apk"
62
+
63
+ if s.respond_to? :specification_version then
64
+ s.specification_version = 3
65
+
66
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
67
+ s.add_runtime_dependency(%q<rubyzip>, [">= 0"])
68
+ s.add_development_dependency(%q<rspec>, ["~> 2.11.0"])
69
+ s.add_development_dependency(%q<bundler>, ["~> 1.1.5"])
70
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
71
+ s.add_development_dependency(%q<yard>, [">= 0"])
72
+ s.add_development_dependency(%q<redcarpet>, [">= 0"])
73
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
74
+ else
75
+ s.add_dependency(%q<rubyzip>, [">= 0"])
76
+ s.add_dependency(%q<rspec>, ["~> 2.11.0"])
77
+ s.add_dependency(%q<bundler>, ["~> 1.1.5"])
78
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
79
+ s.add_dependency(%q<yard>, [">= 0"])
80
+ s.add_dependency(%q<redcarpet>, [">= 0"])
81
+ s.add_dependency(%q<simplecov>, [">= 0"])
82
+ end
83
+ else
84
+ s.add_dependency(%q<rubyzip>, [">= 0"])
85
+ s.add_dependency(%q<rspec>, ["~> 2.11.0"])
86
+ s.add_dependency(%q<bundler>, ["~> 1.1.5"])
87
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
88
+ s.add_dependency(%q<yard>, [">= 0"])
89
+ s.add_dependency(%q<redcarpet>, [">= 0"])
90
+ s.add_dependency(%q<simplecov>, [">= 0"])
91
+ end
92
+ end
93
+