From 969fdaa49921d8a4a6c73c740abf20b36ccc2325 Mon Sep 17 00:00:00 2001 From: baldurk Date: Mon, 29 Jan 2018 22:37:55 +0000 Subject: [PATCH] Add the ability to patch android binary XML manifests in-place * This lets us add the debuggable flag we need, at the cost of needing to re-sign the APK. It works in many cases although sometimes it does fail - but this is provided just as a 'best effort' and not as a recommended workflow. --- docs/credits_acknowledgements.rst | 2 +- renderdoc/3rdparty/android/android_manifest.h | 251 +++++++ renderdoc/CMakeLists.txt | 2 + renderdoc/android/android_manifest.cpp | 639 ++++++++++++++++++ renderdoc/android/android_patch.cpp | 203 +++++- renderdoc/android/android_utils.h | 2 + renderdoc/renderdoc.vcxproj | 2 + renderdoc/renderdoc.vcxproj.filters | 9 + 8 files changed, 1103 insertions(+), 7 deletions(-) create mode 100644 renderdoc/3rdparty/android/android_manifest.h create mode 100644 renderdoc/android/android_manifest.cpp diff --git a/docs/credits_acknowledgements.rst b/docs/credits_acknowledgements.rst index f3d3d3776..9c5ba02c2 100644 --- a/docs/credits_acknowledgements.rst +++ b/docs/credits_acknowledgements.rst @@ -110,7 +110,7 @@ The following libraries and components are incorporated into RenderDoc, listed h * `AOSP `_ - Copyright (c) 2006-2016, The Android Open Source Project, distributed under the Apache 2.0 License. - Used to simplify Android workflows by distributing some tools from the android SDK. + Used to simplify Android workflows by distributing some tools from the android SDK, as well as patching android manifest files to enable debugging. Thanks ------ diff --git a/renderdoc/3rdparty/android/android_manifest.h b/renderdoc/3rdparty/android/android_manifest.h new file mode 100644 index 000000000..013ca68a6 --- /dev/null +++ b/renderdoc/3rdparty/android/android_manifest.h @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +////////////////////////////////////////////////////////////////////////////////// +// +// These constants are taken from ResourceTypes.h: +// https://android.googlesource.com/platform/frameworks/base/+/42ebcb80b50834a1ce4755cd4ca86918c96ca3c6/libs/androidfw/include/androidfw/ResourceTypes.h +// +// The ones we need are extracted here to not be dependent on the rest of the android framework. +// There are some slight modifications for ease of integration, but this is still all under the +// android license. + +enum class ResType : uint16_t +{ + Null = 0x0000, + StringPool = 0x0001, + XML = 0x0003, + NamespaceStart = 0x0100, + NamespaceEnd = 0x0101, + StartElement = 0x0102, + EndElement = 0x0103, + CDATA = 0x0104, + ResourceMap = 0x0180, +}; + +/** + * Header that appears at the front of every data chunk in a resource. + */ +struct ResChunk_header +{ + // Type identifier for this chunk. The meaning of this value depends + // on the containing chunk. + ResType type; + + // Size of the chunk header (in bytes). Adding this value to + // the address of the chunk allows you to find its associated data + // (if any). + uint16_t headerSize; + + // Total size of this chunk (in bytes). This is the chunkSize plus + // the size of any data associated with the chunk. Adding this value + // to the chunk allows you to completely skip its contents (including + // any child chunks). If this value is the same as chunkSize, there is + // no data associated with the chunk. + uint32_t size; +}; + +/** + * Reference to a string in a string pool. + */ +struct ResStringPool_ref +{ + // Index into the string pool table (uint32_t-offset from the indices + // immediately after ResStringPool_header) at which to find the location + // of the string data in the pool. + uint32_t index; +}; + +/** + * Representation of a value in a resource, supplying type + * information. + */ +struct Res_value +{ + // Number of bytes in this structure. + uint16_t size; + + // Always set to 0. + uint8_t res0; + + // Type of the data value. + enum class DataType : uint8_t + { + // The 'data' holds an index into the containing resource table's + // global value string pool. + String = 0x03, + // The 'data' is either 0 or 1, for input "false" or "true" respectively. + Boolean = 0x12, + } dataType; + + // The data for this item, as interpreted according to dataType. + uint32_t data; +}; + +/** + * Definition for a pool of strings. The data of this chunk is an + * array of uint32_t providing indices into the pool, relative to + * stringsStart. At stringsStart are all of the UTF-16 strings + * concatenated together; each starts with a uint16_t of the string's + * length and each ends with a 0x0000 terminator. If a string is > + * 32767 characters, the high bit of the length is set meaning to take + * those 15 bits as a high word and it will be followed by another + * uint16_t containing the low word. + * + * If styleCount is not zero, then immediately following the array of + * uint32_t indices into the string table is another array of indices + * into a style table starting at stylesStart. Each entry in the + * style table is an array of ResStringPool_span structures. + */ +struct ResStringPool_header +{ + struct ResChunk_header header; + + // Number of strings in this pool (number of uint32_t indices that follow + // in the data). + uint32_t stringCount; + + // Number of style span arrays in the pool (number of uint32_t indices + // follow the string indices). + uint32_t styleCount; + + // Flags. + enum StringFlags : uint32_t + { + // If set, the string index is sorted by the string values (based + // on strcmp16()). + SORTED_FLAG = 1 << 0, + + // String pool is encoded in UTF-8 + UTF8_FLAG = 1 << 8 + }; + StringFlags flags; + + // Index from header of the string data. + uint32_t stringsStart; + + // Index from header of the style data. + uint32_t stylesStart; +}; + +/** + * Basic XML tree node. A single item in the XML document. Extended info + * about the node can be found after header.headerSize. + */ +struct ResXMLTree_node +{ + struct ResChunk_header header; + + // Line number in original source file at which this element appeared. + uint32_t lineNumber; + + // Optional XML comment that was associated with this element; -1 if none. + ResStringPool_ref comment; +}; + +/** + * Extended XML tree node for namespace start/end nodes. + * Appears header.headerSize bytes after a ResXMLTree_node. + */ +struct ResXMLTree_namespaceExt +{ + // The prefix of the namespace. + struct ResStringPool_ref prefix; + + // The URI of the namespace. + struct ResStringPool_ref uri; +}; + +/** + * Extended XML tree node for element start/end nodes. + * Appears header.headerSize bytes after a ResXMLTree_node. + */ +struct ResXMLTree_endElementExt +{ + // String of the full namespace of this element. + struct ResStringPool_ref ns; + + // String name of this node if it is an ELEMENT; the raw + // character data if this is a CDATA node. + struct ResStringPool_ref name; +}; + +/** + * Extended XML tree node for start tags -- includes attribute + * information. + * Appears header.headerSize bytes after a ResXMLTree_node. + */ +struct ResXMLTree_attrExt +{ + // String of the full namespace of this element. + struct ResStringPool_ref ns; + + // String name of this node if it is an ELEMENT; the raw + // character data if this is a CDATA node. + struct ResStringPool_ref name; + + // Byte offset from the start of this structure where the attributes start. + uint16_t attributeStart; + + // Size of the ResXMLTree_attribute structures that follow. + uint16_t attributeSize; + + // Number of attributes associated with an ELEMENT. These are + // available as an array of ResXMLTree_attribute structures + // immediately following this node. + uint16_t attributeCount; + + // Index (1-based) of the "id" attribute. 0 if none. + uint16_t idIndex; + + // Index (1-based) of the "class" attribute. 0 if none. + uint16_t classIndex; + + // Index (1-based) of the "style" attribute. 0 if none. + uint16_t styleIndex; +}; + +struct ResXMLTree_attribute +{ + // Namespace of this attribute. + struct ResStringPool_ref ns; + + // Name of this attribute. + struct ResStringPool_ref name; + + // The original raw string value of this attribute. + struct ResStringPool_ref rawValue; + + // Processesd typed value of this attribute. + struct Res_value typedValue; +}; + +/** + * Extended XML tree node for CDATA tags -- includes the CDATA string. + * Appears header.headerSize bytes after a ResXMLTree_node. + */ +struct ResXMLTree_cdataExt +{ + // The raw CDATA character data. + struct ResStringPool_ref data; + + // The typed value of the character data if this is a CDATA node. + struct Res_value typedData; +}; \ No newline at end of file diff --git a/renderdoc/CMakeLists.txt b/renderdoc/CMakeLists.txt index 915d77289..f142bc355 100644 --- a/renderdoc/CMakeLists.txt +++ b/renderdoc/CMakeLists.txt @@ -93,6 +93,7 @@ set(sources android/android_patch.cpp android/android_tools.cpp android/android_utils.cpp + android/android_manifest.cpp android/android.h android/android_utils.h android/jdwp.h @@ -156,6 +157,7 @@ set(sources 3rdparty/jpeg-compressor/jpgd.h 3rdparty/jpeg-compressor/jpge.cpp 3rdparty/jpeg-compressor/jpge.h + 3rdparty/android/android_manifest.h 3rdparty/catch/catch.cpp 3rdparty/catch/catch.hpp 3rdparty/pugixml/pugixml.cpp diff --git a/renderdoc/android/android_manifest.cpp b/renderdoc/android/android_manifest.cpp new file mode 100644 index 000000000..4a5b5b368 --- /dev/null +++ b/renderdoc/android/android_manifest.cpp @@ -0,0 +1,639 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2018 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "3rdparty/android/android_manifest.h" +#include "core/core.h" +#include "strings/string_utils.h" +#include "android_utils.h" + +const uint32_t debuggableResourceId = 0x0101000f; +const uint32_t addingStringIndex = 0x8b8b8b8b; + +namespace Android +{ +std::string GetStringPoolValue(ResStringPool_header *stringpool, ResStringPool_ref ref) +{ + byte *base = (byte *)stringpool; + + uint32_t stringCount = stringpool->stringCount; + uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize); + byte *stringData = base + stringpool->stringsStart; + + if(ref.index == ~0U) + return ""; + + if(ref.index >= stringCount) + return "__invalid_string__"; + + byte *strdata = stringData + stringOffsets[ref.index]; + + // strdata now points at len characters of string. Check if it's UTF-8 or UTF-16 + if((stringpool->flags & ResStringPool_header::UTF8_FLAG) == 0) + { + uint16_t *str = (uint16_t *)strdata; + + uint32_t len = *(str++); + + // see comment above ResStringPool_header - if high bit is set, then this string is >32767 + // characters, so it's followed by another uint16_t with the low word + if(len & 0x8000) + { + len &= 0x7fff; + len <<= 16; + len |= *(str++); + } + + std::wstring wstr; + + // wchar_t isn't always 2 bytes, so we iterate over the uint16_t and cast. + for(uint32_t i = 0; i < len; i++) + wstr.push_back(wchar_t(str[i])); + + return StringFormat::Wide2UTF8(wstr); + } + else + { + byte *str = (byte *)strdata; + + uint32_t len = *(str++); + + // the length works similarly for UTF-8 data but with single bytes instead of uint16s. + if(len & 0x80) + { + len &= 0x7f; + len <<= 8; + len |= *(str++); + } + + // the length is encoded twice. I can only assume to preserve uint16 size although I don't see + // why that would be necessary - it can't be fully backwards compatible even with the alignment + // except with readers that ignore the length entirely and look for trailing NULLs. + if(len < 0x80) + str++; + else + str += 2; + + return std::string((char *)str, (char *)(str + len)); + } +} + +void ShiftStringPoolValue(ResStringPool_ref &ref, uint32_t insertedLocation) +{ + // if we found our added attribute, then set the index here (otherwise we'd remap it with the + // others!) + if(ref.index == addingStringIndex) + ref.index = insertedLocation; + else if(ref.index != ~0U && ref.index >= insertedLocation) + ref.index++; +} + +void ShiftStringPoolValue(Res_value &val, uint32_t insertedLocation) +{ + if(val.dataType == Res_value::DataType::String && val.data >= insertedLocation) + val.data++; +} + +template +void InsertBytes(std::vector &bytes, byte *pos, const T &data) +{ + byte *start = &bytes[0]; + byte *byteData = (byte *)&data; + + size_t offs = pos - start; + + bytes.insert(bytes.begin() + offs, byteData, byteData + sizeof(T)); +} + +template <> +void InsertBytes(std::vector &bytes, byte *pos, const std::vector &data) +{ + byte *start = &bytes[0]; + + size_t offs = pos - start; + + bytes.insert(bytes.begin() + offs, data.begin(), data.end()); +} + +bool PatchManifest(std::vector &manifestBytes) +{ + // Whether to insert a new string & resource ID at the start or end of the resource map table. I + // can't find anything that indicates there is any required ordering to these, so either should be + // valid. + const bool insertStringAtStart = false; + + // reserve room for our modifications up front, to be sure that if we do make them we'll never + // invalidate any pointers. We could add: + manifestBytes.reserve( + manifestBytes.size() + + // - a string (uint32 offset, uint16 length and string characters (possibly + // in UTF-16) including NULL) + sizeof(uint32_t) + sizeof(uint16_t) + sizeof("debuggable") * 2 + + // - a resource ID mapping (one uint32) + sizeof(uint32_t) + + // - an attribute (ResXMLTree_attribute) + sizeof(ResXMLTree_attribute) + + // and we add 16 bytes more just for a safety margin with any necessary padding + 16); + + // save the capacity so we can check we never resize + size_t capacity = manifestBytes.capacity(); + + byte *start = &manifestBytes[0]; + byte *end = start + manifestBytes.size(); + + byte *cur = start; + + ResChunk_header *xmlroot = (ResChunk_header *)cur; + + if((byte *)(xmlroot + 1) > end) + { + RDCERR("Manifest is truncated, %zu bytes doesn't contain full XML header", manifestBytes.size()); + return false; + } + + if(xmlroot->type != ResType::XML) + { + RDCERR("XML Header is malformed, type is %u expected %u", xmlroot->type, ResType::XML); + return false; + } + + if(xmlroot->headerSize != sizeof(*xmlroot)) + { + RDCERR("XML Header is malformed, header size is reported as %u but expected %u", + xmlroot->headerSize, sizeof(*xmlroot)); + return false; + } + + // this isn't necessarily fatal, but it is unexpected. + if(xmlroot->size != manifestBytes.size()) + RDCWARN("XML header is malformed, size is reported as %u but %zu bytes found", xmlroot->size, + manifestBytes.size()); + + cur += xmlroot->headerSize; + + ResStringPool_header *stringpool = (ResStringPool_header *)cur; + + if(stringpool->header.type != ResType::StringPool) + { + RDCERR("Manifest format is unsupported, expected string pool but got %u", + stringpool->header.type); + return false; + } + + if(stringpool->header.headerSize != sizeof(*stringpool)) + { + RDCERR("String pool is malformed, header size is reported as %u but expected %u", + stringpool->header.headerSize, sizeof(*stringpool)); + return false; + } + + if(cur + stringpool->header.size > end) + { + RDCERR("String pool is truncated, expected %u more bytes but only have %u", + stringpool->header.size, uint32_t(end - cur)); + return false; + } + + cur += stringpool->header.size; + + ResChunk_header *resMap = (ResChunk_header *)cur; + + if(resMap->type != ResType::ResourceMap) + { + RDCERR("Manifest format is unsupported, expected resource table but got %u", resMap->type); + return false; + } + + if(resMap->headerSize != sizeof(*resMap)) + { + RDCERR("Resource map is malformed, header size is reported as %u but expected %u", + resMap->headerSize, sizeof(*resMap)); + return false; + } + + if(cur + resMap->size > end) + { + RDCERR("Resource map is truncated, expected %u more bytes but only have %u", resMap->size, + uint32_t(end - cur)); + return false; + } + + uint32_t *resourceMapping = (uint32_t *)(cur + resMap->headerSize); + uint32_t resourceMappingCount = (resMap->size - resMap->headerSize) / sizeof(uint32_t); + + cur += resMap->size; + + bool stringAdded = false; + + // now chunks will come along. There will likely first be a namespace begin, then XML tag open and + // close. Since the tag is only valid in one place in the XML we can just continue + // iterating until we find it - we don't actually need to care about the structure of the XML + // since we are identifying a unique tag and adding one attribute. + while(cur < end) + { + ResChunk_header *node = (ResChunk_header *)cur; + + if(node->type != ResType::StartElement) + { + cur += node->size; + continue; + } + + ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->headerSize); + + std::string name = GetStringPoolValue(stringpool, startElement->name); + + if(name != "application") + { + cur += node->size; + continue; + } + + // found the application tag! Now search its attribtues to see if it already has a debuggable + // attribute (that might be set explicitly to false instead of defaulting) + if(startElement->attributeSize != sizeof(ResXMLTree_attribute)) + { + RDCWARN("Declared attribute size %u doesn't match what we expect %zu", + startElement->attributeSize, sizeof(ResXMLTree_attribute)); + } + + if(startElement->attributeStart != sizeof(*startElement)) + { + RDCWARN("Declared attribute start offset %u doesn't match what we expect %zu", + startElement->attributeStart, sizeof(*startElement)); + } + + byte *attributesStart = cur + node->headerSize + startElement->attributeStart; + + bool found = false; + + for(uint32_t i = 0; i < startElement->attributeCount; i++) + { + ResXMLTree_attribute *attribute = + (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i); + + std::string attr = GetStringPoolValue(stringpool, attribute->name); + + if(attr != "debuggable") + continue; + + uint32_t resourceId = 0; + + if(attribute->name.index < resourceMappingCount) + { + resourceId = resourceMapping[attribute->name.index]; + } + else + { + RDCWARN("Found debuggable attribute, but it's not linked to any resource ID"); + + if(attribute->typedValue.dataType != Res_value::DataType::Boolean) + { + RDCERR("Found debuggable attribute that isn't boolean typed! Not modifying"); + return false; + } + else + { + RDCDEBUG("Setting non-resource ID debuggable attribute to true"); + attribute->typedValue.data = ~0U; + + if(attribute->rawValue.index != ~0U) + { + RDCWARN("attribute has raw value '%s' which we aren't patching", + GetStringPoolValue(stringpool, attribute->rawValue).c_str()); + } + + // we'll still add a debuggable attribute that is resource ID linked, so we don't mark the + // attribute as found and break out of the loop yet + continue; + } + } + + if(resourceId != debuggableResourceId) + { + RDCERR( + "Found debuggable attribute mapped to resource %x, not %x as we expect! Not modifying", + resourceId, debuggableResourceId); + return false; + } + + RDCDEBUG("Found debuggable attribute."); + + if(attribute->typedValue.dataType != Res_value::DataType::Boolean) + { + RDCERR("Found debuggable attribute that isn't boolean typed! Not modifying"); + return false; + } + else + { + RDCDEBUG("Setting resource ID debuggable attribute to true"); + attribute->typedValue.data = ~0U; + + if(attribute->rawValue.index != ~0U) + { + RDCWARN("attribute has raw value '%s' which we aren't patching", + GetStringPoolValue(stringpool, attribute->rawValue).c_str()); + } + } + + found = true; + break; + } + + if(found) + break; + + if(startElement->attributeSize != sizeof(ResXMLTree_attribute)) + { + RDCERR("Unexpected attribute size %u, can't add missing attribute", + startElement->attributeSize); + return false; + } + + // default to an invalid value (the manifest would have to be GBs to have this as a valid string + // index. + // If we don't find the existing string to use, then this will be remapped below when we're + // remapping all the other indices. + ResStringPool_ref stringIndex = {addingStringIndex}; + + // we didn't find the attribute, so we need to search for the appropriate string, add it if not + // there, and add the attribute. + for(uint32_t i = 0; i < resourceMappingCount; i++) + { + if(resourceMapping[i] == debuggableResourceId) + { + std::string str = GetStringPoolValue(stringpool, {i}); + + if(str != "debuggable") + { + RDCWARN("Found debuggable resource ID, but it was linked to string '%s' not 'debuggable'", + str.c_str()); + continue; + } + + stringIndex = {i}; + } + } + + // declare the debuggable attribute + ResXMLTree_attribute debuggable; + debuggable.ns.index = ~0U; + debuggable.name = stringIndex; + debuggable.rawValue.index = ~0U; + debuggable.typedValue.size = sizeof(Res_value); + debuggable.typedValue.res0 = 0; + debuggable.typedValue.dataType = Res_value::DataType::Boolean; + debuggable.typedValue.data = ~0U; + + // search the stringpool for the schema, it should be there already. + for(uint32_t i = 0; i < stringpool->stringCount; i++) + { + std::string val = GetStringPoolValue(stringpool, {i}); + if(val == "http://schemas.android.com/apk/res/android") + { + debuggable.ns.index = i; + break; + } + } + + if(debuggable.ns.index == ~0U) + RDCWARN("Couldn't find android schema, declaring attribute without schema"); + + // it seems the attribute must be added so that the attributes are sorted in resource ID order. + // We assume the attributes are already sorted according to this order, so we insert at the + // index of the first attribute we encounter with either no resource ID (i.e. if we only + // encountered lower resource IDs then we hit a non-resource ID attribute), or a higher resource + // ID than ours (in which case we're inserting it in the right place). + uint32_t attributeInsertIndex = 0; + + for(uint32_t i = 0; i < startElement->attributeCount; i++) + { + ResXMLTree_attribute *attr = + (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i); + + if(attr->name.index >= resourceMappingCount) + { + attributeInsertIndex = i; + RDCDEBUG("Inserting attribute before %s, with no resource ID", + GetStringPoolValue(stringpool, attr->name).c_str()); + break; + } + + uint32_t resourceId = resourceMapping[attr->name.index]; + + if(resourceId >= debuggableResourceId) + { + attributeInsertIndex = i; + RDCDEBUG("Inserting attribute before %s, with resource ID %x", + GetStringPoolValue(stringpool, attr->name).c_str(), resourceId); + break; + } + + RDCDEBUG("Skipping past attribute %s, with resource ID %x", + GetStringPoolValue(stringpool, attr->name).c_str(), resourceId); + } + + InsertBytes(manifestBytes, attributesStart + startElement->attributeSize * attributeInsertIndex, + debuggable); + + // update header + startElement->attributeCount++; + node->size += sizeof(ResXMLTree_attribute); + + stringAdded = (stringIndex.index == addingStringIndex); + + break; + } + + // if we added the string, we need to update the string pool and resource map, then finally update + // all stringrefs in the nodes. We do this in reverse order so that we don't invalidate pointers + // with insertions + if(stringAdded) + { + uint32_t insertIdx = insertStringAtStart ? 0 : resourceMappingCount; + + // add to the resource map + { + if(insertIdx == 0) + InsertBytes(manifestBytes, (byte *)resMap + resMap->headerSize, debuggableResourceId); + else + InsertBytes(manifestBytes, (byte *)resMap + resMap->size, debuggableResourceId); + resMap->size += sizeof(uint32_t); + } + + // add to the string pool + { + // add the offset + stringpool->header.size += sizeof(uint32_t); + stringpool->stringCount++; + stringpool->stringsStart += sizeof(uint32_t); + // if we're adding a string we don't bother to do it sorted, so remove the sorted flag + stringpool->flags = + ResStringPool_header::StringFlags(stringpool->flags & ~ResStringPool_header::SORTED_FLAG); + + byte *base = (byte *)stringpool; + + uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize); + + // we duplicate the offset at the position we're inserting. Then when we fix up all the other + // offsets the duplicated one shifts by the right amount. + InsertBytes(manifestBytes, + (byte *)stringpool + stringpool->header.headerSize + sizeof(uint32_t) * insertIdx, + stringOffsets[insertIdx]); + + uint32_t shift = 0; + + byte *stringData = (byte *)stringpool + stringpool->stringsStart; + + // insert the string, with length prefix and trailing NULL + if(stringpool->flags & ResStringPool_header::UTF8_FLAG) + { + std::vector bytes = {0xA, 0xA, 'd', 'e', 'b', 'u', 'g', 'g', 'a', 'b', 'l', 'e', 0}; + shift = (uint32_t)bytes.size(); + + InsertBytes(manifestBytes, stringData + stringOffsets[insertIdx], bytes); + } + else + { + std::vector bytes = {0xA, 0x0, 'd', 0, 'e', 0, 'b', 0, 'u', 0, 'g', 0, + 'g', 0, 'a', 0, 'b', 0, 'l', 0, 'e', 0, 0, 0}; + shift = (uint32_t)bytes.size(); + + InsertBytes(manifestBytes, stringData + stringOffsets[insertIdx], bytes); + } + + // account for added string + stringpool->header.size += shift; + + // shift all the offsets *after* the string we inserted (we inserted precisely at that + // offset). + for(uint32_t i = insertIdx + 1; i < stringpool->stringCount; i++) + stringOffsets[i] += shift; + + // if the stringpool isn't integer aligned, add padding bytes + uint32_t alignedSize = AlignUp4(stringpool->header.size); + + if(alignedSize > stringpool->header.size) + { + uint32_t paddingLen = alignedSize - stringpool->header.size; + + RDCDEBUG("Inserting %u padding bytes to align %u up to %u", paddingLen, + stringpool->header.size, alignedSize); + + InsertBytes(manifestBytes, base + stringpool->header.size, + std::vector((size_t)paddingLen, 0)); + + stringpool->header.size += paddingLen; + } + } + + // now iterate over all nodes and fixup any stringrefs pointing after our insert point + cur = start + xmlroot->headerSize; + // skip string pool + cur += ((ResChunk_header *)cur)->size; + // skip resource map + cur += ((ResChunk_header *)cur)->size; + + while(cur < end) + { + ResXMLTree_node *node = (ResXMLTree_node *)cur; + + if(node->header.headerSize != sizeof(*node)) + RDCWARN("Headersize was reported as %u, but we expected ResXMLTree_node size %zu", + node->header.headerSize, sizeof(*node)); + + ShiftStringPoolValue(node->comment, insertIdx); + + switch(node->header.type) + { + // namespace start and end are identical + case ResType::NamespaceStart: + case ResType::NamespaceEnd: + { + ResXMLTree_namespaceExt *ns = (ResXMLTree_namespaceExt *)(cur + node->header.headerSize); + + ShiftStringPoolValue(ns->prefix, insertIdx); + ShiftStringPoolValue(ns->uri, insertIdx); + break; + } + case ResType::EndElement: + { + ResXMLTree_endElementExt *endElement = + (ResXMLTree_endElementExt *)(cur + node->header.headerSize); + + ShiftStringPoolValue(endElement->ns, insertIdx); + ShiftStringPoolValue(endElement->name, insertIdx); + break; + } + case ResType::CDATA: + { + ResXMLTree_cdataExt *cdata = (ResXMLTree_cdataExt *)(cur + node->header.headerSize); + + ShiftStringPoolValue(cdata->data, insertIdx); + ShiftStringPoolValue(cdata->typedData, insertIdx); + break; + } + case ResType::StartElement: + { + ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->header.headerSize); + + ShiftStringPoolValue(startElement->ns, insertIdx); + ShiftStringPoolValue(startElement->name, insertIdx); + + // update attributes + byte *attributesStart = cur + node->header.headerSize + startElement->attributeStart; + + for(uint32_t i = 0; i < startElement->attributeCount; i++) + { + ResXMLTree_attribute *attr = + (ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i); + + ShiftStringPoolValue(attr->ns, insertIdx); + ShiftStringPoolValue(attr->name, insertIdx); + ShiftStringPoolValue(attr->rawValue, insertIdx); + ShiftStringPoolValue(attr->typedValue, insertIdx); + } + break; + } + default: + RDCERR("Unhandled chunk %x, can't patch stringpool references", node->header.type); + return false; + } + + cur += node->header.size; + } + } + + xmlroot->size = (uint32_t)manifestBytes.size(); + + if(manifestBytes.capacity() > capacity) + { + RDCERR( + "manifest vector resized during patching! Update reserve() at the start of " + "Android::PatchManifest"); + } + + return true; +} +}; \ No newline at end of file diff --git a/renderdoc/android/android_patch.cpp b/renderdoc/android/android_patch.cpp index 913d93733..b14d4699a 100644 --- a/renderdoc/android/android_patch.cpp +++ b/renderdoc/android/android_patch.cpp @@ -23,6 +23,7 @@ ******************************************************************************/ #include +#include "3rdparty/miniz/miniz.h" #include "api/replay/version.h" #include "core/core.h" #include "strings/string_utils.h" @@ -87,6 +88,86 @@ bool RemoveAPKSignature(const string &apk) return true; } +bool ExtractAndRemoveManifest(const std::string &apk, std::vector &manifest) +{ + // pull out the manifest with miniz + mz_zip_archive zip; + RDCEraseEl(zip); + + mz_bool b = mz_zip_reader_init_file(&zip, apk.c_str(), 0); + + if(b) + { + mz_uint numfiles = mz_zip_reader_get_num_files(&zip); + + for(mz_uint i = 0; i < numfiles; i++) + { + mz_zip_archive_file_stat zstat; + mz_zip_reader_file_stat(&zip, i, &zstat); + + if(!strcmp(zstat.m_filename, "AndroidManifest.xml")) + { + size_t sz = 0; + byte *buf = (byte *)mz_zip_reader_extract_to_heap(&zip, i, &sz, 0); + + RDCLOG("Got manifest of %zu bytes", sz); + + manifest.insert(manifest.begin(), buf, buf + sz); + } + } + } + else + { + RDCERR("Couldn't open %s", apk.c_str()); + } + + mz_zip_reader_end(&zip); + + if(manifest.empty()) + return false; + + std::string aapt = getToolPath(ToolDir::BuildTools, "aapt", false); + + RDCDEBUG("Removing AndroidManifest.xml"); + execCommand(aapt, "remove \"" + apk + "\" AndroidManifest.xml"); + + std::string fileList = execCommand(aapt, "list \"" + apk + "\"").strStdout; + std::vector files; + split(fileList, files, ' '); + + for(const std::string &f : files) + { + if(trim(f) == "AndroidManifest.xml") + { + RDCERR("AndroidManifest.xml found, that means removal failed!"); + return false; + } + } + + return true; +} + +bool AddManifestToAPK(const std::string &apk, const std::string &tmpDir, + const std::vector &manifest) +{ + std::string aapt = getToolPath(ToolDir::BuildTools, "aapt", false); + + // write the manifest to disk + FileIO::dump((tmpDir + "AndroidManifest.xml").c_str(), manifest.data(), manifest.size()); + + // run aapt to add the manifest + Process::ProcessResult result = + execCommand(aapt, "add \"" + apk + "\" AndroidManifest.xml", tmpDir); + + if(result.strStdout.empty()) + { + RDCERR("Failed to add manifest to APK. STDERR: %s", result.strStderror.c_str()); + return false; + } + + return true; +} + bool RealignAPK(const string &apk, string &alignedAPK, const string &tmpDir) { std::string zipalign = getToolPath(ToolDir::BuildTools, "zipalign", false); @@ -243,7 +324,10 @@ bool ReinstallPatchedAPK(const string &deviceID, const string &apk, const string { RDCLOG("Reinstalling APK"); - adbExecCommand(deviceID, "install --abi " + abi + " \"" + apk + "\"", workDir); + if(abi == "null" || abi.empty()) + adbExecCommand(deviceID, "install \"" + apk + "\"", workDir); + else + adbExecCommand(deviceID, "install --abi " + abi + " \"" + apk + "\"", workDir); // Wait until re-install completes string reinstallResult; @@ -315,6 +399,37 @@ bool CheckPatchingRequirements() return true; } +std::string DetermineInstalledABI(const std::string &deviceID, const std::string &packageName) +{ + RDCLOG("Checking installed ABI for %s", packageName.c_str()); + string abi; + + string dump = adbExecCommand(deviceID, "shell pm dump " + packageName).strStdout; + if(dump.empty()) + RDCERR("Unable to pm dump %s", packageName.c_str()); + + // Walk through the output and look for primaryCpuAbi + std::istringstream contents(dump); + string line; + string prefix("primaryCpuAbi="); + while(std::getline(contents, line)) + { + line = trim(line); + if(line.compare(0, prefix.size(), prefix) == 0) + { + // Extract the abi + abi = line.substr(line.find_last_of("=") + 1); + RDCLOG("primaryCpuAbi found: %s", abi.c_str()); + break; + } + } + + if(abi.empty()) + RDCERR("Unable to determine installed abi for: %s", packageName.c_str()); + + return abi; +} + bool PullAPK(const string &deviceID, const string &pkgPath, const string &apk) { RDCLOG("Pulling APK to patch"); @@ -369,10 +484,7 @@ std::string GetFirstMatchingLine(const std::string &haystack, const std::string size_t needleOffset = haystack.find(needle); if(needleOffset == std::string::npos) - { - RDCERR("Couldn't get pkgFlags from adb"); return ""; - } size_t nextLine = haystack.find('\n', needleOffset + 1); @@ -430,6 +542,85 @@ extern "C" RENDERDOC_API void RENDERDOC_CC RENDERDOC_CheckAndroidPackage(const c extern "C" RENDERDOC_API AndroidFlags RENDERDOC_CC RENDERDOC_MakeDebuggablePackage( const char *hostname, const char *packageName, RENDERDOC_ProgressCallback progress) { - // stub for now - return AndroidFlags::ManifestPatchFailure; + Process::ProcessResult result = {}; + std::string package(basename(std::string(packageName))); + + int index = 0; + std::string deviceID; + Android::ExtractDeviceIDAndIndex(hostname, index, deviceID); + + // make sure progress is valid so we don't have to check it everywhere + if(!progress) + progress = [](float) {}; + + progress(0.0f); + + if(!Android::CheckPatchingRequirements()) + return AndroidFlags::MissingTools; + + progress(0.02f); + + std::string abi = Android::DetermineInstalledABI(deviceID, package); + + // Find the APK on the device + std::string pkgPath = Android::GetPathForPackage(deviceID, package) + "base.apk"; + + std::string tmpDir = FileIO::GetTempFolderFilename(); + std::string origAPK(tmpDir + package + ".orig.apk"); + std::string alignedAPK(origAPK + ".aligned.apk"); + std::vector manifest; + + // Try the following steps, bailing if anything fails + if(!Android::PullAPK(deviceID, pkgPath, origAPK)) + return AndroidFlags::ManifestPatchFailure; + + progress(0.4f); + + if(!Android::RemoveAPKSignature(origAPK)) + return AndroidFlags::ManifestPatchFailure; + + progress(0.425f); + + if(!Android::ExtractAndRemoveManifest(origAPK, manifest)) + return AndroidFlags::ManifestPatchFailure; + + progress(0.45f); + + if(!Android::PatchManifest(manifest)) + return AndroidFlags::ManifestPatchFailure; + + progress(0.46f); + + if(!Android::AddManifestToAPK(origAPK, tmpDir, manifest)) + return AndroidFlags::ManifestPatchFailure; + + progress(0.475f); + + if(!Android::RealignAPK(origAPK, alignedAPK, tmpDir)) + return AndroidFlags::RepackagingAPKFailure; + + progress(0.5f); + + if(!Android::DebugSignAPK(alignedAPK, tmpDir)) + return AndroidFlags::RepackagingAPKFailure; + + progress(0.525f); + + if(!Android::UninstallOriginalAPK(deviceID, packageName, tmpDir)) + return AndroidFlags::RepackagingAPKFailure; + + progress(0.6f); + + if(!Android::ReinstallPatchedAPK(deviceID, alignedAPK, abi, packageName, tmpDir)) + return AndroidFlags::RepackagingAPKFailure; + + progress(0.95f); + + if(!Android::IsDebuggable(deviceID, packageName)) + return AndroidFlags::ManifestPatchFailure; + + progress(1.0f); + + // All clean! + return AndroidFlags::Debuggable; } diff --git a/renderdoc/android/android_utils.h b/renderdoc/android/android_utils.h index 5472fbc02..cfc172343 100644 --- a/renderdoc/android/android_utils.h +++ b/renderdoc/android/android_utils.h @@ -65,4 +65,6 @@ ABI GetABI(const std::string &abiName); std::vector GetSupportedABIs(const std::string &deviceID); std::string GetRenderDocPackageForABI(ABI abi); std::string GetPathForPackage(const std::string &deviceID, const std::string &packageName); + +bool PatchManifest(std::vector &manifest); }; diff --git a/renderdoc/renderdoc.vcxproj b/renderdoc/renderdoc.vcxproj index fb9dbe04e..dee7119fc 100644 --- a/renderdoc/renderdoc.vcxproj +++ b/renderdoc/renderdoc.vcxproj @@ -105,6 +105,7 @@ + @@ -308,6 +309,7 @@ + diff --git a/renderdoc/renderdoc.vcxproj.filters b/renderdoc/renderdoc.vcxproj.filters index b904265a7..482b1d843 100644 --- a/renderdoc/renderdoc.vcxproj.filters +++ b/renderdoc/renderdoc.vcxproj.filters @@ -112,6 +112,9 @@ {f9ed2873-6120-4b34-a3c2-e57b2de31eb9} + + {6e798cbc-0eae-4f48-9d7c-f3fe58bd32dc} + @@ -384,6 +387,9 @@ Android + + 3rdparty\android + @@ -677,6 +683,9 @@ Android + + Android +