安卓恶意代码分析工具详解(一)——MobSF

  以学习的目的,来看看前人在安卓恶意代码自动化方面都做过些什么,都做的怎么样了,有哪些好的思想和方法是值得借鉴的。毕竟,站在巨人的肩膀上,能看的更远一点。本文将主要介绍各分析工具在对APK进行静态、动态分析时,使用到的技术进行分析。首先要介绍的一款安卓恶意代码分析工具是——MobSF。

MobSF

  MobSF,全称(Mobile-Security-Framework),是一款优秀的开源移动应用自动分析平台。该平台可对安卓、苹果应用程序进行恶意代码自动分析,并在web端输出报告。该平台同时包含静态分析和动态分析功能,静态分析适用于安卓、苹果应用程序,而动态分析暂时只支持安卓应用程序。(PS:其web界面相当美观,而且支持分析结果存入数据库,方便检索)

静态分析实现原理

  MobSF实现静态分析的代码位于StaticAnalyzer目录下,其目录中文件如下:

Alt text
  静态分析同时支持对APK、IPA两种文件格式的支持,本文主要分析其处理APK文件的流程及技术。其主要处理流程在android.py中。先定位到StaticAnalyzer函数中,忽视所有web处理方面的内容,得到其关键代码以及流程如下:

FILES=Unzip(APP_PATH,APP_DIR)         #解压到APP_DIR 返回apk中文件列表
……
CERTZ = GetHardcodedCertKeystore(FILES) #返回是否存在证书信息
……
PARSEDXML= GetManifest(APP_DIR,TOOLS_DIR,'',True) #Manifest XML 访问AndroidManifest.xml判断其是否为2进制文件,若是,使用AXMLPrinter2.jar反编译

SERVICES,ACTIVITIES,RECEIVERS,PROVIDERS,LIBRARIES,PERM,PACKAGENAME,MAINACTIVITY,MIN_SDK,MAX_SDK,TARGET_SDK,ANDROVER,ANDROVERNAME=ManifestData(PARSEDXML,APP_DIR) #从AndroidManifest.xml中提取SERVICES、ACTIVITIES、RECEIVERS等信息

MANIFEST_ANAL,EXPORTED_ACT,EXPORTED_CNT=ManifestAnalysis(PARSEDXML,MAINACTIVITY) #分析AndroidManifest.xml中权限、active等

PERMISSIONS=FormatPermissions(PERM) #在web前端格式化输出权限列表
……
CERT_INFO,ISSUED=CertInfo(APP_DIR,TOOLS_DIR) #APK中证书信息 CertPrint.jar解析
Dex2Jar(APP_PATH,APP_DIR,TOOLS_DIR) #dex转jar d2j2、enjarify文件夹下
Dex2Smali(APP_DIR,TOOLS_DIR) #dex转smali baksmali.jar
Jar2Java(APP_DIR,TOOLS_DIR) #jar转java jd-core.jar cfr_0_115.jar procyon-decompiler-0.5.30.jar

API,DANG,URLS,DOMAINS,EMAILS,CRYPTO,OBFUS,REFLECT,DYNAMIC,NATIVE=CodeAnalysis(APP_DIR,MD5,PERMISSIONS,"apk") #代码分析
……
STRINGS=Strings(APP_FILE,APP_DIR,TOOLS_DIR) #收集apk中字符串信息 strings_from_apk.jar

  分析上述主代码流程可知,MobSF中主要进行恶意代码分析的函数有:ManifestAnalysis、CodeAnalysisStrings。对apk进行处理的流程如下:

a. 解压apk
b. 获取文本模式AndroidManifest.xml
c. 自动分析AndroidManifest.xml中信息
d. dex转jar
e. dex转smali
f. jar转java
g. 自动分析反编译得到的java代码

  其中ManifestAnalysis函数主要功能大致如下:

    def ManifestAnalysis(mfxml,mainact):
#获取信息
manifest = mfxml.getElementsByTagName("manifest")
……
permissions = mfxml.getElementsByTagName("permission")
for node in manifest:
package = node.getAttribute("package")
PERMISSION_DICT = dict()

##解析所有权限PERMISSION 对权限进行分级,包含正常、危险、签名、系统四类。
for permission in permissions:
if permission.getAttribute("android:protectionLevel"):
protectionlevel = permission.getAttribute("android:protectionLevel")
if protectionlevel == "0x00000000":
protectionlevel = "normal"
……
elif permission.getAttribute("android:name"):
PERMISSION_DICT[permission.getAttribute("android:name")] = "normal"


##解析所有APPLICATIONS RET为返回到页面的信息 节省排版,将重复选项省去
for application in applications:
if application.getAttribute("android:debuggable") == "true":
……
for node in application.childNodes:
……
#Exported Check
if ('NIL' != itmname):
if (node.getAttribute("android:exported") == 'true'):
……

##GRANT-URI-PERMISSIONS
for granturi in granturipermissions:
if granturi.getAttribute("android:pathPrefix") == '/':
……

##DATA
for data in datas:
if data.getAttribute("android:scheme") == "android_secret_code":
……

##INTENTS
for intent in intents:
if intent.getAttribute("android:priority").isdigit():
……
##ACTIONS
for action in actions:
if action.getAttribute("android:priority").isdigit():
……

  从ManifestAnalysis函数主要的作用是对AndroidManifest.xml进行解析,提取其中permission、granturipermissions、application、activties、services、intents、actions等,将分析结果直接统计并返回到前端页面进行展示。
  CodeAnalysis功能大致如下:

def CodeAnalysis(APP_DIR,MD5,PERMS,TYP):
try:
……
#遍历 只取文件夹中java源码文件
for dirName, subDir, files in os.walk(JS):
for jfile in files:
……
if jfile.endswith('.java') and (any(cls in repath for cls in settings.SKIP_CLASSES) == False):
dat=''
with io.open(jfile_path,mode='r',encoding="utf8",errors="ignore") as f:
dat=f.read()
……
#Code Analysis 开始分析
if (re.findall('MODE_WORLD_READABLE|Context\.MODE_WORLD_READABLE',dat) or re.findall('openFileOutput\(\s*".+"\s*,\s*1\s*\)',dat)):
c['d_con_world_readable'].append(jfile_path.replace(JS,''))
……

#=========================Android API Analysis =========================
……
if (('android.util.Base64') in dat and ('.decode') in dat):
c['bdecode'].append(jfile_path.replace(JS,''))
……

#URLs My Custom regex
p = re.compile(ur'((?:https?://|s?ftps?://|file://|javascript:|data:|www\d{0,3}[.])[\w().=/;,#:@?&~*+!$%\'{}-]+)', re.UNICODE)
urllist=re.findall(p, dat.lower())
……
#Email Etraction Regex
regex = re.compile("[\w.-]+@[\w-]+\.[\w.]+")
……

  其源码分析部分主要利用正则表达式对java源码进行匹配来实现的。主要通过匹配常见方法中的关键词来提取源码中用到的方法,通过匹配敏感关键词来提取账号密码等信息,通过匹配常见API字符串来判定是否有调用这些API,通过匹配URL、Email的格式来提取源码中的URL和邮箱信息。
  匹配得到结果后,整合结果,输出到前端展示出来。至此,动态分析结果全部输出。前端展示如图所示:

Alt text

动态分析实现原理

  MobSF同时还支持对安卓程序进行动态分析,主要是利用安卓虚拟机,安装例如xposed等框架,而后安装并运行需分析的样本。得到样本输出的日志,分析日志并在前端展示出来。
  其动态分析主要目录结构如下图:

Alt text
  DynamicAnalyzer/views目录下的android.py则正是需要重点分析的,根据其前端展示页面可知其主要功能有:

Alt text

  1. Environment Created
  2. Start / Stop Screen
  3. Install / Remove MobSF RootCA
  4. Start Exported Activity Tester
  5. Start Activity Tester
  6. Take a Screenshot
  7. Finish

  接下来对源码中针对各功能的实现过程进行分析。

Environment Created

  这部分,主要是做一些环境的检测,以及设置web代理、设置adb连接、样本安装运行工作,主要代码如下:

def GetEnv(request):
print "\n[INFO] Setting up Dynamic Analysis Environment"
try: #处理web前端传过来的参数
if request.method == 'POST':
data = {}
MD5=request.POST['md5']
PKG=request.POST['pkg']
LNCH=request.POST['lng']
#防止RCE
if re.findall(";|\$\(|\|\||&&",PKG) or re.findall(";|\$\(|\|\||&&",LNCH):
print "[ATTACK] Possible RCE"
return HttpResponseRedirect('/error/')
m=re.match('^[0-9a-f]{32}$',MD5)
if m: #如果存在MD5(存在样本文件)
DIR=settings.BASE_DIR
APP_DIR=os.path.join(settings.UPLD_DIR, MD5+'/') #APP DIRECTORY
APP_FILE=MD5 + '.apk' #NEW FILENAME
APP_PATH=APP_DIR+APP_FILE #APP PATH
TOOLS_DIR=os.path.join(DIR, 'DynamicAnalyzer/tools/') #TOOLS DIR
DWD_DIR=settings.DWD_DIR
PROXY_IP=settings.PROXY_IP #Proxy IP
PORT=str(settings.PORT) #Proxy Port
WebProxy(APP_DIR,PROXY_IP,PORT) #设置web代理
Connect(TOOLS_DIR) #建立连接 adb connect ip:port 并且重新挂载/system文件夹 若是真实机器则 adb -s ip:port shell su -c mount -o rw,remount,rw /system 虚拟机则再加上一条 adb -s ip:port shell mount -o rw,remount -t rfs /dev/block/sda6 /system
InstallRun(TOOLS_DIR, APP_PATH,PKG,LNCH,True) #Change True to support non-activity components 直接安装apk并启动程序 adb -s ip:port install -r xxx.apk adb -s ip:port shell am start -m xxx
SCREEN_WIDTH, SCREEN_HEIGHT = GetRes() #获取屏幕大小 adb -s ip:port shell dumpsys windows 再找到包含mUnrestrictedScreen的那一行后接的分辨率
……

  所以,整个流程大致为:

  1. 利用pyWebProxy中提供的功能设置web代理。用来抓取APP访问流量。
  2. 建立adb连接 adb connect ip:port
  3. 安装运行程序 adb install -r xxx.apk adb shell am start -m xxx
  4. 获取屏幕大小 adb shell dumpsys windows | grep mUnrestrictedScreen
Start / Stop Screen  

  MobSF中提供实时操作功能,其实现主要利用屏幕录制软件screencast提供的服务,其实现代码如下:

#AJAX
def ScreenCast(request):
print "\n[INFO] Invoking ScreenCast Service in VM/Device"
try:
global tcp_server_mode
data = {}
if (request.method == 'POST'):
mode=request.POST['mode']
TOOLSDIR=os.path.join(settings.BASE_DIR, 'DynamicAnalyzer/tools/') #TOOLS DIR
adb=getADB(TOOLSDIR) #得到adb所在目录
IP = settings.SCREEN_IP
PORT = str(settings.SCREEN_PORT)
if mode == "on":
args=[adb,"-s", getIdentifier(),"shell","am","startservice","-a",IP+":"+PORT, "opensecurity.screencast/.StartScreenCast"] #开启ScreenCast服务 实时截屏,并放到特定文件夹
data = {'status': 'on'}
tcp_server_mode = "on"
elif mode == "off":
args=[adb, "-s", getIdentifier(), "shell", "am", "force-stop", "opensecurity.screencast"] #关闭ScreenCast服务
data = {'status': 'off'}
tcp_server_mode = "off"
if (mode in ["on", "off"]):
try:
subprocess.call(args)
t = threading.Thread(target=ScreenCastService)
t.setDaemon(True)
t.start()
……

  在其中开启服务后,另起了一个线程ScreenCastService来对screencast服务进行处理:

def ScreenCastService():
global tcp_server_mode
print "\n[INFO] ScreenCast Service Status: " + tcp_server_mode
try:
SCREEN_DIR=settings.SCREEN_DIR
if not os.path.exists(SCREEN_DIR):
os.makedirs(SCREEN_DIR)

s = socket.socket()
if tcp_server_mode == "on":
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ADDR = (settings.SCREEN_IP,settings.SCREEN_PORT)
s.bind(ADDR)
s.listen(10)
while (tcp_server_mode == "on"):
ss, address = s.accept()
print "Got Connection from: ", address[0]
if settings.REAL_DEVICE:
IP = settings.DEVICE_IP
else:
IP = settings.VM_IP
if address[0] == IP:
with open(SCREEN_DIR+'screen.png','wb') as f:
while True:
data = ss.recv(1024)
if not data:
break
f.write(data)
……

  ScreenCastService不停地访问SCREEN_DIR,读取该文件夹下的图片文件,并且将图片数据输出到web端进行显示。从而完成远程实时显示的功能。同时MobSF还提供远程操作的功能,具体实现代码在Touch函数中,具体实现如下:

def Touch(request):
print "\n[INFO] Sending Touch Events"
try:
data = {}
#单击屏幕中实时屏幕显示区域后,会触发POST操作,回传坐标
if (request.method == 'POST') and (is_number(request.POST['x'])) and (is_number(request.POST['y'])):
x_axis=request.POST['x']
y_axis=request.POST['y']
TOOLSDIR=os.path.join(settings.BASE_DIR, 'DynamicAnalyzer/tools/') #TOOLS DIR
adb=getADB(TOOLSDIR) #获取adb目录
#利用 adb -s IP:PORT shell input tap x,y 来实现点击操作
args=[adb, "-s", getIdentifier(), "shell","input","tap",x_axis,y_axis]
data = {'status': 'success'}
try:
subprocess.call(args)
……

  由源码可知其主要实现过程是,获取屏幕点击时的坐标,通过adb shell input tap来完成点击操作。

Install / Remove MobSF RootCA

  安装、卸载RootCA,主要是为了方便对样本中HTTPS流量进行截获。主要实现过程如下:

  1. adb push ca.crt /data/local/tmp/xxx
  2. adb shell su -c cp /data/local/tmp/xxx /system/etc/security/cacerts/
Start /Stop Exported Activity Tester

  这部分主要是想尽量多的触发样本中所有行为,MobSF的做法是:遍历AndroidManifest.xml中的所有Exported Activity,并利用am start来依次启动,以方便xposed能获取到更多的日志。

def ExportedActivityTester(request):
print "\n[INFO] Exported Activity Tester"
try:
MD5=request.POST['md5']
PKG=request.POST['pkg']
m=re.match('^[0-9a-f]{32}$',MD5)
if m:
if re.findall(";|\$\(|\|\||&&",PKG):
print "[ATTACK] Possible RCE"
return HttpResponseRedirect('/error/')
if request.method == 'POST':
……
#从数据库中提取静态分析得到的EXPORTED_ACT
DB=StaticAnalyzerAndroid.objects.filter(MD5=MD5)
if DB.exists():
print "\n[INFO] Fetching Exported Activity List from DB"
EXPORTED_ACT=python_list(DB[0].EXPORTED_ACT)
if EXPORTED_ACT:
……
for line in EXPORTED_ACT: #遍历
try:
n+=1
print "\n[INFO] Launching Exported Activity - "+ str(n)+ ". "+line
subprocess.call([adb, "-s", getIdentifier(), "shell", "am","start", "-n", PKG+"/"+line])
Wait(4)
subprocess.call([adb, "-s", getIdentifier(), "shell", "screencap", "-p", "/data/local/screen.png"])
#? get appended from Air :-() if activity names are used
subprocess.call([adb, "-s", getIdentifier(), "pull", "/data/local/screen.png", SCRDIR + "expact-"+str(n)+".png"])
print "\n[INFO] Activity Screenshot Taken"
subprocess.call([adb, "-s", getIdentifier(), "shell", "am", "force-stop", PKG])
print "\n[INFO] Stopping App"
……

  其主要流程是:

  1. 获取静态分析得到的exported activity列表
  2. 遍历activity,并用adb -s IP:PORT shell am start -n PACKAGE/ACTIVITY 启动相应的activity
  3. 获取当前activity运行时的屏幕截图 adb -s IP:PORT shell screencap -p /data/local/screen.png
  4. 保存该截屏
  5. 强制关闭该应用 adb -s IP:PORT shell am force-stop PACKAGE
Start / Stop Activity Tester

  与Exported Activity不同的是,这个测试将会遍历AndroidManifest.xml中所有Activity,而不单单是Exported。其流程与处理Exported Activity一致。所以不做重复阐述。

Take a Screenshot

  即,截屏,然后保存到本地。具体实现是:
  adb -s IP:PORT shell screencap -p /data/local/screen.png
  adb -s IP:PORT pull /data/local/screen.png xxxx/xxx.png

Finish

  除了前面介绍的几个功能外,还需要介绍其主要动态信息获取以及输出日志分析时用到的一些函数。
  在FinialTest中,主要做一些扫尾的工作,将程序运行过程中所有dalvikvm的Warning和ActivityManager的Information收集起来:adb -s IP:PORT logcat -d dalvikvm:W ActivityManager:I > logcat.txt。同时,将Xposed目录下的API监控日志提取出来:adb -s IP:PORT pull /data/data/de.robv.android.xposed.installer/log/error.log x_logcat.txt。再dumpsys:adb -s IP:PORT shell dumpsys > dump.txt
  除此之外,平台还会利用datapusher来打包样本安装运行后留下的文件:adb -s IP:PORT shell am startservice -a PACKAGE opensecurity.ajin.datapusher/.GetPackageLocation
  MobSF对日志的分析功能主要在APIAnalysis和RunAnalysis两个函数中,和静态日志分析一样,动态日志分析也是以正则匹配为主,APIAnalysis主要对x_logcat.txt中Droidmon.apk产生的日志进行处理,主要进行API调用分析,主要代码如下:

def APIAnalysis(PKG,LOCATION):
print "\n[INFO] Dynamic API Analysis"
……
try:
with open(LOCATION,"r") as f:
dat=f.readlines()
ID="Droidmon-apimonitor-" + PKG +":"
for line in dat:
line = line.decode('utf8', 'ignore')
if (ID) in line: #如果是Droidmon生成的日志
param, value = line.split(ID,1)
try:
APIs=json.loads(value,strict=False)
……
if re.findall("android.util.Base64",CLS):
……

  其RunAnalysis函数主要处理样本运行后留下的WebTraffic.txt、logcat.txt、x_logcat.txt中

def RunAnalysis(APKDIR,MD5,PACKAGE):
……
Web=os.path.join(APKDIR,'WebTraffic.txt')
Logcat=os.path.join(APKDIR,'logcat.txt')
xLogcat=os.path.join(APKDIR,'x_logcat.txt')
……
#URLs My Custom regex
p = re.compile(ur'((?:https?://|s?ftps?://|file://|javascript:|data:|www\d{0,3}[.])[\w().=/;,#:@?&~*+!$%\'{}-]+)', re.UNICODE) #匹配url
urllist=re.findall(p, traffic.lower())
……
DOMAINS = MalwareCheck(urllist) #恶意域名检测
……
#Email Etraction Regex
……
regex = re.compile(("[\w.-]+@[\w-]+\.[\w.]+")) #匹配邮箱
……
#Extract Device Data 对打包上传的样本运行文件进行分类,不做分析处理
……

  在RunAnalysis中,MobSF首先用正则匹配出所有可能的url,而后再从网上同步下来最新的恶意url集合,然后再一一对比完成对url的鉴定。
  此外,RunAnalysis还会对样本运行产生数据进行分类,同时匹配出可能的邮箱。
  至此,MobSF完成了所有的检测和分析工作,并且将所有可用信息输出到web界面,方便分析人员进行分析。其动态分析结果界面如图:

Alt text

总结

  从上文对源码的分析大致可知MobSF的工作原理以及流程。
  在对样本进行静态分析时,MobSF主要使用了现有的dex2jar、dex2smali、jar2java、AXMLPrinter、CertPrint等工具。其主要完成了两项工作:解析AndroidManifest.xml得到了应用程序的各类相关信息、对apk进行反编译得到java代码,而后利用正则匹配找出该样本主要进行了哪些工作。
  而在对样本进行动态分析时,MobSF主要利用到了Xposed框架、Droidmon实现对应用程序调用API的情况进行监控,并且可灵活维护一份需要hook的API列表。同时,MobSF还使用了DataPusher来对样本数据进行打包、使用了ScreenCast结合adb shell input完成对手机的远程控制功能。当然,其中还使用隐藏root权限、伪造成正式机器等技术来应对一些反虚拟机的程序。其主要做了一下几件事:1、利用webproxy实现代理进而拦截样本流量。2、安装证书以便拦截https流量。3、遍历所有activity,尽量多的获取各activity运行得到的日志。4、利用正则匹配出API及参数和返回值。5、实时更新恶意url库,以url信息特征进行查杀。
  其实,最最最重要的一点是,MobSF所有分析结果都在web端展示,关键是,界面很美、很美……

——Tracy_梓朋
2016年8月31日16:09:01