from struct import pack, unpack
import collections
from apkutils.axml.chunk import BuffHandle, StringPoolChunk
RES_NULL_TYPE = 0x0000
RES_STRING_POOL_TYPE = 0x0001
RES_TABLE_TYPE = 0x0002
RES_XML_TYPE = 0x0003
# Chunk types in RES_XML_TYPE
RES_XML_FIRST_CHUNK_TYPE = 0x0100
RES_XML_START_NAMESPACE_TYPE = 0x0100
RES_XML_END_NAMESPACE_TYPE = 0x0101
RES_XML_START_ELEMENT_TYPE = 0x0102
RES_XML_END_ELEMENT_TYPE = 0x0103
RES_XML_CDATA_TYPE = 0x0104
RES_XML_LAST_CHUNK_TYPE = 0x017f
# This contains a uint32_t array mapping strings in the string
# pool back to resource identifiers. It is optional.
RES_XML_RESOURCE_MAP_TYPE = 0x0180
#
# Chunk types in RES_TABLE_TYPE
RES_TABLE_PACKAGE_TYPE = 0x0200
RES_TABLE_TYPE_TYPE = 0x0201
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
TYPE_ATTRIBUTE = 2
TYPE_DIMENSION = 5
TYPE_FIRST_COLOR_INT = 28
TYPE_FIRST_INT = 16
TYPE_FLOAT = 4
TYPE_FRACTION = 6
TYPE_INT_BOOLEAN = 18
TYPE_INT_COLOR_ARGB4 = 30
TYPE_INT_COLOR_ARGB8 = 28
TYPE_INT_COLOR_RGB4 = 31
TYPE_INT_COLOR_RGB8 = 29
TYPE_INT_DEC = 16
TYPE_INT_HEX = 17
TYPE_LAST_COLOR_INT = 31
TYPE_LAST_INT = 31
TYPE_NULL = 0
TYPE_REFERENCE = 1
TYPE_STRING = 3
TYPE_TABLE = {
TYPE_ATTRIBUTE: "attribute",
TYPE_DIMENSION: "dimension",
TYPE_FLOAT: "float",
TYPE_FRACTION: "fraction",
TYPE_INT_BOOLEAN: "int_boolean",
TYPE_INT_COLOR_ARGB4: "int_color_argb4",
TYPE_INT_COLOR_ARGB8: "int_color_argb8",
TYPE_INT_COLOR_RGB4: "int_color_rgb4",
TYPE_INT_COLOR_RGB8: "int_color_rgb8",
TYPE_INT_DEC: "int_dec",
TYPE_INT_HEX: "int_hex",
TYPE_NULL: "null",
TYPE_REFERENCE: "reference",
TYPE_STRING: "string",
}
[docs]def getPackage(i):
if i >> 24 == 1:
return "android:"
return ""
RADIX_MULTS = [0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010]
DIMENSION_UNITS = ["px", "dip", "sp", "pt", "in", "mm"]
FRACTION_UNITS = ["%", "%p"]
COMPLEX_UNIT_MASK = 15
[docs]def complexToFloat(xcomplex):
return (float)(xcomplex & 0xFFFFFF00) * RADIX_MULTS[(xcomplex >> 4) & 3]
[docs]class ARSCParser(object):
def __init__(self, raw_buff):
self.analyzed = False
self._resolved_strings = None
self.buff = BuffHandle(raw_buff)
self.header = ARSCHeader(self.buff)
self.packageCount = unpack('<i', self.buff.read(4))[0]
self.stringpool_main = StringPoolChunk(self.buff)
self.next_header = ARSCHeader(self.buff)
self.packages = {}
self.values = {}
self.resource_values = collections.defaultdict(collections.defaultdict)
self.resource_configs = collections.defaultdict(
lambda: collections.defaultdict(set))
self.resource_keys = collections.defaultdict(
lambda: collections.defaultdict(collections.defaultdict))
for i in range(0, self.packageCount):
current_package = ARSCResTablePackage(self.buff)
package_name = current_package.get_name()
self.packages[package_name] = []
mTableStrings = StringPoolChunk(self.buff)
mKeyStrings = StringPoolChunk(self.buff)
self.packages[package_name].append(current_package)
self.packages[package_name].append(mTableStrings)
self.packages[package_name].append(mKeyStrings)
pc = PackageContext(current_package, self.stringpool_main,
mTableStrings, mKeyStrings)
current = self.buff.get_idx()
while not self.buff.end():
header = ARSCHeader(self.buff)
self.packages[package_name].append(header)
if header.type == RES_TABLE_TYPE_SPEC_TYPE:
self.packages[package_name].append(ARSCResTypeSpec(
self.buff, pc))
elif header.type == RES_TABLE_TYPE_TYPE:
a_res_type = ARSCResType(self.buff, pc)
self.packages[package_name].append(a_res_type)
self.resource_configs[package_name][a_res_type].add(
a_res_type.config)
self.buff.set_idx(header.start+header.header_size)
entries = []
for j in range(0, a_res_type.entryCount):
current_package.mResId = current_package.mResId & 0xffff0000 | j
entries.append((unpack('<i', self.buff.read(4))[
0], current_package.mResId))
self.packages[package_name].append(entries)
for entry, res_id in entries:
if self.buff.end():
break
if entry != -1:
self.buff.set_idx(
header.start+a_res_type.entriesStart+entry)
ate = ARSCResTableEntry(self.buff, res_id, pc)
self.packages[package_name].append(ate)
elif header.type == RES_TABLE_PACKAGE_TYPE:
break
else:
print("unknown type")
break
current += header.size
self.buff.set_idx(current)
def _analyse(self):
if self.analyzed:
return
self.analyzed = True
for package_name in self.packages:
self.values[package_name] = {}
nb = 3
while nb < len(self.packages[package_name]):
header = self.packages[package_name][nb]
if isinstance(header, ARSCHeader):
if header.type == RES_TABLE_TYPE_TYPE:
a_res_type = self.packages[package_name][nb + 1]
if a_res_type.config.get_language(
) not in self.values[package_name]:
self.values[package_name][
a_res_type.config.get_language()
] = {}
self.values[package_name][a_res_type.config.get_language(
)]["public"] = []
c_value = self.values[package_name][
a_res_type.config.get_language()
]
entries = self.packages[package_name][nb + 2]
nb_i = 0
for entry, res_id in entries:
if entry != -1:
ate = self.packages[
package_name][nb + 3 + nb_i]
self.resource_values[ate.mResId][
a_res_type.config] = ate
self.resource_keys[package_name][
a_res_type.get_type()][ate.get_value()] = ate.mResId
if ate.get_index() != -1:
c_value["public"].append(
(a_res_type.get_type(), ate.get_value(),
ate.mResId))
if a_res_type.get_type() not in c_value:
c_value[a_res_type.get_type()] = []
if a_res_type.get_type() == "string":
c_value["string"].append(
self.get_resource_string(ate))
elif a_res_type.get_type() == "id":
if not ate.is_complex():
c_value["id"].append(
self.get_resource_id(ate))
elif a_res_type.get_type() == "bool":
if not ate.is_complex():
c_value["bool"].append(
self.get_resource_bool(ate))
elif a_res_type.get_type() == "integer":
c_value["integer"].append(
self.get_resource_integer(ate))
elif a_res_type.get_type() == "color":
c_value["color"].append(
self.get_resource_color(ate))
elif a_res_type.get_type() == "dimen":
c_value["dimen"].append(
self.get_resource_dimen(ate))
nb_i += 1
nb += 3 + nb_i - 1 # -1 to account for the nb+=1 on the next line
nb += 1
[docs] def get_resource_string(self, ate):
return [ate.get_value(), ate.get_key_data()]
[docs] def get_resource_id(self, ate):
x = [ate.get_value()]
if ate.key.get_data() == 0:
x.append("false")
elif ate.key.get_data() == 1:
x.append("true")
return x
[docs] def get_resource_bool(self, ate):
x = [ate.get_value()]
if ate.key.get_data() == 0:
x.append("false")
elif ate.key.get_data() == -1:
x.append("true")
return x
[docs] def get_resource_integer(self, ate):
return [ate.get_value(), ate.key.get_data()]
[docs] def get_resource_color(self, ate):
entry_data = ate.key.get_data()
return [
ate.get_value(),
"#%02x%02x%02x%02x" % (
((entry_data >> 24) & 0xFF),
((entry_data >> 16) & 0xFF),
((entry_data >> 8) & 0xFF),
(entry_data & 0xFF))
]
[docs] def get_resource_dimen(self, ate):
try:
return [
ate.get_value(), "%s%s" % (
complexToFloat(ate.key.get_data()),
DIMENSION_UNITS[ate.key.get_data() & COMPLEX_UNIT_MASK])
]
except IndexError:
return [ate.get_value(), ate.key.get_data()]
# FIXME
[docs] def get_resource_style(self, ate):
return ["", ""]
[docs] def get_packages_names(self):
return list(self.packages.keys())
[docs] def get_locales(self, package_name):
self._analyse()
return list(self.values[package_name].keys())
[docs] def get_types(self, package_name, locale):
self._analyse()
return list(self.values[package_name][locale].keys())
[docs] def get_public_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["public"]:
buff += '<public type="%s" name="%s" id="0x%08x" />\n' % (
i[0], i[1], i[2])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_string_resources(self, package_name, locale='\x00\x00'):
self._analyse()
res = []
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
import binascii
for i in self.values[package_name][locale]["string"]:
item = {}
item['name'], item['value'] = i[0], binascii.hexlify(
i[1].encode('utf-8')).decode()
res.append(item)
except KeyError:
pass
return res
[docs] def get_strings_resources(self):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += "<packages>\n"
for package_name in self.get_packages_names():
buff += "<package name=\"%s\">\n" % package_name
for locale in self.get_locales(package_name):
buff += "<locale value=%s>\n" % repr(locale)
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["string"]:
buff += '<string name="%s">%s</string>\n' % (i[0], i[
1])
except KeyError:
pass
buff += '</resources>\n'
buff += '</locale>\n'
buff += "</package>\n"
buff += "</packages>\n"
return buff.encode('utf-8')
[docs] def get_id_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["id"]:
if len(i) == 1:
buff += '<item type="id" name="%s"/>\n' % (i[0])
else:
buff += '<item type="id" name="%s">%s</item>\n' % (i[0],
i[1])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_bool_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["bool"]:
buff += '<bool name="%s">%s</bool>\n' % (i[0], i[1])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_integer_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["integer"]:
buff += '<integer name="%s">%s</integer>\n' % (i[0], i[1])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_color_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["color"]:
buff += '<color name="%s">%s</color>\n' % (i[0], i[1])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_dimen_resources(self, package_name, locale='\x00\x00'):
self._analyse()
buff = '<?xml version="1.0" encoding="utf-8"?>\n'
buff += '<resources>\n'
try:
for i in self.values[package_name][locale]["dimen"]:
buff += '<dimen name="%s">%s</dimen>\n' % (i[0], i[1])
except KeyError:
pass
buff += '</resources>\n'
return buff.encode('utf-8')
[docs] def get_id(self, package_name, rid, locale='\x00\x00'):
self._analyse()
try:
for i in self.values[package_name][locale]["public"]:
if i[2] == rid:
return i
except KeyError:
return None
[docs] class ResourceResolver(object):
def __init__(self, android_resources, config=None):
self.resources = android_resources
self.wanted_config = config
[docs] def resolve(self, res_id):
result = []
self._resolve_into_result(result, res_id, self.wanted_config)
return result
def _resolve_into_result(self, result, res_id, config):
configs = self.resources.get_res_configs(res_id, config)
if configs:
for config, ate in configs:
self.put_ate_value(result, ate, config)
[docs] def put_ate_value(self, result, ate, config):
if ate.is_complex():
complex_array = []
result.append(config, complex_array)
for _, item in ate.item.items:
self.put_item_value(complex_array, item,
config, complex_=True)
else:
self.put_item_value(result, ate.key, config, complex_=False)
[docs] def put_item_value(self, result, item, config, complex_):
if item.is_reference():
res_id = item.get_data()
if res_id:
self._resolve_into_result(
result,
item.get_data(),
self.wanted_config)
else:
if complex_:
result.append(item.format_value())
else:
result.append((config, item.format_value()))
[docs] def get_resolved_res_configs(self, rid, config=None):
resolver = ARSCParser.ResourceResolver(self, config)
return resolver.resolve(rid)
[docs] def get_resolved_strings(self):
self._analyse()
if self._resolved_strings:
return self._resolved_strings
r = {}
for package_name in self.get_packages_names():
r[package_name] = {}
k = {}
for locale in self.values[package_name]:
v_locale = locale
if v_locale == '\x00\x00':
v_locale = 'DEFAULT'
r[package_name][v_locale] = {}
try:
for i in self.values[package_name][locale]["public"]:
if i[0] == 'string':
r[package_name][v_locale][i[2]] = None
k[i[1]] = i[2]
except KeyError:
pass
try:
for i in self.values[package_name][locale]["string"]:
if i[0] in k:
r[package_name][v_locale][k[i[0]]] = i[1]
except KeyError:
pass
self._resolved_strings = r
return r
[docs] def get_res_configs(self, rid, config=None):
self._analyse()
if not rid:
raise ValueError("'rid' should be set")
try:
res_options = self.resource_values[rid]
if len(res_options) > 1 and config:
return [(
config,
res_options[config])]
else:
return list(res_options.items())
except KeyError:
return []
[docs] def get_string(self, package_name, name, locale='\x00\x00'):
self._analyse()
try:
for i in self.values[package_name][locale]["string"]:
if i[0] == name:
return i
except KeyError:
return None
[docs] def get_res_id_by_key(self, package_name, resource_type, key):
try:
return self.resource_keys[package_name][resource_type][key]
except KeyError:
return None
[docs] def get_items(self, package_name):
self._analyse()
return self.packages[package_name]
[docs] def get_type_configs(self, package_name, type_name=None):
if package_name is None:
package_name = self.get_packages_names()[0]
result = collections.defaultdict(list)
for res_type, configs in list(self.resource_configs[package_name].items()):
if res_type.get_package_name() == package_name and (
type_name is None or res_type.get_type() == type_name):
result[res_type.get_type()].extend(configs)
return result
[docs]class PackageContext(object):
def __init__(self, current_package, stringpool_main, mTableStrings,
mKeyStrings):
self.stringpool_main = stringpool_main
self.mTableStrings = mTableStrings
self.mKeyStrings = mKeyStrings
self.current_package = current_package
[docs] def get_mResId(self):
return self.current_package.mResId
[docs] def set_mResId(self, mResId):
self.current_package.mResId = mResId
[docs] def get_package_name(self):
return self.current_package.get_name()
# print("ARSC Header:")
# print(' - type:', self.type)
# print(' - header_size:', self.header_size)
# print(' - size: ', self.size)
[docs]class ARSCResTablePackage(object):
'''解析Package Header
'''
def __init__(self, buff):
self.start = buff.get_idx()
# package id
self.id = unpack('<I', buff.read(4))[0]
# package name
self.name = buff.readNullString(256)
# 资源类型 string pool 偏移
self.typeStrings = unpack('<I', buff.read(4))[0]
self.lastPublicType = unpack('<I', buff.read(4))[0]
# 资源关键字 string pool 偏移
self.keyStrings = unpack('<I', buff.read(4))[0]
self.lastPublicKey = unpack('<I', buff.read(4))[0]
self.mResId = self.id << 24
[docs] def get_name(self):
name = self.name.decode("utf-16", 'replace')
name = name[:name.find("\x00")]
return name
[docs]class ARSCResTypeSpec(object):
def __init__(self, buff, parent=None):
self.start = buff.get_idx()
self.parent = parent
self.id = unpack('<b', buff.read(1))[0]
self.res0 = unpack('<b', buff.read(1))[0]
self.res1 = unpack('<h', buff.read(2))[0]
self.entryCount = unpack('<I', buff.read(4))[0]
self.typespec_entries = []
for i in range(0, self.entryCount):
self.typespec_entries.append(unpack('<I', buff.read(4))[0])
[docs]class ARSCResType(object):
def __init__(self, buff, parent=None):
self.start = buff.get_idx()
self.parent = parent
self.id = unpack('<b', buff.read(1))[0]
self.res0 = unpack('<b', buff.read(1))[0]
self.res1 = unpack('<h', buff.read(2))[0]
self.entryCount = unpack('<i', buff.read(4))[0]
self.entriesStart = unpack('<i', buff.read(4))[0]
self.mResId = (0xff000000 & self.parent.get_mResId()) | self.id << 16
self.parent.set_mResId(self.mResId)
self.config = ARSCResTableConfig(buff)
[docs] def get_type(self):
return self.parent.mTableStrings.getString(self.id - 1)
[docs] def get_package_name(self):
return self.parent.get_package_name()
def __repr__(self):
return "ARSCResType(%x, %x, %x, %x, %x, %x, %x, %s)" % (
self.start,
self.id,
self.res0,
self.res1,
self.entryCount,
self.entriesStart,
self.mResId,
"table:" + self.parent.mTableStrings.getString(self.id - 1)
)
[docs]class ARSCResTableConfig(object):
[docs] @classmethod
def default_config(cls):
if not hasattr(cls, 'DEFAULT'):
cls.DEFAULT = ARSCResTableConfig(None)
return cls.DEFAULT
def __init__(self, buff=None, **kwargs):
if buff is not None:
self.start = buff.get_idx()
self.size = unpack('<I', buff.read(4))[0]
self.imsi = unpack('<I', buff.read(4))[0]
self.locale = unpack('<I', buff.read(4))[0]
self.screenType = unpack('<I', buff.read(4))[0]
self.input = unpack('<I', buff.read(4))[0]
self.screenSize = unpack('<I', buff.read(4))[0]
self.version = unpack('<I', buff.read(4))[0]
self.screenConfig = 0
self.screenSizeDp = 0
if self.size >= 32:
self.screenConfig = unpack('<I', buff.read(4))[0]
if self.size >= 36:
self.screenSizeDp = unpack('<I', buff.read(4))[0]
self.exceedingSize = self.size - 36
if self.exceedingSize > 0:
self.padding = buff.read(self.exceedingSize)
else:
self.start = 0
self.size = 0
self.imsi = \
((kwargs.pop('mcc', 0) & 0xffff) << 0) + \
((kwargs.pop('mnc', 0) & 0xffff) << 16)
self.locale = 0
for char_ix, char in kwargs.pop('locale', "")[0:4]:
self.locale += (ord(char) << (char_ix * 8))
self.screenType = \
((kwargs.pop('orientation', 0) & 0xff) << 0) + \
((kwargs.pop('touchscreen', 0) & 0xff) << 8) + \
((kwargs.pop('density', 0) & 0xffff) << 16)
self.input = \
((kwargs.pop('keyboard', 0) & 0xff) << 0) + \
((kwargs.pop('navigation', 0) & 0xff) << 8) + \
((kwargs.pop('inputFlags', 0) & 0xff) << 16) + \
((kwargs.pop('inputPad0', 0) & 0xff) << 24)
self.screenSize = \
((kwargs.pop('screenWidth', 0) & 0xffff) << 0) + \
((kwargs.pop('screenHeight', 0) & 0xffff) << 16)
self.version = \
((kwargs.pop('sdkVersion', 0) & 0xffff) << 0) + \
((kwargs.pop('minorVersion', 0) & 0xffff) << 16)
self.screenConfig = \
((kwargs.pop('screenLayout', 0) & 0xff) << 0) + \
((kwargs.pop('uiMode', 0) & 0xff) << 8) + \
((kwargs.pop('smallestScreenWidthDp', 0) & 0xffff) << 16)
self.screenSizeDp = \
((kwargs.pop('screenWidthDp', 0) & 0xffff) << 0) + \
((kwargs.pop('screenHeightDp', 0) & 0xffff) << 16)
self.exceedingSize = 0
[docs] def get_language(self):
x = self.locale & 0x0000ffff
return chr(x & 0x00ff) + chr((x & 0xff00) >> 8)
[docs] def get_country(self):
x = (self.locale & 0xffff0000) >> 16
return chr(x & 0x00ff) + chr((x & 0xff00) >> 8)
[docs] def get_density(self):
x = ((self.screenType >> 16) & 0xffff)
return x
def _get_tuple(self):
return (
self.imsi,
self.locale,
self.screenType,
self.input,
self.screenSize,
self.version,
self.screenConfig,
self.screenSizeDp,
)
def __hash__(self):
return hash(self._get_tuple())
def __eq__(self, other):
return self._get_tuple() == other._get_tuple()
def __repr__(self):
return repr(self._get_tuple())
[docs]class ARSCResTableEntry(object):
def __init__(self, buff, mResId, parent=None):
self.start = buff.get_idx()
self.mResId = mResId
self.parent = parent
self.size = unpack('<H', buff.read(2))[0]
self.flags = unpack('<H', buff.read(2))[0]
self.index = unpack('<I', buff.read(4))[0]
if self.flags & 1:
self.item = ARSCComplex(buff, parent)
else:
self.key = ARSCResStringPoolRef(buff, self.parent)
[docs] def get_index(self):
return self.index
[docs] def get_value(self):
return self.parent.mKeyStrings.getString(self.index)
[docs] def get_key_data(self):
return self.key.get_data_value()
[docs] def is_public(self):
return self.flags == 0 or self.flags == 2
[docs] def is_complex(self):
return (self.flags & 1) == 1
def __repr__(self):
return "ARSCResTableEntry(%x, %x, %x, %x, %x, %r)" % (
self.start,
self.mResId,
self.size,
self.flags,
self.index,
self.item if self.is_complex() else self.key)
[docs]class ARSCComplex(object):
def __init__(self, buff, parent=None):
self.start = buff.get_idx()
self.parent = parent
self.id_parent = unpack('<I', buff.read(4))[0]
self.count = unpack('<I', buff.read(4))[0]
self.items = []
for i in range(0, self.count):
self.items.append((unpack('<I', buff.read(4))[0],
ARSCResStringPoolRef(buff, self.parent)))
def __repr__(self):
return "ARSCComplex(%x, %d, %d)" % (self.start, self.id_parent, self.count)
[docs]class ARSCResStringPoolRef(object):
def __init__(self, buff, parent=None):
self.start = buff.get_idx()
self.parent = parent
self.skip_bytes = buff.read(3)
self.data_type = unpack('<B', buff.read(1))[0]
self.data = unpack('<I', buff.read(4))[0]
[docs] def get_data_value(self):
return self.parent.stringpool_main.getString(self.data)
[docs] def get_data(self):
return self.data
[docs] def get_data_type(self):
return self.data_type
[docs] def get_data_type_string(self):
return TYPE_TABLE[self.data_type]
[docs] def is_reference(self):
return self.data_type == TYPE_REFERENCE
def __repr__(self):
return "ARSCResStringPoolRef(%x, %s, %x)" % (
self.start,
TYPE_TABLE.get(self.data_type, "0x%x" % self.data_type),
self.data)