ruby_apk 0.4.0

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