一、項目說明
分析日期: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<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環境使用代碼外,其他代碼都會涉及到。
捋完代碼我們再繼續
通過剛纔的分析我們得知,我們先通過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包之後,正式進入靜態分析階段。
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:
......
之後,根據參數的特性,對權限進行了分析判斷,將權限的安全分級爲: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做了一個全面的安全分析
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動態分析源代碼分析就結束了
動態分析總結
沒啥可總結的,該分析的都分析了!
六、總結
本文沒有任何參考文獻,因爲網上所有的源碼分析文章都已經過時很久很久了……
歷時一星期,博主要吐血了!