引言
因为本人从事的金融 IC 卡和移动支付相关的开发工作,在日常研发过程中,对 APP 信息安全防护方面尤为重视,所以现总结下金融支付相关的加解密算法以及常见的安全防范措施。
Android 端常见的加解密算法
加密算法根据内容是否可以还原分为可逆加密和非可逆加密 。
可逆加密根据其加密解密是否使用的同一个密钥而可以分为对称加密和非对称加密。
对称加密即是指在加密和解密时使用的是同一个密钥。
非对称加密在加密和解密过程中使用不同的密钥,即公钥和私钥。公钥用于加密,所有人都可见,私钥用于解密,只有解密者持有。
MD5
MD5 概要
- MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致,是计算机广泛使用的杂凑算法之一(又译摘要算法、哈希算法)。MD5算法将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5的前身有MD2、MD3和MD4。
MD5算法特点
- 压缩性:任意长度的数据,算出的MD5值长度都是固定的
- 容易计算:从原数据计算出MD5值很容易。
- 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有 很大区别。
- 强抗碰撞:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造 数据)是非常困难的。
MD5算法的应用场景
- 当用户登录的时候,系统把用户输入的密码计算成MD5值,然后再去和保存在文件系统中的MD5值进行比较,进而确定输入的密码是否正确。通过这样的步骤,系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这不但可以避免用户的密码被具有系统管理员权限的用户知道,而且还在一定程度上增加了密码被破解的难度。
- MD5加密算法是不可逆的,因此不可能在客户端与服务器交互时,使用MD5对传输的Json串进行加密,因为用md5加密过后,服务器端或者客户端将无法解析出正确的数据并执行相应的逻辑。
RSA
RSA 算法概要
- RSA算法一直是最广为使用的”非对称加密算法”。RSA是目前最有影响力的公钥加密算法,该算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,即公钥,而两个大素数组合成私钥。公钥是可发布的供任何人使用,私钥则为自己所有,供解密之用。
RSA 加解密步骤
- 甲方构建密钥对(公钥和私钥,公钥给对方,私钥留给自己)
- 甲方使用私钥加密数据,然后用私钥对加密后的数据签名,并把这些发送给乙方; 乙方使用公钥、签名来验证待解密数据是否有效,如果有效使用公钥对数据解密。
- 乙方使用公钥加密数据,向甲方发送经过加密后的数据;甲方获得加密数据,通过私钥解密。
DES 3DES
算法概要
- DES是一种对称加密算法,所谓对称加密算法即:加密和解密使用相同密钥的算法。DES加密算法出自IBM的研究,后来被美国政府正式采用,之后开始广泛流传,但是近些年使用越来越少,因为DES使用56位密钥,以现代计算能力,24小时内即可被破解。
- 3DES是DES加密算法的一种模式,它使用3条64位的密钥对数据进行三次加密。数据加密标准(DES)是美国的一种由来已久的加密标准,它使用对称密钥加密法。3DES(即Triple DES)是DES向AES过渡的加密算法(1999年,NIST将3-DES指定为过渡的加密标准),是DES的一个更安全的变形。它以DES为基本模块,通过组合分组方法设计出分组加密算法
AES
AES 算法概要
- AES高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
- AES 是一个迭代的、对称密钥分组的密码,AES算法加密强度大,执行效率高,使用简单,实际开发中建议选择AES 算法。
AES加密在安卓中的运用
- 做一个管理密码的app,我们在不同的网站里使用不同账号密码,很难记住,想做个app 统一管理,但是账号密码保存在手机里,一旦丢失了容易造成安全隐患,所以需要一种加密算法,将账号密码信息加密起来保管,这时候如果使用对称加密算法,将数据进行加密,秘钥我们自己记在心里,只需要记住一个密码。需要的时候可以还原信息。
- android 里需要把一些敏感数据保存到SharedPrefrence 里的时候,也可以使用对称加密,这样可以在需要的时候还原。
- 请求网络接口的时候,我们需要上传一些敏感数据,同样也可以使用对称加密,服务端使用同样的算法就可以解密。或者服务端需要给客户端传递数据,同样也可以先加密,然后客户端使用同样算法解密。
Base64编码
- Base64从严格意义上来说的话不是一种加密算法,而是一种编码算法。它主要的用途是把一些二进制数转成普通字符用于网络传输。由于一些二进制字符在传输协议中属于控制字符,不能直接传送需要转换一下就可以了
项目实战
- 网络传输密文由三部分组成,MD5+AES+RSA
- MD5组成:由6位随机密钥+明文做 MD5 处理,然后全部转换成大写
- AES组成:由6位随机密钥+14293300FF做密钥,0123456789ABCDEF做偏移量,用AES/CBC/PKCS5Padding方式对明文加密后,对byte[]再做Base64编码处理
- RSA 组成:用 RSA/ECB/PKCS1Padding方式使用公钥对6位随机密钥加密后,对 byte[]再做Base64编码处理
获取本地存储的公钥文件
在应用首次启动时,加载本地存储的公钥文件
public static void getPublicKeyFile(Context context) {
if (publicFis == null) {
//获取解密公钥文件
publicFis = context.getResources().openRawResource(R.raw.rsacert);
try {
pubKey = getPubKey();
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}
}
}
这里拿登录请求交易来看,网络请求交易获取到密文,然后调用解密算法对明文进行解密
JSONObject jsonObject = new JSONObject(result);
String data = jsonObject.getString("data");
String resultCode = jsonObject.getString("code");
String msg = jsonObject.getString("msg");
LogUtil.i(TAG, "resultCode=====>" + resultCode);
if (Constants.Code.RESULT_OK.equals(resultCode)) {
HashMap<String, String> acceptMap = RSAUtils.genMap(data);
String decryptText = RSAUtils.defaultDecrypt(acceptMap);
LogUtil.i(TAG, "解密明文=====>" + decryptText);
progressLoginRequest(decryptText);
} else {
// 其他情况直接显示后台内容
LoginActivity.this.showDialog(BaseActivity.MODAL_DIALOG, msg);
}
加解密工具类
public class RSAUtils {
private final static String UTF_8 = "UTF-8";
// 10位公共偏移量
private final static String KEY_SUFFIX = "14293300FF";
private static byte[] iv;
static {
try {
iv = "0123456789ABCDEF".getBytes(UTF_8);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
static String AES_CBC_PKCS5Padding = "AES/CBC/PKCS5Padding";
static String RSA_ECB_PKCS1Padding = "RSA/ECB/PKCS1Padding";
static String MD5 = "MD5";
static int MD5_LEN = 32;
private static String[] TYPES = {AES_CBC_PKCS5Padding,
RSA_ECB_PKCS1Padding};
private static Integer[] MODES = {Cipher.ENCRYPT_MODE, Cipher.DECRYPT_MODE};
public static Key prvKey;
public static Key pubKey;
private static X509Certificate pubCert;
// static String privateFile = RSAUtils.class.getClassLoader().getResource("").getPath() + "pkcs8_private_key.der";
static String privateFile;
// 公钥文件路径
// static String publicFile = RSAUtils.class.getClassLoader().getResource("").getPath() + "rsacert.der";
public static InputStream publicFis;
public static String encode = UTF_8;// 保持平台兼容统一使用utf-8
private static String encryptMD5(String data) throws Exception {
return doEncryptMD5(data, MD5);
}
public static SecretKey getKeyAES(String strKey) throws Exception {
SecretKeySpec key = new SecretKeySpec(strKey.getBytes(encode), "AES");
return key;
}
public static HashMap<String, String> genMap(String acc)
throws RuntimeException {
System.out.println("accept data:" + acc);
HashMap<String, String> tmpMap = new HashMap<String, String>();
if (acc == null || acc.length() < 26) {
throw new RuntimeException("非法数据");
}
// 第一个|在第24位(从0开始算)
if (acc.indexOf("|") == MD5_LEN) {
String md5 = acc.substring(0, MD5_LEN);
acc = acc.substring(MD5_LEN + 1);
tmpMap.put("md5", md5);
// 第二个|在第8位及以后(从0开始算)
int tmpInt = acc.indexOf("|");
if (acc.length() > 9 && tmpInt > 7 && tmpInt % 2 == 0) {
String data = acc.substring(0, tmpInt);
acc = acc.substring(tmpInt + 1);
tmpMap.put("data", data);
// 第二个|后数据长度都在16以上
tmpInt = acc.length();
if (tmpInt > 15) {
tmpMap.put("key", acc);
} else {
throw new RuntimeException("非法key数据");
}
} else {
throw new RuntimeException("非法data数据");
}
} else {
throw new RuntimeException("非法md5数据");
}
return tmpMap;
}
/**
* //默认加密 MD5 AES_CBC_PKCS5Padding RSA_ECB_PKCS1Padding(私钥加密)
*
* @param dataToEncypt 数据
* @param pwd 对称密钥
* @return Map
* @throws Exception
*/
public static HashMap<String, String> defaultEncrypt(byte[] dataToEncypt,
String pwd) throws Exception {
return encrypt(dataToEncypt, pwd, true);
}
/**
* //默认加密 MD5 AES_CBC_PKCS5Padding RSA_ECB_PKCS1Padding(私钥加密)
*
* @param dataToEncypt 数据
* @param pwd 对称密钥
* @param isPrvEncrypt 是否使用私钥加密
* @return Map
* @throws Exception
*/
public static HashMap<String, String> encrypt(byte[] dataToEncypt,
String pwd, boolean isPrvEncrypt) throws Exception {
if (pwd == null || pwd.getBytes(encode).length != 6) {
throw new RuntimeException("非法密钥");
}
Key key = prvKey;
if (!isPrvEncrypt) {
key = pubKey;
}
// md5 key+data
// byte[] md5Byte = encryptMD5(pwd + new String(dataToEncypt, encode));
// String md5Base64 = Base64Utils.encode(md5Byte);
String md5Base64 = doEncryptMD5(pwd + new String(dataToEncypt, encode), MD5);
byte[] encryptData = doCrypt(AES_CBC_PKCS5Padding, Cipher.ENCRYPT_MODE,
new IvParameterSpec(iv), getKeyAES(pwd + KEY_SUFFIX),
dataToEncypt);
// String dataBase64 = Base64Utils.encode(encryptData);
String dataBase64 = doBase64Encode(encryptData);
byte[] encryptKey = doCrypt(RSA_ECB_PKCS1Padding, Cipher.ENCRYPT_MODE,
null, key, pwd.getBytes(encode));
// String keyBase64 = Base64Utils.encode(encryptKey);
String keyBase64 = doBase64Encode(encryptKey);
HashMap<String, String> data = new HashMap<String, String>();
data.put("data", dataBase64);
data.put("key", keyBase64);
data.put("md5", md5Base64);
data.put("send", md5Base64 + "|" + dataBase64 + "|" + keyBase64);
return data;
}
/**
* //默认解密 MD5 AES_CBC_PKCS5Padding RSA_ECB_PKCS1Padding(公钥解密)
*
* @param dataToEncypt 数据
* @param pwd 对称密钥
* @return Map
* @throws Exception
*/
public static String defaultDecrypt(HashMap<String, String> data)
throws Exception {
return decrypt(data, true);
}
/**
* //默认解密 MD5 AES_CBC_PKCS5Padding RSA_ECB_PKCS1Padding(公钥解密)
*
* @param dataToEncypt 数据
* @param pwd 对称密钥
* @param isPubDecrypt 是否使用公钥解密
* @return Map
* @throws Exception
*/
public static String decrypt(HashMap<String, String> data,
boolean isPubDecrypt) throws Exception {
Key key = pubKey;
if (!isPubDecrypt) {
key = prvKey;
}
String dataBase64 = data.get("data");
String keyBase64 = data.get("key");
String md5Base64Src = data.get("md5");
// byte[] encryptedKey = Base64Utils.decode(keyBase64);
byte[] encryptedKey = doBase64Decode(keyBase64);
byte[] decryptedKey = doCrypt(RSA_ECB_PKCS1Padding,
Cipher.DECRYPT_MODE, null, key, encryptedKey);
String pwd = new String(decryptedKey, encode);
if (pwd == null || pwd.getBytes(encode).length != 6) {
throw new RuntimeException("伪造密钥");
}
// byte[] encryptedData = Base64Utils.decode(dataBase64);
byte[] encryptedData = doBase64Decode(dataBase64);
byte[] decryptedData = doCrypt(AES_CBC_PKCS5Padding,
Cipher.DECRYPT_MODE, new IvParameterSpec(iv), getKeyAES(pwd
+ KEY_SUFFIX), encryptedData);
String textDecrypt = new String(decryptedData, encode);
// md5 key+data
// byte[] md5Byte = encryptMD5(pwd + textDecrypt);
// String md5Base64 = Base64Utils.encode(md5Byte);
String md5Base64 = doEncryptMD5(pwd + textDecrypt, MD5);
if (!md5Base64.equals(md5Base64Src)) {
throw new RuntimeException("伪造数据");
}
return textDecrypt;
}
public static Key getPubKey() throws CertificateException, IOException {
// 读取公钥
CertificateFactory certificatefactory = CertificateFactory
.getInstance("X.509");
InputStream bais = publicFis;
Certificate cert = certificatefactory.generateCertificate(bais);
bais.close();
pubCert = (X509Certificate) cert;
PublicKey puk = cert.getPublicKey();
return puk;
}
public static Key getPrvKey() throws IOException, NoSuchAlgorithmException,
InvalidKeySpecException {
FileInputStream in = new FileInputStream(privateFile);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] tmpbuf = new byte[1024];
int count = 0;
while ((count = in.read(tmpbuf)) != -1) {
bout.write(tmpbuf, 0, count);
}
in.close();
// 读取私钥
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
bout.toByteArray());
PrivateKey prk = keyFactory.generatePrivate(privateKeySpec);
return prk;
}
public static String doEncryptMD5(String data, String type)
throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(data.getBytes("UTF-8"));
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
if (MD5_LEN == 32) {
// 32位加密
return buf.toString().toUpperCase();
} else {
// 16位的加密
return buf.toString().substring(8, 24);
}
}
public static String getPassword() {
String pwd = (new Random().nextInt(1000000) + 1000000 + "")
.substring(1);
return pwd;
}
// pkcs8_der.key文件为私钥 只能保存在服务端
// public_key.der为公钥文件,保存在客户端
public static void main(String[] args) {
try {
prvKey = getPrvKey();
pubKey = getPubKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}
String text = "{\"TxnCode\":\"hce1003\",\"TxnStatus\":\"true\",\"ReplyCode\":\"000000\",\"ReplyMsg\":\"交易成功\",\"Data\":\"[{\\\"accountNo\\\":\\\"623091019******3297\\\",\\\"accountType\\\":\\\"1\\\",\\\"certNo\\\":\\\"***\\\",\\\"certType\\\":\\\"101\\\",\\\"customerName\\\":\\\"***\\\",\\\"mobileNo\\\":\\\"***\\\"},{\\\"accountNo\\\":\\\"***\\\",\\\"accountType\\\":\\\"1\\\",\\\"certNo\\\":\\\"***\\\",\\\"certType\\\":\\\"101\\\",\\\"customerName\\\":\\\"***\\\",\\\"mobileNo\\\":\\\"***\\\"}]\"}";
// String text = "c";
// String text = "提起黄飞鸿,我们隐隐约约地知道他是一个真实的历史人物,但恐怕很少有人能在脑海中勾勒出他本人真实的面貌,上了岁数的人大概还依稀记得关德兴那张冷峻硬朗的面庞;三四十岁左右的人想到的应该是风度翩翩的李连杰,关之琳扮演娇美的十三姨,如影随形不离左右;再年轻一点的观众或许更倾心于赵文卓甚至是彭于晏的扮相。“黄飞鸿”这个名字,经过文学和影视作品的塑造,早已由一个平凡的历史人物变成了整个华人世界的偶像,侠之大者、一代宗师的化身。那么,真实历史中的黄飞鸿,究竟是怎样一个人?为什么在他死后,会由一介平凡的岭南武师,变为全体华人心目中的英雄?";
String pwd = getPassword();
// String pwd="663810";
// 加密
HashMap<String, String> data;
try {
data = defaultEncrypt(text.getBytes(encode), pwd);
} catch (Exception e) {
e.printStackTrace();
return;
}
String accept = data.get("send");
// String accept =
// "mQ6uBpadJfNFxFHez3zWwQ==|2xZJpm3xO5gKaID9pYYqfTQ8e64GHo4esj7E51DlyCZM32DJAPUN3RKqLFkfR5rUCA+SsSD8vQOH+iS/Uh7YlCZhatuTOgNqJi6TfLbp4yZx4iTqFRda5jBVD1vNgsUf8jZdoJLY6rg2OhvcOjL+/lGeijdVv5f0RkykAtfKUHWeO5jWk+jUALQqx/ugO46Npna6MoeelUDzIbmdHL2NmZRmDzPvqpkQmUz9Pk8P19R/kWZuVfxVuOzPuaic69Roq1zbNyrKhGfqITQRwSsVOGMQi3qGYfY3Sv/hrbIuwZ4=|eAoqLUr9bC+sfACRZN6UFnm9g8R8h/jtq1k50m5aOB6AhxtaJWN/PiE2iaHBwF8a5z1gqdQdt0HERLFZm6tzvE6N2+RwF/XylK4oxfhLeCGOW85ahpnwlEpVGD86oCq8JRp4fglhaFkt9MAwmfpWGnT6GIlB9OiXFzNkIgrUdIk=";
HashMap<String, String> acceptMap = genMap(accept);
// 解密
try {
defaultDecrypt(acceptMap);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param type 算法/加密方式/填充方式 如:AES/CBC/PKCS5Padding
* @param mode 加密/或者解密 Cipher.DECRYPT_MODE/Cipher.ENCRYPT_MODE
* @param zeroIv 初始化向量 如:new IvParameterSpec(iv)
* @param key 密钥
* @param data 需要加密的数据
* @return byte[]
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws InvalidAlgorithmParameterException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
public static byte[] doCrypt(String type, int mode, IvParameterSpec zeroIv,
Key key, byte[] data) throws NoSuchAlgorithmException,
NoSuchPaddingException, InvalidKeyException,
InvalidAlgorithmParameterException, IllegalBlockSizeException,
BadPaddingException {
if (!checkCrypt(type, mode, zeroIv, key, data)) {
throw new RuntimeException("参数非法");
}
if (!pubCertValid()) {
throw new RuntimeException("证书失效");
}
Cipher cipher = Cipher.getInstance(type);
if (type.contains("CBC") && zeroIv != null) {
cipher.init(mode, key, zeroIv);
} else {
cipher.init(mode, key);
}
return cipher.doFinal(data);
}
private static boolean checkCrypt(String type, int mode,
IvParameterSpec zeroIv, Key key, byte[] data)
throws NoSuchAlgorithmException, NoSuchPaddingException,
InvalidKeyException, InvalidAlgorithmParameterException,
IllegalBlockSizeException, BadPaddingException {
if (type == null || !isValid(TYPES, type)) {
return false;
}
if (!isValid(MODES, mode)) {
return false;
}
if (key == null) {
return false;
}
if (type.contains("RSA") && zeroIv != null) {
return false;
}
if (data == null || data.length == 0) {
return false;
}
return true;
}
private static boolean isValid(Object[] ts, Object t) {
if (ts instanceof String[] && t instanceof String) {
String[] strs = (String[]) ts;
String tmp = (String) t;
for (String str : strs) {
if (tmp.equals(str)) {
return true;
}
}
}
if (ts instanceof Integer[] && t instanceof Integer) {
Integer[] ints = (Integer[]) ts;
Integer intT = (Integer) t;
for (Integer str : ints) {
if (intT == str) {
return true;
}
}
}
return false;
}
public static boolean pubCertValid() {
if (pubCert == null) {
return false;
}
try {
pubCert.checkValidity(new Date());
} catch (Exception e) {
System.err.println("证书失效" + e.getMessage());
e.printStackTrace();
return false;
}
return true;
}
public static String doBase64Encode(byte[] abc) throws UnsupportedEncodingException {
String abce = null;
abce = Base64.encode(abc);
return abce;
}
public static byte[] doBase64Decode(String abc) throws UnsupportedEncodingException {
byte[] abce = null;
abce = Base64.decode(abc);
return abce;
}
public static void getPublicKeyFile(Context context) {
if (publicFis == null) {
//获取解密公钥文件
publicFis = context.getResources().openRawResource(R.raw.rsacert);
try {
pubKey = getPubKey();
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}
}
}
}
Android 端常见的安全问题
组件安全
Activity访问权限的控制(可能会导致恶意调用页面,接收恶意数据)
- 1.私有Activity不应被其他应用启动且应该确保相对是安全的
- 2.关于Intent的使用要谨慎处理接收的Intent以及其携带的信息,尽量不发送敏感信息,并进行数据校验
- 3.设置android:exported属性,不需要被外部程序调用的组件应该添加android:exported=”false”属性
Activity劫持(正常的Activity界面被恶意攻击者替换上仿冒的恶意Activity界面进行攻击和非法用途。)
- 1.在登录窗口或者用户隐私输入等关键Activity的onPause方法中检测最前端Activity应用是不是自身或者是系统应用,如果发现恶意风险,则给用户一些警示信息,提示用户其登陆界面以被覆盖,并给出覆盖正常Activity的类名。
- 2.设置Activity的属性:可防止系统截屏
this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE)
Service劫持、拒绝服务以拒绝服务为例,常见的两种漏洞:
- 1.Java.lang.NullPointerException 空指针异常导致的拒绝服务
- 2.类型转换异常导致的拒绝服务ClassCastException
解决方案 - 1.应用内部使用的Service应设置为私有
- 2.针对Service接收到的数据应该验证并谨慎处理
- 3.内部Service需要使用签名级别的protectionLevel来判断是否未内部应用调用
Content Provider扮演着应用间数据共享桥梁的角色,其主要的安全风险为SQL注入和本地文件目录遍历
- 1.设置AndroidManifest.xml ,android:exported=”true” 属性
- 2.通过控制权限来限制对Content Provider的访问
WebView安全
- 1.Android API level 16以及之前的版本存在远程代码执行安全漏洞,该漏洞源于程序没有正确限制使用WebView.addJavascriptInterface方法,远程攻击者可通过使用Java Reflection API利用该漏洞执行任意Java对象的方法,简单的说就是通过addJavascriptInterface给WebView加入一个JavaScript桥接接口,JavaScript通过调用这个接口可以直接操作本地的JAVA接口。
- 2.Android系统通过WebView.addJavascriptInterface方法注册可供JavaScript调用的Java对象,以用于增强JavaScript的功能。但是系统并没有对注册Java类的方法调用的限制。导致攻击者可以利用反射机制调用未注册的其它任何Java类,最终导致JavaScript能力的无限增强。攻击者利用该漏洞可以根据客户端能力为所欲为。
密码软键盘安全
- 输入数据监听攻击
- 键盘截屏攻击
- 输入数据篡改攻击
- 未加密前篡改
- 来自系统底层的内存dump攻击
通讯安全
- 客户端是否使用了TSL/SSL通信协议进行加密通信传输。没有经过使用加密协议进行加密的网络通信可以轻易地进行MITM(中间人攻击)查看敏感数据。
- 客户端程序提交数据给服务端时,密码等关键字段是否进行了加密和校验,防止恶意用户嗅探和修改用户数据包中的密码等敏感信息。
- 客户端访问的URL页面是否仅能由手机客户端访问
策略安全
- 客户端是否限制登录尝试次数。防止木马使用穷举法暴力破解用户密码
- 客户端在一定时间内无操作后,是否会使会话超时并要求重新登录。超时时间设置是否合理。
- 若用户帐号被盗,攻击者会进行帐号登录,如果客户端没有帐号登录限制,那么攻击者登录帐号进行操作会变得没有障碍。而客户端若进行了多点登录检测,那么用户就可以通过登录把攻击者踢下线,妨碍攻击者进行账户操作
- 客户端程序在切换到后台或其他应用时,是否能恰当响应(如清除表单或退出会话),防止用户敏感信息泄露。
- 客户端的各个界面,测试是否存在敏感信息泄露问题
- 界面劫持是指当客户端程序调用一个activity组件来显示窗口时,被恶意的第三方程序探知,如果该界面组件式恶意程序预设的攻击对象,恶意程序立即启动自己的仿冒界面并覆盖在客户端程序界面之上。当发生界面劫持时,用户在无察觉的情况下将自己的账号、密码信息输入到仿冒界面中,恶意程序再将这些数据发送给攻击者。
安全问题的检测与预防
安全问题的检测
静态分析:
在不运行源码的情况下,通过IDA、apktool、dex2jar、jd-gui、smali2dex等静态分析工具对应用进行反编译,并对反编译后的java文件、xml文件等文件静态扫描分析,通过关键词搜索等静态方式将具有安全隐患的代码进行摘录并存入到检测平台后台,为后续的安全检测报告提供数据依据。
动态调试:
对应用软件安装、运行过程的行为监测和分析。检测的方式包括沙箱模型和虚拟机方式。
虚拟机方式通过建立与Android手机终端软件运行环境几乎一样的虚拟执行环境,手机应用软件在其中独立运行,从外界观察应用程序的执行过程和动态,进而记录应用程序可能表现出来的恶意行为。
人工检测:
1.企业招聘相关安全人员,对APP应用软件安装、运行过程的行为监测和分析,记录APP程序可能表现出来的风险行为。
2.使用第三方众测平台,帮助企业发现APP应用漏洞及风险。
安全问题的预防技巧
反调试
通常APP可能会被不法人员或者其他不法程序使用特定的方法进行调试、跟踪和劫持,被调试后的APP的一切操作和行为都可以被轻易的查看和修改。
- 上线前关闭调试模式android:debuggable=”false“
- 使用双进程守护实现反调试,我们知道通常一个进程只允许有一个调试器,所以在进程起来的同时,会FORK一个守护进程,并跟踪被保护的APP进程,它会一直存活,直到APP退出。在这期间一旦发现有不法调试行为发生,守护进程便会拦截调试器的入口,保证其他程序无法再调试当前APP。如果恶意关闭守护进程,则当前 APP也会随之关闭,已达到保护APP的目的。
反注入
注入是指当前 APP进程被其他程序使用特定方法动态插入不属于当前 APP的模块。然后恶意如收集 APP的隐私数据,并使用 Hook等方法劫持 APP的正常运行流程等(包括HOOK)。
- 针对Java Hook我们可以通过检查vm dex内存的fields域和method域来判断是否有java注入。
- 针对So Hook我们可以查看app 进程空间,查看加载的库是不是都在/system或/data/data/app底下,如果不是,则一定有注入。
混淆日志
我们在开发过程中大都是统一设置日志开关,上线时把日志关闭,其实这样做也不保险,因为逆向APP第一步动作就是先找Log的开关,找到开关之后,把它打开,然后跑一遍程序,就很容易找到隐私信息。建议对于安全性要求较高的APP,尽量还是把日志混淆或者干脆把所有的Log信息都去掉。
防篡改、签名校验
在发布前校验一下签名使用的key是否正确,以防被恶意第三方应用覆盖安装或者篡改等可使用下列命令检查:
jarsigner -verify -verbose -certs apk包路径若结果为“jar 已验证”,说明签名校验成功。
allowBackup
allowBackup,是Google提供了一个备份的功能,如果用户要换手机了,可以用它把APP导到另一个手机上,用户本身使用的一些数据(聊天记录)会跟着一起备份过去。除了备份使用外,它还带来了两个问题:
1. 恶意程序也可以利用这个备份,并把你备份的数据传到网上,在他的手机上打开。如果我们在做换手机操作过程中没有做全面的校验,那将会造成直接性的用户隐私信息泄露,用户的个人信息和隐私操作将会被暴露出来
2.这个备份的时候,会生成一个加密的文件。这个加密文件是可以被篡改的,而且你再利用这个备份文件导到另一个手机的时候,校验的时候可以直接过。就是你把备份导出来,病毒可以在导入文件加入恶意的代码或者其他的数据。你去进行备份的时候,相当于重新安装了,你在另外一个手机重新安装的时候,你重装的的APP上就携带着的病毒,后果非常危险。
APP完整性检验
为确保安装包不会在测试完成到最终交付过程中因为一些原因而发生文件损坏,需要对安装包进行完整性校验,通常做法是检查文件的md5值,而且一般可以通过自动化做校验。
数据加密
对于APP中需要存储的重要数据,一定要加密后再存储到数据库中。平时在于测试环节,一定要要求测试人员使用抓包工具对接口数据进行审阅,确保数据是加密后传输。
证书校验
严格来讲,我们要对数字证书合法性进行验证。即便使用了安全通信,例如HTTPS,我们也需要在客户端代码中对服务端证书进行合法性校验。测试中可以使用Fiddler、Charles等等工具模拟中间人攻击方法。如果客户端对于这些第三方工具的证书没有校验而能正常调用,则存在安全隐患。
权限问题
通常用户对自己的隐私问题十分敏感,为此,我们需要对APP申请某些特定权限的必要性进行检查,如访问通讯录等。对于没有必要的权限,一般都建议开发直接删除。切勿出现因为开发便利等原因,而盲目在配置文件中向用户索取过多无用权限的事情。