移动安全--47--MobSF-v3.0源代码分析【长文巨献】

移动安全 专栏收录该内容
50 篇文章 604 订阅 ¥9.90 ¥99.00

一、项目说明

源码地址:https://github.com/MobSF/Mobile-Security-Framework-MobSF

关于如何搭建源码分析环境,请阅读我的另一篇博客:移动安全–44–MobSF框架安装与开发环境搭建(基于3.0)

移动安全框架(MobSF)是一种自动化的移动应用程序(Android/iOS/Windows)测试框架,能够执行静态、动态和恶意软件分析。 它可用于Android、iOS和Windows移动应用程序的有效和快速安全分析,并支持二进制文件(APK,IPA和APPX)和压缩源代码。 MobSF可以在运行时为Android应用程序进行动态应用程序测试,并具有由CapFuzz(一种特定于Web API的安全扫描程序)提供支持的Web API模糊测试。MobSF旨在使您的CI/CD或DevSecOps管道集成无缝。

二、项目入口

项目结构如下:

在这里插入图片描述

我们首先浏览项目源码,找到项目入口,然后从入口开始分析,这在分析任何源码都是一样的。

由于MobSF使用Django框架开发的,我们浏览源码后可以看到,入口在/MobSF/urls.py中,通过浏览器访问相应的URL地址,功能映射到对应的代码逻辑。

代码如下所示:

为了便于阅读,我将备注重写为中文备注

urlpatterns = [
    # 一般功能URL
    url(r'^$', home.index, name='home'),
    url(r'^upload/$', home.Upload.as_view),
    url(r'^download/', home.download),
    url(r'^about$', home.about, name='about'),
    url(r'^api_docs$', home.api_docs, name='api_docs'),
    url(r'^recent_scans/$', home.recent_scans, name='recent'),
    url(r'^delete_scan/$', home.delete_scan),
    url(r'^search$', home.search),
    url(r'^error/$', home.error, name='error'),
    url(r'^not_found/$', home.not_found),
    url(r'^zip_format/$', home.zip_format),
    url(r'^mac_only/$', home.mac_only),

    # 静态分析URL
    # Android应用静态分析URL
    url(r'^StaticAnalyzer/$', android_sa.static_analyzer),
    url(r'^ViewSource/$', view_source.run),
    url(r'^Smali/$', smali.run),
    url(r'^Java/$', java.run),
    url(r'^Find/$', find.run),
    url(r'^generate_downloads/$', generate_downloads.run),
    url(r'^ManifestView/$', manifest_view.run),
    # IOS应用静态分析URL
    url(r'^StaticAnalyzer_iOS/$', ios_sa.static_analyzer_ios),
    url(r'^ViewFile/$', io_view_source.run),
    # Windows应用静态分析URL
    url(r'^StaticAnalyzer_Windows/$', windows.staticanalyzer_windows),
    # PDF报告
    url(r'^PDF/$', shared_func.pdf),
    # 应用比较
    url(r'^compare/(?P<hash1>[0-9a-f]{32})/(?P<hash2>[0-9a-f]{32})/$',
        shared_func.compare_apps),

    # 动态分析URL
    url(r'^dynamic_analysis/$',
        dz.dynamic_analysis,
        name='dynamic'),
    url(r'^android_dynamic/$',
        dz.dynamic_analyzer,
        name='dynamic_analyzer'),
    url(r'^httptools$',
        dz.httptools_start,
        name='httptools'),
    url(r'^logcat/$', dz.logcat),
    # Android设备操作
    url(r'^mobsfy/$', operations.mobsfy),
    url(r'^screenshot/$', operations.take_screenshot),
    url(r'^execute_adb/$', operations.execute_adb),
    url(r'^screen_cast/$', operations.screen_cast),
    url(r'^touch_events/$', operations.touch),
    url(r'^get_component/$', operations.get_component),
    url(r'^mobsf_ca/$', operations.mobsf_ca),
    # 动态测试
    url(r'^activity_tester/$', tests_common.activity_tester),
    url(r'^download_data/$', tests_common.download_data),
    url(r'^collect_logs/$', tests_common.collect_logs),
    # Frida框架
    url(r'^frida_instrument/$', tests_frida.instrument),
    url(r'^live_api/$', tests_frida.live_api),
    url(r'^frida_logs/$', tests_frida.frida_logs),
    url(r'^list_frida_scripts/$', tests_frida.list_frida_scripts),
    url(r'^get_script/$', tests_frida.get_script),
    # 动态扫描报告
    url(r'^dynamic_report/$', report.view_report),
    url(r'^dynamic_view_file/$', report.view_file),

    # REST API
    url(r'^api/v1/upload$', rest_api.api_upload),
    url(r'^api/v1/scan$', rest_api.api_scan),
    url(r'^api/v1/delete_scan$', rest_api.api_delete_scan),
    url(r'^api/v1/download_pdf$', rest_api.api_pdf_report),
    url(r'^api/v1/report_json$', rest_api.api_json_report),
    url(r'^api/v1/view_source$', rest_api.api_view_source),
    url(r'^api/v1/scans$', rest_api.api_recent_scans),

    # Test
    url(r'^tests/$', tests.start_test),

]

可以很明显的看到,MobSF的功能分为四大块,分别是:

一般功能:包括上传APP、下载报告、关于说明、搜索、删除扫描等常规功能;
静态扫描:Android应用、iOS应用、windows应用的静态扫描,APP比较等;
动态分析:只支持Android应用的动态分析,包括Android设备操作、Frida框架等、报告生成等;
REST API:封装好的可以调用的API接口,使得MobSF的功能可以接入到其他任何系统中。

三、一般功能分析

由于一般功能并非核心功能(核心功能是静态扫描和动态分析),那我们只分析上传功能即可,用来热热身。

对应代码如下:

# General
url(r'^$', home.index, name='home'),
url(r'^upload/$', home.Upload.as_view),   # 这个是我们要分析的
url(r'^download/', home.download),
url(r'^about$', home.about, name='about'),
url(r'^api_docs$', home.api_docs, name='api_docs'),
url(r'^recent_scans/$', home.recent_scans, name='recent'),
url(r'^delete_scan/$', home.delete_scan),
url(r'^search$', home.search),
url(r'^error/$', home.error, name='error'),
url(r'^not_found/$', home.not_found),
url(r'^zip_format/$', home.zip_format),
url(r'^mac_only/$', home.mac_only),

编译器中双击as_view选中,按下Command+B跟踪(苹果系统快捷键),我们来到/MobSF/views/home.py中。

随后跟进到upload_html函数,首先看到的是,上传只支持POST方法,其他方法不支持。

代码如下:

if request.method != 'POST':
    logger.error('Method not Supported!')
    response_data['description'] = 'Method not Supported!'
    response_data['status'] = HTTP_BAD_REQUEST
    return self.resp_json(response_data)

之后,是对上传的无效文件和不支持文件的错误处理,会给出错误提示。

对于使用windows平台下上传的ipa包,也会给出错误提示,要求操作系统必须是MAC或者Linux。

接下来是upload_api函数,这个是REST API的上传功能,我们这里不做分析。

随后对上传的文件做分类,对不同类型的应用包做对应的扫描。代码如下:

def upload(self):
    request = self.request
    scanning = Scanning(request)     # 就是这里,对上传的文件做扫描,稍后跟进
    file_type = self.file_content_type
    file_name_lower = self.file_name_lower
    
    # 判断上传的应用包的类型,对不同类型的包做对应的扫描
    logger.info('MIME Type: %s FILE: %s', file_type, file_name_lower)
    if self.file_type.is_apk():
        return scanning.scan_apk()    # 扫描APK包
    elif self.file_type.is_zip():
        return scanning.scan_zip()    # 扫描ZIP包
    elif self.file_type.is_ipa():
        return scanning.scan_ipa()    # 扫描IPA包
    elif self.file_type.is_appx():
        return scanning.scan_appx()   # 扫描APPX包

至此/MobSF/views/home.py文件中的上传代码就分析完了,其他代码是其他功能,包括api_docs、关于说明、错误、最近扫描等一大堆功能,从其他URL可以进入分析,我们此处不做分析。

我们来看看上传之后是如何进行扫描的,跟进Scanning(request),来到/MobSF/views/scanning.py的Scanning类中。可以看到这里的扫描分了4类,分别是对APK包的扫描、对ZIP包的扫描、对IPA包的扫描、对APPX包的扫描。

我们来看看对APK包的扫描,先是通过文件名和文件类型(.apk)算出一个MD5值,这也是我们使用的时候看到的那个MD5值,以及首页查询框中要输入的MD5值。

代码如下:

md5 = handle_uploaded_file(self.file, '.apk')

随后,将扫描任务添加到最近的扫描列表,这也是我们在使用时可以从最近的扫描列表找到之前扫描过的任务,不用再重新做扫描。

我们看扫描APK的代码:

def scan_apk(self):
    """Android APK."""
    md5 = handle_uploaded_file(self.file, '.apk')
    url = 'StaticAnalyzer/?name={}&type=apk&checksum={}'.format(    # 注意,此处使到了URL
        self.file_name, md5)
    data = {
        'url': url,
        'status': 'success',
        'hash': md5,
        'scan_type': 'apk',
        'file_name': self.file_name,
    }

    add_to_recent_scan(self.file_name, md5, data['url'])

    logger.info('Performing Static Analysis of Android APK')
    return data

可以看到,URL参数中使用了StaticAnalyzer的URL,此时回到最开始的/MobSF/urls.py中,找到StaticAnalyzer跟入,至此,上传的文件正式进入到静态扫描。

URL:http://127.0.0.1:8000/StaticAnalyzer/?name=homesecurity.apk&type=apk&checksum=ef13eb870fa8538cd1bb450f7179dec5

请看截图中的URL哦!

在这里插入图片描述

四、静态扫描分析

先捋一下代码

在做完静态扫描的源码分析后,我觉得这一小节的内容应当加在最前面,于是我就将它写在了最前面。

静态分析的核心部分在/StaticAnalyzer/views/目录下,另外我们这次只分析Android的APK,因此所分析的代码集中在/StaticAnalyzer/views/android/目录下。

android/android_apis.py:常见的API规则库文件
android/android_manifest_desc.py:AndroidManifest规则库文件
android/android_rules.py:要检测的API列表文件
android/binary_analysis.py:二进制分析文件
android/cert_analysis.py:证书分析文件
android/code_analysis.py:代码分析文件
android/converter.py:反编译Java/smali代码文件
android/db_interaction.py:数据库交互文件
android/dvm_permissions.py:权限规则库文件
android/find.py:查找源代码文件
android/generate_downloads.py:生成下载文件
android/icon_analysis.py:图标分析文件
android/java.py:Java代码展示文件
android/manifest_analysis.py:AndroidManifest分析文件
android/manifest_view.py:AndroidManifest视图文件
android/playstore.py:应用商店分析文件
android/smali.py:Smali代码展示文件
android/static_analyzer.py:静态分析流程文件(主文件)
android/strings.py:常量字符串获取文件
android/view_source.py:文件源查看
android/win_fixes.py:windows环境下会使用
comparer.py:静态分析结果比较文件
shared_func.py:静态分析文件

我们接下来的分析中,我们会按照流程一步一步走完静态分析,出了非必要的,如规则库代码、windows环境使用代码外,其他代码都会涉及到。

捋完代码我们再继续

通过刚才的分析我们得知,我们先通过uploadURL上传了我们的APK文件,经过系统的一梭罗处理后,系统自动使用StaticAnalyzerURL开始对我们的APK文件进行静态分析。

我们回到最开始的定义URL的地方,/MobSF/urls.py文件中,找到静态分析的地方。

代码如下:

# Android
url(r'^StaticAnalyzer/$', android_sa.static_analyzer),
url(r'^ViewSource/$', view_source.run),
url(r'^Smali/$', smali.run),
url(r'^Java/$', java.run),
url(r'^Find/$', find.run),
url(r'^generate_downloads/$', generate_downloads.run),
url(r'^ManifestView/$', manifest_view.run),

跟入static_analyzer函数,即来到静态分析主流程文件:/StaticAnalyzer/views/android/static_analyzer.py文件中。

分析这里的代码非常伤,因为代码很长,又是缩进语法的Python,因此你要盯好缩进,不然就蒙圈了。

静态分析一上来,提取参数,包括包类型、hash值、文件名、rescan等。之后判断上传的文件是APK包还是ZIP包还是其他包,对不同的包做对应的静态分析。此处我们分析对APK包的静态分析。

如果这个APP是之前扫描过的,则直接从数据库拉取数据,如果是第一次扫描,则从零开始做扫描。

代码如下:

if db_entry.exists() and rescan == '0':
    context = get_context_from_db_entry(db_entry)
else:
    ......

这样的话,if下的代码我们就不跟进去看了,因为没啥可看的。只看else下的代码。

开始静态分析后,首先提取APK文件名和APK路径,之后解压APK包,如果APK包解压失败则报错。

代码如下:

app_dic['files'] = unzip(
    app_dic['app_path'], app_dic['app_dir'])
if not app_dic['files']:
    # Can't Analyze APK, bail out.
    msg = 'APK file is invalid or corrupt'
    if api:
        return print_n_send_error_response(
            request,
            msg,
            True)
    else:
        return print_n_send_error_response(
            request,
            msg,
            False)
app_dic['certz'] = get_hardcoded_cert_keystore(app_dic['files'])

在成功解压APK包之后,正式进入静态分析阶段。

4.1、AndroidManifest.xml安全分析

首先分析AndroidManifest.xml文件,代码如下:

app_dic['parsed_xml'] = get_manifest(
    app_dic['app_path'],
    app_dic['app_dir'],
    app_dic['tools_dir'],
    '',
    True,
)

我们跟进去,来到了/StaticAnalyzer/views/android/manifest_analysis.py文件中。

这个文件近900行代码,看得我快睡着了。其实并没啥高深的东西,首先解压APK.

代码如下:

manifest = None
if (len(settings.APKTOOL_BINARY) > 0 and is_file_exists(settings.APKTOOL_BINARY)):
    apktool_path = settings.APKTOOL_BINARY
else:
    apktool_path = os.path.join(tools_dir, 'apktool_2.4.1.jar')
output_dir = os.path.join(app_dir, 'apktool_out')
args = [settings.JAVA_BINARY,
        '-jar',
        apktool_path,
        '--match-original',
        '--frame-path',
        tempfile.gettempdir(),
        '-f', '-s', 'd',
        app_path,
        '-o',
        output_dir]
manifest = os.path.join(output_dir, 'AndroidManifest.xml')
if is_file_exists(manifest):
    # APKTool already created readable XML
    return manifest
logger.info('Converting AXML to XML')
subprocess.check_output(args)
return manifest

不用多说,一目了然,使用apktool2.4.1对APK进行解压,其使用的参数也是很明显的。

之后读取AndroidManifest.xml文件,这里分为从解压的后目录中读取,和从源码目录中读取(如果上传的是ZIP包的话)。

读取到AndroidManifest.xml文件后,开始解析该xml文件,提取该xml文件中的数据,包括application、uses-permission、manifest、activity、service、provider……等所有参数。

这里还穿插着对可浏览的Activity做了一个单独的读取分析,因为可浏览的Activity参数是比较特殊的。

代码如下:

if cat.getAttribute('android:name') == 'android.intent.category.BROWSABLE':
    datas = node.getElementsByTagName('data')
    for data in datas:
        ......

之后,根据参数的特性,对权限进行了分析判断,将权限的安全分级为:normaldangeroussignaturesignatureOrSystem

对其他配置也做了安全分析,如:android:allowBackupandroid:debuggable……等参数.

对四大组件的配置也做了安全分析,将配置的安全分级为:normaldangeroussignaturesignatureOrSystem

整个分析是基于android:exported = "true"android:exported != "false"的,注意这里是不等于flase,也就是说要么明确写明导出为true,要么没有声明。因为这两种方式对应的分析方法不同,所以这里是分开处理的。如果android:exported = "false"的话,那自然是安全的,就没啥可说的了。

在分析的过程中,还分了小于Android 4.2和大于等于Android 4.2版本的情况。


综述:整个/StaticAnalyzer/views/android/manifest_analysis.py代码是对AndroidManifest.xml做了一个全面的安全分析


4.2、继续前进

上小节我们从get_manifest跟入后,看到了系统对AndroidManifest.xml做了一个全面的安全分析,现在我们回来继续向后前进。

代码如下:

# 上小节我们是从这里跟入的
app_dic['parsed_xml'] = get_manifest(
    app_dic['app_path'],
    app_dic['app_dir'],
    app_dic['tools_dir'],
    '',
    True,
)

# 现在我们退回来,继续向后,跟入这里
app_dic['real_name'] = get_app_name(
    app_dic['app_path'],
    app_dic['app_dir'],
    app_dic['tools_dir'],
    True,
)

跟入get_app_name,我们发现这是一个获取APP名字的。

其分为2种,要么读取AndroidManifest.xml文件的<application>标签下的android:label属性值。要么读取res/values/strings.xml文件中的appname属性值。代码如下:

# 读取AndroidManifest.xml文件的`<application>`标签下的`android:label`属性值
if is_apk:
    a = apk.APK(app_path)
    real_name = a.get_app_name()
    return real_name

# 读取res/values/strings.xml文件中的`appname`属性值
else:
    strings_path = os.path.join(app_dir, 'app/src/main/res/values/strings.xml')
    eclipse_path = os.path.join(app_dir, 'res/values/strings.xml')
    if os.path.exists(strings_path):
        strings_file = strings_path
    elif os.path.exists(eclipse_path):
        strings_file = eclipse_path
if not os.path.exists(strings_file):
    logger.warning('Cannot find app name')
    return ''

with open(strings_file, 'r', encoding='utf-8') as f:
    data = f.read()

app_name_match = re.search(r'<string name=\"app_name\">(.*)</string>', data)

# 为空则返回空
if len(app_name_match.groups()) <= 0:
    return ''
return app_name_match.group(app_name_match.lastindex)

之后干了啥?没了,我们只能返回继续向后。

接下来,开始获取APP的图标(icon)。跟入到/StaticAnalyzer/views/android/icon_analysis.py中,这里面其实没啥可看的。

4.3、设置manifest连接

我们回到起点继续向下走,接下来是设置AndroidManifest.xml的连接。这里的量就比较大了,我在代码中写了备注,请阅读。

代码如下:

# 设置manifest连接
app_dic['mani'] = ('../ManifestView/?md5='
                   + app_dic['md5']
                   + '&type=apk&bin=1')
# manifest_data是对AndroidManifest.xml文件的处理,4.1节已介绍
man_data_dic = manifest_data(app_dic['parsed_xml'])

# get_app_details获取APP详细数据,稍后介绍
app_dic['playstore'] = get_app_details(
    man_data_dic['packagename'])

# manifest_analysis是对AndroidManifest.xml文件的处理,4.1节已介绍
man_an_dic = manifest_analysis(
    app_dic['parsed_xml'],
    man_data_dic)
bin_an_buff = []

# elf_analysis是二进制分析,稍后介绍
bin_an_buff += elf_analysis(app_dic['app_dir'])

# res_analysis是二进制分析,稍后介绍
bin_an_buff += res_analysis(app_dic['app_dir'])

# cert_info是对证书的分析,稍后介绍
cert_dic = cert_info(
    app_dic['app_dir'],
    app_dic['app_file'])

# apkid_analysis是对apkid的分析,稍后介绍
apkid_results = apkid_analysis(app_dic[
    'app_dir'], app_dic['app_path'], app_dic['app_name'])
    
# Trackers追踪检测,稍后介绍
tracker = Trackers.Trackers(
    app_dic['app_dir'], app_dic['tools_dir'])
tracker_res = tracker.get_trackers()

# apk_2_java反编译为Java代码,稍后介绍
apk_2_java(app_dic['app_path'], app_dic['app_dir'],
           app_dic['tools_dir'])

# dex_2_smali反编译为smali代码,稍后介绍
dex_2_smali(app_dic['app_dir'], app_dic['tools_dir'])

# code_analysis代码分析,稍后介绍
code_an_dic = code_analysis(
    app_dic['app_dir'],
    man_an_dic['permissons'],
    'apk')

好啦,看完我写的备注,应该已经一目了然了。接下来我们逐个跟入,看看到底是咋实现的!

1-跟入manifest_data/manifest_analysis

跟入后包括对AndroidManifest.xml的解析,这就又回到/StaticAnalyzer/views/android/manifest_analysis.py文件中了,该文件的功能在4.1、AndroidManifest.xml安全分析小节中介绍过,功能其实也没啥,就是对AndroidManifest.xml的处理,就不再介绍了。

2-跟入get_app_details

接下来是通过应用商店对APP的细节数据做一个读取,包括APP名字、评分、价格、下载URL……等待数据。跟入到/StaticAnalyzer/views/android/playstore.py文件中。

3-跟入elf_analysis/res_analysis

之后,进入二进制分析阶段,跟入到/StaticAnalyzer/views/android/binary_analysis.py文件中。

话说,博主本来想着好好写的,看了半天发现这个代码文件中竟然没啥可讲的。整个文件中的功能是对二进制文件做了分析处理。包括res、assets目录下的资源文件,lib下的.so文件等。

4-跟入cert_info

接下来是对证书做分析处理,跟入到/StaticAnalyzer/views/android/cert_analysis.py文件中。

这个文件代码一共有2个函数,因此,也只有2个功能。

get_hardcoded_cert_keystore该函数并不是我们跟进来的函数,不过既然在一个文件中,那就一并讲解下。该函数的功能是查找证书文件或密钥文件并返回。包括cer、pem、cert、crt、pub、key、pfx、p12等证书文件,以及jks、bks等密钥库文件。

cert_info该函数是我们跟进来的函数。该函数的功能是获取证书文件信息并对其进行分析。包括debug签名、SHA1哈希不安全的签名、正常签名等。其实也没啥好说的。

5-跟入apkid_analysis

接下来是apkid分析梳理,跟入到/MalwareAnalyzer/views/apkid.py文件中。

对APKID进行的分析处理,其中背后的核心库在/venv/lib/python3.7/site-packages/apkid目录下,由于这个是安装时会自动下载的,因此这种库的东西我们不做分析。

对apkid的分析处理并没有很复杂,在做了简单的判断之后,就开始分析出了。

代码如下:

# 从导入库可以看到端倪
from apkid.apkid import Scanner, Options
from apkid.output import OutputFormatter
from apkid.rules import RulesManager

logger.info('Running APKiD %s', apkid_ver)

# 跟进Options到site-packages/apkid/apkid.py中
options = Options(
    timeout=30,
    verbose=False,
    entry_max_scan_size=100 * 1024 * 1024,
    recursive=True,
)

# 跟进OutputFormatter到site-packages/apkid/output.py中
output = OutputFormatter到(
    json_output=True,
    output_dir=None,
    rules_manager=RulesManager(),
)

# 以下的函数跟进后也是在上两个代码文件中
rules = options.rules_manager.load()
scanner = Scanner(rules, options)
res = scanner.scan_file(apk_file)
try:
    findings = output._build_json_output(res)['files']
except AttributeError:
    # apkid >= 2.0.3
    findings = output.build_json_output(res)['files']
sanitized = {}
6-跟入Trackers

接下来是追踪检测。跟入到/MalwareAnalyzer/views/Trackers.py文件中。

那么将该文件中的所有函数功能一并讲解下:

_update_tracker_db函数的主要功能是更新跟踪检测数据库。

_compile_signatures函数的主要功能是编译与每个签名相关的正则表达式,以此加快跟踪器的检测速度。

load_trackers_signatures函数的主要功能是从官方数据库加载跟踪器签名。

get_embedded_classes函数的主要功能是从所有DEX文件中获取Java类的列表,这里使用的工具是baksmali。

detect_trackers_in_list函数的功能是根据上个函数提供的Java类列表,检测嵌入在其中的跟踪器,并返回嵌入的跟踪器列表。

detect_trackers函数的主要功能是检测嵌入的跟踪器,并返回嵌入的跟踪器列表。

get_trackers函数的主要功能是获取跟踪器。

7-跟入apk_2_java/dex_2_smali

看完跟踪器,我们回过头继续。

现在要跟入的是将APK反编译为Java代码的功能和将dex反编译为smali代码的功能。跟入到/StaticAnalyzer/views/android/converter.py文件中。

dex_2_smali函数是通过baksmali工具将dex反编译为smali代码。

其使用的参数如下:

for dex_path in dexes:
    logger.info('Converting %s to Smali Code',
                filename_from_path(dex_path))
    if (len(settings.BACKSMALI_BINARY) > 0
            and is_file_exists(settings.BACKSMALI_BINARY)):
        bs_path = settings.BACKSMALI_BINARY
    else:
        bs_path = os.path.join(tools_dir, 'baksmali-2.3.4.jar')
    output = os.path.join(app_dir, 'smali_source/')
    smali = [
        settings.JAVA_BINARY,
        '-jar',
        bs_path,
        'd',
        dex_path,
        '-o',
        output,
    ]

apk_2_java函数是通过jadx工具将APK反编译为Java代码。

相关代码比较长,因为需要将参数的源头也写入。

代码如下:

def apk_2_java(app_path, app_dir, tools_dir):
    """Run jadx."""
    try:
        logger.info('APK -> JAVA')
        args = []
        output = os.path.join(app_dir, 'java_source/')
        logger.info('Decompiling to Java with jadx')

        if os.path.exists(output):
            shutil.rmtree(output)

        if (len(settings.JADX_BINARY) > 0
                and is_file_exists(settings.JADX_BINARY)):
            jadx = settings.JADX_BINARY
        else:
            if platform.system() == 'Windows':
                jadx = os.path.join(tools_dir, 'jadx/bin/jadx.bat')
            else:
                jadx = os.path.join(tools_dir, 'jadx/bin/jadx')
                # Set write permission, if JADX is not executable
                if not os.access(jadx, os.X_OK):
                    os.chmod(jadx, stat.S_IEXEC)
            args = [
                jadx,
                '-ds',
                output,
                '-q',
                '-r',
                '--show-bad-code',
                app_path,
            ]
            fnull = open(os.devnull, 'w')
            subprocess.call(args,
                            stdout=fnull,
                            stderr=subprocess.STDOUT)
    except Exception:
        logger.exception('Decompiling to JAVA')
8-跟入code_analysis

回过头继续,接下来是代码分析,跟入到/StaticAnalyzer/views/android/code_analysis.py文件中。

核心代码如下:

# 源码情况下的代码分析
relative_java_path = jfile_path.replace(java_src, '')
code_rule_matcher(
    code_findings,
    list(perms.keys()),
    dat,
    relative_java_path,
    code_rules)
# 使用API情况下的代码分析
api_rule_matcher(api_findings, list(perms.keys()),
                 dat, relative_java_path, api_rules)
# 通过URL或邮件提取结果
urls, urls_nf, emails_nf = url_n_email_extract(
    dat, relative_java_path)
9-再次跟入到shared_func

以上代码跟入后,发现均来到了/StaticAnalyzer/views/shared_func.py文件中。那我们继续来看这个文件中的代码逻辑。

这个文件的代码主要是对APP做静态分析的,包括APK、IPA、APPX等,因为将三者的静态分析共同的部分放在一起,因此文件名叫共享功能(shared_func.py)。

其中,生成哈希(hash_gen函数)、解压(unzip函数)、报告处理(pdf函数)、API相关的(add_apis函数和api_rule_matcher函数)、URL和邮件地址提取(url_n_email_extract函数)我们就不看了,不是主要功能。

静态分析规则匹配(code_rule_matcher函数)主要是通过遍历规则来分析得到相应的结果,其分为两部分,规则类型为正则表达式的和规则类型为字符串的。

规则类型为正则表达式的又分为单个正则表达式、多个与关系的正则表达式、多个或关系的正则表达式、多个与关系的固定的正则表达式等,其通过匹配列表(get_list_match_items函数)和代码分析结果(add_findings函数)做规则匹配,最终产生结果。

规则类型为字符串的就比较复杂一点了,其分为单个字符串、多个与关系的字符串、多个或关系的字符串、多个与或关系的字符串、多个或与关系的字符串、多个与关系的固定的字符串、多个或与关系的固定的字符串等,通过匹配列表(get_list_match_items函数)和代码分析结果(add_findings函数)做规则匹配,最终产生结果。

代码如下:

# 规则类型为正则表达式的
if rule['type'] == 'regex':
    # 单个正则表达式的
    if rule['match'] == 'single_regex':
        if re.findall(rule['regex1'], tmp_data):
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个与关系的正则表达式的
    elif rule['match'] == 'regex_and':
        and_match_rgx = True
        match_list = get_list_match_items(rule)
        for match in match_list:
            if bool(re.findall(match, tmp_data)) is False:
                and_match_rgx = False
                break
        if and_match_rgx:
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个或关系的正则表达式的
    elif rule['match'] == 'regex_or':
        match_list = get_list_match_items(rule)
        for match in match_list:
            if re.findall(match, tmp_data):
                add_findings(findings, rule[
                             'desc'], file_path, rule)
                break
    
    # 多个与关系的固定的正则表达式的
    elif rule['match'] == 'regex_and_perm':
        if (rule['perm'] in perms
                and re.findall(rule['regex1'], tmp_data)):
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 其他情况的,报错
    else:
        logger.error('Code Regex Rule Match Error\n %s', rule)

# 规则类型为字符串的
elif rule['type'] == 'string':
    # 单个字符串的
    if rule['match'] == 'single_string':
        if rule['string1'] in tmp_data:
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个与关系的字符串的
    elif rule['match'] == 'string_and':
        and_match_str = True
        match_list = get_list_match_items(rule)
        for match in match_list:
            if (match in tmp_data) is False:
                and_match_str = False
                break
        if and_match_str:
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个或关系的字符串的
    elif rule['match'] == 'string_or':
        match_list = get_list_match_items(rule)
        for match in match_list:
            if match in tmp_data:
                add_findings(findings, rule[
                             'desc'], file_path, rule)
                break
    
    # 多个与或关系的字符串的
    elif rule['match'] == 'string_and_or':
        match_list = get_list_match_items(rule)
        string_or_stat = False
        for match in match_list:
            if match in tmp_data:
                string_or_stat = True
                break
        if string_or_stat and (rule['string1'] in tmp_data):
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个或与关系的字符串的
    elif rule['match'] == 'string_or_and':
        match_list = get_list_match_items(rule)
        string_and_stat = True
        for match in match_list:
            if match in tmp_data is False:
                string_and_stat = False
                break
        if string_and_stat or (rule['string1'] in tmp_data):
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个与关系的固定的字符串的
    elif rule['match'] == 'string_and_perm':
        if (rule['perm'] in perms
                and rule['string1'] in tmp_data):
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 多个或与关系的固定的字符串的
    elif rule['match'] == 'string_or_and_perm':
        match_list = get_list_match_items(rule)
        string_or_ps = False
        for match in match_list:
            if match in tmp_data:
                string_or_ps = True
                break
        if (rule['perm'] in perms) and string_or_ps:
            add_findings(findings, rule[
                         'desc'], file_path, rule)
    
    # 其他情况的,报错
    else:
        logger.error('Code String Rule Match Error\n%s', rule)
        
# 规则类型为其他的直接报错
else:
    logger.error('Code Rule Error\n%s', rule)

在这之后,做了从源码中提取URL地址和邮件地址的操作,没啥可讲的,通过正则遍历源码实现的。

接下来呢,是个两个APP比较的功能,注意哈希值一样的两个APP不能比较哦。

再之后,是一个记分功能,就是通过AVG CVSS记分的,高危(high)减去15分,警告(warning)减去10分,好(good)增加5分。很简单的功能实现。

再之后更新了最后扫描时间(update_scan_timestamp函数),检测打开的Firebase数据库(open_firebase函数),检测Firebase的URL(firebase_analysis函数)。

之后,没有之后了,这个就完结了!

4.4、获取字符串

不忘初心,我们回到/StaticAnalyzer/views/android/static_analyzer.py文件中。

接下来是获取APP的常量字符串。

代码如下:

string_res = strings_jar(
    app_dic['app_file'],
    app_dic['app_dir'])
if string_res:
    app_dic['strings'] = string_res['strings']
    code_an_dic['urls_list'].extend(
        string_res['urls_list'])
    code_an_dic['urls'].extend(string_res['url_nf'])
    code_an_dic['emails'].extend(string_res['emails_nf'])
else:
    app_dic['strings'] = []

我们跟入strings_jar,来到/StaticAnalyzer/views/android/strings.py文件中。它只有一个功能:从APP中提取常量字符串。

4.5、数据准备及入库

我们回到源头继续向后,接下来是数据入库前的检查以及数据存入数据库。

代码如下:

# Firebase数据库检查
code_an_dic['firebase'] = firebase_analysis(
    list(set(code_an_dic['urls_list'])))

# 域名提取和恶意软件检查
logger.info(
    'Performing Malware Check on extracted Domains')
code_an_dic['domains'] = malware_check(
    list(set(code_an_dic['urls_list'])))

# 复制APP图标
copy_icon(app_dic['md5'], app_dic['icon_path'])
app_dic['zipped'] = 'apk'

其中firebase_analysis函数(/StaticAnalyzer/views/shared_func.py)我们刚才看过了,copy_icon函数(/StaticAnalyzer/views/android/static_analyzer.py)没啥可看的。那我们就来看看malware_check函数吧。

跟入malware_check函数,我们来到/MalwareAnalyzer/views/domian_check.py文件中。

这个文件夹主要是分析处理恶意软件,对应静态分析报告中的Malware Analysis栏目,其包括Domain Malware Check子项目。
update_malware_db函数的功能是更新恶意软件数据库;
malware_check函数的功能是校验恶意软件;
verify_domain函数的功能是验证URL;
get_netloc函数的功能是获取单个URL,注意代码中是用的是domain;
get_domains函数的功能是获取多个URL,注意代码中使用的是domains。

好了,看完domian_check.py文件,我们回过头继续看static_analyzer.py文件。

接下来就是把数据往数据库里面放了,这里跟入了get_context_from_analysis函数,来到了/StaticAnalyzer/views/android/db_interaction.py文件中。不过这个文件中的代码没啥可分析的。

之后又跟入了VirusTotal函数,来到了/MalwareAnalyzer/views/VirusTotal.py文件中。这是一个统计安全问题总数的地方,也没啥可说的。

代码如下:

if settings.VT_ENABLED:
    vt = VirusTotal.VirusTotal()     # 从此处跟入
    context['virus_total'] = vt.get_result(
        os.path.join(app_dic['app_dir'],
                     app_dic['md5']) + '.apk',
        app_dic['md5'])

至此,上传APK包的静态分析就结束了,接下来是上传ZIP源码包的静态分析,我们就不看了。

静态分析总结

如果你认真阅读完本篇分析文章,再对应看静态分析报告,你会发现,所有的功能我们都分析到了,现在我们也知道这些强大的功能背后是如何实现的了。

静态扫描的源码分析就结束了!

五、动态扫描分析

先捋一下代码

和静态分析篇一样,我将它写在了最前面。

动态分析的核心部分在/DynamicAnalyzer/views/目录下,另外我们这次只分析Android的APK,因此所分析的代码集中在/DynamicAnalyzer/views/android/目录下。

tools/webproxy.py:设置代理,httptools相关
views/android/analysis.py:对动态分析得到的数据进行分析处理
views/android/dynamic_analyzer.py:动态分析流程文件(主文件)
views/android/environment.py:动态分析环境配置相关
views/android/frida_core.py:Frida框架核心操作部分
views/android/frida_scripts.py:Frida框架脚本
views/android/operations.py:动态分析操作
views/android/report.py:动态分析报告输出
views/android/tests_common.py:命令测试
views/android/tests_frida.py:Frida框架测试
views/android/tests_xposed.py:Xposed框架测试

我们接下来的分析中,我们会按照流程一步一步走完动态分析,出了非必要的,其他代码都会涉及到。

捋完代码我们再继续

再经过漫长的静态源码分析后,我们现在开始进行动态扫描源码分析。通过浏览/DynamicAnalyzer文件下的代码,我们发现动态扫描其实只有Android才有。

截止博主分析日期,最新的3.0beta已经不再支持物理设备,仅支持虚拟设备(主要是genymotion)。对于小于Android 5.0的系统版本,会使用Xposed框架,对于大于等于Android 5.0的系统版本,会使用Frida框架。

Android 5.0以下的系统属于骨灰级,都2020年了,这些老系统连学习的价值都没有。因此,下文分析中所有遇到Xposed的都将跳过不做分析。

我们回到最开始的定义URL的地方,/MobSF/urls.py文件中,找到动态分析的地方。

代码如下:

url(r'^dynamic_analysis/$',
    dz.dynamic_analysis,
    name='dynamic'),
url(r'^android_dynamic/$',
    dz.dynamic_analyzer,
    name='dynamic_analyzer'),
url(r'^httptools$',
    dz.httptools_start,
    name='httptools'),
url(r'^logcat/$', dz.logcat),

以上任意函数跟入,我们来到/DynamicAnalyzer/views/android/dynamic_analyzer.py文件中。

我们先不管它跟进来是到哪个函数,我们从上到下逐次分析。

5.1、dynamic_analysis函数

dynamic_analysis函数是动态分析的入口点

这里首先会检测模拟器,如果模拟器正常运行起来并被检测到,则获取设备数据,跟进到/MobSF/utils.py文件的get_device函数,之后设置代理IP,跟进到/MobSF/utils.py文件的get_proxy_ip函数,其功能是获取网络IP并根据它设置代理IP。

如果模拟器未启动或没有被检测到,则报错,跟进到/MobSF/utils.py文件的print_n_send_error_response函数。

5.2、dynamic_analyzer函数

dynamic_analyzer函数主要功能是配置/创建动态分析环境

在获取到设备信息后:

......
identifier = get_device()
......
env = Environment(identifier)
......

我们跟入Environment函数,来到环境配置,在/DynamicAnalyzer/views/android/environment.py文件中。

Environment.py文件分析

我们还是先不管跟入的是哪个函数,从上到下讲解下各个函数功能。

connect_n_mount函数的功能是重启adb服务,之后尝试adb连接设备。

adb_command函数的功能是adb命令包装,所有将要执行的命令都会经过包装后成为可以执行的命令,然后执行。

dz_cleanup函数的功能是清除之前的动态分析记录和数据,以便于新的动态分析不受影响。

configure_proxy函数的主要功能是设置代理。具体步骤是先调用Httptools杀死请求,再在代理模式下开启Httptools。

代码如下:

def configure_proxy(self, project):
    proxy_port = settings.PROXY_PORT
    logger.info('Starting HTTPs Proxy on %s', proxy_port)
    stop_httptools(proxy_port)    # 调用Httptools杀死请求
    start_proxy(proxy_port, project)    # 在代理模式下开启Httptools

这两个函数均跟入到/DynamicAnalyzer/tools/webproxy.py文件中。这个文件中的代码很简单,就不再分析了。

install_mobsf_ca函数的主要功能是安装或删除MobSF的跟证书(ROOT CA)。

set_global_proxy函数的主要功能是给设备设置全局代理,这个功能仅支持Android 4.4及以上系统,设置代理IP的功能会跟入到/MobSF/utils.py的get_proxy_ip函数中。对于小于Android 4.4的系统版本,会将代理设置为:127.0.0.1:1337

unset_global_proxy函数的主要功能是取消设置的全局代理。

enable_adb_reverse_tcp函数的主要功能是开启adb反向TCP代理,该功能仅支持Android 5.0以上的系统。

start_clipmon函数的主要功能是开始剪切板监控。

get_screen_res函数的主要功能是获取当前设备的屏幕分辨率。

screen_shot函数的主要功能是截屏,并保存为/data/local/screen.png。

screen_stream函数的主要功能是分析屏幕流。

android_component函数的主要功能是获取APK的组件,包括Activity、Receiver、Provider、Service、Library等。

get_android_version函数的主要功能是获取Android版本。

get_android_arch函数的主要功能是获取Android体系结构。

launch_n_capture函数的主要功能是启动和捕获Activity,是通过截屏实现的。

is_mobsfyied函数的主要功能是获取Android的MobSfyed实例,读取Xposed或Frida文件并输出。

代码如下:

if android_version < 5:
    agent_file = '.mobsf-x'
    agent_str = b'MobSF-Xposed'
else:
    agent_file = '.mobsf-f'
    agent_str = b'MobSF-Frida'
try:
    out = subprocess.check_output(
        [get_adb(),
         '-s', self.identifier,
         'shell',
         'cat',
         '/system/' + agent_file])
    if agent_str not in out:
        return False
except Exception:
    return False
return True

mobsfy_init函数的主要功能是设置MobSF代理,安装Xposed或Frida框架。

代码如下:

# 系统版本小于5.0,安装Xposed框架
if version < 5:
    self.xposed_setup(version)
    self.mobsf_agents_setup('xposed')

# 系统版本大于等于5.0,安装Frida框架
else:
    self.frida_setup()
    self.mobsf_agents_setup('frida')
logger.info('MobSFying Completed!')
return version

mobsf_agents_setup函数的主要功能是安装MobSF根证书,设置MobSF代理。

xposed_setup函数的主要功能是安装Xposed框架。

frida_setup函数的主要功能是安装Frida框架。

run_frida_server函数的主要功能是运行Frida框架。

至此,/DynamicAnalyzer/views/android/environment.py文件的代码我们就过了一遍了,整个文件主要是做动态分析的环境准备工作,代码逻辑非常简单,特别容易理解。

回过头继续

刚才分析了environment.py文件,我们是按照代码从上到下的顺序分析的,并不是按运行逻辑顺序分析的。现在我们按逻辑运行顺序继续向下看一下。

看我写的代码备注即可。

# 动态分析环境准备
env = Environment(identifier)

# 如果测试ADB连接失败
if not env.connect_n_mount():
    msg = 'Cannot Connect to ' + identifier
    return print_n_send_error_response(request, msg)

# 获取Android版本
version = env.get_android_version()
logger.info('Android Version identified as %s', version)
xposed_first_run = False

# 根据系统版本获取Android的MobSfyed实例,如果失败
if not env.is_mobsfyied(version):
    msg = ('This Android instance is not MobSfyed.\n'
           'MobSFying the android runtime environment')
    logger.warning(msg)
    # 设置MobSF代理,如果失败
    if not env.mobsfy_init():
        return print_n_send_error_response(
            request,
            'Failed to MobSFy the instance')
    if version < 5:
        xposed_first_run = True

# 第一次运行Xposed框架,会重启设备以启用所有模块
if xposed_first_run:
    msg = ('Have you MobSFyed the instance before'
           ' attempting Dynamic Analysis?'
           ' Install Framework for Xposed.'
           ' Restart the device and enable'
           ' all Xposed modules. And finally'
           ' restart the device once again.')
    return print_n_send_error_response(request, msg)

# 清除之前的动态分析记录和数据
env.dz_cleanup(bin_hash)

# 设置web代理
env.configure_proxy(package)

# 开启adb反向TCP代理,仅支持5.0以上系统
env.enable_adb_reverse_tcp(version)

# 给设备设置全局代理,这个功能仅支持Android 4.4及以上系统
env.set_global_proxy(version)

# 开始剪切板监控
env.start_clipmon()

# 获取当前设备的屏幕分辨率
screen_width, screen_height = env.get_screen_res()
logger.info('Installing APK')

# APP目录
app_dir = os.path.join(settings.UPLD_DIR, bin_hash + '/')

# APP路径
apk_path = app_dir + bin_hash + '.apk'

# adb命令包装并执行
env.adb_command(['install', '-r', apk_path], False, True)

logger.info('Testing Environment is Ready!')
context = {'screen_witdth': screen_width,
           'screen_height': screen_height,
           'package': package,
           'md5': bin_hash,
           'android_version': version,
           'version': settings.MOBSF_VER,
           'title': 'Dynamic Analyzer'}
template = 'dynamic_analysis/android/dynamic_analyzer.html'

# 通过HttpResponse返回数据
return render(request, template, context)

5.3、httptools_start函数

httptools_start函数的主要功能是在代理模式下开启Httptools。

这里是先调用Httptools杀死请求,再在代理模式下开启Httptools。

代码如下:

stop_httptools(settings.PROXY_PORT)
start_httptools_ui(settings.PROXY_PORT)
time.sleep(3)
logger.info('httptools UI started')

webproxy.py文件分析

我们跟入到/DynamicAnalyzer/tools/webproxy.py文件中,这个文件中的代码很简单。

stop_httptools函数的主要功能是杀死httptools,分为两步,第一步是通过调用httptools UI杀死请求,第二步是通过调用httptools代理杀死请求。

start_proxy函数的主要功能是在代理模式下开启Httptools。

start_httptools_ui函数的功能是启动httptools的UI。

create_ca函数的功能是第一次运行时创建CA

get_ca_dir函数的功能时获取CA目录

5.4、logcat函数

logcat函数主要是启动logcat流,获取日志的。

这个函数没啥可分析的,就不做分析了。

来看看operations.py文件

这个文件的主要功能是动态分析操作,我们从上到下看一下。

json_response函数的主要功能是返回JSON响应

is_attack_pattern函数的主要功能是通过正则表达式验证攻击

strict_package_check函数的主要功能是通过正则表达式校验包名称

is_path_traversal函数的主要功能是检查路径遍历

is_md5函数的主要功能是通过正则表达式检查是否是有效的MD5

invalid_params函数的主要功能是检查无效参数响应

mobsfy函数的主要功能是通过POST方法配置实例以进行动态分析

execute_adb函数的主要功能是通过POST方法执行ADB命令

get_component函数的主要功能是通过POST方法获取Android组件

take_screenshot函数的主要功能是通过POST方法截屏

screen_cast函数的主要功能是通过POST方法投屏

touch函数的主要功能是通过POST方法发送触摸事件

mobsf_ca函数的主要功能是通过POST方法安装或删除MobSF代理的ROOT CA

再看看analysis.py文件

该文件的主要功能是对动态分析获取的数据进行分析,我们也是从上到下看一下。

run_analysis函数的主要功能是运行动态文件分析。

首先收集了日志数据并对日志进行遍历筛选处理,代码如下:

# 收集日志
datas = get_log_data(apk_dir, package)
clip_tag = 'I/CLIPDUMP-INFO-LOG'
clip_tag2 = 'I CLIPDUMP-INFO-LOG'
# 遍历日志数据,对日志数据进行处理
for log_line in datas['logcat']:
    if clip_tag in log_line:
        clipboard.append(log_line.replace(clip_tag, 'Process ID '))
    if clip_tag2 in log_line:
        log_line = log_line.split(clip_tag2)[1]
        clipboard.append(log_line)

通过正则表达式收集的URL数据,代码如下:

url_pattern = re.compile(
    r'((?:https?://|s?ftps?://|file://|'
    r'javascript:|data:|www\d{0,3}'
    r'[.])[\w().=/;,#:@?&~*+!$%\'{}-]+)', re.UNICODE)
urls = re.findall(url_pattern, datas['traffic'].lower())
if urls:
    urls = list(set(urls))
else:
    urls = []

然后对恶意URL进行检查,通过匹配这些URL是否出现在恶意软件列表里实现,代码如下:

domains = malware_check(urls)

跟入到/MalwareAnalyzer/views/domian_check.py文件的malware_check函数。domian_check.py文件在本篇的4.5、数据准备及入库章节有讲解,此处就不再讲解了。

之后通过正则提取了所有的电子邮件地址,代码如下:

emails = []
regex = re.compile(r'[\w.-]+@[\w-]+\.[\w]{2,}')
for email in regex.findall(datas['traffic'].lower()):
    if (email not in emails) and (not email.startswith('//')):
        emails.append(email)

然后做了结果汇总,代码如下:

all_files = get_app_files(apk_dir, md5_hash, package)
analysis_result['urls'] = urls
analysis_result['domains'] = domains
analysis_result['emails'] = emails
analysis_result['clipboard'] = clipboard
analysis_result['xml'] = all_files['xml']
analysis_result['sqlite'] = all_files['sqlite']
analysis_result['other_files'] = all_files['others']

最后返回分析结果。

get_screenshots函数的主要功能是获截图。

get_log_data函数的主要功能是对日志数据进行分析。

通过执行adb或其他可执行文件进行处理,得到web数据、日志数据、域名数据、API数据、Frida数据,并返回。

get_app_files函数的主要功能是从设备获取APP文件。

包括提取设备数据,对设备中的数据做静态分析等。

generate_download函数的主要功能是生成文件下载。

生成文件下载后,会删除现有数据,然后复制新数据。

至此,Android动态分析源代码分析就结束了

动态分析总结

没啥可总结的,该分析的都分析了!

六、总结

本文没有任何参考文献,因为网上所有的源码分析文章都已经过时很久很久了……

历时一星期,博主要吐血了!

©️2021 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值