ApplicationScanner源码阅读

近期有Apk漏洞扫描的需求,参考一下优秀的开源项目

https://github.com/paradiseduo/ApplicationScanner

image-20211216163420638

项目结构

│  .gitignore
│  AppScanner.py
│  LICENSE
│  README.md
│  requirements.txt
│  __init__.py
│
├─lib
│  │  apk.py
│  │  Base.py
│  │  info.py
│  │  ipa.py
│  │  tools.py
│  │  translation.py
│  │  __init__.py
│  │
│  ├─Android
│  │      BackupCheck.py
│  │      BroadcastCheck.py
│  │      ClipboardCheck.py
│  │      DBCheck.py
│  │      DexLoadCheck.py
│  │      EncryptCheck.py
│  │      FFmpegCheck.py
│  │      FragmentCheck.py
│  │      HiddenIntentCheck.py
│  │      IPCheck.py
│  │      JSCheck.py
│  │      LogCheck.py
│  │      PendingIntentCheck.py
│  │      PortCheck.py
│  │      ReadFileCheck.py
│  │      ReflectCheck.py
│  │      ScreenshotCheck.py
│  │      SoCheck.py
│  │      SoLoadCheck.py
│  │      SQLInjectCheck.py
│  │      URLCheck.py
│  │      WebStorageCheck.py
│  │      WebViewCheck.py
│  │      XSSCheck.py
│  │      ZipCheck.py
│  │      __init__.py
│  │
│  └─iOS
│          APICheck.py
│          ARCCheck.py
│          ASLRCheck.py
│          CanaryCheck.py
│          CodeSignCheck.py
│          IBackDoorCheck.py
│          IPCheck.py
│          MallocCheck.py
│          MprotectCheck.py
│          NSLogCheck.py
│          ObscureCheck.py
│          RestrictCheck.py
│          SignatureCheck.py
│          SQLiteCheck.py
│          URLCheck.py
│          WeakCryptCheck.py
│          WeakHashCheck.py
│          WeakRandomCheck.py
│          WebViewCheck.py
│          XcodeGhostCheck.py
│          ZipperDownCheck.py
│          __init__.py
│
└─ThirdTools
        apksigner.jar
        apktool.jar

1. 入口分析

该项目的启动命令是

python3 AppScanner.py -i test.apk

阅读下Appscanner.py文件. 抽离下关键代码. 主要是调用了apkScan, 进入到lib.apk看看

from lib.apk import apkScan

def main(argv):
    inputfile = ''
    save = False
    if len(inputfile) > 0:
        if not os.path.exists(inputfile):
            console.print('File not exist!', style='red bold')
            sys.exit(0)
        if '.apk' in inputfile:
            apkScan(inputfile, save)
        elif '.ipa' in inputfile:
            ipaScan(inputfile, save)
        else:
            console.print('Application must be *.apk or *.ipa', style='red bold')
            sys.exit(2)
    else:
        printUse()
        sys.exit(2)

2. lib/apk.py文件

这边有个比较有意思的点, 在前面看了他的文件结构,可以知道,他是一个文件,一个检测脚本的. 所以他是怎么实现的呢?

反射手段

scanners = {}

def register(scanner_class):
    scanners[scanner_class.__name__] = scanner_class

def scanner(scanner_key):
    scanner_class = scanners.get(scanner_key, None)
    if scanner_class is None:
        return None
    return scanner_class

def import_scanners(scanners_imports):
    for runner_import in scanners_imports:
        __import__(runner_import)

from . import Android  # 执行导入包到 scanners

from . import Android # 执行导入包到 scanners执行后,会执行__init__.py

Android/__init__.py文件中,发现,他通过importlib.import_module 来动态的导入了对象.

import os
import importlib
import logging

from .. import ROOT_PATH, name

logger = logging.getLogger(__name__)

for item in os.listdir(os.path.join(ROOT_PATH, "Android")):
    if '__' in item or '.DS_Store' in item:
        continue
    else:
        prefix, suffix = os.path.splitext(item)
        if suffix == ".py":
            try:
                importlib.import_module(f"{name}.Android.{prefix}")
            except Exception as e:
                logger.exception(e)

紧接着就到了apkScan函数了


def apkScan(inputfile, save):
    # 解压apk包
    console.print('\n[magenta]Unzip apk [/magenta][bold magenta]' + inputfile + '[/bold magenta]')
    filePath = inputfile.replace('.apk', '').split('/')[-1] + randomStr(6)
    strline = f'java -jar {apktool} d -f {inputfile} -o {filePath} --only-main-classes'

    subprocess.Popen(strline, shell=True).communicate()
    console.print('[bold green]Finish[/bold green]')
    filePath = os.path.abspath(filePath)
    try:
        apkInfo(filePath)
        permissionAndExport(filePath)
        appSign(inputfile)
        fingerPrint(filePath)

        for key in scanners.keys():
            // 动态扫描的关键.
            c = scanner(key)
            if c:
                c(filePath).scan()
    except:
        print(traceback.format_exc())

    if not save:
        console.print('\n[bold magenta]Clean cache...[/bold magenta]')
        shutil.rmtree(filePath)
        console.print('[bold green]Finish[/bold green]')
    

动态执行脚本的关键代码如下:

for key in scanners.keys():
            // 动态扫描的关键.
            c = scanner(key)
            if c:
                c(filePath).scan()

那么是哪里对scanners变量进行操作了呢?

.libAndroidBackupCheck.py

原来是在导入包的时候就已经注册了函数,把各个类给了scanners变量. 妙哉妙哉~. 最后在循环一下,调用scan方法. 如此这般.

import xml.dom.minidom
from ..Base import Base
from ..info import Info
from ..apk import register
import os
from lib.translation import *


class BackupCheck(Base):
    def scan(self):
        set_values_for_key(key='BACKUPCHECKTITLE', zh='应用数据任意备份风险检测', en='Application data arbitrary backup risk detection')
        set_values_for_key(key='BACKUPCHECHINFO', zh='检测App是否存在应用数据被任意备份的风险', en='Detect whether the app has the risk of app data being arbitrarily backed up')

        TITLE = get_value('BACKUPCHECKTITLE')
        LEVEL = 2
        INFO = get_value('BACKUPCHECHINFO')

        strline = 'find ' + self.appPath +' -name AndroidManifest.xml | grep -v "/original/"'
        arr = os.popen(strline).readlines()
        for item in arr:
            tree = xml.dom.minidom.parse(item[:-1])
            root = tree.documentElement
            application = root.getElementsByTagName('application')
            result = ''
            for a in application:
                if a.getAttribute('android:allowBackup') == 'true':
                    result = 'android:allowBackup = true'
            Info(key=self.__class__, title=TITLE, level=LEVEL, info=INFO, result=result).description()


register(BackupCheck)

3. 总结

本次分析没有分析其如何对apk进行漏洞扫描的, 原因是现在大部分的apk都进行了加固,这种扫描有时候的意义并不是很大的.

如果要做漏洞扫描的话,小编认为可以这么做:

  1. 动态dump dex
  2. 在把dump下来的dex进行动态扫描.

这样子可以尽可能的扫描的全一点吧~

本次主要还是学习该项目对许许多多的脚本的时候,如何更加规范的编写代码,防止代码像个狗屎一样(目前面临的问题.)

再会~~