详解Androguard静态分析APK方法

时间:2023-01-13 10:05:13

Androguard的文档写的很粗略,很多API的用法都是查源码得到的,真是源码里的注释都比文档写的详细。
APK静态分析,主要用Androguard里的androlyze.py。下面从APK级别,到package级别,和class级别,讲解用androlyze.py静态分析APK最常用的方法。

androlyze.py使用方法

使用androlyze.py之前,要先安装配置好Androguard环境(参考这里)。

然后,在终端提示符下,执行androlyze.py -s就会进入androlyze的Shell交互环境。一般就是在这个交互环境中执行不同的命令,来分析APK文件。

分析之前,首先要初始化三个对象:APK文件对象DEX文件对象分析结果对象

# androlyze.py -s # 进入androlyze的Shell交互环境
In [1]: a = APK("./1.apk")# APK文件对象
In [6]: d = DalvikVMFormat(a.get_dex())# DEX文件对象
In [7]: dx = VMAnalysis(d)# 分析结果对象

APK文件对象

Androguard源码中可以看出,创建APK文件对象的类其实就是androguard.core.bytecodes.apk.APK。这个类用于访问APK文件中的所有元素。从类中的一些方法中也可以看出它的功能:

  • 获取manifest文件:get_AndroidManifest()
  • 判断APK是否有效:is_valid_APK()
  • 获取APK文件名:get_filename()
  • 获取APP名:get_app_name()
  • 获取package名:get_package()
  • 获取android版本名:get_androidversion_code()
  • 获取APK中的文件列表:get_files()
  • 获取APK中所有activity名称列表:get_activities()
  • 获取APK中的主activity名称:get_main_activity()
  • 获取APK中所有service名称列表:get_services()
  • 获取APK中所有receiver名称列表:get_receivers()
  • 获取APK中所有provider名称列表:get_providers()
  • 获取APK中签名:get_signature()

除了这些信息,还能获取APK权限相关的信息,看看下面这些函数就知道它的重要性了。

  • get_permissions()
  • get_requested_permissions()
  • get_declared_permissions()
  • get_certificate()

如果只想大致看看APK的基本信息,有一个简单的函数就能满足你,show()。它会将APK内部的文件、权限、Activity相关的信息显示给你。

DEX文件对象

创建该对象的类为androguard.core.bytecodes.dvm.DalvikVMFormat,源码位于这里。这个类的主要功能是解析APK文件中classes.dex,并获取其相关信息。同样的,只需要用创建的DEX文件对象的show()方法,就能显示classes.dex中的基本信息。

  • 获取APK/DEX文件中的所有类:get_classes()
  • 获取APK/DEX文件中的所有方法:get_methods()
  • 获取APK/DEX文件中的所有成员变量:get_all_fields()
  • 获取APK/DEX文件中的所有字符串:get_strings()

分析结果对象

创建该对象的类为androguard.core.analysis.analysis.VMAnalysis,源码位于这里VMAnalysis类的主要功能,就是分析DEX文件对象。比如根据字符串,搜索APK中的package:dx.get_tainted_packages().search_packages('')。该类其它功能详见源码。

APK级别的分析

APK级别的分析,可以用APK文件对象提供的方法,来获取manifest文件、APK的签名、权限等相关信息。除了直接用APK文件对象,还能根据下面的例子,来获取、查找APK中含有的类和package。

获取整个APK中所有的class

# androlyze.py -s
In [1]: a = APK("./1.apk")
In [6]: d = DalvikVMFormat(a.get_dex())
In [7]: dx = VMAnalysis(d)
In [21]: class_list = d.get_classes()
In [47]: for i in range(4750,4757):
...: class_item = class_list[i]
...: class_name = class_item.get_name()
...: print(class_name)
...:
Lcom/google/android/gms/internal/ib$3;
Lcom/google/android/gms/internal/ib$4;
Lcom/google/android/gms/internal/ib$5;
Lcom/google/android/gms/internal/jf$1;
Lcom/google/android/gms/internal/jf$2;
Lcom/google/android/gms/internal/jf$3;
Lcom/google/android/gms/internal/jf$4;

注意结果中的$是用于分隔外部类内部类的,按照[外部类]$[内部类]的格式。

根据string,来search package,并打印search得到的结果

In [1]: a,d,dx = AnalyzeAPK("1.apk", decompiler="dad")
In [3]: res = dx.get_tainted_packages().search_packages('Landroid/support/v4/widget')
In [13]: analysis.show_Path(d, res[0])
1 Landroid/support/v4/widget/SearchViewCompatHoneycomb;->newOnQueryTextListener(Landroid/support/v4/widget/SearchViewCompatHoneycomb$OnQueryTextListenerCompatBridge;)Ljava/lang/Object; (0x4) ---> Landroid/support/v4/widget/SearchViewCompatHoneycomb$1;-><init>(Landroid/support/v4/widget/SearchViewCompatHoneycomb$OnQueryTextListenerCompatBridge;)V

res[0]就是搜索结果中的第一个package。由show_Path()结果可知,搜索到的Landroid/support/v4/widget/SearchViewCompatHoneycomb(注意这其实是一个class),就是我们需要的结果。

获取整个APK中所有的methods

In [40]: f = d.get_methods()[0]
In [41]: f.pretty_show()
########## Method Information
Landroid/support/v4/net/TrafficStatsCompat$BaseTrafficStatsCompatImpl$SocketTags;-><init>()V [access_flags=private constructor]
########## Params
local registers: v0...v1
- return: void
####################
***************************************************************************
<init>-BB@0x0 :
0 (00000000) invoke-direct v1, Ljava/lang/Object;-><init>()V
1 (00000006) const/4 v0, -1
2 (00000008) iput v0, v1, Landroid/support/v4/net/TrafficStatsCompat$BaseTrafficStatsCompatImpl$SocketTags;->statsTag I
3 (0000000c) return-void

***************************************************************************
########## XREF
F: Landroid/support/v4/net/TrafficStatsCompat$BaseTrafficStatsCompatImpl$SocketTags; <init> (Landroid/support/v4/net/TrafficStatsCompat$1;)V 0
####################

package级别的分析

根据分析结果对象类的说明,package是可以用get_tainted_packages()来获取的。但在我本机上实验发现两个问题:

  • get_tainted_packages()获取的是class级别的信息,比如:
In [17]: pkg = dx.get_tainted_packages()
In [20]: for i in pkg.get_packages():
...: print(i)
(<androguard.core.analysis.analysis.TaintedPackage object at 0x1efbbd10>, 'Ljava/io/FileNotFoundException;')

但其得到的Ljava/io/FileNotFoundException是一个class,并不是package。

  • get_tainted_packages()获取的是所有 tainted package,并非所有package。关于什么是tainted package,在*, androguard开源github, 看雪论坛都发帖问了这个问题,给Androgurad的作者也发了邮件询问,已经得到回复。

根据我的理解,只要将class信息(比如Ljava/io/FileNotFoundException)中的class(FileNotFoundException)去掉,就是package了(Ljava/io)。所以我用下面的方式来获取APK中的所有package。

根据class,获取所有package。

pkgs = set()
for c in d.get_classes():
s = c.get_name()
pkg_without_classinfo = s.replace('/{}'.format(s.split('/')[-1]), '')
pkgs.add(pkg_without_classinfo)

调用某个特殊API的class-method信息

假设要查询这个API(class: ContextCompat, method: buildPath)被APK中哪些类调用

In [76]: paths = dx.get_tainted_packages().search_methods('ContextCompat', 'buildPath', '.')
In [77]: for p in paths:
...: i = p.get_src_idx()
...: m = d.get_method_by_idx(i)
...: print(m.get_class_name())
...: print(m.get_name())
...:
...:
Landroid/support/v4/content/ContextCompat;
getExternalCacheDirs
Landroid/support/v4/content/ContextCompat;
getExternalFilesDirs
Landroid/support/v4/content/ContextCompat;
getObbDirs

可知这个API被三个地方调用:
* 类Landroid/support/v4/content/ContextCompat中的方法getExternalCacheDirs代码中调用过这个API
* 类Landroid/support/v4/content/ContextCompat中的方法getExternalFilesDirs代码中调用过这个API
* 类Landroid/support/v4/content/ContextCompat中的方法getObbDirs代码中调用过这个API

class级别的分析

获得整个类的反编译后的samli代码

In [21]: class_list = d.get_classes()#获取APK中的所有class
In [45]: class_item = class_list[10]# 第10个class
In [46]: class_item.get_class_data().pretty_show()

获取类的Java代码

In [21]: class_list = d.get_classes()#获取APK中的所有class
In [45]: class_item = class_list[10]# 第10个class
In [87]: class_item.source()

获取类的直接方法个数

In [45]: class_item = class_list[10]
In [47]: class_item.get_class_data().get_direct_methods_size()
Out[47]: 8

注意这里方法只包括类自己创建的方法,里面当然是包括了构造函数的,但不包括继承的方法。

获取类的虚拟方法个数

class_item.get_class_data().get_virtual_methods_size()

获取类的静态变量个数

class_item.get_class_data().get_static_fields_size()

获取类的实例变量个数

class_item.get_class_data().get_instance_fields_size()

获取类的(private, public)信息

class_item.get_access_flags()

获取类的名称

class_item.get_name()

获取父类的名称

class_item.get_superclassname()

function级别的分析

获取method对象

In [2]: class_list = d.get_classes()
In [3]: class_item = class_list[10]
In [4]: methods = class_item.get_methods()
In [6]: m = methods[1]

获取method的访问属性(public static)

In [14]: m.get_access_flags_string()
Out[14]: 'public static'

获取method的大小

函数对象在dex文件中占的空间。

In [24]: m.get_length()
Out[24]: 5

获取method的Java代码

m.source()

public void setStyle(int p3, int p4)
{
this.mStyle = p3;
if ((this.mStyle == 2) || (this.mStyle == 3)) {
this.mTheme = 16973913;
}
if (p4 != 0) {
this.mTheme = p4;
}
return;
}

获取method的描述符

m.get_descriptor()

(I I)V # 说明有两个形参,都是int型,返回值是void

对比它的Java代码,就能知道其含义。

获取method的基本信息

In [21]: m.get_information()
Out[21]:
{'params': [(1, 'android.accessibilityservice.AccessibilityServiceInfo')],
'registers': (0, 0),
'return': 'boolean'}

获取method中的const string

In [41]: v = dx.get_tainted_variables()
In [42]: for c in class_list:
...: ms = c.get_methods()
...: for m in ms:
...: d = v.get_strings_by_method(m)
...: if(len(d)>0):
...: for k in d:
...: print( 'STRING: {}\n'.format(k.get_info()) )
...: print( m.show() )
...: print('\n\n')