mirror of
https://github.com/vxunderground/VXUG-Papers.git
synced 2026-06-17 00:09:29 +00:00
Add files via upload
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user