分析日期:2020-01-17
源码地址:https://github.com/MobSF/Mobile-Security-Framework-MobSF
关于如何搭建源码分析环境,请阅读我的另一篇博客:MobSF移动安全框架实践–基于3.0beta版
移动安全框架(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[0-9a-f]{32})/(?P[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环境使用代码外,其他代码都会涉及到。
通过刚才的分析我们得知,我们先通过upload
URL上传了我们的APK文件,经过系统的一梭罗处理后,系统自动使用StaticAnalyzer
URL开始对我们的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包之后,正式进入静态分析阶段。
首先分析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:
......
之后,根据参数的特性,对权限进行了分析判断,将权限的安全分级为:normal
、dangerous
、signature
、signatureOrSystem
。
对其他配置也做了安全分析,如:android:allowBackup
、android:debuggable
……等参数.
对四大组件的配置也做了安全分析,将配置的安全分级为:normal
、dangerous
、signature
、signatureOrSystem
。
整个分析是基于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做了一个全面的安全分析
上小节我们从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文件的
标签下的android:label
属性值。要么读取res/values/strings.xml文件中的appname
属性值。代码如下:
# 读取AndroidManifest.xml文件的``标签下的`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'(.*) ', 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中,这里面其实没啥可看的。
我们回到起点继续向下走,接下来是设置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')
好啦,看完我写的备注,应该已经一目了然了。接下来我们逐个跟入,看看到底是咋实现的!
跟入后包括对AndroidManifest.xml的解析,这就又回到/StaticAnalyzer/views/android/manifest_analysis.py文件中了,该文件的功能在4.1、AndroidManifest.xml安全分析
小节中介绍过,功能其实也没啥,就是对AndroidManifest.xml的处理,就不再介绍了。
接下来是通过应用商店对APP的细节数据做一个读取,包括APP名字、评分、价格、下载URL……等待数据。跟入到/StaticAnalyzer/views/android/playstore.py文件中。
之后,进入二进制分析阶段,跟入到/StaticAnalyzer/views/android/binary_analysis.py文件中。
话说,博主本来想着好好写的,看了半天发现这个代码文件中竟然没啥可讲的。整个文件中的功能是对二进制文件做了分析处理。包括res、assets目录下的资源文件,lib下的.so文件等。
接下来是对证书做分析处理,跟入到/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哈希不安全的签名、正常签名等。其实也没啥好说的。
接下来是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 = {}
接下来是追踪检测。跟入到/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
函数的主要功能是获取跟踪器。
看完跟踪器,我们回过头继续。
现在要跟入的是将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')
回过头继续,接下来是代码分析,跟入到/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)
以上代码跟入后,发现均来到了/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
函数)。
之后,没有之后了,这个就完结了!
不忘初心,我们回到/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中提取常量字符串。
我们回到源头继续向后,接下来是数据入库前的检查以及数据存入数据库。
代码如下:
# 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文件中。
我们先不管它跟进来是到哪个函数,我们从上到下逐次分析。
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
函数。
dynamic_analyzer
函数dynamic_analyzer
函数主要功能是配置/创建动态分析环境
在获取到设备信息后:
......
identifier = get_device()
......
env = Environment(identifier)
......
我们跟入Environment
函数,来到环境配置,在/DynamicAnalyzer/views/android/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)
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')
我们跟入到/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目录
logcat
函数logcat
函数主要是启动logcat流,获取日志的。
这个函数没啥可分析的,就不做分析了。
这个文件的主要功能是动态分析操作,我们从上到下看一下。
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
该文件的主要功能是对动态分析获取的数据进行分析,我们也是从上到下看一下。
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动态分析源代码分析就结束了
没啥可总结的,该分析的都分析了!
本文没有任何参考文献,因为网上所有的源码分析文章都已经过时很久很久了……
历时一星期,博主要吐血了!