215 lines
6.2 KiB
Go
215 lines
6.2 KiB
Go
package main
|
|
|
|
// Pure-Go fallback parsers for the APK auditor, used when the Android SDK
|
|
// build-tools (aapt2 / apksigner) or a JDK are not installed. This makes the
|
|
// audit fully self-contained for users who only have adb/fastboot.
|
|
//
|
|
// Reference path (aapt2 + apksigner) stays the default when those tools are
|
|
// present; these functions are only invoked as a fallback.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/avast/apkparser"
|
|
"github.com/avast/apkverifier"
|
|
)
|
|
|
|
// hasBuildTool reports whether a build-tool can be resolved on this machine.
|
|
func (a *App) hasBuildTool(name string) bool {
|
|
_, err := a.resolveBuildTool(name)
|
|
return err == nil
|
|
}
|
|
|
|
// parseManifestGo decodes the binary AndroidManifest.xml with apkparser and
|
|
// fills the same audit fields the aapt2 path would (minus the resource-resolved
|
|
// app label, which needs the resource table).
|
|
func parseManifestGo(path string, audit *APKAudit) {
|
|
var buf bytes.Buffer
|
|
enc := xml.NewEncoder(&buf)
|
|
enc.Indent("", " ")
|
|
zipErr, _, _ := apkparser.ParseApk(path, enc)
|
|
_ = enc.Flush()
|
|
if zipErr != nil {
|
|
return
|
|
}
|
|
audit.ManifestXML = buf.String()
|
|
|
|
dec := xml.NewDecoder(strings.NewReader(audit.ManifestXML))
|
|
compIdx := -1
|
|
allowBackupSeen, cleartextSeen := false, false
|
|
|
|
for {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
switch t := tok.(type) {
|
|
case xml.StartElement:
|
|
attr := func(k string) string {
|
|
for _, a := range t.Attr {
|
|
if a.Name.Local == k {
|
|
return a.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
has := func(k string) bool {
|
|
for _, a := range t.Attr {
|
|
if a.Name.Local == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
switch t.Name.Local {
|
|
case "manifest":
|
|
audit.PackageName = attr("package")
|
|
if v := attr("versionName"); v != "" {
|
|
audit.VersionName = v
|
|
}
|
|
if v := attr("versionCode"); v != "" {
|
|
audit.VersionCode = v
|
|
}
|
|
if v := attr("compileSdkVersion"); v != "" {
|
|
audit.CompileSDK = v
|
|
}
|
|
case "uses-sdk":
|
|
if v := attr("minSdkVersion"); v != "" {
|
|
audit.MinSDK = v
|
|
}
|
|
if v := attr("targetSdkVersion"); v != "" {
|
|
audit.TargetSDK = v
|
|
}
|
|
case "uses-permission", "uses-permission-sdk-23":
|
|
if n := attr("name"); n != "" {
|
|
audit.Permissions = append(audit.Permissions, Permission{Name: n, Dangerous: dangerousPermissions[n]})
|
|
}
|
|
case "application":
|
|
if has("debuggable") {
|
|
audit.Debuggable = isTrue(attr("debuggable"))
|
|
}
|
|
if has("allowBackup") {
|
|
allowBackupSeen = true
|
|
audit.AllowBackup = isTrue(attr("allowBackup"))
|
|
}
|
|
if has("usesCleartextTraffic") {
|
|
cleartextSeen = true
|
|
audit.UsesCleartext = isTrue(attr("usesCleartextTraffic"))
|
|
}
|
|
if has("networkSecurityConfig") {
|
|
audit.HasNSC = true
|
|
}
|
|
case "activity", "activity-alias", "service", "receiver", "provider":
|
|
typ := t.Name.Local
|
|
if typ == "activity-alias" {
|
|
typ = "activity"
|
|
}
|
|
c := Component{Type: typ, Name: attr("name"), Permission: attr("permission")}
|
|
if has("exported") {
|
|
c.Exported = isTrue(attr("exported"))
|
|
c.explicitExported = true
|
|
}
|
|
audit.Components = append(audit.Components, c)
|
|
compIdx = len(audit.Components) - 1
|
|
case "intent-filter":
|
|
if compIdx >= 0 {
|
|
audit.Components[compIdx].IntentFilters = append(audit.Components[compIdx].IntentFilters, "")
|
|
}
|
|
case "action", "category":
|
|
if n := attr("name"); n != "" && compIdx >= 0 && len(audit.Components[compIdx].IntentFilters) > 0 {
|
|
idx := len(audit.Components[compIdx].IntentFilters) - 1
|
|
sep := ""
|
|
if audit.Components[compIdx].IntentFilters[idx] != "" {
|
|
sep = ", "
|
|
}
|
|
audit.Components[compIdx].IntentFilters[idx] += sep + shortName(n)
|
|
}
|
|
}
|
|
|
|
case xml.EndElement:
|
|
switch t.Name.Local {
|
|
case "activity", "activity-alias", "service", "receiver", "provider":
|
|
compIdx = -1
|
|
}
|
|
}
|
|
}
|
|
|
|
// defaults the manifest may omit
|
|
if !allowBackupSeen {
|
|
audit.AllowBackup = true
|
|
}
|
|
if !cleartextSeen {
|
|
if v := atoiSafe(audit.TargetSDK); v > 0 && v < 28 {
|
|
audit.UsesCleartext = true
|
|
}
|
|
}
|
|
for i := range audit.Components {
|
|
c := &audit.Components[i]
|
|
if !c.Exported && !c.explicitExported && len(c.IntentFilters) > 0 {
|
|
c.ExportedImplicit = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseCertGo extracts and verifies the signing certificate with apkverifier.
|
|
func parseCertGo(path string, audit *APKAudit) {
|
|
res, err := apkverifier.Verify(path, nil)
|
|
if err != nil {
|
|
audit.Cert.Error = firstLine(err.Error())
|
|
}
|
|
|
|
if len(res.SignerCerts) > 0 && len(res.SignerCerts[0]) > 0 {
|
|
leaf := res.SignerCerts[0][0]
|
|
audit.Cert.Verified = err == nil
|
|
audit.Cert.Subject = leaf.Subject.String()
|
|
audit.Cert.Issuer = leaf.Issuer.String()
|
|
audit.Cert.Serial = leaf.SerialNumber.String()
|
|
audit.Cert.SigAlgo = leaf.SignatureAlgorithm.String()
|
|
audit.Cert.ValidFrom = leaf.NotBefore.Format("2006-01-02")
|
|
audit.Cert.ValidTo = leaf.NotAfter.Format("2006-01-02")
|
|
s256 := sha256.Sum256(leaf.Raw)
|
|
audit.Cert.SHA256 = hex.EncodeToString(s256[:])
|
|
s1 := sha1.Sum(leaf.Raw)
|
|
audit.Cert.SHA1 = hex.EncodeToString(s1[:])
|
|
}
|
|
|
|
// Only claim a signing scheme when verification actually succeeded — this
|
|
// matches apksigner, which reports all-false for unsigned/broken APKs.
|
|
// (apkverifier otherwise defaults SchemeId to 1 even when nothing verifies.)
|
|
if err == nil {
|
|
switch {
|
|
case res.SigningSchemeId >= 3: // 3 or 3.1
|
|
audit.Cert.V3 = true
|
|
case res.SigningSchemeId == 2:
|
|
audit.Cert.V2 = true
|
|
case res.SigningSchemeId == 1:
|
|
audit.Cert.V1 = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// finalizeCert derives debug/weak/expired flags from whichever path populated
|
|
// the cert fields, so both reference and fallback paths behave the same.
|
|
func finalizeCert(audit *APKAudit) {
|
|
subjIssuer := audit.Cert.Subject + " " + audit.Cert.Issuer
|
|
if strings.Contains(strings.ToLower(subjIssuer), "android debug") ||
|
|
strings.Contains(subjIssuer, "CN=Android Debug") {
|
|
audit.Cert.IsDebug = true
|
|
}
|
|
algoUp := strings.ToUpper(audit.Cert.SigAlgo)
|
|
if strings.Contains(algoUp, "MD5") || strings.Contains(algoUp, "SHA1") || strings.Contains(algoUp, "SHA-1") {
|
|
audit.Cert.WeakAlgo = true
|
|
}
|
|
if audit.Cert.ValidTo != "" {
|
|
if t, ok := parseCertTime(audit.Cert.ValidTo); ok && t.Before(time.Now()) {
|
|
audit.Cert.Expired = true
|
|
}
|
|
}
|
|
}
|