Add files via upload

This commit is contained in:
vxunderground
2020-10-11 00:40:44 -05:00
committed by GitHub
parent e1d25298f4
commit ad3ed5a13f
20 changed files with 5942 additions and 0 deletions
@@ -0,0 +1,174 @@
// Package apkparser parses AndroidManifest.xml and resources.arsc from Android APKs.
package manifest
import (
"common"
"fmt"
"io"
"log"
"os"
)
type ApkParser struct {
apkPath string
zip *ZipReader
encoder ManifestEncoder
resources *ResourceTable
}
// save manifest to disk for binary patching
func (p *ApkParser) SaveManifestToDisk() {
file := p.zip.File["AndroidManifest.xml"]
if file == nil {
fmt.Errorf("Failed to find %s in APK!", "AndroidManifest.xml")
}
if err := file.Open(); err != nil {
panic(err)
}
defer file.Close()
// open output file
fo, err := os.Create(common.ManifestBinaryPath)
if err != nil {
panic(err)
}
// close fo on exit and check for its returned error
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
// make a buffer to keep chunks that are read
buf := make([]byte, 1024)
for {
// read a chunk
n, err := file.Read(buf)
if err != nil && err != io.EOF {
panic(err)
}
if n == 0 {
break
}
// write a chunk
if _, err := fo.Write(buf[:n]); err != nil {
panic(err)
}
}
}
// Calls ParseApkReader
func ParseApk(path string, encoder ManifestEncoder) {
f, zipErr := os.Open(path)
if zipErr != nil {
log.Panic("Failed to open apk")
}
defer f.Close()
ParseApkReader(f, encoder)
}
// Parse APK's Manifest, including resolving refences to resource values.
// encoder expects an XML encoder instance, like Encoder from encoding/xml package.
//
// zipErr != nil means the APK couldn't be opened. The manifest will be parsed
// even when resourcesErr != nil, just without reference resolving.
func ParseApkReader(r io.ReadSeeker, encoder ManifestEncoder) {
zip, zipErr := OpenZipReader(r)
if zipErr != nil {
log.Panic("Failed to open zip reader")
}
defer zip.Close()
ParseApkWithZip(zip, encoder)
}
// Parse APK's Manifest, including resolving refences to resource values.
// encoder expects an XML encoder instance, like Encoder from encoding/xml package.
//
// Use this if you already opened the zip with OpenZip or OpenZipReader before.
// This method will not Close() the zip.
//
// The manifest will be parsed even when resourcesErr != nil, just without reference resolving.
func ParseApkWithZip(zip *ZipReader, encoder ManifestEncoder) {
apkParser := ApkParser{
zip: zip,
encoder: encoder,
}
fmt.Println("\t--Parsing resources...")
apkParser.parseResources()
fmt.Println("\t--Parsing manifest...")
apkParser.ParseXml("AndroidManifest.xml")
apkParser.SaveManifestToDisk()
}
// Prepare the ApkParser instance, load resources if possible.
// encoder expects an XML encoder instance, like Encoder from encoding/xml package.
//
// This method will not Close() the zip, you are still the owner.
func NewParser(zip *ZipReader, encoder ManifestEncoder) (parser *ApkParser) {
parser = &ApkParser{
zip: zip,
encoder: encoder,
}
parser.parseResources()
return
}
func (p *ApkParser) parseResources() {
if p.resources != nil {
log.Panic("resources is not nil")
}
defer func() {
if r := recover(); r != nil {
log.Panic("recover() not nil")
}
}()
resourcesFile := p.zip.File["resources.arsc"]
if resourcesFile == nil {
log.Panic("resource.arsc not found")
}
if err := resourcesFile.Open(); err != nil {
log.Panic("Failed to open resources.arsc: %s", err.Error())
}
defer resourcesFile.Close()
p.resources = ParseResourceTable(resourcesFile)
}
func (p *ApkParser) ParseXml(name string) {
file := p.zip.File[name]
if file == nil {
log.Panicf("Failed to find %s in APK!", name)
}
if err := file.Open(); err != nil {
log.Panic("Failed to open manifest")
}
defer file.Close()
var lastErr error
for file.Next() {
if err := ParseXml(&myReader{r: file}, p.encoder, p.resources); err != nil {
lastErr = err
}
}
if lastErr == ErrPlainTextManifest {
log.Panic("Manifest in plaintext")
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,375 @@
package manifest
import (
"bytes"
"encoding/binary"
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"unsafe"
)
type binxmlParseInfo struct {
strings stringTable
resourceIds []uint32
encoder ManifestEncoder
res *ResourceTable
}
// Some samples have manifest in plaintext, this is an error.
// 2c882a2376034ed401be082a42a21f0ac837689e7d3ab6be0afb82f44ca0b859
var ErrPlainTextManifest = errors.New("xml is in plaintext, binary form expected")
// Deprecated: just calls ParseXML
func ParseManifest(r io.Reader, enc ManifestEncoder, resources *ResourceTable) error {
return ParseXml(r, enc, resources)
}
// the main purpose of this reader is to
// count number of bytes readed after parsing string table
// so we can calc offset to the end of the string table
type myReader struct {
read int
r io.Reader
}
type myRead interface {
GetRead() int
}
func (mr *myReader) Read(p []byte) (n int, err error) {
n, err = mr.r.Read(p)
mr.read += n
return
}
func (mr *myReader) GetRead() int {
return mr.read
}
// Parse the binary Xml format. The resources are optional and can be nil.
func ParseXml(r io.Reader, enc ManifestEncoder, resources *ResourceTable) error {
x := binxmlParseInfo{
encoder: enc,
res: resources,
}
id, headerLen, totalLen, err := parseChunkHeader(r)
if err != nil {
return err
}
//check if manifest is binary not plaintext
if (id & 0xFF) == '<' {
buf := bytes.NewBuffer(make([]byte, 0, 8))
binary.Write(buf, binary.LittleEndian, &id)
binary.Write(buf, binary.LittleEndian, &headerLen)
binary.Write(buf, binary.LittleEndian, &totalLen)
if s := buf.String(); strings.HasPrefix(s, "<?xml ") || strings.HasPrefix(s, "<manif") {
return ErrPlainTextManifest
}
}
// Android doesn't care.
/*if id != chunkAxmlFile {
return fmt.Errorf("Invalid top chunk id: 0x%08x", id)
}*/
defer x.encoder.Flush()
totalLen -= chunkHeaderSize
var len uint32
var lastId uint16
for i := uint32(0); i < totalLen; i += len {
id, _, len, err = parseChunkHeader(r)
if err != nil {
return fmt.Errorf("Error parsing header at 0x%08x of 0x%08x %08x: %s", i, totalLen, lastId, err.Error())
}
lastId = id
lm := &io.LimitedReader{R: r, N: int64(len) - 2*4}
switch id {
case chunkStringTable:
x.strings, err = parseStringTable(lm)
case chunkResourceIds:
err = x.parseResourceIds(lm)
default:
if (id & chunkMaskXml) == 0 {
err = fmt.Errorf("Unknown chunk id 0x%x", id)
break
}
// skip line number and unknown 0xFFFFFFFF
if _, err = io.CopyN(ioutil.Discard, lm, 2*4); err != nil {
break
}
switch id {
case chunkXmlNsStart:
err = x.parseNsStart(lm)
case chunkXmlNsEnd:
err = x.parseNsEnd(lm)
case chunkXmlTagStart:
err = x.parseTagStart(lm)
case chunkXmlTagEnd:
err = x.parseTagEnd(lm)
case chunkXmlText:
err = x.parseText(lm)
default:
err = fmt.Errorf("Unknown chunk id 0x%x", id)
}
}
if err == ErrEndParsing {
break
} else if err != nil {
return fmt.Errorf("Chunk: 0x%08x: %s", id, err.Error())
} else if lm.N != 0 {
return fmt.Errorf("Chunk: 0x%08x: was not fully read", id)
}
}
return x.encoder.Flush()
}
func (x *binxmlParseInfo) parseResourceIds(r *io.LimitedReader) error {
if (r.N % 4) != 0 {
return fmt.Errorf("Invalid chunk size!")
}
count := uint32(r.N / 4)
var id uint32
for i := uint32(0); i < count; i++ {
if err := binary.Read(r, binary.LittleEndian, &id); err != nil {
return err
}
x.resourceIds = append(x.resourceIds, id)
}
return nil
}
func (x *binxmlParseInfo) parseNsStart(r *io.LimitedReader) error {
var err error
ns := &xml.Name{}
var idx uint32
if err = binary.Read(r, binary.LittleEndian, &idx); err != nil {
return err
}
if ns.Local, err = x.strings.get(idx); err != nil {
return err
}
if err = binary.Read(r, binary.LittleEndian, &idx); err != nil {
return err
}
if ns.Space, err = x.strings.get(idx); err != nil {
return err
}
// TODO: what to do with this?
_ = ns
return nil
}
func (x *binxmlParseInfo) parseNsEnd(r *io.LimitedReader) error {
if _, err := io.CopyN(ioutil.Discard, r, 2*4); err != nil {
return fmt.Errorf("error skipping: %s", err.Error())
}
// TODO: what to do with this?
return nil
}
func (x *binxmlParseInfo) parseTagStart(r *io.LimitedReader) error {
var namespaceIdx, nameIdx, attrCnt, classAttrIdx uint32
if err := binary.Read(r, binary.LittleEndian, &namespaceIdx); err != nil {
return fmt.Errorf("error reading namespace idx: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &nameIdx); err != nil {
return fmt.Errorf("error reading name idx: %s", err.Error())
}
if _, err := io.CopyN(ioutil.Discard, r, 4); err != nil {
return fmt.Errorf("error skipping flag: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &attrCnt); err != nil {
return fmt.Errorf("error reading attrCnt: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &classAttrIdx); err != nil {
return fmt.Errorf("error reading classAttr: %s", err.Error())
}
idAttributeIdx := (attrCnt >> 16) - 1
attrCnt = (attrCnt & 0xFFFF)
styleAttrIdx := (classAttrIdx >> 16) - 1
classAttrIdx = (classAttrIdx & 0xFFFF)
_ = styleAttrIdx
_ = idAttributeIdx
namespace, err := x.strings.get(namespaceIdx)
if err != nil {
return fmt.Errorf("error decoding namespace: %s", err.Error())
}
name, err := x.strings.get(nameIdx)
if err != nil {
return fmt.Errorf("error decoding name: %s", err.Error())
}
tok := xml.StartElement{
Name: xml.Name{Local: name, Space: namespace},
}
var attrData [attrValuesCount]uint32
for i := uint32(0); i < attrCnt; i++ {
if err := binary.Read(r, binary.LittleEndian, &attrData); err != nil {
return fmt.Errorf("error reading attrData: %s", err.Error())
}
// Android actually reads attributes purely by their IDs (see frameworks/base/core/res/res/values/attrs_manifest.xml
// and its generated R class, that's where the indexes come from, namely the AndroidManifestActivity array)
// but good guy android actually puts the strings into the string table on the same indexes anyway, most of the time.
// This is for the samples that don't have it, mostly due to obfuscators/minimizers.
// The ID can't change, because it would break current APKs.
// Sample: 98d2e837b8f3ac41e74b86b2d532972955e5352197a893206ecd9650f678ae31
//
// The exception to this rule is the "package" attribute in the root manifest tag. That one MUST NOT use
// resource ids, instead, it needs to use the string table. The meta attrs 'platformBuildVersion*'
// are the same, except Android never parses them so it's just for manual analysis.
// Sample: a3ee88cf1492237a1be846df824f9de30a6f779973fe3c41c7d7ed0be644ba37
//
// In general, android doesn't care about namespaces, but if a resource ID is used, it has to have been
// in the android: namespace, so we fix that up.
// frameworks/base/core/jni/android_util_AssetManager.cpp android_content_AssetManager_retrieveAttributes
// frameworks/base/core/java/android/content/pm/PackageParser.java parsePackageSplitNames
var attrName string
if attrData[attrIdxName] < uint32(len(x.resourceIds)) {
attrName = getAttributteName(x.resourceIds[attrData[attrIdxName]])
}
var attrNameFromStrings string
if attrName == "" || name == "manifest" {
attrNameFromStrings, err = x.strings.get(attrData[attrIdxName])
if err != nil {
if attrName == "" {
return fmt.Errorf("error decoding attrNameIdx: %s", err.Error())
}
} else if attrName != "" && attrNameFromStrings != "package" && !strings.HasPrefix(attrNameFromStrings, "platformBuildVersion") {
attrNameFromStrings = ""
}
}
attrNameSpace, err := x.strings.get(attrData[attrIdxNamespace])
if err != nil {
return fmt.Errorf("error decoding attrNamespaceIdx: %s", err.Error())
}
if attrNameFromStrings != "" {
attrName = attrNameFromStrings
} else if attrNameSpace == "" {
attrNameSpace = "http://schemas.android.com/apk/res/android"
}
attr := xml.Attr{
Name: xml.Name{Local: attrName, Space: attrNameSpace},
}
switch attrData[attrIdxType] >> 24 {
case AttrTypeString:
attr.Value, err = x.strings.get(attrData[attrIdxString])
if err != nil {
return fmt.Errorf("error decoding attrStringIdx: %s", err.Error())
}
case AttrTypeIntBool:
attr.Value = strconv.FormatBool(attrData[attrIdxData] != 0)
case AttrTypeIntHex:
attr.Value = fmt.Sprintf("0x%x", attrData[attrIdxData])
case AttrTypeFloat:
val := (*float32)(unsafe.Pointer(&attrData[attrIdxData]))
attr.Value = fmt.Sprintf("%g", *val)
case AttrTypeReference:
isValidString := false
if x.res != nil {
var e *ResourceEntry
if attr.Name.Local == "icon" || attr.Name.Local == "roundIcon" {
e, err = x.res.GetIconPng(attrData[attrIdxData])
} else {
e, err = x.res.GetResourceEntry(attrData[attrIdxData])
}
if err == nil {
attr.Value, err = e.value.String()
isValidString = err == nil
}
}
if !isValidString && attr.Value == "" {
attr.Value = fmt.Sprintf("@%x", attrData[attrIdxData])
}
default:
attr.Value = strconv.FormatInt(int64(int32(attrData[attrIdxData])), 10)
}
tok.Attr = append(tok.Attr, attr)
}
return x.encoder.EncodeToken(tok)
}
func (x *binxmlParseInfo) parseTagEnd(r *io.LimitedReader) error {
var namespaceIdx, nameIdx uint32
if err := binary.Read(r, binary.LittleEndian, &namespaceIdx); err != nil {
return fmt.Errorf("error reading namespace idx: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &nameIdx); err != nil {
return fmt.Errorf("error reading name idx: %s", err.Error())
}
namespace, err := x.strings.get(namespaceIdx)
if err != nil {
return fmt.Errorf("error decoding namespace: %s", err.Error())
}
name, err := x.strings.get(nameIdx)
if err != nil {
return fmt.Errorf("error decoding name: %s", err.Error())
}
return x.encoder.EncodeToken(xml.EndElement{Name: xml.Name{Local: name, Space: namespace}})
}
func (x *binxmlParseInfo) parseText(r *io.LimitedReader) error {
var idx uint32
if err := binary.Read(r, binary.LittleEndian, &idx); err != nil {
return fmt.Errorf("error reading idx: %s", err.Error())
}
text, err := x.strings.get(idx)
if err != nil {
return fmt.Errorf("error decoding idx: %s", err.Error())
}
if _, err := io.CopyN(ioutil.Discard, r, 2*4); err != nil {
return fmt.Errorf("error skipping: %s", err.Error())
}
return x.encoder.EncodeToken(xml.CharData(text))
}
@@ -0,0 +1,67 @@
package manifest
import (
"encoding/binary"
"io"
)
const (
chunkNull = 0x0000
chunkStringTable = 0x0001
chunkTable = 0x0002
chunkAxmlFile = 0x0003
chunkResourceIds = 0x0180
chunkTablePackage = 0x0200
chunkTableType = 0x0201
chunkTableTypeSpec = 0x0202
chunkTableLibrary = 0x0203
chunkMaskXml = 0x0100
chunkXmlNsStart = 0x0100
chunkXmlNsEnd = 0x0101
chunkXmlTagStart = 0x0102
chunkXmlTagEnd = 0x0103
chunkXmlText = 0x0104
attrIdxNamespace = 0
attrIdxName = 1
attrIdxString = 2
attrIdxType = 3
attrIdxData = 4
attrValuesCount = 5
chunkHeaderSize = (2 + 2 + 4)
)
type AttrType uint8
const (
AttrTypeNull AttrType = 0x00
AttrTypeReference = 0x01
AttrTypeAttribute = 0x02
AttrTypeString = 0x03
AttrTypeFloat = 0x04
AttrTypeIntDec = 0x10
AttrTypeIntHex = 0x11
AttrTypeIntBool = 0x12
AttrTypeIntColorArgb8 = 0x1c
AttrTypeIntColorRgb8 = 0x1d
AttrTypeIntColorArgb4 = 0x1e
AttrTypeIntColorRgb4 = 0x1f
)
func parseChunkHeader(r io.Reader) (id, headerLen uint16, len uint32, err error) {
if err = binary.Read(r, binary.LittleEndian, &id); err != nil { // id
return
}
if err = binary.Read(r, binary.LittleEndian, &headerLen); err != nil { //header
return
}
if err = binary.Read(r, binary.LittleEndian, &len); err != nil {
return
}
return
}
@@ -0,0 +1,16 @@
package manifest
import (
"encoding/xml"
"errors"
)
// Return this error from EncodeToken to tell apkparser to finish parsing,
// to be used when you found the value you care about and don't need the rest.
var ErrEndParsing = errors.New("end manifest parsing")
// Encoder for writing the XML data. For example Encoder from encoding/xml matches this interface.
type ManifestEncoder interface {
EncodeToken(t xml.Token) error
Flush() error
}
@@ -0,0 +1,300 @@
package manifest
import (
"bytes"
"common"
"encoding/binary"
"encoding/xml"
"golang.org/x/text/encoding/unicode"
"io/ioutil"
"log"
"path/filepath"
)
const (
fileLenOffset = 0x4
offsetTableOffset = 0x24
offsetStringTableLen = 0xc
stringTableInfoSizeOffset = 0x1c
// Name of application in our stub dex
// It is MUST be longer than any average name
newAppNameUTF8 = "aaaaaaaa.aaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaa.InjectedApp"
newAppNameUTF8Len = uint8(len(newAppNameUTF8))
)
var alignCount uint32
var oldAppNameUTF16 string
var newAppNameUTF16 string
var OldAppNameUTF8 string
var PlainPath, _ = filepath.Abs("AndroidManifest_plaintext.xml")
func patchApplication() ([]byte, int) {
log.Printf("Getting original application name...")
OldAppNameUTF8 = getAppName()
log.Printf("Original applciation name = %s\n", OldAppNameUTF8)
if OldAppNameUTF8 == "" {
log.Panic("Application name wasn't found")
//TODO if not found - we should add our
}
// read bytes from binary xml
androidManifestRaw, err := ioutil.ReadFile(common.ManifestBinaryPath)
if err != nil {
log.Panicf("Failed to read %s", common.ManifestBinaryPath)
}
log.Printf("Original manifest (binary) size = 0x%0x\n", len(androidManifestRaw))
// encode name to UTF-16
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
oldAppNameUTF16, err = encoder.String(OldAppNameUTF8)
// searching application name position in binary manifest
pos := bytes.Index(androidManifestRaw, []byte(oldAppNameUTF16))
//get lenght of string
originalLen := int(androidManifestRaw[pos-2]) * 2
log.Printf("pos = 0x%0x, original applciation name length = 0x%0x\n", pos, originalLen)
//patch length with new value. length = characters count
// pos-2 - because every string is followed by len
androidManifestRaw[pos-2] = newAppNameUTF8Len
//patch application name with new name
// do not forget about alignment!
newAppNameUTF16, err = encoder.String(newAppNameUTF8)
newAppNameUTF16Len := len(newAppNameUTF16)
// how many bytes we add to manifest
lenDiff := newAppNameUTF16Len - originalLen
//// we need enough space to insert our name
androidManifestRawNew := make([]byte, len(androidManifestRaw)+newAppNameUTF16Len-originalLen)
log.Printf("new applciation name = %s, new application length = 0x%0x\n",
newAppNameUTF8, newAppNameUTF16Len)
// copy everything until application name string
copy(androidManifestRawNew, androidManifestRaw[:pos])
// copy our name
copy(androidManifestRawNew[pos:], []byte(newAppNameUTF16))
// copy everything after name
copy(androidManifestRawNew[pos+len([]byte(newAppNameUTF16)):], androidManifestRaw[pos+originalLen:])
// calc position where we should insert alignment bytes
alignPos := (newAppNameUTF16Len - originalLen) + StringTableEndPos
log.Printf("alignPos = 0x%0x\n", alignPos)
// how many bytes we should insert?
// The main idea - data after string table should be
// aligned to 4 bytes
alignCount = uint32(alignPos % 4)
log.Printf("align = %d\n", alignCount)
if alignCount != 0 {
var alignSlice = make([]byte, alignCount)
// insert byte alignment
androidManifestRawNew = append(androidManifestRawNew[:alignPos], append(alignSlice, androidManifestRawNew[alignPos:]...)...)
}
return androidManifestRawNew, lenDiff
}
// we should find from what offset in StringOffsets
// we should start changing offsets by incrementing them to
// number of characters application name expanded
// manifest_strings.dmp contains all strings
// we should count strings after application name
// it will be position of offset
func getAppNameOffset() uint32 {
// position in string offset
//var appNameOff uint32 = 1
var pos uint32
data, err := ioutil.ReadFile(ManifestStringsDmp)
if err != nil {
panic(err)
}
// searching application name position in string dump
// we substract 2 because real offset is the offset to strLen + str
// but we found offset to just str
pos = uint32(bytes.Index(data, []byte(oldAppNameUTF16)) - 2)
log.Printf("application name position in string dump = 0x%x", pos)
return pos
}
func patchOffsetTable(data []byte, appNameOff, lenDiff uint32) {
var offset uint32
offsetTableReader := bytes.NewReader(data)
var j uint32 = 0
for i := uint32(1); i <= StringCnt - appNameOff; i++ {
//read offset
err := binary.Read(offsetTableReader, binary.LittleEndian, &offset)
if err != nil {
log.Panic("Failed to read offset", err)
}
log.Printf("Original offset = 0x%x", offset)
//increment it to length of symbol added
offset += lenDiff
log.Printf("New offset = 0x%x", offset)
binary.LittleEndian.PutUint32(data[j:], offset)
j += 4
}
}
func patchStringTableLen(data []byte) {
var stringTableLen uint32
stringTableLenReader := bytes.NewReader(data)
err := binary.Read(stringTableLenReader, binary.LittleEndian, &stringTableLen)
if err != nil {
log.Panic("Failed to read offset", err)
}
// calc how many bytes we added to manifest
// it's a difference between new name and old name
// *2 - because they are in UTF-16
// IMPORTANT! stringTableLen - must be 4 byte aligned
newLen := len(newAppNameUTF16)
oldLen := len(oldAppNameUTF16)
stringTableLenNew := uint32(int(stringTableLen) + newLen - oldLen)
// align
stringTableLenNew += alignCount
binary.LittleEndian.PutUint32(data, stringTableLenNew)
}
func Patch() {
var androidManifestRaw, lenDiff = patchApplication()
log.Printf("New manifest len = 0x%0x\n", len(androidManifestRaw))
// after we insert new application name we need to increase length of manifest len
binary.LittleEndian.PutUint32(androidManifestRaw[fileLenOffset:], uint32(len(androidManifestRaw)))
var appNameOff = getAppNameOffset()
// search offset in manifest
appNameOffArr := make([]byte, 4)
binary.LittleEndian.PutUint32(appNameOffArr, appNameOff)
pos := uint32(bytes.Index(androidManifestRaw, appNameOffArr))
log.Printf("application name offset in manifest = 0x%x", pos)
// we step to next offset after our found app name offset
pos += 4
// locate the end of stringTableOffset (equals to the start of strings)
var stringTableInfoSize uint32
var stringOffsetTableEnd uint32
stringTableInfoSizeReader := bytes.NewReader(androidManifestRaw[stringTableInfoSizeOffset:])
err := binary.Read(stringTableInfoSizeReader, binary.LittleEndian, &stringTableInfoSize)
if err != nil {
log.Panic("Failed to read offset", err)
}
log.Printf("stringTableInfoSize = 0x%x", stringTableInfoSize)
// 0x8 - start of StringTableInfo section
stringOffsetTableEnd = 0x8 + stringTableInfoSize
log.Printf("stringOffsetTableEnd = 0x%x", stringOffsetTableEnd)
//start reading & patching
offsetTableReader := bytes.NewReader(androidManifestRaw[pos:])
var j = pos
var offset uint32
for i := pos; i < stringOffsetTableEnd; {
//read offset
err := binary.Read(offsetTableReader, binary.LittleEndian, &offset)
if err != nil {
log.Panic("Failed to read offset", err)
}
//log.Printf("Original offset = 0x%x", offset)
//increment it to length of symbol added
offset += uint32(lenDiff)
//log.Printf("New offset = 0x%x", offset)
//patch with new value
binary.LittleEndian.PutUint32(androidManifestRaw[j:], offset)
j += 4
i += 4
}
patchStringTableLen(androidManifestRaw[offsetStringTableLen:])
common.WriteChanges(androidManifestRaw, common.ManifestBinaryPath)
}
// Search application name in decoded android manifest
func getAppName() string {
// read manifest to byte array
content, err := ioutil.ReadFile(PlainPath)
if err != nil {
panic(err)
}
//defer func() {
// err = os.Remove(manifestPlainPath)
//
// if err != nil {
// panic(err)
// }
//} ()
// structs for XML nodes
type Application struct {
Name string `xml:"name,attr"`
}
type Result struct {
XMLName xml.Name `xml:"manifest"`
Application Application `xml:"application"`
}
v := new(Result)
err = xml.Unmarshal(content, v)
if err != nil {
log.Panic("Failed to unmarshal XML", err)
return ""
}
return v.Application.Name
}
@@ -0,0 +1,682 @@
package manifest
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math"
"strings"
"unicode/utf16"
)
var ErrUnknownResourceDataType = errors.New("Unknown resource data type")
// Contains parsed resources.arsc file.
type ResourceTable struct {
mainStrings stringTable
nextPackageId uint32
packages map[uint32]*packageGroup
}
type packageGroup struct {
Name string
Id uint32
Packages []*resourcePackage
table *ResourceTable
largestTypeId uint8
types map[uint8][]resourceTypeSpec
}
type resourcePackage struct {
Id uint32
Name string
typeIdOffset uint32
typeStrings stringTable
keyStrings stringTable
}
type resourceTypeSpec struct {
Id uint8
Entries []uint32
Package *resourcePackage
Configs []*resourceType
}
type resourceType struct {
chunkData []byte
entryCount uint32
entriesStart uint32
indexesStart uint32
// ResTable_config config;
}
const (
tableEntryComplex = 0x0001
tableEntryPublic = 0x0002
tableEntryWeak = 0x0004
)
// Describes one resource entry, for example @drawable/icon in the original XML, in one particular config option.
type ResourceEntry struct {
size uint16
flags uint16
ResourceType string
Key string
Package string
value ResourceValue
}
// Handle to the resource's actual value.
type ResourceValue struct {
dataType AttrType
data uint32
globalStringTable *stringTable
convertedData interface{}
}
// Resource config option to pick from options - when @drawable/icon is referenced,
// use /res/drawable-xhdpi/icon.png or use /res/drawable-mdpi/icon.png?
//
// This is not fully implemented, so you can pick only first seen or last seen option.
type ResourceConfigOption int
const (
ConfigFirst ResourceConfigOption = iota // Usually the smallest
ConfigLast // Usually the biggest
// Try to find the biggest png icon, otherwise same as ConfigLast.
//
// Deprecated: use GetIconPng
ConfigPngIcon
)
// Parses the resources.arsc file
func ParseResourceTable(r io.Reader) *ResourceTable {
res := ResourceTable{
nextPackageId: 2,
packages: make(map[uint32]*packageGroup),
}
id, hdrLen, totalLen, err := parseChunkHeader(r)
if err != nil {
log.Panic("parseChunkHeader() failed", err)
}
var packageCurrent, packagesCnt uint32
if err = binary.Read(r, binary.LittleEndian, &packagesCnt); err != nil {
log.Panic("Failed to read packagesCnt", err)
}
if hdrLen < chunkHeaderSize+4 {
log.Panicf("Invalid header length: %d", hdrLen)
}
totalLen -= uint32(hdrLen)
hdrLen -= chunkHeaderSize + 4
if _, err = io.CopyN(ioutil.Discard, r, int64(hdrLen)); err != nil {
log.Panic("Failed to read header padding: %s", err.Error())
}
var len uint32
var lastId uint16
for i := uint32(0); i < totalLen; i += len {
id, hdrLen, len, err = parseChunkHeader(r)
if err != nil {
log.Panicf("Error parsing header at 0x%08x of 0x%08x %08x: %s", i, totalLen, lastId, err.Error())
}
lastId = id
lm := &io.LimitedReader{R: r, N: int64(len) - chunkHeaderSize}
switch id {
case chunkStringTable:
if res.mainStrings.isEmpty() {
res.mainStrings, err = parseStringTable(lm)
}
case chunkTablePackage:
if packageCurrent >= packagesCnt {
log.Panicf("Chunk: 0x%08x: Too many package chunks", id)
}
err = res.parsePackage(lm, hdrLen)
packageCurrent++
default:
err = fmt.Errorf("Unknown chunk: 0x%08x at %d.", id, i+chunkHeaderSize+4)
//_, err = io.CopyN(ioutil.Discard, lm, lm.N)
}
if err != nil {
log.Panicf("Chunk: 0x%08x: %s", id, err.Error())
} else if lm.N != 0 {
log.Panicf("Chunk: 0x%08x: was not fully read", id)
}
}
return &res
}
func (x *ResourceTable) parsePackage(r *io.LimitedReader, hdrLen uint16) error {
pkgBlock, err := ioutil.ReadAll(r)
if err != nil {
return fmt.Errorf("error reading package block: %s", err.Error())
}
pkgReader := bytes.NewReader(pkgBlock)
const valsSize = chunkHeaderSize + 4 + 2*128 + 4*5
vals := struct {
Id uint32
Name [128]uint16
TypeStrings uint32
LastPublicType uint32
KeyStrings uint32
LastPublicKey uint32
TypeIdOffset uint32
}{}
if err := binary.Read(pkgReader, binary.LittleEndian, &vals); err != nil {
return fmt.Errorf("error reading values: %s", err.Error())
}
if vals.Id >= 256 {
return fmt.Errorf("package id out of range: %d", vals.Id)
}
if vals.Id == 0 {
vals.Id = x.nextPackageId
x.nextPackageId++
}
pkg := &resourcePackage{
Id: vals.Id,
}
// TypeIdOffset was added later and may not be present (frameworks/base@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e)
if hdrLen >= valsSize {
pkg.typeIdOffset = vals.TypeIdOffset
}
pkg.Name = string(utf16.Decode(vals.Name[:]))
if idx := strings.IndexRune(pkg.Name, 0); idx != -1 {
pkg.Name = pkg.Name[:idx]
}
if vals.TypeStrings < chunkHeaderSize || vals.KeyStrings <= chunkHeaderSize {
return fmt.Errorf("Invalid strings offset: %d %d", vals.TypeStrings, vals.KeyStrings)
}
vals.TypeStrings -= chunkHeaderSize
vals.KeyStrings -= chunkHeaderSize
if _, err := pkgReader.Seek(int64(vals.TypeStrings), io.SeekStart); err != nil {
return err
}
if pkg.typeStrings, err = parseStringTableWithChunk(pkgReader); err != nil {
return err
}
if _, err := pkgReader.Seek(int64(vals.KeyStrings), io.SeekStart); err != nil {
return err
}
if pkg.keyStrings, err = parseStringTableWithChunk(pkgReader); err != nil {
return err
}
group, prs := x.packages[pkg.Id]
if !prs {
group = &packageGroup{
Id: pkg.Id,
Name: pkg.Name,
table: x,
types: make(map[uint8][]resourceTypeSpec),
}
x.packages[pkg.Id] = group
/*
// Find all packages that reference this package
size_t N = mpackageGroups.size();
for (size_t i = 0; i < N; i++) {
mpackageGroups[i]->dynamicRefTable.addMapping(
group->name, static_cast<uint8_t>(group->id));
}
*/
}
group.Packages = append(group.Packages, pkg)
if _, err := pkgReader.Seek(int64(hdrLen-chunkHeaderSize), io.SeekStart); err != nil {
return err
}
for {
chunkStartOffset, _ := pkgReader.Seek(0, io.SeekCurrent)
id, hdrLen, totalLen, err := parseChunkHeader(pkgReader)
if err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("Error parsing package internal header: %s", err.Error())
}
// Sample: 7e97541191621e72bd794b5b2d60eb2f68669ea8782421e54ec719ccda06c8a4
if chunkStartOffset+int64(totalLen) >= int64(len(pkgBlock)) {
totalLen = uint32(int64(len(pkgBlock)) - chunkStartOffset)
}
lm := &io.LimitedReader{R: pkgReader, N: int64(totalLen) - chunkHeaderSize}
switch id {
case chunkTableTypeSpec:
err = x.parseTypeSpec(lm, pkg, group)
case chunkTableType:
block := pkgBlock[chunkStartOffset : chunkStartOffset+int64(totalLen)]
if err = x.parseType(lm, pkg, group, block, hdrLen); err != nil {
break
}
fallthrough
default:
_, err = io.CopyN(ioutil.Discard, lm, lm.N)
}
if err != nil {
return fmt.Errorf("Chunk: 0x%08x: %s", id, err.Error())
} else if lm.N != 0 {
return fmt.Errorf("Chunk: 0x%08x: was not fully read", id)
}
}
return nil
}
func (x *ResourceTable) parseTypeSpec(r io.Reader, pkg *resourcePackage, group *packageGroup) error {
var id uint8
if err := binary.Read(r, binary.LittleEndian, &id); err != nil {
return fmt.Errorf("Failed to read type spec id: %s", err.Error())
}
if id == 0 {
return fmt.Errorf("Invalid type spec id: %d", id)
}
if _, err := io.CopyN(ioutil.Discard, r, 1+2); err != nil {
return fmt.Errorf("Failed to skip padding: %s", err.Error())
}
var entryCount uint32
if err := binary.Read(r, binary.LittleEndian, &entryCount); err != nil {
return fmt.Errorf("Failed to read entryCount: %s", err.Error())
}
if entryCount > 0 {
var entries []uint32
for i := uint32(0); i < entryCount; i++ {
var e uint32
if err := binary.Read(r, binary.LittleEndian, &e); err != nil {
return fmt.Errorf("Failed to read type spec entry: %s", err.Error())
}
entries = append(entries, e)
}
group.types[id] = append(group.types[id], resourceTypeSpec{
Id: id,
Entries: entries,
Package: pkg,
})
if id > group.largestTypeId {
group.largestTypeId = id
}
}
return nil
}
func (x *ResourceTable) parseType(r io.Reader, pkg *resourcePackage, group *packageGroup, chunkData []byte, hdrLen uint16) error {
vals := struct {
Id uint8
Res0 uint8
Res1 uint16
EntryCount uint32
EntriesStart uint32
//ResTable_config config;
}{}
if err := binary.Read(r, binary.LittleEndian, &vals); err != nil {
return fmt.Errorf("error reading values: %s", err.Error())
}
if vals.Id == 0 {
return fmt.Errorf("Invalid type id: %d", vals.Id)
}
if vals.EntryCount > 0 {
typeList := group.types[vals.Id]
if len(typeList) == 0 {
return fmt.Errorf("No spec entry for type %d", vals.Id)
}
i := len(typeList) - 1
typeList[i].Configs = append(typeList[i].Configs, &resourceType{
chunkData: chunkData,
entryCount: vals.EntryCount,
entriesStart: vals.EntriesStart,
indexesStart: uint32(hdrLen),
})
}
return nil
}
// Converts the resource id to readable name including the package name like "@drawable:com.example.app.icon".
func (x *ResourceTable) GetResourceName(resId uint32) (string, error) {
pkgId := (resId >> 24)
typ := ((resId >> 16) & 0xFF) - 1
entryId := (resId & 0xFFFF)
group := x.packages[pkgId]
if group == nil {
return "", fmt.Errorf("Invalid package identifier.")
}
entry, err := x.getEntry(group, typ, entryId, ConfigFirst)
if err != nil {
return "", err
}
return fmt.Sprintf("@%s:%s.%s", entry.ResourceType, group.Name, entry.Key), nil
}
// Returns the resource entry for resId and the first configuration option it finds.
func (x *ResourceTable) GetResourceEntry(resId uint32) (*ResourceEntry, error) {
return x.GetResourceEntryEx(resId, ConfigFirst)
}
// Returns the resource entry for resId and config configuration option.
func (x *ResourceTable) GetResourceEntryEx(resId uint32, config ResourceConfigOption) (*ResourceEntry, error) {
if config == ConfigPngIcon {
return x.GetIconPng(resId)
}
pkgId := (resId >> 24)
typ := ((resId >> 16) & 0xFF) - 1
entryId := (resId & 0xFFFF)
group := x.packages[pkgId]
if group == nil {
return nil, fmt.Errorf("Invalid package identifier.")
}
return x.getEntry(group, typ, entryId, config)
}
// Return the biggest last config ending with .png. Falls back to GetResourceEntry() if none found.
func (x *ResourceTable) GetIconPng(resId uint32) (*ResourceEntry, error) {
pkgId := (resId >> 24)
typ := ((resId >> 16) & 0xFF) - 1
entryId := (resId & 0xFFFF)
group := x.packages[pkgId]
if group == nil {
return nil, fmt.Errorf("Invalid package identifier.")
}
entries, err := x.getEntryConfigs(group, typ, entryId, 256)
if len(entries) == 0 {
return nil, err
}
var res *ResourceEntry
for i := 0; i < len(entries) && i < 1024; i++ {
e := entries[i]
if e.value.dataType == AttrTypeReference {
pkgId = (e.value.data >> 24)
typ = ((e.value.data >> 16) & 0xFF) - 1
entryId = (e.value.data & 0xFFFF)
if more, _ := x.getEntryConfigs(group, typ, entryId, 256); len(more) != 0 {
entries = append(entries, more...)
}
} else if val, _ := e.value.String(); strings.HasSuffix(val, ".png") {
res = e
}
}
if res == nil {
return x.GetResourceEntry(resId)
}
return res, nil
}
func (x *ResourceTable) getEntry(group *packageGroup, typeId, entry uint32, config ResourceConfigOption) (*ResourceEntry, error) {
limit := 1024
if config == ConfigFirst {
limit = 1
}
entries, err := x.getEntryConfigs(group, typeId, entry, limit)
if len(entries) == 0 {
return nil, err
}
res := entries[len(entries)-1]
return res, err
}
func (x *ResourceTable) getEntryConfigs(group *packageGroup, typeId, entry uint32, limit int) ([]*ResourceEntry, error) {
typeList := group.types[uint8(typeId+1)]
if len(typeList) == 0 {
return nil, fmt.Errorf("Invalid type: %d", typeId)
}
var lastErr error
var entries []*ResourceEntry
for _, typ := range typeList {
for _, thisType := range typ.Configs {
if entry >= thisType.entryCount {
continue
}
r := bytes.NewReader(thisType.chunkData)
if _, err := r.Seek(int64(thisType.indexesStart+entry*4), io.SeekStart); err != nil {
return nil, err
}
var thisOffset uint32
if err := binary.Read(r, binary.LittleEndian, &thisOffset); err != nil {
return nil, fmt.Errorf("Failed to read this type offset: %s", err.Error())
}
if thisOffset == math.MaxUint32 {
continue
}
offset := thisType.entriesStart + thisOffset
if int(offset) >= len(thisType.chunkData) || ((offset & 0x03) != 0) {
return nil, fmt.Errorf("Invalid entry 0x%04x offset: %d!", entry, offset)
}
if _, err := r.Seek(int64(offset), io.SeekStart); err != nil {
return nil, err
}
res, err := x.parseEntry(r, typ.Package, typeId)
if err != nil {
lastErr = err
} else {
entries = append(entries, res)
}
if len(entries) >= limit {
goto exit
}
}
}
if len(entries) == 0 {
return nil, fmt.Errorf("No entry found.")
}
exit:
return entries, lastErr
}
func (x *ResourceTable) parseEntry(r io.Reader, pkg *resourcePackage, typeId uint32) (*ResourceEntry, error) {
var err error
var res ResourceEntry
var keyIndex uint32
if err := binary.Read(r, binary.LittleEndian, &res.size); err != nil {
return nil, fmt.Errorf("Failed to read entry size: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &res.flags); err != nil {
return nil, fmt.Errorf("Failed to read entry flags: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &keyIndex); err != nil {
return nil, fmt.Errorf("Failed to read entry key index: %s", err.Error())
}
res.Package = pkg.Name
res.ResourceType, err = pkg.typeStrings.get(typeId - pkg.typeIdOffset)
if err != nil {
return nil, fmt.Errorf("Invalid typeString: %s", err.Error())
}
res.Key, err = pkg.keyStrings.get(keyIndex)
if err != nil {
return nil, fmt.Errorf("Invalid keyString: %s", err.Error())
}
if !res.IsComplex() {
var size uint16
if err := binary.Read(r, binary.LittleEndian, &size); err != nil {
return nil, fmt.Errorf("Failed to read entry value size: %s", err.Error())
}
if size < 8 {
return nil, fmt.Errorf("Invalid Res_value size: %d!", size)
}
if _, err := io.CopyN(ioutil.Discard, r, 1); err != nil {
return nil, fmt.Errorf("Failed to read entry value res0: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &res.value.dataType); err != nil {
return nil, fmt.Errorf("Failed to read entry value data type: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &res.value.data); err != nil {
return nil, fmt.Errorf("Failed to read entry value data: %s", err.Error())
}
res.value.globalStringTable = &x.mainStrings
} else {
// NYI
}
return &res, nil
}
// Returns true if the resource entry is complex (for example arrays, string plural arrays...).
//
// Complex ResourceEntries are not yet supported.
func (e *ResourceEntry) IsComplex() bool {
return (e.flags & tableEntryComplex) != 0
}
// Returns the resource value handle
func (e *ResourceEntry) GetValue() *ResourceValue {
return &e.value
}
// Returns the resource data type
func (v *ResourceValue) Type() AttrType {
return v.dataType
}
// Returns the raw data of the resource
func (v *ResourceValue) RawData() uint32 {
return v.data
}
// Returns the data converted to their native type (e.g. AttrTypeString to string).
//
// Returns ErrUnknownResourceDataType if the type is not handled by this library
func (v *ResourceValue) Data() (interface{}, error) {
if v.convertedData != nil {
return v.convertedData, nil
}
var val interface{}
var err error
switch v.dataType {
case AttrTypeNull:
case AttrTypeString:
val, err = v.globalStringTable.get(v.data)
if err != nil {
return nil, err
}
case AttrTypeIntDec, AttrTypeIntHex, AttrTypeIntBool,
AttrTypeIntColorArgb8, AttrTypeIntColorRgb8,
AttrTypeIntColorArgb4, AttrTypeIntColorRgb4,
AttrTypeReference:
val = v.data
default:
return nil, ErrUnknownResourceDataType
}
v.convertedData = val
return val, nil
}
// Returns the data converted to a readable string, to the format it was likely in the original AndroidManifest.xml.
//
// Unknown data types are returned as the string from ErrUnknownResourceDataType.Error().
func (v *ResourceValue) String() (res string, err error) {
switch v.dataType {
case AttrTypeNull:
res = "null"
case AttrTypeIntHex:
res = fmt.Sprintf("0x%x", v.data)
case AttrTypeIntBool:
if v.data != 0 {
res = "true"
} else {
res = "false"
}
case AttrTypeIntColorArgb8:
res = fmt.Sprintf("#%08x", v.data)
case AttrTypeIntColorRgb8:
res = fmt.Sprintf("#%06x", v.data)
case AttrTypeIntColorArgb4:
res = fmt.Sprintf("#%04x", v.data)
case AttrTypeIntColorRgb4:
res = fmt.Sprintf("#%03x", v.data)
case AttrTypeReference:
res = fmt.Sprintf("@%x", v.data)
default:
var val interface{}
val, err = v.Data()
if err == nil {
res = fmt.Sprintf("%v", val)
}
}
return
}
@@ -0,0 +1,251 @@
package manifest
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"math"
"path/filepath"
"strings"
"unicode/utf16"
"unicode/utf8"
)
const (
stringFlagSorted = 0x00000001
stringFlagUtf8 = 0x00000100
)
var StringCnt uint32
var ManifestStringsDmp, _ = filepath.Abs("manifest_strings.dmp")
var StringTableEndPos int
type stringTable struct {
isUtf8 bool
stringOffsets []byte
data []byte
cache map[uint32]string
}
func parseStringTableWithChunk(r io.Reader) (res stringTable, err error) {
id, _, totalLen, err := parseChunkHeader(r)
if err != nil {
return
}
if id != chunkStringTable {
err = fmt.Errorf("Invalid chunk id 0x%08x, expected 0x%08x", id, chunkStringTable)
return
}
return parseStringTable(&io.LimitedReader{R: r, N: int64(totalLen - chunkHeaderSize)})
}
func check(e error) {
if e != nil {
panic(e)
}
}
func dumpStrings(data []byte) {
err := ioutil.WriteFile(ManifestStringsDmp, data, 0644)
check(err)
}
func parseStringTable(r *io.LimitedReader) (stringTable, error) {
var err error
var stringOffset, flags uint32
var res stringTable
// stringCnt - STRING COUNT
if err := binary.Read(r, binary.LittleEndian, &StringCnt); err != nil {
return res, fmt.Errorf("error reading stringCnt: %s", err.Error())
}
// skip styles count
if _, err = io.CopyN(ioutil.Discard, r, 4); err != nil {
return res, fmt.Errorf("error reading styleCnt: %s", err.Error())
}
if err := binary.Read(r, binary.LittleEndian, &flags); err != nil {
return res, fmt.Errorf("error reading flags: %s", err.Error())
}
res.isUtf8 = (flags & stringFlagUtf8) != 0
if res.isUtf8 {
flags &^= stringFlagUtf8
}
flags &^= stringFlagSorted // just ignore
if flags != 0 {
return res, fmt.Errorf("Unknown string flag: 0x%08x", flags)
}
if err := binary.Read(r, binary.LittleEndian, &stringOffset); err != nil {
return res, fmt.Errorf("error reading stringOffset: %s", err.Error())
}
// skip styles offset
if _, err = io.CopyN(ioutil.Discard, r, 4); err != nil {
return res, fmt.Errorf("error reading styleOffset: %s", err.Error())
}
// Read lengths
if StringCnt >= 2*1024*1024 {
return res, fmt.Errorf("Too many strings in this file (%d).", StringCnt)
}
// allocate memory for each offset. 1 offset for 1 string. 1 offset = 4 bytes
res.stringOffsets = make([]byte, 4*StringCnt)
// fill stringOffssets array with offsets. Read from manifest
if _, err := io.ReadFull(r, res.stringOffsets); err != nil {
return res, fmt.Errorf("Failed to read string offsets data: %s", err.Error())
}
remainder := int64(stringOffset) - 7*4 - 4*int64(StringCnt)
if remainder < 0 {
return res, fmt.Errorf("Wrong string offset (got remainder %d)", remainder)
} else if remainder > 0 {
if _, err = io.CopyN(ioutil.Discard, r, remainder); err != nil {
return res, fmt.Errorf("error reading styleArray: %s", err.Error())
}
}
// read STRINGS
// TODO Здесь в r.N попал resourceID, а это не должно быть
res.data = make([]byte, r.N)
if _, err := io.ReadFull(r, res.data); err != nil {
return res, fmt.Errorf("Failed to read string table data: %s", err.Error())
}
// write res.data to stdout. res.data contains = resource strings and manifest in plaintext
if mr, ok := r.R.(myRead); ok {
StringTableEndPos = mr.GetRead()
}
dumpStrings(res.data)
res.cache = make(map[uint32]string)
return res, nil
}
func (t *stringTable) parseString16(r io.Reader) (string, error) {
var strCharacters uint32
var strCharactersLow, strCharactersHigh uint16
if err := binary.Read(r, binary.LittleEndian, &strCharactersHigh); err != nil {
return "", fmt.Errorf("error reading string char count: %s", err.Error())
}
if (strCharactersHigh & 0x8000) != 0 {
if err := binary.Read(r, binary.LittleEndian, &strCharactersLow); err != nil {
return "", fmt.Errorf("error reading string char count: %s", err.Error())
}
strCharacters = (uint32(strCharactersHigh&0x7FFF) << 16) | uint32(strCharactersLow)
} else {
strCharacters = uint32(strCharactersHigh)
}
buf := make([]uint16, int64(strCharacters))
if err := binary.Read(r, binary.LittleEndian, &buf); err != nil {
return "", fmt.Errorf("error reading string : %s", err.Error())
}
decoded := utf16.Decode(buf)
for len(decoded) != 0 && decoded[len(decoded)-1] == 0 {
decoded = decoded[:len(decoded)-1]
}
return string(decoded), nil
}
func (t *stringTable) parseString8Len(r io.Reader) (int64, error) {
var strCharacters int64
var strCharactersLow, strCharactersHigh uint8
if err := binary.Read(r, binary.LittleEndian, &strCharactersHigh); err != nil {
return 0, fmt.Errorf("error reading string char count: %s", err.Error())
}
if (strCharactersHigh & 0x80) != 0 {
if err := binary.Read(r, binary.LittleEndian, &strCharactersLow); err != nil {
return 0, fmt.Errorf("error reading string char count: %s", err.Error())
}
strCharacters = (int64(strCharactersHigh&0x7F) << 8) | int64(strCharactersLow)
} else {
strCharacters = int64(strCharactersHigh)
}
return strCharacters, nil
}
func (t *stringTable) parseString8(r io.Reader) (string, error) {
// Length of the string in UTF16
_, err := t.parseString8Len(r)
if err != nil {
return "", err
}
len8, err := t.parseString8Len(r)
if err != nil {
return "", err
}
buf := make([]uint8, len8)
if err := binary.Read(r, binary.LittleEndian, &buf); err != nil {
return "", fmt.Errorf("error reading string : %s", err.Error())
}
for len(buf) != 0 && buf[len(buf)-1] == 0 {
buf = buf[:len(buf)-1]
}
return string(buf), nil
}
func (t *stringTable) get(idx uint32) (string, error) {
if idx == math.MaxUint32 {
return "", nil
} else if idx >= uint32(len(t.stringOffsets)/4) {
return "", fmt.Errorf("String with idx %d not found!", idx)
}
if str, prs := t.cache[idx]; prs {
return str, nil
}
offset := binary.LittleEndian.Uint32(t.stringOffsets[4*idx : 4*idx+4])
if offset >= uint32(len(t.data)) {
return "", fmt.Errorf("String offset for idx %d is out of bounds (%d >= %d).", idx, offset, len(t.data))
}
r := bytes.NewReader(t.data[offset:])
var err error
var res string
if t.isUtf8 {
res, err = t.parseString8(r)
} else {
res, err = t.parseString16(r)
}
if err != nil {
return "", err
}
if !utf8.ValidString(res) || strings.ContainsRune(res, 0) {
res = strings.Map(func(r rune) rune {
switch r {
case 0, utf8.RuneError:
return '\uFFFE'
default:
return r
}
}, res)
}
t.cache[idx] = res
return res, nil
}
func (t *stringTable) isEmpty() bool {
return t.cache == nil
}
@@ -0,0 +1,352 @@
package manifest
import (
"archive/zip"
"compress/flate"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"path"
)
type zipReaderFileSubEntry struct {
offset int64
method uint16
}
// This struct mimics of Reader from archive/zip. It's purpose is to handle
// even broken archives that Android can read, but archive/zip cannot.
type ZipReader struct {
File map[string]*ZipReaderFile
// Files in the order they were found in the zip. May contain the same ZipReaderFile
// multiple times in case of broken/crafted ZIPs
FilesOrdered []*ZipReaderFile
zipFileReader io.ReadSeeker
ownedZipFile *os.File
}
// This struct mimics of File from archive/zip. The main difference is it can represent
// multiple actual entries in the ZIP file in case it has more than one with the same name.
type ZipReaderFile struct {
Name string
IsDir bool
zipFile io.ReadSeeker
internalReader io.Reader
internalCloser io.Closer
zipEntry *zip.File
entries []zipReaderFileSubEntry
curEntry int
}
// Opens the file(s) for reading. After calling open, you should iterate through all possible entries that
// go by that Filename with for f.Next() { f.Read()... }
func (zr *ZipReaderFile) Open() error {
if zr.internalReader != nil {
return errors.New("File is already opened.")
}
if zr.zipEntry != nil {
var err error
zr.curEntry = 0
rc, err := zr.zipEntry.Open()
if err != nil {
return err
}
zr.internalReader = rc
zr.internalCloser = rc
} else {
zr.curEntry = -1
}
return nil
}
// Reads data from current opened file. Returns io.EOF at the end of current file, but another file entry might exist.
// Use Next() to check for that.
func (zr *ZipReaderFile) Read(p []byte) (int, error) {
if zr.internalReader == nil {
if zr.curEntry == -1 && !zr.Next() {
return 0, io.ErrUnexpectedEOF
}
if zr.curEntry >= len(zr.entries) {
return 0, io.ErrUnexpectedEOF
}
_, err := zr.zipFile.Seek(zr.entries[zr.curEntry].offset, 0)
if err != nil {
return 0, err
}
switch zr.entries[zr.curEntry].method {
case zip.Store:
zr.internalReader = zr.zipFile
default: // case zip.Deflate: // Android treats everything but 0 as deflate
rc := flate.NewReader(zr.zipFile)
zr.internalReader = rc
zr.internalCloser = rc
}
}
return zr.internalReader.Read(p)
}
// Moves this reader to the next file represented under it's Name. Returns false if there are no more to read.
func (zr *ZipReaderFile) Next() bool {
if len(zr.entries) == 0 && zr.internalReader != nil {
zr.curEntry++
return zr.curEntry == 1
}
zr.Close()
if zr.curEntry+1 >= len(zr.entries) {
return false
}
zr.curEntry++
return true
}
// Closes this reader and all opened files.
func (zr *ZipReaderFile) Close() error {
if zr.internalReader != nil {
if zr.internalCloser != nil {
zr.internalCloser.Close()
zr.internalCloser = nil
}
zr.internalReader = nil
}
return nil
}
// Get the file header from ZIP (can return nil with broken archives)
func (zr *ZipReaderFile) ZipHeader() *zip.FileHeader {
if zr.zipEntry != nil {
return &zr.zipEntry.FileHeader
}
return nil
}
// Closes this ZIP archive and all it's ZipReaderFile entries.
func (zr *ZipReader) Close() error {
if zr.zipFileReader == nil {
return nil
}
for _, zf := range zr.File {
zf.Close()
}
var err error
if zr.ownedZipFile != nil {
err = zr.ownedZipFile.Close()
zr.ownedZipFile = nil
}
zr.zipFileReader = nil
return err
}
type readAtWrapper struct {
io.ReadSeeker
}
func (wr *readAtWrapper) ReadAt(b []byte, off int64) (n int, err error) {
if readerAt, ok := wr.ReadSeeker.(io.ReaderAt); ok {
return readerAt.ReadAt(b, off)
}
oldpos, err := wr.Seek(off, io.SeekCurrent)
if err != nil {
return
}
if _, err = wr.Seek(off, io.SeekStart); err != nil {
return
}
if n, err = wr.Read(b); err != nil {
return
}
_, err = wr.Seek(oldpos, io.SeekStart)
return
}
// Attempts to open ZIP for reading.
func OpenZip(path string) (zr *ZipReader, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
zr, err = OpenZipReader(f)
if err != nil {
f.Close()
} else {
zr.ownedZipFile = f
}
return
}
// Attempts to open ZIP for reading. Might Seek the reader to arbitrary
// positions.
func OpenZipReader(zipReader io.ReadSeeker) (zr *ZipReader, err error) {
zr = &ZipReader{
File: make(map[string]*ZipReaderFile),
zipFileReader: zipReader,
}
f := &readAtWrapper{zipReader}
var zipinfo *zip.Reader
zipinfo, err = tryReadZip(f)
if err == nil {
for i, zf := range zipinfo.File {
// Android treats anything but 0 as deflate.
if zf.Method != zip.Store && zf.Method != zip.Deflate {
zipinfo.File[i].Method = zip.Deflate
}
cl := path.Clean(zf.Name)
if zr.File[cl] == nil {
zf := &ZipReaderFile{
Name: cl,
IsDir: zf.FileInfo().IsDir(),
zipFile: f,
zipEntry: zf,
}
zr.File[cl] = zf
zr.FilesOrdered = append(zr.FilesOrdered, zf)
}
}
return
}
if _, err = f.Seek(0, io.SeekStart); err != nil {
return
}
var off int64
for {
off, err = findNextFileHeader(f)
if off == -1 || err != nil {
return
}
var nameLen, extraLen, method uint16
if _, err = f.Seek(off+8, 0); err != nil {
return
}
if err = binary.Read(f, binary.LittleEndian, &method); err != nil {
return
}
if _, err = f.Seek(off+26, 0); err != nil {
return
}
if err = binary.Read(f, binary.LittleEndian, &nameLen); err != nil {
return
}
if err = binary.Read(f, binary.LittleEndian, &extraLen); err != nil {
return
}
buf := make([]byte, nameLen)
if _, err = f.ReadAt(buf, off+30); err != nil {
return
}
fileName := path.Clean(string(buf))
fileOffset := off + 30 + int64(nameLen) + int64(extraLen)
zrf := zr.File[fileName]
if zrf == nil {
zrf = &ZipReaderFile{
Name: fileName,
zipFile: f,
curEntry: -1,
}
zr.File[fileName] = zrf
}
zr.FilesOrdered = append(zr.FilesOrdered, zrf)
zrf.entries = append([]zipReaderFileSubEntry{zipReaderFileSubEntry{
offset: fileOffset,
method: method,
}}, zrf.entries...)
if _, err = f.Seek(off+4, 0); err != nil {
return
}
}
}
func tryReadZip(f *readAtWrapper) (r *zip.Reader, err error) {
defer func() {
if pn := recover(); pn != nil {
err = fmt.Errorf("%v", pn)
r = nil
}
}()
size, err := f.Seek(0, io.SeekEnd)
if err != nil {
return
}
r, err = zip.NewReader(f, size)
return
}
func findNextFileHeader(f io.ReadSeeker) (offset int64, err error) {
start, err := f.Seek(0, 1)
if err != nil {
return -1, err
}
defer func() {
if _, serr := f.Seek(start, 0); serr != nil && err == nil {
err = serr
}
}()
buf := make([]byte, 64*1024)
toCmp := []byte{0x50, 0x4B, 0x03, 0x04}
ok := 0
offset = start
for {
n, err := f.Read(buf)
if err != nil && err != io.EOF {
return -1, err
}
if n == 0 {
return -1, nil
}
for i := 0; i < n; i++ {
if buf[i] == toCmp[ok] {
ok++
if ok == len(toCmp) {
offset += int64(i) - int64(len(toCmp)-1)
return offset, nil
}
} else {
ok = 0
}
}
offset += int64(n)
}
}