From 0e3dcc91ac475c9ef126918c9b97135cfed7caef Mon Sep 17 00:00:00 2001 From: Tim Strazzere Date: Thu, 22 Oct 2015 16:01:01 -0700 Subject: [PATCH] Add injection ability, boost tests, version bump 0.1.6 --- build.gradle | 2 +- .../android/content/res/AXMLResource.java | 25 +++++ .../chunk/sections/GenericChunkSection.java | 1 + .../res/chunk/sections/StringSection.java | 39 +++++-- .../content/res/chunk/types/Attribute.java | 30 +++++ .../content/res/chunk/types/StartTag.java | 30 ++++- .../android/content/res/TestAXMLResource.java | 104 ++++++++++++++++++ src/test/resources/large_from_malware.xml | Bin 0 -> 17080 bytes 8 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 src/test/java/android/content/res/TestAXMLResource.java create mode 100644 src/test/resources/large_from_malware.xml diff --git a/build.gradle b/build.gradle index 367a4be..4206f1a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' apply plugin: 'jacoco' apply plugin: 'com.github.kt3k.coveralls' -version = '0.1.5' +version = '0.1.6' repositories { mavenCentral() diff --git a/src/main/java/android/content/res/AXMLResource.java b/src/main/java/android/content/res/AXMLResource.java index c37bb9a..9d0a126 100644 --- a/src/main/java/android/content/res/AXMLResource.java +++ b/src/main/java/android/content/res/AXMLResource.java @@ -20,7 +20,9 @@ import android.content.res.chunk.sections.ResourceSection; import android.content.res.chunk.sections.StringSection; import android.content.res.chunk.types.AXMLHeader; +import android.content.res.chunk.types.Attribute; import android.content.res.chunk.types.Chunk; +import android.content.res.chunk.types.StartTag; import java.io.IOException; import java.io.InputStream; @@ -53,6 +55,29 @@ public AXMLResource(InputStream stream) throws IOException { } } + public void injectApplicationAttribute(Attribute attribute) { + StartTag tag = getApplicationTag(); + + tag.insertOrReplaceAttribute(attribute); + } + + public StartTag getApplicationTag() { + Iterator iterator = chunks.iterator(); + while (iterator.hasNext()) { + Chunk chunk = iterator.next(); + if (chunk instanceof StartTag && + ((StartTag) chunk).getName(stringSection).equalsIgnoreCase("application")) { + return (StartTag) chunk; + } + } + + return null; + } + + public StringSection getStringSection() { + return stringSection; + } + public boolean read(InputStream stream) throws IOException { IntReader reader = new IntReader(stream, false); diff --git a/src/main/java/android/content/res/chunk/sections/GenericChunkSection.java b/src/main/java/android/content/res/chunk/sections/GenericChunkSection.java index 4f8009c..c8dfe34 100644 --- a/src/main/java/android/content/res/chunk/sections/GenericChunkSection.java +++ b/src/main/java/android/content/res/chunk/sections/GenericChunkSection.java @@ -38,6 +38,7 @@ public GenericChunkSection(ChunkType chunkType, IntReader reader) { reader.skip(Math.abs(reader.getBytesRead() - getStartPosition() - size)); } catch (IOException e) { + // Catching this here allows us to continue reading e.printStackTrace(); } } diff --git a/src/main/java/android/content/res/chunk/sections/StringSection.java b/src/main/java/android/content/res/chunk/sections/StringSection.java index 7e971cb..61aea6f 100644 --- a/src/main/java/android/content/res/chunk/sections/StringSection.java +++ b/src/main/java/android/content/res/chunk/sections/StringSection.java @@ -114,8 +114,31 @@ private void readPool(ArrayList pool, int flags, IntReader inputReader } } + public int getStringIndex(String string) { + if (string != null) { + for (PoolItem item : stringChunkPool) { + if (item.getString().equals(string)) { + return stringChunkPool.indexOf(item); + } + } + } + + return -1; + } + + public int putStringIndex(String string) { + int currentPosition = getStringIndex(string); + if (currentPosition != -1) { + return currentPosition; + } + + stringChunkPool.add(new PoolItem(-1, string)); + + return getStringIndex(string); + } + public String getString(int index) { - if ((index > -1) && (index < stringChunkCount)) { + if ((index > -1) && (index < stringChunkPool.size())) { return stringChunkPool.get(index).getString(); } @@ -151,14 +174,14 @@ public int getSize() { int styleDataSize = 0; for (PoolItem item : styleChunkPool) { - stringDataSize += item.getString().length() * (((stringChunkFlags & UTF8_FLAG) == 0) ? 2 : 1); + styleDataSize += item.getString().length() * (((stringChunkFlags & UTF8_FLAG) == 0) ? 2 : 1); } return (2 * 4) + // Header (5 * 4) + // static sections - (stringChunkCount * 4) + // string table offset size + (stringChunkPool.size() * 4) + // string table offset size stringDataSize + - (styleChunkCount * 4) + // style table offset size + (styleChunkPool.size() * 4) + // style table offset size styleDataSize; } @@ -247,21 +270,21 @@ public byte[] toBytes() { int newStringChunkOffset = 0; if (!stringChunkPool.isEmpty()) { newStringChunkOffset = (5 * 4) /* header + 3 other ints above it */ - + stringChunkCount * 4 /* index table size */ + + stringChunkPool.size() * 4 /* index table size */ + 8 /* (this space and the style chunk offset */; } int newStyleChunkOffset = 0; if (!styleChunkPool.isEmpty()) { newStyleChunkOffset = (6 * 4) /* header + 4 other ints above it */ - + styleChunkCount * 4 /* index table size */ + + styleChunkPool.size() * 4 /* index table size */ + 8 /* (this space and the style chunk offset */; } byte[] body = ByteBuffer.allocate(5 * 4) .order(ByteOrder.LITTLE_ENDIAN) - .putInt(stringChunkCount) - .putInt(styleChunkCount) + .putInt(stringChunkPool.size()) + .putInt(styleChunkPool.size()) .putInt(stringChunkFlags) .putInt(newStringChunkOffset) .putInt(newStyleChunkOffset) diff --git a/src/main/java/android/content/res/chunk/types/Attribute.java b/src/main/java/android/content/res/chunk/types/Attribute.java index 8beb4b9..d8d18db 100644 --- a/src/main/java/android/content/res/chunk/types/Attribute.java +++ b/src/main/java/android/content/res/chunk/types/Attribute.java @@ -39,6 +39,28 @@ public class Attribute implements Chunk { private int attributeType; private int data; + public Attribute(String uri, + String name, + String stringData, + AttributeType type, + Object data, + StringSection stringSection) { + this.uri = stringSection.getStringIndex(uri); + this.name = stringSection.getStringIndex(name); + this.stringData = stringSection.getStringIndex(stringData); + this.attributeType = type.getIntType(); + + if (attributeType == AttributeType.STRING.getIntType()) { + if (this.stringData == -1) { + this.stringData = stringSection.putStringIndex(stringData); + } + this.data = -1; + } else { + this.data = (int) data; + } + + } + public Attribute(IntReader reader) { try { uri = reader.readInt(); @@ -82,6 +104,14 @@ public int getSize() { return 4 * 5; } + public int getNameIndex() { + return name; + } + + public int getStringDataIndex() { + return stringData; + } + /* * (non-Javadoc) * diff --git a/src/main/java/android/content/res/chunk/types/StartTag.java b/src/main/java/android/content/res/chunk/types/StartTag.java index b35f866..7e0608c 100644 --- a/src/main/java/android/content/res/chunk/types/StartTag.java +++ b/src/main/java/android/content/res/chunk/types/StartTag.java @@ -24,6 +24,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Iterator; /** * StartTag type of Chunk, differs from a Namespace as there will be specific metadata inside of it @@ -59,8 +60,9 @@ public void readHeader(IntReader inputReader) throws IOException { flags = inputReader.readInt(); attributeCount = inputReader.readInt(); classAttribute = inputReader.readInt(); + + attributes = new ArrayList<>(); if (attributeCount > 0) { - attributes = new ArrayList<>(); for (int i = 0; i < attributeCount; i++) { attributes.add(new Attribute(inputReader)); } @@ -76,6 +78,26 @@ public int getSize() { return (9 * 4) + (attributeCount * 20); } + public ArrayList getAttributes() { + return attributes; + } + + public void insertOrReplaceAttribute(Attribute newAttribute) { + Iterator iterator = attributes.iterator(); + while (iterator.hasNext()) { + Attribute attribute = iterator.next(); + if (attribute.getNameIndex() == newAttribute.getNameIndex()) { + iterator.remove(); + } + } + + attributes.add(newAttribute); + } + + public String getName(StringSection stringSection) { + return stringSection.getString(name); + } + /* * (non-Javadoc) * @@ -118,13 +140,13 @@ public byte[] toBytes() { .putInt(namespaceUri) .putInt(name) .putInt(flags) - .putInt(attributeCount) + .putInt(attributes.size()) .putInt(classAttribute) .array(); byte[] dynamicBody; - if (attributeCount > 0) { - ByteBuffer attributeData = ByteBuffer.allocate(attributeCount * 20) + if (attributes.size() > 0) { + ByteBuffer attributeData = ByteBuffer.allocate(attributes.size() * 20) .order(ByteOrder.LITTLE_ENDIAN); for (Attribute attribute : attributes) { attributeData.put(attribute.toBytes()); diff --git a/src/test/java/android/content/res/TestAXMLResource.java b/src/test/java/android/content/res/TestAXMLResource.java new file mode 100644 index 0000000..0c2eef9 --- /dev/null +++ b/src/test/java/android/content/res/TestAXMLResource.java @@ -0,0 +1,104 @@ +package android.content.res; + +import android.content.res.chunk.AttributeType; +import android.content.res.chunk.types.Attribute; +import android.content.res.chunk.types.StartTag; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import java.io.*; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author tstrazzere + */ +@RunWith(Enclosed.class) +public class TestAXMLResource { + public static class FunctionalTest { + + // Legacy files from original repo + String[] oldTestFiles = {"test.xml", "test1.xml", "test2.xml", "test3.xml"}; + + // Large file with weird tricks that broke tools in the past + String largeFromMalware = "large_from_malware.xml"; + + AXMLResource underTest; + + @Before + public void setUp() { + underTest = new AXMLResource(); + } + + @Test + public void testReadingOldFiles() throws IOException { + for (String file : oldTestFiles) { + InputStream testStream = this.getClass().getClassLoader().getResourceAsStream(file); + + // Should throw no exceptions + underTest.read(testStream); + } + } + + @Test + public void testPrinting() throws IOException { + InputStream testStream = this.getClass().getClassLoader().getResourceAsStream(largeFromMalware); + + underTest = new AXMLResource(testStream); + + underTest.print(); + } + + @Test + public void testInsertApplicationAttribute() throws IOException { + InputStream testStream = this.getClass().getClassLoader().getResourceAsStream(largeFromMalware); + + underTest.read(testStream); + + Attribute attribute = new Attribute("android", + "name", + "test", + AttributeType.STRING, + null, + underTest.getStringSection()); + + underTest.injectApplicationAttribute(attribute); + + StartTag startTag = underTest.getApplicationTag(); + + assertTrue(startTag.getAttributes().contains(attribute)); + } + + @Test + public void testWriteInsertedApplicationAttribute() throws IOException { + InputStream testStream = this.getClass().getClassLoader().getResourceAsStream(largeFromMalware); + + underTest.read(testStream); + + Attribute attribute = new Attribute("android", + "name", + "test", + AttributeType.STRING, + null, + underTest.getStringSection()); + + underTest.injectApplicationAttribute(attribute); + + File file = File.createTempFile("axml-func-test", "xml-test"); + file.deleteOnExit(); + + underTest.write(new FileOutputStream(file)); + + underTest = new AXMLResource(new FileInputStream(file)); + StartTag startTag = underTest.getApplicationTag(); + + assertEquals(underTest.getStringSection().getString(startTag.getAttributes().get(3).getNameIndex()), + "name"); + assertEquals(underTest.getStringSection().getString(startTag.getAttributes().get(3).getStringDataIndex()), + "test"); + } + } +} diff --git a/src/test/resources/large_from_malware.xml b/src/test/resources/large_from_malware.xml new file mode 100644 index 0000000000000000000000000000000000000000..423790c812dec1fdac0b6d43b1daacea44ac68a6 GIT binary patch literal 17080 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 literal 0 HcmV?d00001