前段时间导师要对中国app市场(我们选的是百度,其实哪家都一样)上app的通信安全(登录的时候,用户名密码有没有加密)做自动化检测。
解决的方法就是在emulator里自动登录app,然后用mitmproxy抓捕数据,对数据进行分析。(怎么分析以后再讲,今天先写操控emulator的工具,图片就不放了,反正也没啥人看)。下面都是自己的一家之言,如果有什么摸索的不对的,欢迎指正。
第一个是google的hierarchyviewer。
hierarchyviewer的优点:
1.给出手机(以下不提及都是只phone或者emulatoer都可以)当前显示屏幕的UI结构。会用一棵树的形式表现出来。你选择不同的节点,会在hierarchyviewer右下角的分区显示这个节点对应屏幕上的哪一块。
2.另外有个模式,能定位你鼠标所指位置的像素坐标。我觉得这个可能对美工很有用吧,没接触过美工的工具,但是这个能自动放大你鼠标所在位置的图片,像素点很明显的一个个方块。
hierarchyviewer的缺点:
1.很卡,我笔记本是外星人的,还是很卡,用起来很不爽。
2.也就是让你看看UI结构,获得信息并不多。也不能对手机做出操作。
第二个是monkeyrunner。
monkeyrunner是Jython(用JAVA实现的python,它是一个Python语言在Java中的完全实现)。但是我们完整的自动检测程序都打算用python来写,所以这个用用也就放弃了,因为我们找到了可以替代它的,python实现的工具。(下面就会讲)。
不过monkeyrunner可以应付很多安卓测试了吧(没有在公司干过,不知道测试岗具体要做啥)。点击,滑动,输入,截屏,monkeyrunner都可以实现。
这是官方文档的地址:http://developer.android.com/tools/help/MonkeyDevice.html
然后有个中文版的小例子,基本功能也有介绍了:http://fengbohaishang.blog.51cto.com/5106297/962705
第三个是AndroidViewClient
dtmilano大神用python做的,个人觉得比monkeyrunner更强大好用。在我们自己写工具前,主要用的就是它啦。一个python实现的强大的moudle。你可以把它下载下来,在你的代码里作为import。https://github.com/dtmilano/AndroidViewClient
不过它有个问题,UiAutomator limitation。你在使用dump获取当前UI信息的时候,如果太频繁就会崩。所以师兄后来看了androidviewclient的源码,我们自己写了我们需要的库。
第四个我们自己系的代码
这里我就直接贴代码了。名字起的都是人能看懂的,所以没怎么注释。
import subprocess # class Deivce
import time
import xml.etree.ElementTree as ET
class View(object):
def __init__(self, d):
'''
initalize with a dict obj
scrollable : bool, text: str
long_clickable : bool, focused: bool
checkable: bool, clickable: bool,
password: bool, classtype: (class)str e.g. TextView
index: int, checked: bool
package: str e.g.com.qiyi.video, selected: bool
enabled: bool, bounds: ((x1,y1),(x2,y2))
content_desc: str, resource_id: str e.g. com.qiyi.video:id/xxx
focusable: bool, naf: (NAF)(optional) bool
center: (x1, y1) center of bounds
'''
self.scrollable = True if d.get('scrollable')=='true' else False
self.text = d.get('text')
self.long_clickable = True if d.get('long-clickable')=='true' else False
self.focused = True if d.get('focused')=='true' else False
self.checkable = True if d.get('checkable')=='true' else False
self.clickable = True if d.get('clickable')=='true' else False
self.password = True if d.get('password')=='true' else False
self.classtype = (d.get('class')).replace('android.widget.','')
self.index = int(d.get('index'))
self.checked = True if d.get('checked')=='true' else False
self.package = d.get('package')
self.selected = True if d.get('selected')=='true' else False
self.enabled = True if d.get('enabled')=='true' else False
tmp = tuple((d.get('bounds')).strip('[]').split('][')) # ('1,2','3,4')
pos1, pos2 = tmp[0].split(','), tmp[1].split(',') # ['1','2']
self.bounds = (int(pos1[0]),int(pos1[1])), (int(pos2[0]),int(pos2[1]))
self.content_desc = d.get('content-desc')
self.resource_id = d.get('resource-id')
self.focusable = True if d.get('focusable')=='true' else False
self.naf = True if d.get('NAF')=='true' else False
(x1, y1), (x2, y2) = self.bounds
self.center = x1+(x2-x1)/2, y1+(y2-y1)/2
def getDefaultConfig():
import configparser
cfg = configparser.SafeConfigParser()
cfg.read('config.cfg')
dtype=cfg.get('device','type')
height = cfg.get(dtype, 'height')
width = cfg.get(dtype, 'width')
go2appspos = tuple((cfg.get(dtype, 'go2appspos')).split(','))
appspos = tuple((cfg.get(dtype, 'appspos')).split(','))
welcomepos = tuple((cfg.get(dtype, 'welcomepos')).split(','))
default_apps = (cfg.get(dtype, 'apps')).split(',')
default_pkgs = (cfg.get(dtype, 'pkg')).split(',')
config = {
'height':height,
'width':width,
'go2appspos':go2appspos,
'appspos':appspos,
'welcomepos':welcomepos,
'default_apps':default_apps,
'default_pkgs':default_pkgs
}
return config
class Device(object):
def __init__(self, sno, config=getDefaultConfig()):
'''
'''
self.sno = sno
self.height =int(config['height'])
self.width = int(config['width'])
self.welcomepos = config['welcomepos']
self.go2appspos = config['go2appspos']
self.appspos=config['appspos']
self.default_apps = config['default_apps']
self.default_pkgs = config['default_pkgs']
self.adbshellpf = 'adb -s '+self.sno+' shell '
self.adbpf = 'adb -s '+self.sno+' '
@staticmethod
def shell(cmd, runtime):
res = subprocess.check_output(cmd, shell=True, universal_newlines=True, timeout=runtime)
return res
def parseDump2vl(self, fpath):
'''parse dump to view-list([view,...])'''
tree = ET.parse(fpath)
vl = []
for e in tree.iter():
if e.tag == 'hierarchy':
continue
vl.append(View(e.attrib))
return vl
def dump(self, filepath='', savefile=False):
'''get list of view from current ui
return [ view, ...]'''
if filepath == '':
fpath = '/tmp/trafficgen/'+self.sno+'/uidump.xml'
else:
fpath = filepath
t = 1
for chance in range(6):
time.sleep(t-1) # '-1': no wait for first attempt of dump
try:
cmd = self.adbshellpf+' uiautomator dump --compressed'
self.shell(cmd, 10)
cmd = self.adbpf+' pull /storage/sdcard/window_dump.xml '+fpath
self.shell(cmd, 10)
except:
if(chance == 5):
if not savefile:
cmd = 'rm '+fpath
self.shell(cmd, 10)
print('dump failed')
raise
t *= 2
else:
break
finally:
cmd = self.adbshellpf+' rm /storage/sdcard/window_dump.xml'
self.shell(cmd, 10)
vl = self.parseDump2vl(fpath)#default: /tmp/trafficgen/sno/dumpui.xml
if not savefile:
cmd = 'rm '+fpath
self.shell(cmd, 10)
return vl
def tap(self, pos, waittime=0):
'''pos: (x, y), sleeptime: sleep time'''
x, y = pos
cmd = self.adbshellpf+' input tap '+str(x)+' '+str(y)
self.shell(cmd, 10)
if waittime > 0:
time.sleep(waittime)
def swipe(self, startpos, endpos, duration=512):
'''startpos, endpos : (x1,y1),(x2,y2)'''
x1, y1 = startpos
x2, y2 = endpos
x1,y1,x2,y2,duration = str(x1),str(y1),str(x2),str(y2),str(duration)
cmd = self.adbshellpf+' input swipe '+' '.join((x1,y1,x2,y2,duration))
self.shell(cmd, 10)
def swipeLeft(self):
margin = self.width/10
w,h = self.width, self.height
startpos, endpos = (w-margin, h/2),(margin, h/2)
self.swipe(startpos, endpos, 200)
def swipeRight(self):
margin = self.width/10
w,h = self.width, self.height
endpos, startpos = (w-margin, h/2),(margin, h/2)
self.swipe(startpos, endpos, 200)
def swipeUp(self):
margin = self.height/5
w,h = self.width, self.height
startpos, endpos = (w/2, h - margin), (w/2, margin)
self.swipe(startpos, endpos, 200)
def swipeDown(self):
margin = self.height/5
w,h = self.width, self.height
endpos, startpos = (w/2, h - margin), (w/2, margin)
self.swipe(startpos, endpos, 200)
def inputText(self, msg):
'''
adb shell input text msg
msg only accept [0-9][a-zA-Z]|@|.
space no supported
'''
cmd = self.adbshellpf+' input text '+msg
self.shell(cmd, 10)
def screencap(self, filepath='.'):
'''
adb shell screencap /storage/sdcard/sc.png
adb pull /storage/sdcard/sc.png filepath
filepath: local file path e.g. ./dir/screen.png
'''
remotefile = '/storage/sdcard/sc.png'
cmd = self.adbshellpf+' screencap '+remotefile
self.shell(cmd, 10)
cmd = self.adbpf+' pull '+remotefile+' '+filepath
self.shell(cmd, 10)
def go2home(self):
"""input keyevent KEYCODE_HOME"""
cmd = self.adbshellpf+' input keyevent KEYCODE_HOME'
self.shell(cmd, 10)
time.sleep(1)
def go2apps(self):
self.tap(self.go2appspos, 1)
def installApp(self, fpath):
cmd = self.adbpf + ' install '+ fpath
self.shell(cmd, 60)
def getOne3rdPartyApp(self):
"""return 1 user-installed 3rd party package
return: None | pkg name"""
cmd = self.adbshellpf + " pm list packages -3 "
tmp_res = self.shell(cmd, 30)
tmp_res = tmp_res.split('\r\n')
for line in list(tmp_res): # x86 img contain WARNING msg
if ('WARNING' in line) or ('' == line):
tmp_res.remove(line)
res = None
for entry in tmp_res:
pkg = (entry.split(':'))[1]
if pkg not in self.default_pkgs:
res = pkg
return res
def uninstallApp(self):
"""uninstall the only package pkg
do nothing | remove the pkg"""
pkg_name = self.getOne3rdPartyApp()
if(pkg_name == None):
return False
cmd = self.adbpf + " uninstall " + pkg_name
self.shell(cmd, 60)
return True
def startApp(self):
'''precondition: at Apps page
start new installed app'''
flag = False
self.tap(self.appspos, 1)
self.swipeRight()
for pageindex in range(2):
vl = self.dump()
for v in vl:
if ('TextView' == v.classtype) and \
(v.text not in self.default_apps):
flag = True
self.tap(v.center, 1)
return flag
self.swipeLeft()
return flag
def forcestopApp(self):
"""force stop app by pkg-name adb shell am force-stop pkg-name"""
flag = False
pkg = self.getOne3rdPartyApp()
if pkg == None:
return flag
cmd = self.adbshellpf + " am force-stop " + pkg
self.shell(cmd, 10)
flag = True
return flag
def cleanupSdcard(self):
'''
rm -r /storage/sdcard/*
mkdir /storage/sdcard/D1 ...
'''
sdir = '/storage/sdcard/'
dirs1 = ['Alarms','DCIM','Download','LOST.DIR']
dirs2 = ['Movies','Music','Notifications']
dirs3 = ['Pictures','Podcasts','Ringtones']
dirs = dirs1+dirs2+dirs3
cmd = '; mkdir '
for d in dirs:
d = sdir+d+' '
cmd += d
cmd = ' rm -r '+sdir+'* '+cmd
cmd = self.adbshellpf+" '"+cmd+"'"
self.shell(cmd, 10)
def skipWelcomeMsg(self):
self.tap(self.welcomepos, 2)
self.go2apps()
self.tap(self.welcomepos, 5)
self.go2home()
def startadbserv():
cmd = 'adb start-server'
subprocess.check_output(cmd, shell=True, timeout=30)
def stopadbserv():
cmd = 'adb kill-server'
subprocess.check_output(cmd, shell=True, timeout=30)
if __name__=='__main__':
sno = 'emulator-5554'
device = Device(sno)