APK的结构组成

APK,全程Android application package(安卓应用程序包),也就是我们常说的安卓应用安装包。一个APK通常包含代码、资源、签名文件和清单文件等部分,我们随便找一个apk文件,将后缀名改为zip,然后解压一下查看其文件结构目录如下:
图片描述
如上图所示,该apk是由签名信息(META-INF)、资源(res、resource.arsc)、代码(classes.dex)及今天的主角AndroidManifest.xml几部分组成。当然并不是所有的apk文件都是这种结构,例如有的apk文件可能还会存在lib目录来存放so库,assets目录存放一些不需要编译的资源文件等等。

关于AmdroidManifest.xml

简介

AmdroidManifest.xml对于安卓开发人员来说是一个最熟悉不过的东西了,但是往往对于其结构和配置非常熟悉,但是很难用言语对它进行描述。每个安卓应用必须含有AndroidManifest.xml文件,对于它的定义我们可以将其看作一个安卓清单文件,对应用必要的一些信息进行描述,如应用的包名、应用名等等。

作用

前面提到了,AndroidManifest.xml对于安卓应用程序来说是一个必需品!那么它存在的意义或者它的作用是什么呢?
1、描述应用的包名,该包名作为应用的唯一标识,一台设备不允许两个不同的APP包名一样,同样的各应用市场也不会允许该情况的出现
2、描述应用的基本信息,应用的名称、图标、以及是否允许调试等信息都是由清单文件进行配置和描述的。
3、描述各组件的信息,清单文件会记录项目中使用到的Activity、Service和广播接收器,如果在清单文件中没有描述的组件,在应用运行过程中是不允许访问和使用的。
4、描述权限,清单文件声明应用正常运行所需的所有权限,只有获取了相应的权限才可保障APP的正常运行。

安全问题

通过解压apk文件,我们会获取到一个清单文件,所以理论上我们是可以修改该文件,然后重新打包成一个新的apk。正是因为这个原因,资源清单文件便有下面几个方面的风险:
1、APP破坏风险:清单文件描述了组件的信息,并且如果所有在程序运行时用到的组件都需要在清单文件中声明,并在安装的时候进行注册。如果通过修改清单文件,将某个Activity删掉,这样就会导致应用在Activity跳转的时候因为目标Activity未注册而崩溃。当然,这只是一种场景,还有很多攻击方案会导致APP运行崩溃。
2、allowBackup风险:在Android Api level 8之后,安卓引入了应用数据的备份功能,可以通过在清单文件中修改allowBackup属性设置是否允许备份,默认值为true。为了安全考虑,许多厂商会将该属性修改为false,我们可以通过反编译后修改清单文件中的值,重新打包。
3、debuggable设置风险:该属性的作用是是否允许调试,该属性默认是false。如果该值不为false,则别人可以利用JDB等工具对应用进行动态调试,从而了解应用的运行流程,可能会找出一些攻击破绽。
4、组件导出风险;在安卓开发中,允许通过startActivity启动第三方APP,如果一个Activity允许被第三方应用调用,则需要将exported属性的值设为true,所以我们可以利用该属性导出一些原本不允许调用的组件。
5、权限控制风险:清单文件声明了应用所需的权限列表,我们可以通过删除某权限破坏应用的正常运行,当然这属于第一个风险的场景。在这个风险里面指的是,我们可以通过增加权限的声明,来让应用增加一些原本不需要的权限,或者进行一些越权操作等。尤其在6.0之前,安卓还没有引入动态权限,该风险的危险系数更高。
6、应用信息修改风险:可以修改应用名等信息,该风险意义并不是很大,但却有是客观存在的事实,可能会存在伪装应用等危险。

模拟攻击

攻击思路

我们通过解压的方式,可以获取到清单文件,但是这时候的文件是编译之后的,我们无法直接修改。我们可以使用apktool进行反编译,这样就可以对清单文件进行修改,并重新打包签名,一次比较完整的基于清单文件的攻击大致有下面几个步骤:
1、借助apktool反编译apk文件,获得AndroidManifest.xml
2、修改或者替换AndroidManifest.xml
3、利用apktool重新编译成apk文件
4、对新生成的apk文件签名
5、安装验证是否成功

攻击方案

我们通过一个简单的例子来体验下,通过修改AndroidManifest.xml的修改模拟一次攻击,我们先编写一个简单的APP。该APP正常启动后展示的文案是“Hello World!”,现在我们通过对apk文件处理,修改启动项为另外一个展示其他文案的Activity。我们编写一个Java程序,作为一个攻击的工具,为了简单的展示该方案,我们提前准备好一个用于替换的清单文件。所以攻击工具的逻辑流程如下:
1、反编译apk文件
2、将提前准备好的清单文件内容写入原清单文件
3、编译打包
4、apk签名

关键代码

1、反编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void decompile(File apkFile) throws Exception{
String cmd[] = {"cmd.exe","/C","apktool d "+apkFile.getAbsolutePath()};
System.out.println(cmd[cmd.length-1]);
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start decompile");
String inStr = consumeInputStream(process.getInputStream());
String errStr = consumeInputStream(process.getErrorStream());
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
System.out.println("finish decompile");
process.destroy();
}

PS:反编译主要就是调用命令”apktool d XXX”实现反编译,”XXX”代指apk文件的绝对路径
2、复制并替换清单文件内容

1
2
3
4
5
6
7
8
9
10
11
12
public static void copyFile(File srcFile,File targetFile) throws Exception{
//
RandomAccessFile raf = new RandomAccessFile(srcFile, "r");
byte[] bytes = new byte[(int) raf.length()];
raf.readFully(bytes);
raf.close();
//
FileOutputStream fos = new FileOutputStream(targetFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write(bytes);
bos.flush();bos.close();
}

PS:新的清单文件与原清单文件唯一的区别就是启动项的不同,如下所示:
原清单文件
替换后的清单文件
3、重编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void recompile(File dir) throws Exception{
String cmd[] = {"cmd.exe","/C","apktool b "+dir.getAbsolutePath()};
System.out.println(cmd[cmd.length-1]);
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start recompile");
String inStr = consumeInputStream(process.getInputStream());
String errStr = consumeInputStream(process.getErrorStream());
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
System.out.println("finish recompile");
process.destroy();
}

PS:与编译类似,需要调用“apktool b XXX”使用apktool工具进行编译,XXX为要打包的目录路径,生成的apk文件所在位置如下图:
图片描述
4、签名:
经过重新编译后生成的apk文件,如果直接安装肯定是失败的,如果我们解压现在的apk文件,会发现它丢失了签名信息,所以我们需要重新签名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
String cmd[] = {"cmd.exe","/C","jarsigner -verbose -keystore D:/AS/keys/key01.jks -signedjar "+signedApk.getAbsolutePath()+" "+unsignedApk.getAbsolutePath()+" key0 -storepass 123456"};
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start sign");
String inStr = consumeInputStream(process.getInputStream());
String errStr = consumeInputStream(process.getErrorStream());
try {
int waitResult = process.waitFor();
System.out.println("waitResult: " + waitResult);
} catch (InterruptedException e) {
e.printStackTrace();
throw e;
}
System.out.println("process.exitValue() " + process.exitValue() );
System.out.println("finish signed");
process.destroy();
}

PS:使用JDK提供的工具jarsigner对apk文件进行签名

运行截图

1、原运行结果
图片描述
2、修改清单文件后的结果
图片描述