Android安全开发初步(二)

继续更新!Android的安全问题太多太多,这里只是总结了下我所了解(强制 = = )到的安全问题
话说,实际开发中还能想到多少呢??

现在感觉各行各业越来越重视安全,如果有机会接触到感觉还是很爽的!
其实我想装逼

截屏风险

在登录和注册,或修改密码等敏感数据操作时,如果手机中有后台默认隐藏截屏的应用,在输入是一直截屏,就有可能盗取敏感数据信息。
解决方案:
在Activity onCreate 中加入:

1
2
//一般写在setContentView上面
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);

官方的意思就是设置了这个flag后, 系统会把当前窗口的内容视为安全隐私内容, 系统会阻止这些内容被截屏或者在不安全可靠的场景显示出来.
它起到的主要作用是:

  • 阻止屏幕截图

  • 在Recent apps(任务切换界面)中只显示应用名字和图标, 不显示内容

  • Google App的Now on tap功能不会去分析你的页面的内容

最后,对于国内各种ROM对Android的丧心病狂的更改还是要测试下实际效果的。。。

关注debuggable

android:debuggable 属性的设置可能会引起 被动态调试的风险。

1
2
3
4
5
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:debuggable="true">

debuggable 属性有两个值“true|false”;
只有Android:debuggable=”true”时我们才可以在手机上调试Android程序。
但是当我们没在AndroidManifest.xml中设置其debug属性时:
使用Eclipse运行这种方式打包时其debug属性为true,使用Eclipse导出这种方式打包时其debug属性为法false.
在使用ant打包时,其值就取决于ant的打包参数是release还是debug.
因此在AndroidMainifest.xml中最好不设置android:debuggable属性置,而是由打包方式来决定其值。
如果设置了 android:debuggable=”true” 那么在正式打包时 把它设置成false吧!!!

关注allowBackup

android:allowBackup 属性的设置可能会引起用数据被任意备份的风险

1
2
3
4
5
<application
android:allowBackup="false"
android:label="@string/app_name">
......
</application>

Android API Level 8 及其以上 Android 系统提供了为应用程序数据的备份和恢复功能,此功能的开关决定于该应用程序中 AndroidManifest.xml 文件中的 allowBackup 属性值,其属性值默认是 True。当 allowBackup 标志为 true 时,用户即可通过 adb backup 和 adb restore 来进行对应用数据的备份和恢复。
一旦应用程序支持备份和恢复功能,攻击者即可通过 adb backup 和 adb restore 进行恢复新安装的同一个应用来查看聊天记录等信息;对于支付金融类应用,攻击者可通过此来进行恶意支付、盗取存款等;因此为了安全起见,开发者务必将 allowBackup 标志值设置为 false 来关闭应用程序的备份和恢复功能,以免造成信息泄露和财产损失。

安全的打印日志

如何打印日志?这不是很简单,直接使用android.util.Log这个类不就行了?然而,日志属于非常敏感的信息;逆向工程师在逆向你的程序的时候,本来需要捕捉你程序的各种输出,然后进行推测,顺藤摸瓜然后得到需要的信息;一旦你的日志泄漏,无异于门户洞开,破解你的程序如入无人之境。
我们打印日志是用Log.d(TAG, msg);当把APK进行反编译后,TAG这个字符串会原封不动的还原出来,推理推理也就差不多了,不管你是否混淆过….
安全的概念本来就是相对的,如果破解你程序的代价远远大于破解得到的价值,那么就可以认为程序是“安全的”;这里就分析一下,为了提高程序的安全性,在打印日志的时候应该注意什么。

让release版本里面不包含日志代码

我们想要的是在开发的时候,正常打印日志;一旦需要发布版本,把所有打印日志的语句代码,全部删除掉。
这里我们可以采用日志开关+proguard的方式来进行优化,关于proguard这个工具,很多认只是觉得他是一个代码混淆的工具,实际上,它还可以帮你剔除无用代码!
无用代码就是类似下面的:

1
2
3
if (true) {
// statement;
}

静态编译的时候被认为“永远不会执行的代码”,就被认为是无用代码,会被这个工具直接优化掉,生成的class文件里面,这个if语句直接就没有了。这个功能,完美符合我们的需求;我们只需要把输出日志的代码用这样的if语句包围起来,然后release的时候肯定会用这个工具混淆;然后,在release版本里面,所有的输出日志的代码全部都没有了!不会像以前一样,留下一个影子,只是不做事。
所以我们这样写:

1
2
3
4
5
private static final boolean DEBUG = true; // 必须是static final 也就是常量,这样才能在编译器优化;删除if块

if (DEBUG) {
android.util.Log.d(TAG, "msg to print");
}

那么当DEBUG变量为False的时候proguard可以理所当然地认为,这一部分代码时绝对不会被执行的,这样,打印日志的语句就会被优化(删除)掉.
这里还需要注意的是,不要把打印日志进行封装,往里传个TAG和MSG,想省去写if包裹语句,这样的话就会使之前的工作失去作用,反编译后传参的部分会暴露出来….所以不要这么搞!
如果你实在懒得打,AS的话有框架提示,打个ifd就会自动生成代码块!
AS的话还有另一种方式,详情去参考里翻一翻。

SQLite数据库安全风险

使用SQLite来存储数据却存在着一个问题。因为大多数的Android手机都是Root过的,而Root过的手机都可以进入到/data/data/<package_name>/databases目录下面,在这里就可以查看到数据库中存储的所有数据。如果是一般的数据还好,但是当涉及到一些账号密码,或者聊天内容的时候,我们的程序就会面临严重的安全漏洞隐患。我们可以借助SQLCipher来解决这个安全性问题。

SQLCipher是一个在SQLite基础之上进行扩展的开源数据库,它主要是在SQLite的基础之上增加了数据加密功能,如果我们在项目中使用它来存储数据的话,就可以大大提高程序的安全性。SQLCipher支持很多种不同的平台。

使用SQLCipher替换掉程序中的SQLite的数据。将SQLCipher数据包导入项目相应目录中,将原有的SQlite import文件修改为SQLCipher,在程序启动界面添加SQLiteDatabase.loadLibs(this),并修改mysqlite.getWritableDatabase()方法
首先创建一个MyDatabaseHelper继承自SQLiteOpenHelper,注意导入的包,除了导入的包不同,其他基本都和SQLiteOpenHelper相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import android.content.Context;  
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabase.CursorFactory;
import net.sqlcipher.database.SQLiteOpenHelper;

public class MyDatabaseHelper extends SQLiteOpenHelper {

public static final String CREATE_TABLE = "create table Book(name text, pages integer)";

public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}

@Override
public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {

}
}

然后在使用到的Activity中这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends Activity {  

private SQLiteDatabase db;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//首先将SQLCipher所依赖的so库加载进来
SQLiteDatabase.loadLibs(this);
//创建实例
MyDatabaseHelper dbHelper = new MyDatabaseHelper(this, "demo.db", null, 1);
//获取SQLiteDatabase对象,它接受一个字符串参数
//就是SQLCipher所依赖的key,在对数据库进行加解密的时候SQLCipher都将使用这里指定的key。
db = dbHelper.getWritableDatabase("secret_key");
//即可对db进行操作
//db.insert("Book", null, values);
}
}

需要注意的是:加入SQLCipher后会使APP的安装包增加几M,安全与体积要权衡好

Android签名安全

现在Android逆向越来越火,并且相比PC端的EXE程序感觉Android的逆向还是很简单的,那就会面临着一个问题:会有人将APK进行反编译后修改代码然后进行二次打包发布,造成一些恶劣影响
我们知道打包APK必然要进行签名,原始的签名密钥肯定是安全的唯一的,二次打包会改变APP的签名信息,我们在APP启动的时候进行签名对比,如果不一致就强制JVM退出
当然了,没有绝对的安全,只要你逆向技术够高,这个是拦不住你的….

获取签名

获取签名我们可以采用两种方式,一种手动用keytool命令,还可以在代码里写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//代码方式获取签名,根据包名
private static Signature[] getRawSignature(Context context, String pkgName) {
if ((pkgName == null) || (pkgName.length() == 0)) {
return null;
}
PackageManager pm = context.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(pkgName, PackageManager.GET_SIGNATURES);
if (pi == null) {
return null;
}
//返回是个数字签名的数组,一般apk都是单签名的
//因此一般取Signature[0]做MD5,与已知签名的MD5信息做对比即可
return pi.signatures;
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}

如果是手动用命令查的话,可以直接用RAR之类的打开APK文件,找到Apk文件中META-INF/CERT.RSA文件,解压,然后执行:keytool -printcert -file fileName就可以查到签名了
PS:正常的签名文件查询命令是keytool -list -v -keystore filepath(后缀一般为keystore,其实并不需要后缀)

签名进行对比

这里贴下主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
String signature="e79cf0a46d543ab6092b71f41d835543"; //正确的已知的签名
String pack="com.bfchengnuo.demo"; //包名
/**
* 获取其他应用的签名信息,然后进行对比,错误强制系统退出
* @param paramString
*/
private void getSign(String paramString) {
//getRawSignature方法返回的就是上面获得的签名MD5数组(类似,大概..)
Signature[] arrayOfSignature = getRawSignature(this, paramString);
if ((arrayOfSignature == null) || (arrayOfSignature.length == 0)) {
errout("signs is null");
return;
}
int i = arrayOfSignature.length;
//与已知的签名进行对比,如果全部匹配不成功则退出JVM
//签名数组要进行MD5加密处理下,为了保险起见,这里把数组的每一个都进行了对比
for (int j = 0; j < i; j++)
stdout(MD5.getMessageDigest(arrayOfSignature[j].toByteArray()));
}

private void stdout(String paramString) {
if(signature.equals(paramString))
{
// do thing
Toast.makeText(this, "签名正确", 0).show();
}else{
System.exit(0);
}
}

BroadCastReceiver安全风险

Android 可以在配置文件中声明一个receiver或者动态注册一个receiver来接收广播信息,攻击者假冒APP构造广播发送给被攻击的receiver,是被攻击的APP执行某些敏感行为或者返回敏感信息等,如果receiver接收到有害的数据或者命令时可能泄露数据或者做一些不当的操作,会造成用户的信息泄漏甚至是财产损失
那么如何避免应用中注册的广播响应其他应用发送的广播呢,对于显式的广播除非是别人故意攻击,一般很少出现响应别人的广播,但是对于隐式的广播就很容易出现上述问题,因为action很容易是一样的,一旦是一样的就出问题了

解决方案

  • 如果仅在应用内部通信,可以使用私有receiver,设置exported="false",该receiver可以接收相同应用程序组件或带有相同用户ID的应用程序所发出的消息[参考]。

    1
    <receiver android:name=".permittedReceiver" android:exported="false" />
  • 若只在当前进程内通信,可以使用LocalBroadcastManager,使其他应用程序不能向该receiver发送广播

  • 对于动态注册的广播registerReceiver(BroadcastReceiver, IntentFilter, String permission, android.os.Handler),指定receiver必须具备的permission。
    如果只允许自己的产品族使用,可以设置android:protectionLevel="signature" ,若提供给其他APP使用,则设置android:protectionLevel="normal",同时要避免敏感信息的传递。
    其实就是自定义权限~~

    1
    2
    3
    4
    5
    6
    7
    8
    <permission android:name="com.android.permission.send_permission" 		android:protectionLevel="signature" />

    <receiver android:name=".permittedReceiver"
    android:permission="com.android.permission.send_permission">
    <intent-filter>
    <action android:name="com.android.permitted_ACTION" />
    </intent-filter>
    </receiver>
  • 对接收来的广播进行验证,返回结果时需注意接收app是否会泄露信息

关于自定义权限的补充

首先说一下protectionLevel这个属性:

  • normal:默认的,应用安装前,用户可以看到相应的权限,但无需用户主动授权。
  • dangerous:normal安全级别控制以外的任何危险操作。需要dangerous级别权限时,Android会明确要求用户进行授权。常见的如:网络使用权限,相机使用权限及联系人信息使用权限等。
  • signature:它要求权限声明应用和权限使用应用使用相同的keystore进行签名。如果使用同一keystore,则该权限由系统授予,否则系统会拒绝。并且权限授予时,不会通知用户。它常用于应用内部。

上面的订阅方例子用到了signature这个值,多说一下,如果别的应用使用的不是同一个签名文件,就没办法使用该权限,从而保护了自己的接收者(可以理解为只接受拥有此权限的应用发送的广播)。
发送方和订阅方都是需要加入这个权限的,只不过订阅方需要在注册接收器的时候再写一遍权限,上面的例子是静态注册receiver,如果用动态的方式注册那就是registerReceiver(receiver, filter, permission, null);,直接指定发送者应该具有的权限

android:permission —如果设置,具有相应权限的广播发送方发送的广播才能被此broadcastReceiver所接收

参考

http://wolfeye.baidu.com/blog/recieve-broadcast-security/
BroadcastReceiver安全问题

剪切板安全风险

同一部手机中安装的其他app,甚至是一些权限不高的app,都可以通过剪贴板功能获取密码管理器中的账户密码信息。原因是Android剪贴板的内容向任何权限的app开放,很容易就被嗅探泄密,如上代码剪切板中存有明文内容,如果是明文内容将会有信息泄露的风险
如下代码可以在任意APP中读取剪切板的内容:

1
2
3
ClipboardManager cm = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
ClipData cd2 = cm.getPrimaryClip();
str2 = cd2.getItemAt(0).getText().toString();

所以,使用完clipboard及时清空,并避免使用剪贴板明文存储敏感信息

其他风险

  • 在配置Database配置模式的时候要注意:避免使用MODE_WORLD_WRITEABLEMODE_WORLD_READABLE模式创建数据库(Database),最好还是使用MODE_PRIVATE模式

  • 安卓SecureRandom安全:
    在Android 4.2以下,SecureRandom是基于老版的Bouncy Castle实现的。如果生成SecureRandom对象后马上调用setSeed方法。SecureRandom会用用户设置的seed代替默认的随机源。使得每次生成随机数时都是会使用相同的seed作为输入。从而导致生成的随机数是相同的。
    解决方案推荐:不要使用自定义随机源代替系统默认随机源,就是说不要调用以下函数:

    SecureRandom#SecureRandom(byte[] seed)
    SecureRandom#setSeed(long seed)
    SecureRandom#setSeed(byte[] seed)

    其实还可以在调用setSeed方法前先调用任意nextXXX方法(nextBytes(byte[] bytes))不过不推荐这种方式

    现在基本上不用考虑了,毕竟已经到Android7.1+了,但是考虑到国内情况嘛….

参考

Android项目安全注意事项
如何安全打印日志

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~