安卓apk信息提取,敏感信息搜集——apkinfo

作者:Sec-Labs | 发布时间:

项目介绍

一个基于Python的安卓APK分析工具,可以快速提取敏感信息

项目地址

https://github.com/saucer-man/apkinfo

使用说明

apkinfo

一键解析app信息+检查敏感信息

用法:

python3 -m pip install apkutils

python3 apkinfo -a path/to/app
python3 apkinfo -f dir/to/app

 

功能:

  • 解压app
  • 查壳
  • manifest解析
  • 签名查询
  • 敏感信息查询
    • url、ips
    • 手机号、身份证、银行卡等
    • 其他敏感信息,比如oss key、数据库链接地址等
    • 如果想自定义关键词,直接编辑config/keys.json即可

核心代码

import argparse
import os
import sys
import zipfile
import subprocess
from apkutils import APK
import platform
import re
import json


class APKInfo():
    def __init__(self, apk_path):
        self.apk_path = os.path.abspath(apk_path)
        self.base_path = os.path.abspath(os.path.dirname(__file__))
        self.apk = None
        self.result_path = None
        self.apk_name = None
        self.shell_type = None
        self.patterns = None
        self.pattern_file = None
        self.shellfeatures = None
        self.check()

    def shell_detect(self):
        try:
            zipfiles = zipfile.ZipFile(self.apk_path)
            namelist = zipfiles.namelist()  # 得到压缩包里所有文件
            # zipfiles.extractall(path=os.path.join(
            #     self.result_path, "files"))  # 解压zip到result目录下

            for filename in namelist:
                for k, v in self.shellfeatures.items():
                    for shell in v:
                        if shell in filename:
                            shell_type = k
                            return True, shell_type
        except:
            pass
        return False, "未加壳"

    def dex2jar(apk_path, savePath):
        try:
            subprocess.run(
                f"{dex2jar_path} --force {apk_path} --output {os.path.join(savePath, 'classed-dex2jar.jar')}", shell=True)
            return True
        except:
            pass
        return False

    def manifest(self):
        with open(os.path.join(self.result_path, "AndroidManifest.xml"), "w") as f:
            f.write(self.apk.get_manifest())

        print(f"{self.apkname}基础信息:package: {self.apk.package_name} Version: {self.apk._version_name} MainActivity: {self.apk.get_manifest_main_activities()}")

    def signinfo(self):
        try:
            # 获取签名信息
            for k, _ in self.apk.get_certs():
                print(f"{self.apkname}签名信息:Signer:{k}")
        except:
            pass

    def finder(self, name, pattern, strings):
        matcher = re.compile(pattern)
        found = []
        for string in strings:
            mo = matcher.search(string)
            if mo:
                found.append(mo.group())
        return sorted(list(set(found)))

    def save(self, name, found):
        if not found:
            return
        with open(os.path.join(self.result_path, name), "a") as f:
            f.write("\n".join(found))

    def detect_strings(self):
        strings = set()
        for string in self.apk.get_dex_strings():
            try:
                strings.add(string.decode(encoding="UTF-8"))
            except:
                pass
        with open(os.path.join(self.result_path, "strings.txt"), "w") as f:
            for string in strings:
                f.write(f"{string}\n")

        for name, pattern in self.patterns.items():
            if isinstance(pattern, list):
                for p in pattern:
                    found = self.finder(name, p, strings)
                    self.save(name, found)
            else:
                found = self.finder(name, pattern, strings)
                self.save(name, found)

    def check(self):
        print(f"下面开始分析:{self.apk_path}")
        if not os.path.exists(self.apk_path):
            raise(f"当前文件不存在")

        _, self.apkname = os.path.split(apk_path)
        self.result_path = os.path.join(self.base_path, "result", self.apkname)
        if not os.path.exists(self.result_path):
            os.makedirs(self.result_path)
        else:
            print(f"{self.apkname}: 检测到文件已经分析过,将会覆盖")
        self.pattern_file = os.path.join(self.base_path, "config", "keys.json")
        with open(self.pattern_file) as f:
            self.patterns = json.load(f)
        shell_path = os.path.join(self.base_path, "config", "shell.json")
        with open(shell_path) as f:
            self.shellfeatures = json.load(f)

    def analyse(self):
        # 先解压 + 查壳
        print(f"{self.apkname}: 开始查壳...")
        flag, self.shell_type = self.shell_detect()
        if flag:
            print(f"{self.apkname}: 经检测,该apk使用了{self.shell_type}进行加固")
            return
        else:
            print(f"{self.apkname}: 未检测到壳")

        self.apk = APK.from_file(self.apk_path)
        print(f"{self.apkname}: 下面开始解析apk...")
        # 解析manifest获取apk信息
        self.manifest()

        # 查看签名
        self.signinfo()

        # 字符串解析,查找敏感字符串
        self.detect_strings()
        print(f"{self.apkname}: 分析结束,结果保存在{self.result_path}")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='apk信息提取')
    parser.add_argument('-a', '--apk',
                        help='指定apk路径')
    parser.add_argument('-d', '--dir',
                        help='指定apk路径')
    args = parser.parse_args()
    apks = []
    if args.apk:
        apks.append(args.apk)
    if args.dir:
        for filename in os.listdir(args.dir):
            if filename.endswith(".apk"):
                apks.append(os.path.join(args.dir, filename))
    if not apks:
        print(f"请使用-a或者-f指定apk")
        sys.exit()

    for apk_path in apks:
        try:
            apk = APKInfo(apk_path)
            apk.analyse()
        except Exception as e:
            print(f"{apk_path}出错: {e}")
            import traceback
            traceback.print_exc()

常见特征代码如下

{
	"Amazon_AWS_Access_Key_ID": "([^A-Z0-9]|^)(AKIA|A3T|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{12,}",
	"Amazon_AWS_S3_Bucket": [
		"//s3-[a-z0-9-]+\\.amazonaws\\.com/[a-z0-9._-]+",
		"//s3\\.amazonaws\\.com/[a-z0-9._-]+",
		"[a-z0-9.-]+\\.s3-[a-z0-9-]\\.amazonaws\\.com",
		"[a-z0-9.-]+\\.s3-website[.-](eu|ap|us|ca|sa|cn)",
		"[a-z0-9.-]+\\.s3\\.amazonaws\\.com",
		"amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
	],
	"Artifactory_API_Token": "(?:\\s|=|:|\"|^)AKC[a-zA-Z0-9]{10,}",
	"Artifactory_Password": "(?:\\s|=|:|\"|^)AP[\\dABCDEF][a-zA-Z0-9]{8,}",
	"Authorization_Basic": "basic\\s[a-zA-Z0-9_\\-:\\.=]+",
	"Authorization_Bearer": "bearer\\s[a-zA-Z0-9_\\-:\\.=]+",
	"AWS_API_Key": "AKIA[0-9A-Z]{16}",
	"Basic_Auth_Credentials": "(?<=:\/\/)[a-zA-Z0-9]+:[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z]+",
	"Cloudinary_Basic_Auth": "cloudinary:\/\/[0-9]{15}:[0-9A-Za-z]+@[a-z]+",
	"DEFCON_CTF_Flag": "O{3}\\{.*\\}",
	"Discord_BOT_Token": "((?:N|M|O)[a-zA-Z0-9]{23}\\.[a-zA-Z0-9-_]{6}\\.[a-zA-Z0-9-_]{27})$",
	"Facebook_Access_Token": "EAACEdEose0cBA[0-9A-Za-z]+",
	"Facebook_ClientID": "[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K](.{0,20})?['\"][0-9]{13,17}",
	"Facebook_OAuth": "[f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K].*['|\"][0-9a-f]{32}['|\"]",
	"Facebook_Secret_Key": "([f|F][a|A][c|C][e|E][b|B][o|O][o|O][k|K]|[f|F][b|B])(.{0,20})?['\"][0-9a-f]{32}",
	"Firebase": "[a-z0-9.-]+\\.firebaseio\\.com",
	"Generic_API_Key": "[a|A][p|P][i|I][_]?[k|K][e|E][y|Y].*['|\"][0-9a-zA-Z]{32,45}['|\"]",
	"Generic_Secret": "[s|S][e|E][c|C][r|R][e|E][t|T].*['|\"][0-9a-zA-Z]{32,45}['|\"]",
	"GitHub": "[g|G][i|I][t|T][h|H][u|U][b|B].*['|\"][0-9a-zA-Z]{35,40}['|\"]",
	"GitHub_Access_Token": "([a-zA-Z0-9_-]*:[a-zA-Z0-9_-]+@github.com*)$",
	"Google_API_Key": "AIza[0-9A-Za-z\\-_]{35}",
	"Google_Cloud_Platform_OAuth": "[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com",
	"Google_Cloud_Platform_Service_Account": "\"type\": \"service_account\"",
	"Google_OAuth_Access_Token": "ya29\\.[0-9A-Za-z\\-_]+",
	"HackerOne_CTF_Flag": "[h|H]1(?:[c|C][t|T][f|F])?\\{.*\\}",
	"HackTheBox_CTF_Flag": "[h|H](?:[a|A][c|C][k|K][t|T][h|H][e|E][b|B][o|O][x|X]|[t|T][b|B])\\{.*\\}$",
	"Heroku_API_Key": "[h|H][e|E][r|R][o|O][k|K][u|U].*[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}",
	"IP_Address": "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])",
	"JSON_Web_Token": "(?i)^((?=.*[a-z])(?=.*[0-9])(?:[a-z0-9_=]+\\.){2}(?:[a-z0-9_\\-\\+\/=]*))$",
	"LinkFinder": "(?:\"|')(((?:[a-zA-Z]{1,10}:\/\/|\/\/)[^\"'\/]{1,}\\.[a-zA-Z]{2,}[^\"']{0,})|((?:\/|\\.\\.\/|\\.\/)[^\"'><,;| *()(%%$^\/\\\\\\[\\]][^\"'><,;|()]{1,})|([a-zA-Z0-9_\\-\/]{1,}\/[a-zA-Z0-9_\\-\/]{1,}\\.(?:[a-zA-Z]{1,4}|action)(?:[\\?|#][^\"|']{0,}|))|([a-zA-Z0-9_\\-\/]{1,}\/[a-zA-Z0-9_\\-\/]{3,}(?:[\\?|#][^\"|']{0,}|))|([a-zA-Z0-9_\\-]{1,}\\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:[\\?|#][^\"|']{0,}|)))(?:\"|')",
	"Mac_Address": "(([0-9A-Fa-f]{2}[:]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{2}[-]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{4}[\\.]){2}[0-9A-Fa-f]{4})$",
	"MailChimp_API_Key": "[0-9a-f]{32}-us[0-9]{1,2}",
	"Mailgun_API_Key": "key-[0-9a-zA-Z]{32}",
	"Mailto": "(?<=mailto:)[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+",
	"Password_in_URL": "[a-zA-Z]{3,10}://[^/\\s:@]{3,20}:[^/\\s:@]{3,20}@.{1,100}[\"'\\s]",
	"PayPal_Braintree_Access_Token": "access_token\\$production\\$[0-9a-z]{16}\\$[0-9a-f]{32}",
	"PGP_private_key_block": "-----BEGIN PGP PRIVATE KEY BLOCK-----",
	"Picatic_API_Key": "sk_live_[0-9a-z]{32}",
	"RSA_Private_Key": "-----BEGIN RSA PRIVATE KEY-----",
	"Slack_Token": "(xox[p|b|o|a]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32})",
	"Slack_Webhook": "https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}",
	"Square_Access_Token": "sq0atp-[0-9A-Za-z\\-_]{22}",
	"Square_OAuth_Secret": "sq0csp-[0-9A-Za-z\\-_]{43}",
	"SSH_DSA_Private_Key": "-----BEGIN DSA PRIVATE KEY-----",
	"SSH_EC_Private_Key": "-----BEGIN EC PRIVATE KEY-----",
	"Stripe_API_Key": "sk_live_[0-9a-zA-Z]{24}",
	"Stripe_Restricted_API_Key": "rk_live_[0-9a-zA-Z]{24}",
	"TryHackMe_CTF_Flag": "[t|T](?:[r|R][y|Y][h|H][a|A][c|C][k|K][m|M][e|E]|[h|H][m|M])\\{.*\\}$",
	"Twilio_API_Key": "SK[0-9a-fA-F]{32}",
	"Twitter_Access_Token": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*[1-9][0-9]+-[0-9a-zA-Z]{40}",
	"Twitter_ClientID": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R](.{0,20})?['\"][0-9a-z]{18,25}",
	"Twitter_OAuth": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R].*['|\"][0-9a-zA-Z]{35,44}['|\"]",
	"Twitter_Secret_Key": "[t|T][w|W][i|I][t|T][t|T][e|E][r|R](.{0,20})?['\"][0-9a-z]{35,44}",
	"OSS":"([A|a]ccess[K|k]ey[I|i][d|D]|[A|a]ccess[K|k]ey[S|s]ecret)",
	"sensitives_list":["accessKey", "database", "ssh", "rdp", "smb", "mysql", "sqlserver", "oracle",
		"ftp", "mongodb", "memcached", "postgresql", "telnet", "smtp", "pop3", "imap",
		"vnc", "redis", "admin", "root", "config", "jdbc", ".properties", "aliyuncs",
		"oss"],
	"domain":["http://","https://"],
	"Chinese_Bank_Card_ID":"[^0-9]([1-9]\\d{12,18})[^0-9]",
	"MAC_Address" :"(^([a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5})|[^a-zA-Z0-9]([a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}))",
	"Phone_Number":"[^\\w]((?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[5-79])|(?:5[0-35-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[189]))\\d{8})[^\\w]",
    "IDcard":"'[^0-9]((\\d{8}(0\\d|10|11|12)([0-2]\\d|30|31)\\d{3}$)|(\\d{6}(18|19|20)\\d{2}(0[1-9]|10|11|12)([0-2]\\d|30|31)\\d{3}(\\d|X|x)))[^0-9]'",
	"Email":"(([a-zA-Z0-9][_|\\.])*[a-zA-Z0-9]+@([a-zA-Z0-9][-|_|\\.])*[a-zA-Z0-9]+\\.((?!js|css|jpg|jpeg|png|ico)[a-zA-Z]{2,}))"
}

 

标签:逆向技巧, APP分析