为方便查看Android APP的系统API调用情况,一种措施是将APP的apk安装包反编译,对于反编译之后得到的代码进行扫描遍历,查看系统API调用情况。但实现这一功能的前提是已经提取到系统API,并且按照反编译之后的格式输出。本文不包含具体查看APP系统API调用情况,仅包括提取系统API,并格式化。
Android API 版本: Android23
编程语言: Python
系统: Window 10
开发环境: JetBrains PyCharm
一遍历
遍历Android系统API文件夹,返回所有Android系统.java文件目录,方便之后的访问。
'''
扫描文件夹dir中所有.java文件并按照包名输出
dir 为 Android 系统 API 的相对文件目录
调用方法:
list = GetFileList('../android-23',[])
list 即为所有 系统 .java 文件的目录列表
'''
def GetFileList(dir, fileList):
newDir = dir
if os.path.isfile(dir): # 如果是文件
fileList.append(dir)
elif os.path.isdir(dir):
for s in os.listdir(dir): # 如果是文件夹
newDir = os.path.join(dir, s)
GetFileList(newDir, fileList)
return fileList
二正则匹配
在得到Android系统.java文件目录之后即可进行正则匹配,提取所有API类的函数声明等信息,并进行参数格式化,添加类的引用关系,完成Android系统API列表的提取。但经过一下处理得到的列表,不能区分该方法是类的方法还是该类内部类的方法,并且会产生许多重复的列表信息(因为多个内部类),故这里默认所有方法均为类的方法(暴力笼统了)。
输出样例【包名.类名.方法名(参数列表):返回值】:
android.accessibilityservice.AccessibilityService.onAccessibilityEvent(android.view.accessibility.AccessibilityEvent):void
对于内部类的处理:
A.B.C的内部类D的方法E()返回值为F,那么输出格式为:
A.B.C.E():F
由于有较多重复项目,故还应该进行去重操作。
去重之后,为了统一格式,匹配反编译之后的代码,需要重新格式化
去重及重新格式化稍后讨论
'''
遍历 package 指向的.java 类中所有的方法
1).java 类自己的方法
2).java 类的内部类(接口)的方法
'''
def ergodicClassFunc(package):
'''
:param package:存储每个系统API的Java的相对路径
:return:0:.java类的方法正常遍历;1:.java类的方法没有正常提取
'''
'''
编译正则表达式
'''
pattern = re.compile(
r'^[ \t]*(public |protected |private )((static |final |abstract )*)([\w|_|\$| |,|\r|\n|\t|.|\[|\]|\<|\>|\:|\?|@]*)\(([\w|_|\$| |,|\r|\n|\t|.|\[|\]|\<|\>|\:|\?|@|\(|\)|=]*)(.*)$',
re.M);
# 正则表达式匹配 import 语句
patternImport = re.compile(
r'^(import )([a-zA-Z0-9|_|\$| |,|.|\:|\?|@]*)(;)$',
re.M);
'''
数据处理
'''
flag = 0 # 标志变量:0:.java类的方法正常遍历;1:.java类的方法没有正常提取
# package 存储每个系统API的Java的相对路径
# tmpClassPackage 存储格式化之后的文件路径
tmpClassPackage = apiRouteReform(package)
# 遍历该.java文件提取所有方法
# print(package)
IDXfile = open(package, 'r', encoding='utf-8') # gbk 格式的文件 IDXfile.read() 会报错,以 utf-8 格式打开就行
fileread = IDXfile.read()
IDXfile.close()
methods = re.findall(pattern, fileread) # 匹配 类声明的方法
classImport = re.findall(patternImport, fileread) # 匹配 import 语句
# 构造 类:包名 的对应关系 : 包含 1. import 语句引用的;2.与该类同一个 package 的
# 3.内部类(接口)- public / private / protected 三种类型的都记录下来
classMatch = classMatchPackage(classImport, package) # 构造 map
# print(methods)
# 输出所有未能遍历的.java类 ,更换正则表达式遍历
if len(methods) == 0:
# print(tmp) # tmp 中存储的是 . 连接的类名
flag = 1
else:
for eFunc in methods:
# eFunc 为每个函数的完整的说明
apiFunc = eFunc[3]; # 第四个分组为函数名格式 : void release 或者为类名如:AccessibilityServiceInfo 又或者由三部分组成,这个时候需要细致区分
varFunc = eFunc[4]; # 第五个分组为参数声明分组:其中包含与参数声明无关的 \r,\n,\t 字符,需要先将这几种字符去掉,并且包含 声明括号中右括号)之后到 { 之前内容如:String name)\n throws SAXNotRecognizedException, SAXNotSupportedException
# 函数参数格式化
varFunc = varReform(varFunc, classMatch)
# 如果不是类名则为方法名,用split 分隔
if ' ' in apiFunc:
# 得到该方法的返回值和函数名
apiFuncReturn, apiFuncName = findReturnValueAndFuncName(apiFunc);
# 对返回值的类型进行 类:包名 的对应
apiFuncReturn = varClassIncludePackage(apiFuncReturn, classMatch)
# 去除函数名中多于的空格
apiFuncName = apiFuncNameReform(apiFuncName)
apiFunc = tmpClassPackage + '.' + apiFuncName + '(' + varFunc + '):' + apiFuncReturn;
# 对于构造函数
else:
# 去除函数名中多于的空格
apiFunc = apiFuncNameReform(apiFunc)
apiFunc = tmpClassPackage + '.' + apiFunc + '(' + varFunc + ')';
# 去除 API 中的 \n 等空白字符
apiFunc = removeVarSpaceChar(apiFunc)
# 输出API
print(apiFunc)
return flag
通过以上正则表达式
pattern = re.compile(
r'^[ \t]*(public |protected |private )((static |final |abstract )*)([\w|_|\$| |,|\r|\n|\t|.|\[|\]|\<|\>|\:|\?|@]*)\(([\w|_|\$| |,|\r|\n|\t|.|\[|\]|\<|\>|\:|\?|@|\(|\)|=]*)(.*)$',
re.M);
并不能匹配所有Android系统API,大约有700个(/12000)匹配不成功,这其中大部分为标示性接口,故以上正则表达式能够较好满足提取条件。
在根据路径匹配Android系统API的过程中,需要先把路径格式的文件转换为包名.类名的形式,方便之后添加输出前缀。
2.1路径格式化
'''
Android 系统 API 的相对路径格式化:
将android-23\android\accessibilityservice\AccessibilityService.java
格式化为:
android.accessibilityservice.AccessibilityService
将格式化之后的数据存储在 tmp 中
'''
def apiRouteReform(package):
tmp = '' # 存储此次遍历的类名
flag = 0 # 标志是否开始存储
for str in package:
# print(str,end='')
if (flag == 0) and (str == '\\'):
flag = 1
# print(flag,end='')
continue
# flag == 1 : 开始
if flag == 1:
if str == '\\':
tmp = tmp + '.'
elif str == '.':
break
else:
tmp = tmp + str
return tmp
2.2类:包名.类 引用关系建立
Java类在编辑过程中如果需要引用其他包的类则需要在.java文件开头添加 import 语句。如果需要引用同一个包中的类与内部类则不需要添加 import语句,但是在反编译结果中也需要知道这些类的 包名.类。可是在API函数声明时大部分时候是不包含类名的,所以为了添加参数与返回值列表的 包名.类 的声明,需要自行构造 类:包名.类 的 Dict 映射。
1.正则匹配.java文件中 import 语句部分,构造Dict;
2.与该类统一包中的所有类的 Dict;
3.该类的内部类与接口
但其上构造没有将 String 和 Class 提取出来,这个在之后的重新格式化中实现,并且基础数据类型不替换
'''
输入:import 集合 形如:[('import ', 'android.os.Parcelable', ';'), ('import ', 'android.os.Parcel', ';')]
输出:map 对象 map 对应关系
<1>
List --> java.util.List
SomeArgs --> com.android.internal.os.SomeArgs
类:包名 对应关系
<2>
package 为当前类所在包中所有类的 map 对应关系 tmpClassThisPackageMathch
最后返回的 map 为 import 与 tmpClassThisPackageMathch 的合并
先求 tmpClassThisPackageMathch 后 求 import 是为了更新同名类(不同包,但同名,以import 为准)的引用
<3>
package 所指向类的内部类、接口
如:Callbacks 为 android.accessibilityservice.AccessibilityService 的内部接口
则返回:android.accessibilityservice.AccessibilityServic.Callbacks
即:
Callbacks --> android.accessibilityservice.AccessibilityServic.Callbacks
'''
def classMatchPackage(classImport, package):
# 构造当前 类 所在 package 中的左右类的 类:包名 对应关系
tmpClassThisPackageMathch = classInThisPackageMatch(package)
# package 所指向类的内部类、接口的 类(接口):包名 对应关系
tmpClassOrInterfaceInThisPackage = classOrInterfaceInThisPackage(package)
# 遍历 classImport 集合
for tmpclassImport in classImport:
tmpPackage = tmpclassImport[1]
tmpClassSet = tmpPackage.split('.') # 将包名根据 '.' 分隔
tmpClassName = tmpClassSet[len(tmpClassSet)-1] # 最后一个字符串为类名
tmpClassThisPackageMathch[tmpClassName] = tmpPackage
# 遍历 tmpClassOrInterfaceInThisPackage 集合
for tmpPackage in tmpClassOrInterfaceInThisPackage:
tmpClassSet = tmpPackage.split('.') # 将包名根据 '.' 分隔
tmpClassName = tmpClassSet[len(tmpClassSet)-1] # 最后一个字符串为类名
tmpClassThisPackageMathch[tmpClassName] = tmpPackage
# for item in tmpClassThisPackageMathch:
# print(item, ':',tmpClassThisPackageMathch[item])
#
# print('\n\n')
return tmpClassThisPackageMathch
构造当前 类 所在 package 中的左右类的 类:包名 对应关系
'''
构造当前 package 中 所有 类:包名 对应关系
'''
def classInThisPackageMatch(package):
# 得到当前包中所有类的格式化的形式的列表
tmpClassInThisPackage = classInThisPackage(classRoute(package))
tmpClassInThisPackageMatch = {
'key':'package'}
flag = 0; # 标记是否开始存储数据
for classPackage in tmpClassInThisPackage:
tmpClassSet = classPackage.split('.') # 将包名根据 '.' 分隔
tmpClassName = tmpClassSet[len(tmpClassSet) - 1] # 最后一个字符串为类名
# 如果没有赋值过则先清空 map
if (flag == 0):
flag = 1
tmpClassInThisPackageMatch.clear()
tmpClassInThisPackageMatch[tmpClassName] = classPackage
# 这个包里面没有类则返回空
if (flag == 0):
tmpClassInThisPackageMatch.clear()
# for tmpItem in tmpClassInThisPackageMatch:
# print(tmpClassInThisPackageMatch[tmpItem])
return tmpClassInThisPackageMatch
'''
classRoute 与 classInThisPackage 是针对每一个类本身的路径的提取与同一个包内部
类所在包的情况,这样时间与空间复杂度较高,运行时间长,为此了解决 0-记录.txt 中的
Question 2 和 3 在遍历完所有.java 文件之后即构建 android-23 所有类与对应包名之间的
对应关系
'''
'''
<得到类所在包名的路径>
输入:android-23\android\accessibilityservice\AccessibilityService.java (Java 类的相对路径)
转化成:../android-23\android\accessibilityservice
方便之后遍历文件夹,得到与当前类在同一个包中的所有类的名称
tmpClassRoute 存储类所在包名的路径 如:../android-23\android\accessibilityservice
classRouteReform 存储类所在包名的格式化 如:android.accessibilityservice
'''
def classRoute(package):
tmp = '' # 存储类所在的包名的路径
flag = 0 # 用于标记是否在split得到的串之间加 斜杠
tmpClassRoute = ''
tmp = package.split('\\') # 根据 斜杠 符号分隔最后一个元组为 .java 的名称 去除之后及为要得到的路径
i = 0
while(i < (len(tmp)-1)):
if(flag == 0):
flag = 1
else:
tmpClassRoute += '\\'
tmpClassRoute += tmp[i]
i += 1
return tmpClassRoute
'''
package 的格式:../android-23\android\accessibilityservice 及 classRoute的返回值
得到与本.java 同一个包中所有类的格式化
返回一个列表
'''
def classInThisPackage(package):
classList = GetFileList(package,[]) # 得到这个文件夹下所有的类()
classReformList = []
for tmpPackage in classList:
tmp = apiRouteReform(tmpPackage)
classReformList.append(tmp)
# print(classReformList)
return classReformList
package 所指向类的内部类、接口
'''
package 所指向类的内部类、接口
如:Callbacks 为 android.accessibilityservice.AccessibilityService 的内部接口
则返回:android.accessibilityservice.AccessibilityServic.Callbacks
即:
Callbacks --> android.accessibilityservice.Acc