Skip to content

Commit

Permalink
🚀 new version! Export to JSON, dump additional metadata and refactor …
Browse files Browse the repository at this point in the history
…code!
  • Loading branch information
andpalmier committed Feb 12, 2024
1 parent b7dc729 commit ee81d5c
Show file tree
Hide file tree
Showing 12 changed files with 793 additions and 486 deletions.
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,32 @@
</p>
</p>

apkingo is a tool to get detailed information about an apk file. apkingo will explore the given file to get details on the apk, such as package name, target SDK, permissions, metadata, certificate serial and issuer. The tool will also retrieve information about the specified apk from the Play Store, and (if valid API keys are provided) from [Koodous](https://koodous.com/) and [VirusTotal](https://virustotal.com). In case the file is not in VirusTotal, apkingo allows to upload it using the submitted API key.
apkingo is a utility designed to extract information from an APK file. By analyzing the provided file, apkingo extracts details like package name, target SDK, permissions, metadata, certificate serial, and issuer. Additionally, the tool fetches information about the specified apk from the Play Store and, if valid API keys are provided, from Koodous and VirusTotal. If the file is not available on VirusTotal, apkingo offers the option to upload it.

## Installation

You can can download apkingo from the [releases section](https://github.com/andpalmier/apkingo/releases) or compile it from the source by using:
You can download apkingo from the [releases section](https://github.com/andpalmier/apkingo/releases) or compile it from the source by running:

```
go install github.com/andpalmier/apkingo/cmd/apkingo@latest
```

## Usage

If you have Koodous or VirusTotal API keys, you can use them in apkingo by exporting the environment variables:

You can run apkingo with the following flags:
```
export VT_API_KEY=<your_api_key>
export KOODOUS_API_KEY=<your_api_key>
-apk string (REQUIRED)
Path to APK file
-country string
Country code of the Play Store (default "us")
-json string
Path to export analysis in JSON format
-kapi string
Koodous API key (you can export it using the env variable KOODOUS_API_KEY)
-vtapi string
VirusTotal API key (you can export it using the env variable VT_API_KEY)
```

You can then run apkingo with: `apkingo file.apk`

## Screenshots

apkingo analyzing snapseed:
Expand All @@ -52,14 +57,14 @@ apkingo analyzing an Android malware (I had to cut the screenshot on the permiss

Here is the full list of information which apkingo can retrieve:

- General information: app name, package name, app version, MainActivity, minimum and target SDK
- General information: app name, package name, app version, main activity, minimum and target SDK
- Hashes: md5, sha1 and sha256
- Permissions
- Metadata
- Certificate information: serial, sha1, subject, issuer, validity date and expiration date
- Play Store information: Play Store url, version, summary, release date, number of installations, score, developer name, developer ID, developer mail and developer website
- Koodous info (API key required): Koodous url, Koodous ID, app name, package name, company, version, Koodous link to the app icon, size, Koodous tags, trusted (boolean), installed on devices (boolean), Koodous rating, detected (boolean), corrupted (boolean), statically analyzed (boolean), dynamically analyzed (boolean) and date when the app was submitted to Koodous for the first time
- VirusTotal info (API key required): VirusTotal url, apk names, first submission date, number of submissions, last analysis date and results, reputation, community votes (harmless and malicious), md5 and dhash of icon, providers, receivers, services, interesting string and permissions that are considered dangerous.
- Certificate information: serial, thumbprint, validity, date, expiration date, issuer and subject
- Play Store information: Play Store url, version, release date, last update date, genre, summary, number of installations, score, developer name, developer ID, developer mail and developer website
- Koodous info (API key required): Koodous url, Koodous ID, Koodous link to the app icon, size, Koodous tags, trusted (boolean), Koodous rating, corrupted (boolean) and submission date
- VirusTotal info (API key required): VirusTotal url, apk names, submission date, number of submissions, last analysis date and results, community votes (harmless and malicious), md5 and dhash of icon, providers, receivers, services, interesting strings and permissions that are considered dangerous

## 3rd party libraries used

Expand Down
131 changes: 64 additions & 67 deletions cmd/apkingo/androidapp.go
Original file line number Diff line number Diff line change
@@ -1,85 +1,82 @@
package main

import (
"fmt"
"encoding/json"
"os"

"github.com/shogo82148/androidbinary/apk"
)

// permissionsInfo(apk) - get the permission from apk
func permissionsInfo(apk apk.Apk) {
if len(apk.Manifest().UsesPermissions) == 0 {
italic.Println("no permissions found")
} else {
for _, n := range apk.Manifest().UsesPermissions {
permission, _ := n.Name.String()
if permission != "" {
fmt.Println(permission)
}
}
}
// AndroidApp represents information extracted from an APK file
type AndroidApp struct {
Name string `json:"name"`
PackageName string `json:"package-name"`
Version string `json:"version"`
MainActivity string `json:"main-activity"`
MinimumSDK int32 `json:"minimum-sdk"`
TargetSDK int32 `json:"target-sdk"`
Hashes Hashes `json:"hashes"`
Permissions []string `json:"permissions"`
Metadata []Metadata `json:"metadata"`
Certificate CertificateInfo `json:"certificate"`
PlayStore *PlayStoreInfo `json:"playstore,omitempty"`
Koodous *KoodousInfo `json:"koodous,omitempty"`
VirusTotal *VirusTotalInfo `json:"virustotal,omitempty"`
}

// metadataInfo(apk) - get the metadata from apk
func metadataInfo(apk apk.Apk) {
if len(apk.Manifest().App.MetaData) == 0 {
italic.Println("no metadata found")
} else {
for _, n := range apk.Manifest().App.MetaData {
metaname, _ := n.Name.String()
metavalue, _ := n.Value.String()
fmt.Printf("%s: ", metaname)
if metavalue != "" {
cyan.Printf("%s", metavalue)
}
fmt.Printf("\n")
}
}
// Hashes represents hash values
type Hashes struct {
Md5 string `json:"md5"`
Sha1 string `json:"sha1"`
Sha256 string `json:"sha256"`
}

// getGeneralInfo(apk) - get general info from apk
func generalInfo(apk apk.Apk) {

yellow.Printf("\nApp name:\t")
name, err := apk.Label(nil)
if err == nil {
cyan.Printf("%s\n", name)
} else {
italic.Printf("app name not found\n")
}

yellow.Println("\n* General Info")

fmt.Printf("PackageName:\t")
printer(apk.PackageName())

fmt.Printf("App version:\t")
version, err := apk.Manifest().VersionName.String()
if err != nil {
version = ""
}
printer(version)
// Metadata represents metadata
type Metadata struct {
Name string `json:"name"`
Value string `json:"value,omitempty"`
}

fmt.Printf("Main activity:\t")
mainactivity, err := apk.MainActivity()
// ExportJSON exports AndroidApp struct to a JSON file
func (app *AndroidApp) ExportJSON(jsonpath string) error {
jsonfile, err := json.MarshalIndent(app, "", "\t")
if err != nil {
mainactivity = ""
return err
}
printer(mainactivity)
err = os.WriteFile(jsonpath, jsonfile, 0644)
return err
}

fmt.Printf("Minimum SDK:\t")
sdkmin, err := apk.Manifest().SDK.Min.Int32()
if err != nil {
italic.Println("not found")
} else {
cyan.Printf("%d (%s)\n", sdkmin, androidname[int(sdkmin)])
// setGeneralInfo sets general information about the APK
func (app *AndroidApp) setGeneralInfo(apk *apk.Apk) {
name, _ := apk.Label(nil)
app.Name = name
app.PackageName = apk.PackageName()
version, _ := apk.Manifest().VersionName.String()
app.Version = version
main, _ := apk.MainActivity()
app.MainActivity = main
sdkMin, _ := apk.Manifest().SDK.Min.Int32()
app.MinimumSDK = sdkMin
sdkTarget, _ := apk.Manifest().SDK.Target.Int32()
app.TargetSDK = sdkTarget
for _, n := range apk.Manifest().UsesPermissions {
permission, _ := n.Name.String()
if permission != "" {
app.Permissions = append(app.Permissions, permission)
}
}

fmt.Printf("Target SDK:\t")
sdktarget, err := apk.Manifest().SDK.Target.Int32()
if err != nil {
italic.Println("not found")
} else {
cyan.Printf("%d (%s)\n", sdktarget, androidname[int(sdktarget)])
var m Metadata
for _, n := range apk.Manifest().App.MetaData {
metadataName, _ := n.Name.String()
metadataValue, _ := n.Value.String()
if metadataName != "" {
m.Name = metadataName
m.Value = ""
if metadataValue != "" {
m.Value = metadataValue
}
app.Metadata = append(app.Metadata, m)
}
}
}
72 changes: 55 additions & 17 deletions cmd/apkingo/cert.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,76 @@
package main

import (
"crypto/x509/pkix"
"encoding/hex"
"errors"
"fmt"
"time"

"github.com/avast/apkverifier"
)

// certInfo(path) - retrieve certificate information from apk
func certInfo(filepath string) error {
// CertificateInfo represents certificate information
type CertificateInfo struct {
Serial string `json:"serial"`
Thumbprint string `json:"thumbprint"`
ValidFrom string `json:"valid-from"`
ValidTo string `json:"valid-to"`
Subject CertName `json:"subject"`
Issuer CertName `json:"issuer"`
}

// CertName represents issuer and subject details
type CertName struct {
Country string `json:"country"`
Organization string `json:"organization"`
OrgUnit string `json:"organizational-unit"`
Locality string `json:"locality"`
Province string `json:"province"`
CommonName string `json:"common-name"`
Raw string `json:"raw"`
}

// setCertName sets certificate name details
func (cn *CertName) setCertName(name pkix.Name) {
cn.Country = firstElement(name.Country)
cn.Organization = firstElement(name.Organization)
cn.OrgUnit = firstElement(name.OrganizationalUnit)
cn.Locality = firstElement(name.Locality)
cn.Province = firstElement(name.Province)
cn.CommonName = name.CommonName
}

// firstElement returns the first element of a string slice, or an empty string if the slice is empty
func firstElement(slice []string) string {
if len(slice) > 0 {
return slice[0]
}
return ""
}

// setCertInfo retrieves and sets certificate information
func (androidapp *AndroidApp) setCertInfo(filepath string) error {
res, err := apkverifier.ExtractCerts(filepath, nil)
if err != nil {
return err
}

// this may print an error, but certificate info are still retrieved
cert, _ := apkverifier.PickBestApkCert(res)
cert, certx := apkverifier.PickBestApkCert(res)
if cert == nil {
return errors.New("no certificate found")
}

fmt.Printf("Serial:\t\t")
printer(cert.SerialNumber.String())
fmt.Printf("sha1:\t\t")
printer(cert.Sha1)
fmt.Printf("Issuer:\t\t")
printer(cert.Issuer)
fmt.Printf("Subject:\t")
printer(cert.Subject)
fmt.Printf("Valid from:\t")
printer(cert.ValidFrom.Format(time.RFC822))
fmt.Printf("Valid to:\t")
printer(cert.ValidTo.Format(time.RFC822))
androidapp.Certificate.Serial = hex.EncodeToString(cert.SerialNumber.Bytes())
androidapp.Certificate.Thumbprint = cert.Sha1
androidapp.Certificate.ValidFrom = cert.ValidFrom.Format(time.DateTime)
androidapp.Certificate.ValidTo = cert.ValidTo.Format(time.DateTime)

if certx != nil {
androidapp.Certificate.Subject.setCertName(certx.Subject)
androidapp.Certificate.Issuer.setCertName(certx.Issuer)
}
androidapp.Certificate.Subject.Raw = cert.Subject
androidapp.Certificate.Issuer.Raw = cert.Issuer

return nil
}
53 changes: 26 additions & 27 deletions cmd/apkingo/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,43 @@ import (
"crypto/sha1"
"crypto/sha256"
"fmt"
"hash"
"io"
"os"
)

// getFileHash(h, filepath) - hash the file in the given path with the selected hash
func getFileHash(h hash.Hash, filepath string) ([]byte, error) {
file, err := os.ReadFile(filepath)
// setHashes calculates hashes of the APK file
func (androidapp *AndroidApp) setHashes(path string) error {
file, err := os.Open(path)
if err != nil {
return nil, err
return err
}
h.Write(file)
return h.Sum(nil), nil
}
defer file.Close()

// hashInfo(filepath) - calculate hashes of the apk file
func hashInfo(path string) (string, error) {
h256 := sha256.New()
digestsha256, err := getFileHash(h256, path)
if err != nil {
return "", err
if _, err := io.Copy(h256, file); err != nil {
return err
}
androidapp.Hashes.Sha256 = fmt.Sprintf("%x", h256.Sum(nil))

if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}

h1 := sha1.New()
digestsha1, err := getFileHash(h1, path)
if err != nil {
return "", err
if _, err := io.Copy(h1, file); err != nil {
return err
}
hmd5 := md5.New()
digestmd5, err := getFileHash(hmd5, path)
if err != nil {
return "", err
androidapp.Hashes.Sha1 = fmt.Sprintf("%x", h1.Sum(nil))

if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}

fmt.Printf("md5:\t\t")
cyan.Printf("%x\n", digestmd5)
fmt.Printf("sha1:\t\t")
cyan.Printf("%x\n", digestsha1)
fmt.Printf("sha256:\t\t")
cyan.Printf("%x\n", digestsha256)
hmd5 := md5.New()
if _, err := io.Copy(hmd5, file); err != nil {
return err
}
androidapp.Hashes.Md5 = fmt.Sprintf("%x", hmd5.Sum(nil))

return fmt.Sprintf("%x", digestsha256), nil
return nil
}
Loading

0 comments on commit ee81d5c

Please sign in to comment.