Use offsets rather than pointers when patching android manifest

* Fixes a crash when using bytebuf now would cause resizes
This commit is contained in:
baldurk
2020-01-07 15:19:59 +00:00
parent 37f762a08f
commit b082bbe78f
+170 -173
View File
@@ -30,15 +30,50 @@
const uint32_t debuggableResourceId = 0x0101000f;
const uint32_t addingStringIndex = 0x8b8b8b8b;
// the string pool always immediately follows the XML header, which is just an empty header.
const size_t stringpoolOffset = sizeof(ResChunk_header);
namespace Android
{
rdcstr GetStringPoolValue(ResStringPool_header *stringpool, ResStringPool_ref ref)
template <typename T>
void SetFromBytes(bytebuf &bytes, size_t offs, const T &t)
{
byte *base = (byte *)stringpool;
T *ptr = (T *)(bytes.data() + offs);
memcpy(ptr, &t, sizeof(T));
}
uint32_t stringCount = stringpool->stringCount;
uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize);
byte *stringData = base + stringpool->stringsStart;
template <typename T>
T GetFromBytes(bytebuf &bytes, size_t offs)
{
T ret;
T *ptr = (T *)(bytes.data() + offs);
memcpy(&ret, ptr, sizeof(T));
return ret;
}
template <typename T>
void InsertBytes(bytebuf &bytes, size_t offs, const T &data)
{
byte *byteData = (byte *)&data;
bytes.insert(offs, byteData, sizeof(T));
}
template <>
void InsertBytes(bytebuf &bytes, size_t offs, const bytebuf &data)
{
bytes.insert(offs, data);
}
rdcstr GetStringPoolValue(bytebuf &bytes, ResStringPool_ref ref)
{
ResStringPool_header stringpool = GetFromBytes<ResStringPool_header>(bytes, stringpoolOffset);
byte *base = bytes.data() + stringpoolOffset;
uint32_t stringCount = stringpool.stringCount;
uint32_t *stringOffsets = (uint32_t *)(base + stringpool.header.headerSize);
byte *stringData = base + stringpool.stringsStart;
if(ref.index == ~0U)
return "";
@@ -49,7 +84,7 @@ rdcstr GetStringPoolValue(ResStringPool_header *stringpool, ResStringPool_ref re
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)
if((stringpool.flags & ResStringPool_header::UTF8_FLAG) == 0)
{
uint16_t *str = (uint16_t *)strdata;
@@ -114,127 +149,90 @@ void ShiftStringPoolValue(Res_value &val, uint32_t insertedLocation)
val.data++;
}
template <typename T>
void InsertBytes(bytebuf &bytes, byte *pos, const T &data)
{
byte *byteData = (byte *)&data;
bytes.insert(pos - &bytes[0], byteData, sizeof(T));
}
template <>
void InsertBytes(bytebuf &bytes, byte *pos, const bytebuf &data)
{
bytes.insert(pos - &bytes[0], data);
}
bool PatchManifest(bytebuf &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.front();
byte *cur = start;
ResChunk_header *xmlroot = (ResChunk_header *)cur;
if((byte *)(xmlroot + 1) > &manifestBytes.back())
if(manifestBytes.size() < sizeof(ResChunk_header))
{
RDCERR("Manifest is truncated, %zu bytes doesn't contain full XML header", manifestBytes.size());
return false;
}
if(xmlroot->type != ResType::XML)
size_t cur = 0;
ResChunk_header xmlroot = GetFromBytes<ResChunk_header>(manifestBytes, cur);
if(xmlroot.type != ResType::XML)
{
RDCERR("XML Header is malformed, type is %u expected %u", 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))
if(xmlroot.headerSize != sizeof(xmlroot))
{
RDCERR("XML Header is malformed, header size is reported as %u but expected %u",
xmlroot->headerSize, sizeof(*xmlroot));
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,
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;
cur += xmlroot.headerSize;
ResStringPool_header *stringpool = (ResStringPool_header *)cur;
ResStringPool_header stringpool = GetFromBytes<ResStringPool_header>(manifestBytes, cur);
if(stringpool->header.type != ResType::StringPool)
if(stringpool.header.type != ResType::StringPool)
{
RDCERR("Manifest format is unsupported, expected string pool but got %u",
stringpool->header.type);
RDCERR("Manifest format is unsupported, expected string pool but got %u", stringpool.header.type);
return false;
}
if(stringpool->header.headerSize != sizeof(*stringpool))
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));
stringpool.header.headerSize, sizeof(stringpool));
return false;
}
if(cur + stringpool->header.size > &manifestBytes.back())
if(cur + stringpool.header.size > manifestBytes.size())
{
RDCERR("String pool is truncated, expected %u more bytes but only have %u",
stringpool->header.size, uint32_t(&manifestBytes.back() - cur));
stringpool.header.size, manifestBytes.size() - cur);
return false;
}
cur += stringpool->header.size;
cur += stringpool.header.size;
ResChunk_header *resMap = (ResChunk_header *)cur;
ResChunk_header resMap = GetFromBytes<ResChunk_header>(manifestBytes, cur);
const size_t resMapOffset = cur;
if(resMap->type != ResType::ResourceMap)
if(resMap.type != ResType::ResourceMap)
{
RDCERR("Manifest format is unsupported, expected resource table but got %u", resMap->type);
RDCERR("Manifest format is unsupported, expected resource table but got %u", resMap.type);
return false;
}
if(resMap->headerSize != sizeof(*resMap))
if(resMap.headerSize != sizeof(resMap))
{
RDCERR("Resource map is malformed, header size is reported as %u but expected %u",
resMap->headerSize, sizeof(*resMap));
resMap.headerSize, sizeof(resMap));
return false;
}
if(cur + resMap->size > &manifestBytes.back())
if(cur + resMap.size > manifestBytes.size())
{
RDCERR("Resource map is truncated, expected %u more bytes but only have %u", resMap->size,
uint32_t(&manifestBytes.back() - cur));
RDCERR("Resource map is truncated, expected %u more bytes but only have %u", resMap.size,
manifestBytes.size() - cur);
return false;
}
uint32_t *resourceMapping = (uint32_t *)(cur + resMap->headerSize);
uint32_t resourceMappingCount = (resMap->size - resMap->headerSize) / sizeof(uint32_t);
const uint32_t resourceMappingCount = (resMap.size - resMap.headerSize) / sizeof(uint32_t);
const rdcarray<uint32_t> resourceMapping(
(const uint32_t *)(manifestBytes.data() + cur + resMap.headerSize), resourceMappingCount);
cur += resMap->size;
cur += resMap.size;
bool stringAdded = false;
@@ -242,50 +240,52 @@ bool PatchManifest(bytebuf &manifestBytes)
// close. Since the <application> 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 < &manifestBytes.back())
while(cur < manifestBytes.size())
{
ResChunk_header *node = (ResChunk_header *)cur;
ResChunk_header node = GetFromBytes<ResChunk_header>(manifestBytes, cur);
if(node->type != ResType::StartElement)
if(node.type != ResType::StartElement)
{
cur += node->size;
cur += node.size;
continue;
}
ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->headerSize);
ResXMLTree_attrExt startElement =
GetFromBytes<ResXMLTree_attrExt>(manifestBytes, cur + node.headerSize);
rdcstr name = GetStringPoolValue(stringpool, startElement->name);
rdcstr name = GetStringPoolValue(manifestBytes, startElement.name);
if(name != "application")
{
cur += node->size;
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))
if(startElement.attributeSize != sizeof(ResXMLTree_attribute))
{
RDCWARN("Declared attribute size %u doesn't match what we expect %zu",
startElement->attributeSize, sizeof(ResXMLTree_attribute));
startElement.attributeSize, sizeof(ResXMLTree_attribute));
}
if(startElement->attributeStart != sizeof(*startElement))
if(startElement.attributeStart != sizeof(startElement))
{
RDCWARN("Declared attribute start offset %u doesn't match what we expect %zu",
startElement->attributeStart, sizeof(*startElement));
startElement.attributeStart, sizeof(startElement));
}
byte *attributesStart = cur + node->headerSize + startElement->attributeStart;
const size_t attributeStartOffset = cur + node.headerSize + startElement.attributeStart;
byte *attributesStart = manifestBytes.data() + attributeStartOffset;
bool found = false;
for(uint32_t i = 0; i < startElement->attributeCount; i++)
for(uint32_t i = 0; i < startElement.attributeCount; i++)
{
ResXMLTree_attribute *attribute =
(ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i);
(ResXMLTree_attribute *)(attributesStart + startElement.attributeSize * i);
rdcstr attr = GetStringPoolValue(stringpool, attribute->name);
rdcstr attr = GetStringPoolValue(manifestBytes, attribute->name);
if(attr != "debuggable")
continue;
@@ -313,7 +313,7 @@ bool PatchManifest(bytebuf &manifestBytes)
if(attribute->rawValue.index != ~0U)
{
RDCWARN("attribute has raw value '%s' which we aren't patching",
GetStringPoolValue(stringpool, attribute->rawValue).c_str());
GetStringPoolValue(manifestBytes, attribute->rawValue).c_str());
}
// we'll still add a debuggable attribute that is resource ID linked, so we don't mark the
@@ -345,7 +345,7 @@ bool PatchManifest(bytebuf &manifestBytes)
if(attribute->rawValue.index != ~0U)
{
RDCWARN("attribute has raw value '%s' which we aren't patching",
GetStringPoolValue(stringpool, attribute->rawValue).c_str());
GetStringPoolValue(manifestBytes, attribute->rawValue).c_str());
}
}
@@ -356,10 +356,9 @@ bool PatchManifest(bytebuf &manifestBytes)
if(found)
break;
if(startElement->attributeSize != sizeof(ResXMLTree_attribute))
if(startElement.attributeSize != sizeof(ResXMLTree_attribute))
{
RDCERR("Unexpected attribute size %u, can't add missing attribute",
startElement->attributeSize);
RDCERR("Unexpected attribute size %u, can't add missing attribute", startElement.attributeSize);
return false;
}
@@ -375,7 +374,7 @@ bool PatchManifest(bytebuf &manifestBytes)
{
if(resourceMapping[i] == debuggableResourceId)
{
rdcstr str = GetStringPoolValue(stringpool, {i});
rdcstr str = GetStringPoolValue(manifestBytes, {i});
if(str != "debuggable")
{
@@ -399,9 +398,9 @@ bool PatchManifest(bytebuf &manifestBytes)
debuggable.typedValue.data = ~0U;
// search the stringpool for the schema, it should be there already.
for(uint32_t i = 0; i < stringpool->stringCount; i++)
for(uint32_t i = 0; i < stringpool.stringCount; i++)
{
rdcstr val = GetStringPoolValue(stringpool, {i});
rdcstr val = GetStringPoolValue(manifestBytes, {i});
if(val == "http://schemas.android.com/apk/res/android")
{
debuggable.ns.index = i;
@@ -419,16 +418,16 @@ bool PatchManifest(bytebuf &manifestBytes)
// 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++)
for(uint32_t i = 0; i < startElement.attributeCount; i++)
{
ResXMLTree_attribute *attr =
(ResXMLTree_attribute *)(attributesStart + startElement->attributeSize * i);
(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());
GetStringPoolValue(manifestBytes, attr->name).c_str());
break;
}
@@ -438,20 +437,23 @@ bool PatchManifest(bytebuf &manifestBytes)
{
attributeInsertIndex = i;
RDCDEBUG("Inserting attribute before %s, with resource ID %x",
GetStringPoolValue(stringpool, attr->name).c_str(), resourceId);
GetStringPoolValue(manifestBytes, attr->name).c_str(), resourceId);
break;
}
RDCDEBUG("Skipping past attribute %s, with resource ID %x",
GetStringPoolValue(stringpool, attr->name).c_str(), resourceId);
GetStringPoolValue(manifestBytes, attr->name).c_str(), resourceId);
}
InsertBytes(manifestBytes, attributesStart + startElement->attributeSize * attributeInsertIndex,
debuggable);
InsertBytes(manifestBytes,
attributeStartOffset + startElement.attributeSize * attributeInsertIndex, debuggable);
// update header
startElement->attributeCount++;
node->size += sizeof(ResXMLTree_attribute);
node.size += sizeof(ResXMLTree_attribute);
SetFromBytes(manifestBytes, cur, node);
startElement.attributeCount++;
SetFromBytes(manifestBytes, cur + node.headerSize, startElement);
stringAdded = (stringIndex.index == addingStringIndex);
@@ -463,95 +465,96 @@ bool PatchManifest(bytebuf &manifestBytes)
// with insertions
if(stringAdded)
{
uint32_t insertIdx = insertStringAtStart ? 0 : resourceMappingCount;
uint32_t insertIdx = resourceMappingCount;
// add to the resource map
// add to the resource map first because it's after the string pool, that way we don't have to
// account for string pool modifications in resMapOffset
{
if(insertIdx == 0)
InsertBytes(manifestBytes, (byte *)resMap + resMap->headerSize, debuggableResourceId);
else
InsertBytes(manifestBytes, (byte *)resMap + resMap->size, debuggableResourceId);
resMap->size += sizeof(uint32_t);
InsertBytes(manifestBytes, resMapOffset + resMap.size, debuggableResourceId);
resMap.size += sizeof(uint32_t);
SetFromBytes(manifestBytes, resMapOffset, resMap);
}
// add to the string pool
// add to the string pool next
{
// add the offset
stringpool->header.size += sizeof(uint32_t);
stringpool->stringCount++;
stringpool->stringsStart += sizeof(uint32_t);
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);
stringpool.flags =
ResStringPool_header::StringFlags(stringpool.flags & ~ResStringPool_header::SORTED_FLAG);
byte *base = (byte *)stringpool;
size_t stringpoolStringOffsetsOffset = stringpoolOffset + stringpool.header.headerSize;
uint32_t *stringOffsets = (uint32_t *)(base + stringpool->header.headerSize);
// we insert a zero offset at the position we're inserting. Then when we fix up that and all
// subsequent offsets
InsertBytes(manifestBytes, stringpoolStringOffsetsOffset + sizeof(uint32_t) * insertIdx,
uint32_t(0));
// 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]);
bytebuf stringbytes;
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)
// construct the string, with length prefix and trailing NULL
if(stringpool.flags & ResStringPool_header::UTF8_FLAG)
{
bytebuf 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);
stringbytes = {0xA, 0xA, 'd', 'e', 'b', 'u', 'g', 'g', 'a', 'b', 'l', 'e', 0};
}
else
{
bytebuf 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);
stringbytes = {0xA, 0x0, 'd', 0, 'e', 0, 'b', 0, 'u', 0, 'g', 0,
'g', 0, 'a', 0, 'b', 0, 'l', 0, 'e', 0, 0, 0};
}
// account for added string
stringpool->header.size += shift;
stringpool.header.size += stringbytes.count();
// 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;
uint32_t *stringOffsets = (uint32_t *)(manifestBytes.data() + stringpoolStringOffsetsOffset);
// the one we inserted will be inserted at the offset of whichever was previously at that
// index (which is now one further on)
stringOffsets[insertIdx] = stringOffsets[insertIdx + 1];
for(uint32_t i = insertIdx + 1; i < stringpool.stringCount; i++)
stringOffsets[i] += stringbytes.count();
// now insert the string bytes
InsertBytes(manifestBytes,
stringpoolOffset + stringpool.stringsStart + stringOffsets[insertIdx], stringbytes);
// if the stringpool isn't integer aligned, add padding bytes
uint32_t alignedSize = AlignUp4(stringpool->header.size);
uint32_t alignedSize = AlignUp4(stringpool.header.size);
if(alignedSize > stringpool->header.size)
if(alignedSize > stringpool.header.size)
{
uint32_t paddingLen = 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);
stringpool.header.size, alignedSize);
bytebuf padding;
padding.resize(paddingLen);
InsertBytes(manifestBytes, base + stringpool->header.size, padding);
InsertBytes(manifestBytes, stringpoolOffset + stringpool.header.size, padding);
stringpool->header.size += paddingLen;
stringpool.header.size += paddingLen;
}
// write the updated stringpool
SetFromBytes(manifestBytes, stringpoolOffset, stringpool);
}
// 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;
byte *ptr = manifestBytes.data() + xmlroot.headerSize;
// skip string pool, whatever size it is now
ptr += ((ResChunk_header *)ptr)->size;
// skip resource map, whatever size it is now
ptr += ((ResChunk_header *)ptr)->size;
while(cur < &manifestBytes.back())
while(ptr < manifestBytes.end())
{
ResXMLTree_node *node = (ResXMLTree_node *)cur;
ResXMLTree_node *node = (ResXMLTree_node *)ptr;
if(node->header.headerSize != sizeof(*node))
RDCWARN("Headersize was reported as %u, but we expected ResXMLTree_node size %zu",
@@ -565,7 +568,7 @@ bool PatchManifest(bytebuf &manifestBytes)
case ResType::NamespaceStart:
case ResType::NamespaceEnd:
{
ResXMLTree_namespaceExt *ns = (ResXMLTree_namespaceExt *)(cur + node->header.headerSize);
ResXMLTree_namespaceExt *ns = (ResXMLTree_namespaceExt *)(ptr + node->header.headerSize);
ShiftStringPoolValue(ns->prefix, insertIdx);
ShiftStringPoolValue(ns->uri, insertIdx);
@@ -574,7 +577,7 @@ bool PatchManifest(bytebuf &manifestBytes)
case ResType::EndElement:
{
ResXMLTree_endElementExt *endElement =
(ResXMLTree_endElementExt *)(cur + node->header.headerSize);
(ResXMLTree_endElementExt *)(ptr + node->header.headerSize);
ShiftStringPoolValue(endElement->ns, insertIdx);
ShiftStringPoolValue(endElement->name, insertIdx);
@@ -582,7 +585,7 @@ bool PatchManifest(bytebuf &manifestBytes)
}
case ResType::CDATA:
{
ResXMLTree_cdataExt *cdata = (ResXMLTree_cdataExt *)(cur + node->header.headerSize);
ResXMLTree_cdataExt *cdata = (ResXMLTree_cdataExt *)(ptr + node->header.headerSize);
ShiftStringPoolValue(cdata->data, insertIdx);
ShiftStringPoolValue(cdata->typedData, insertIdx);
@@ -590,13 +593,13 @@ bool PatchManifest(bytebuf &manifestBytes)
}
case ResType::StartElement:
{
ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(cur + node->header.headerSize);
ResXMLTree_attrExt *startElement = (ResXMLTree_attrExt *)(ptr + node->header.headerSize);
ShiftStringPoolValue(startElement->ns, insertIdx);
ShiftStringPoolValue(startElement->name, insertIdx);
// update attributes
byte *attributesStart = cur + node->header.headerSize + startElement->attributeStart;
byte *attributesStart = ptr + node->header.headerSize + startElement->attributeStart;
for(uint32_t i = 0; i < startElement->attributeCount; i++)
{
@@ -615,18 +618,12 @@ bool PatchManifest(bytebuf &manifestBytes)
return false;
}
cur += node->header.size;
ptr += 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");
}
xmlroot.size = (uint32_t)manifestBytes.size();
SetFromBytes(manifestBytes, 0, xmlroot);
return true;
}