androguard/tests/test_axml.py
2023-06-12 09:37:00 +02:00

401 lines
17 KiB
Python

import unittest
from xml.dom import minidom
from lxml import etree
import io
import sys
sys.path.append("./")
from androguard.core import axml
from androguard.core import bytecode
from androguard.util import set_log
def is_valid_manifest(tree):
# We can not really check much more...
print(tree.tag, tree.attrib)
if tree.tag == "manifest" and "package" in tree.attrib:
return True
return False
# text_compare and xml_compare are modified from
# https://bitbucket.org/ianb/formencode/src/tip/formencode/doctest_xml_compare.py
def text_compare(t1, t2):
if not t1 and not t2:
return True
if t1 == '*' or t2 == '*':
return True
return (t1 or '').strip() == (t2 or '').strip()
def xml_compare(x1, x2, reporter=None):
"""
Compare two XML files
x1 must be the plain version
x2 must be the version generated by aapt
"""
if x1.tag != x2.tag:
if reporter:
reporter('Tags do not match: {} and {}'.format(x1.tag, x2.tag))
return False
for name, value in x1.attrib.items():
if value[0] == "@" and x2.attrib.get(name)[0] == "@":
# Can not be sure...
pass
elif x2.attrib.get(name) != value:
if reporter:
reporter('Attributes do not match: %s=%r, %s=%r'
% (name, value, name, x2.attrib.get(name)))
return False
for name in x2.attrib.keys():
if name not in x1.attrib:
if x2.tag == "application" and name == "{http://schemas.android.com/apk/res/android}debuggable":
# Debug attribute might be added by aapt
pass
else:
if reporter:
reporter('x2 has an attribute x1 is missing: %s'
% name)
return False
if not text_compare(x1.text, x2.text):
if reporter:
reporter('text: {!r} != {!r}'.format(x1.text, x2.text))
return False
if not text_compare(x1.tail, x2.tail):
if reporter:
reporter('tail: {!r} != {!r}'.format(x1.tail, x2.tail))
return False
cl1 = x1.getchildren()
cl2 = x2.getchildren()
if len(cl1) != len(cl2):
if reporter:
reporter('children length differs, %i != %i'
% (len(cl1), len(cl2)))
return False
i = 0
for c1, c2 in zip(cl1, cl2):
i += 1
if not xml_compare(c1, c2, reporter=reporter):
if reporter:
reporter('children %i do not match: %s'
% (i, c1.tag))
return False
return True
class AXMLTest(unittest.TestCase):
def testReplacement(self):
"""
Test that the replacements for attributes, names and values are working
:return:
"""
# Fake, Empty AXML file
a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x24\x00\x00\x00"
b"\x01\x00\x1c\x00\x1c\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00")
self.assertIsNotNone(a)
self.assertEqual(a._fix_value("hello world"), "hello world")
self.assertEqual(a._fix_value("Foobar \u000a\u000d\u0b12"), "Foobar \u000a\u000d\u0b12")
self.assertEqual(a._fix_value("hello \U00011234"), "hello \U00011234")
self.assertEqual(a._fix_value("\uFFFF"), "_")
self.assertEqual(a._fix_value("hello\x00world"), "hello")
self.assertEqual(a._fix_name('', 'foobar'), ('', 'foobar'))
self.assertEqual(a._fix_name('', '5foobar'), ('', '_5foobar'))
self.assertEqual(a._fix_name('', 'android:foobar'), ('', 'android_foobar'))
self.assertEqual(a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar'))
self.assertEqual(a._fix_name('', 'sdf:foobar'), ('', 'sdf_foobar'))
self.assertEqual(a._fix_name('', 'android:sdf:foobar'), ('', 'android_sdf_foobar'))
self.assertEqual(a._fix_name('', '5:foobar'), ('', '_5_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'foobar'), ('{http://schemas.android.com/apk/res/android}', 'foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5foobar'), ('{http://schemas.android.com/apk/res/android}', '_5foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:foobar'), ('{http://schemas.android.com/apk/res/android}', 'android_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'androiddd:foobar'), ('{http://schemas.android.com/apk/res/android}', 'androiddd_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'sdf:foobar'), ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:sdf:foobar'), ('{http://schemas.android.com/apk/res/android}', 'android_sdf_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5:foobar'), ('{http://schemas.android.com/apk/res/android}', '_5_foobar'))
# Add a namespace mapping and try again
def new_nsmap(self):
return {"android": "http://schemas.android.com/apk/res/android",
"something": "http://example/url"}
setattr(axml.AXMLParser, 'nsmap', property(new_nsmap))
self.assertEqual(a._fix_name('', 'foobar'), ('', 'foobar'))
self.assertEqual(a._fix_name('', '5foobar'), ('', '_5foobar'))
self.assertEqual(a._fix_name('', 'android:foobar'), ('{http://schemas.android.com/apk/res/android}', 'foobar'))
self.assertEqual(a._fix_name('', 'something:foobar'), ('{http://example/url}', 'foobar'))
self.assertEqual(a._fix_name('', 'androiddd:foobar'), ('', 'androiddd_foobar'))
self.assertEqual(a._fix_name('', 'sdf:foobar'), ('', 'sdf_foobar'))
self.assertEqual(a._fix_name('', 'android:sdf:foobar'), ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'))
self.assertEqual(a._fix_name('', '5:foobar'), ('', '_5_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'foobar'), ('{http://schemas.android.com/apk/res/android}', 'foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5foobar'), ('{http://schemas.android.com/apk/res/android}', '_5foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:foobar'), ('{http://schemas.android.com/apk/res/android}', 'android_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'androiddd:foobar'), ('{http://schemas.android.com/apk/res/android}', 'androiddd_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'sdf:foobar'), ('{http://schemas.android.com/apk/res/android}', 'sdf_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', 'android:sdf:foobar'), ('{http://schemas.android.com/apk/res/android}', 'android_sdf_foobar'))
self.assertEqual(a._fix_name('{http://schemas.android.com/apk/res/android}', '5:foobar'), ('{http://schemas.android.com/apk/res/android}', '_5_foobar'))
def testNoStringPool(self):
"""Test if a single header without string pool is rejected"""
# |TYPE |LENGTH |FILE LENGTH
a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x08\x00\x00\x00")
self.assertFalse(a.is_valid())
def testTooSmallFile(self):
"""Test if a very short file is rejected"""
# |TYPE |LENGTH |FILE LENGTH
a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x08\x00\x00")
self.assertFalse(a.is_valid())
def testWrongHeaderSize(self):
"""Test if a wrong header size is rejected"""
a = axml.AXMLPrinter(b"\x03\x00\x10\x00\x2c\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x01\x00\x1c\x00\x1c\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00")
self.assertFalse(a.is_valid())
def testWrongStringPoolHeader(self):
"""Test if a wrong header type is rejected"""
a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x24\x00\x00\x00" b"\xDE\xAD\x1c\x00\x1c\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00")
self.assertFalse(a.is_valid())
def testWrongStringPoolSize(self):
"""Test if a wrong string pool header size is rejected"""
a = axml.AXMLPrinter(b"\x03\x00\x08\x00\x2c\x00\x00\x00"
b"\x01\x00\x24\x00\x24\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00")
self.assertFalse(a.is_valid())
def testArscHeader(self):
"""Test if wrong arsc headers are rejected"""
with self.assertRaises(axml.ResParserError) as cnx:
axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01")))
self.assertIn("Can not read over the buffer size", str(cnx.exception))
with self.assertRaises(axml.ResParserError) as cnx:
axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\xFF\xFF\x08\x00\x00\x00")))
self.assertIn("smaller than header size", str(cnx.exception))
with self.assertRaises(axml.ResParserError) as cnx:
axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\x01\x00\x08\x00\x00\x00")))
self.assertIn("declared header size is smaller than required size", str(cnx.exception))
with self.assertRaises(axml.ResParserError) as cnx:
axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\x02\x01\x08\x00\x04\x00\x00\x00")))
self.assertIn("declared chunk size is smaller than required size", str(cnx.exception))
a = axml.ARSCHeader(io.BufferedReader(io.BytesIO(b"\xCA\xFE\x08\x00\x10\x00\x00\x00"
b"\xDE\xEA\xBE\xEF\x42\x42\x42\x42")))
self.assertEqual(a.type, 0xFECA)
self.assertEqual(a.header_size, 8)
self.assertEqual(a.size, 16)
self.assertEqual(a.start, 0)
self.assertEqual(a.end, 16)
self.assertEqual(repr(a), "<ARSCHeader idx='0x00000000' type='65226' header_size='8' size='16'>")
def testAndroidManifest(self):
filenames = [
"tests/data/AXML/AndroidManifest.xml",
"tests/data/AXML/AndroidManifest-Chinese.xml",
"tests/data/AXML/AndroidManifestDoubleNamespace.xml",
"tests/data/AXML/AndroidManifestExtraNamespace.xml",
"tests/data/AXML/AndroidManifest_InvalidCharsInAttribute.xml",
"tests/data/AXML/AndroidManifestLiapp.xml",
"tests/data/AXML/AndroidManifestMaskingNamespace.xml",
"tests/data/AXML/AndroidManifest_NamespaceInAttributeName.xml",
"tests/data/AXML/AndroidManifest_NamespaceInAttributeName2.xml",
"tests/data/AXML/AndroidManifestNonZeroStyle.xml",
"tests/data/AXML/AndroidManifestNullbytes.xml",
"tests/data/AXML/AndroidManifestTextChunksXML.xml",
"tests/data/AXML/AndroidManifestUTF8Strings.xml",
"tests/data/AXML/AndroidManifestWithComment.xml",
"tests/data/AXML/AndroidManifest_WrongChunkStart.xml",
"tests/data/AXML/AndroidManifest-xmlns.xml",
]
for filename in filenames:
with open(filename, "rb") as fd:
ap = axml.AXMLPrinter(fd.read())
self.assertIsNotNone(ap)
self.assertTrue(ap.is_valid())
self.assertTrue(is_valid_manifest(ap.get_xml_obj()))
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
#def testFileCompare(self):
# """
# Compare the binary version of a file with the plain text
# """
# binary = "tests/data/AXML/AndroidManifest.xml"
# plain = "tests/data/android/TC/AndroidManifest.xml"
# with open(plain, "rb") as fp:
# x1 = etree.fromstring(fp.read())
# with open(binary, "rb") as fp:
# x2 = axml.AXMLPrinter(fp.read()).get_xml_obj()
# self.assertTrue(xml_compare(x1, x2, reporter=print))
def testNonManifest(self):
filenames = [
"tests/data/AXML/test.xml",
"tests/data/AXML/test1.xml",
"tests/data/AXML/test2.xml",
"tests/data/AXML/test3.xml",
]
for filename in filenames:
with open(filename, "rb") as fp:
ap = axml.AXMLPrinter(fp.read())
self.assertTrue(ap.is_valid())
self.assertEqual(ap.get_xml_obj().tag, "LinearLayout")
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testNonZeroStyleOffset(self):
"""
Test if a nonzero style offset in the string section causes problems
if the counter is 0
"""
filename = "tests/data/AXML/AndroidManifestNonZeroStyle.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testNonTerminatedString(self):
"""
Test if non-null terminated strings are detected.
This sample even segfaults aapt...
"""
filename = "tests/data/AXML/AndroidManifest_StringNotTerminated.xml"
with self.assertRaises(axml.ResParserError) as cnx:
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIn("not null terminated", str(cnx.exception))
def testExtraNamespace(self):
"""
Test if extra namespaces cause problems
"""
filename = "tests/data/AXML/AndroidManifestExtraNamespace.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testTextChunksWithXML(self):
"""
Test for Text chunks containing XML
"""
filename = "tests/data/AXML/AndroidManifestTextChunksXML.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testWrongFilesize(self):
"""
Assert that files with a broken filesize are not parsed
"""
filename = "tests/data/AXML/AndroidManifestWrongFilesize.xml"
with open(filename, "rb") as f:
a = axml.AXMLPrinter(f.read())
self.assertFalse(a.is_valid())
def testNullbytes(self):
"""
Assert that Strings with nullbytes are handled correctly
"""
filename = "tests/data/AXML/AndroidManifestNullbytes.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testMaskingNamespace(self):
"""
Assert that Namespaces which are used in a tag and the tag is closed
are actually correctly parsed.
"""
filename = "tests/data/AXML/AndroidManifestMaskingNamespace.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testDoubleNamespace(self):
"""
Test if weird namespace constelations cause problems
"""
filename = "tests/data/AXML/AndroidManifestDoubleNamespace.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
e = minidom.parseString(ap.get_buff())
self.assertIsNotNone(e)
def testPackers(self):
"""
Assert that Packed files are read
"""
filename = "tests/data/AXML/AndroidManifestLiapp.xml"
with open(filename, "rb") as f:
ap = axml.AXMLPrinter(f.read())
self.assertIsInstance(ap, axml.AXMLPrinter)
self.assertTrue(ap.is_valid())
self.assertTrue(ap.is_packed())
if __name__ == '__main__':
set_log("INFO")
unittest.main()