【Android SDK程序逆向分析与破解系列】之五:Android APK的静态分析

时间:2022-06-26 10:04:37

作者:郭嘉
邮箱:allenwells@163.com
博客:http://blog.csdn.net/allenwells
github:https://github.com/AllenWells

【Android SDK程序逆向分析与破解系列】章节索引

【Android SDK程序逆向分析与破解系列】之一:Android安装程序APK分析
【Android SDK程序逆向分析与破解系列】之二:Android可执行程序DEX分析(一)
【Android SDK程序逆向分析与破解系列】之三:Android可执行程序DEX分析(二)
【Android SDK程序逆向分析与破解系列】之四:Android可执行程序ODEX分析
【Android SDK程序逆向分析与破解系列】之五:Android APK的静态分析

什么是静态分析?

静态分析(Static Analysis)是指在不运行代码的情况下,采用词法分析、语法分析等技术手段对程序文件进行扫描而生成程序的反汇编代码,然会阅读反汇编代码来掌握程序功能的一种技术。

一 如何快速定位Android程序的关键代码

APK程序反编译后生成的文件中包含一个AndroidManifest.xml文件,该文件包含了软件的包名,运行系统的版本,使用的组件是破解的关键点。

关注MainActivity

每个APK只有一个主Activity,它的定义如下图所示:

【Android SDK程序逆向分析与破解系列】之五:Android APK的静态分析

  • android:label:Activity的标题
  • android:name:Activity的包名+类名
  • intent-filter:Activity的启动意图

找到主Activity后,查看该类的onCreate()函数的反汇编代码,这就是程序的入口,从这里一直往下看,追踪软件的执行流程。

关注Application

Application类主要负责在程序的组件之间传递全局变量,或者在Activity启动之前做一些初始化工作,比如商业软件的授权等等Application类在application标签中用包名+类名来注册。

APK反编译的后代码量一般非常庞大,那么定位关键代码就需要有技巧,有方法,常见的定位关键代码的方法如下所示:

1.1 信息反馈法

信息反馈法是先运行目标程序,然后根据程序运行的反馈信息定位关键代码。比如商业软件需要注册,在输入错误的饿注册码时,程序会反馈“无效的注册码”等信息,类似这样的字符串一般会存储在string.xml文件中或硬编码到程序代码中,对于前者会以id的形式访问,直接在反汇编的代码中搜索字符串的id即可,对于后者,在反汇编代码中直接搜索字符串即可。

1.2 特征函数法

特征函数法与信息反馈法类似,消息的提示一般最终都是由Android的API来完成,比如Toast、AlertDialog等。

1.3 顺序查看法

顺序查看法是从软件的启动代码开始,逐行的向下分析,掌握软件的执行流程。这种方法在病毒分析经常用到,

1.4 代码注入法

代码注入法属于动态调试方法,它的原理是手动修改APK的反汇编代码,加入Log输出,配合LogCat查看程序执行到定点的状态数据,这种方法在解密程序时经常使用。

1.5 栈跟踪法

栈跟踪法属于动态调试法,然后查看栈上函数调用序列来理解方法的执行流程。

1.6 方法剖析

方法剖析(Method Profiling)属于动态调试方法,它主要用于热点分析和性能优化,该功能可以记录每个函数占用的CPU时间,还能追踪所有的函数调用关系,并提供比栈跟踪法更详细的函数调用序列报告。

二 Smail语法

2.1 类信息

Smail文件的头三行描述了当前类的信息

.class <访问权限>[修饰关键字]<类名>
.super <父类名>
.source <源文件名>

注意:经过混淆的DEX文件,反编译出来的Smail代码可能没有源文件信息,即.source可能为空。

2.2 主体部分

一个类由多个字段或方法组成。

2.2.1 字段

字段的声明使用.field指令。

  • 静态字段:field<访问权限>staic[修饰关键字]<字段名>:<字段类型>
  • 实例字段:field<访问权限>[修饰关键字]<字段名>:<字段类型>

2.2.2 方法

方法的声明使用.method指令。

  • 直接方法:.method<访问权限>[静态关键字]<方法原型>,起始处会注释# direct methods
  • 虚方法:.method<访问权限>[静态关键字]<方法原型>,起始处会注释#virtual methods

2.2.3 类

内部类

Android的内部类分为成员内部类、静态嵌套类、方法内部类和匿名内部类。

class Outer
{
class Inner()
{

}
}

内部类会被反编译成两个文件Outer.smail和Inner.smail

监听器

监听器的本质是接口

btn.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{

}
});

.implements<接口名>,起始处会注释#interfaces

注释类

.annotation[注释属性]<注释类名> [注释字段 = 值]
.end annotation,起始处会注释#annotations

自动生成的类

R类

R类主要描述APK的资源信息,其中包含内部类,R.smail、R attr.smailR dimen.smail、R drawable.smailR id.smail、R style.smailR string.smail、R$menu.smail等。

BuildConfg类

BuildConfg类中只有一个boolean类型的DEBUG字段,用来标识程序发布的类型,true表示已调试版本发布,false表示以非调试版本发布。

注释类

如果在代码中使用了SuppresLint或TargetApi等注释,程序将会自动生成对应的类,反编译后会在smail\android\annotation目录下生成相应的Smail文件。

2.2.4 循环语句

循环语句主要有两种:

for循环

for(<对象><对象名>; <对象名范围>; <对象递增/递减步长>)
{
[处理单个对象的代码体]
}

for循环对应的Smail代码如下所示:


.local v1, i:I #初始化v1为0
:goto_0 #迭代循环开始
if-1t v1, v5 :cond_0 #如果v1<v5,则跳转到cond_0标号处。
...
...
...
:cond_0
invoke-interface {v0, v1}, Ljava/util/List;->get(I)Ljava/lang/Object; #单个循环项
...
...
...
add-int/lit8 v1, v1, 0x1 #下一个索引
goto :goto_0 #跳转到循环开始处

while循环


while(<迭代器>hasNext())
{
<对象><对象名> = <迭代器>.next()
[处理单个对象的代码体]
}

:goto_0 #迭代循环开始
invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z #开始迭代
...
...
...
invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object #循环获取每一项
...
...
...
goto :goto_0 #跳转到循环开始处

两种循环生成的Smail代码非常相似,总的说来,迭代器循环有如下特点:

  • 迭代器循环会调用迭代器的hasNext()方法检测循环条件是否满足。
  • 迭代器循环调用迭代器的next()方法获取单个对象。
  • 循环中使用goto指令来控制代码的流程。
  • for形式的迭代器循环展开后即为while形式的迭代器循环。

2.2.5 分支语句

switch分支语句

switch(判断项)
{
case 判断项值1:
break

case 判断项值1:
break

case 判断项值1:
break

default:
break;
}

对应的Smail代码如下所示:

packed_switch p1, :pswitch_data_0 #packed_switch分支,pswitch_data_0指定case区域
...
...
...
:pswitch_data_0 #case区域,从0开始

2.2.6 异常捕捉语句

try/catch语句

try
{

}

catch(Exception e)
{

}

对应的Smail语句

:try_start_0 #第一个try开始
...
...
...
:try_end_0 #第一个try结束
...
...
...
:try_start_1 #第二个try开始
...
...
...
:try_end_1 #第二个try结束
...
...
...
:goto_0
...
...
...
:catch_0
...
...
...
:try_start_2 #第三个try开始
...
...
...
:try_end_2 #第三个try结束
...
...
...
:catch_1