Android在开发过程中,要如何去保证安全性的?尤其是对于一些金融类的app,安全显得更为重要,以下是一些关于安全防护的建议和总结。
混淆机制
代码混淆
将代码的各种元素、如变量、函数、类的名字改写成无意义的名字。使代码变得难以阅读和理解。
资源混淆
混淆之后资源的名字全是简单的混淆字母,例如:
<string name=“a”>”#$Ges egg*</string>
很难通过关键字来查找代码。但是也不是没有办法,反编译之后的Java代码中,看到的获取资源值时,并不是资源的name值,而是资源对应的int值,例如:
f local = new f(this,1,getString(2131230929);
这些int值可以在反编译之后的res/values/public.xml中找到:
<public type=“string” name=“a” id=“0x0x7f0800d1”/>
这里2131230929对应的就是0x0x7f0800d1,那么就可以找到对应name即”a”值,其实public这里还是记录了完成了id和name的对应值。这么看来其实资源混淆作用不大,只是一个障眼法。其实资源混淆最大的作用是在减小apk包。
签名保护
每个应用都有唯一的签名,反编译之后是获取不到原来的签名的,但是可以用自己的签名进行二次打包,一些二次打包团队可能先反编译应用市场的包,再植入广告代码,然后重新签名打包发布。所以我们可以针对签名唯一性作一些防护措施。
策略:
在程序入口处进行签名验证,获取应用的签名值,如果和正确的签名值不一致,就立刻退出程序。当然这个签名验证代码也可能在被反编译之后直接删除,为了增强找出这个代码的难度,可以考虑将这个代码放入native中,不过,不管放在哪里,只要在代码中,就有可能被找出。
手动注册native方法
在Android中,当程序在Java层运行:
System.loadLibrary(“jnitest”)
时会去加载libjnitest.so文件,然后去.so文件的函数列表去查找JNI_OnLoad函数并执行。相对应的,当卸载程序时,默认去查找JNI_Unload函数并且执行。但是,这两个函数并不是强制要求的,也可以不实现。这两个函数主要是做初始化和善后的工作。
一般情况,在C组件中的JNI OnLoad函数实现给VM注册接口,以方便Vm可以快速的找到Java代码需要调用的C函数(JNI OnLoad还有一个功能,就是告诉C组件使用哪一个JNI版本,如果没有实现,默认是JNI 1.1)。
风险:
一般定义native方法的时候,对应的native层的函数名是:Java_类名_方法名,样式如下:
JNICALL Java_com_djm_protect_MainActivity_stringFromJNI(JNIEnv *env,jobject)
那么就会出现两个问题:
-
根据Java层的native方法名和返回类型,很容易就定位到对应的native函数;
-
攻击者得到这个so文件之后,查看它的方法参数和返回类型,然后在Java层写一个demo,直接去调用这个so文件的对应函数,如果有一个校验密码或者获取密码的方法是native的,那么就很容易获取到结果。
策略:
(1) 可以通过手动注册native的方法加强函数保护:
手动注册(*env) -> RegisterNatives(env,clazz,methods,methodsLength)
@param Clazz: 需要注册方法的类
@param methods:结构体,
定义如下: typedef struct{
const char* name; //java函数名字
const char* signature; //函数的参数和返回值
void* fnPtr; //函数指针,指向C函数
}JNINativeMethod;
//定义方法映射关系
Static JNINativeMethod method[] = {“isEquals”,”(Ljava/lang/String;)Z”,(void*)method}
(2) 定义jint JNI OnLoad(JavaVM* vm,void* reserved)
加载so时调用,执行上面的注册函数功能。
methodsLength = sizeof(methods) / sizeof(methods[0]);
if((*env)->RegisterNatives(env,clazz,methods,methodsLength) < 0){
LOGD(“RegisterNatives failed for ‘%s’,className);
return JNI_ERR;
}
(3) 定义JNI_OnUnload(JavaVM* vm, void* reserved)。
通过上面这三个方法就可以修改native的函数名,不用按照之前的格式来定义了, 可以增加破解者寻找关键native函数难度。其实也是一种混淆的手段。当然有经验的破解者,常规方法找不到native函数时,也会去JNI_OnLoad里面去分析汇编代码,找到register函数,分析注册方法结构体,找到对应的native方法。
以下是一个简单的例子:
java中调用一个native方法:
public native String native_method();
cpp中实现:
//对应的java中引用的方法
static jstring jni_method(JNIEnv *env, jobject /* this */) {
jstring str = env->NewStringUTF("hello ");
return str;
}
//定义方法映射
static JNINativeMethod gMethods[] = {"native_method", "()Ljava/lang/String;", (jstring *) jni_method};
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
jclass clazz;
if ((*vm).GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
clazz = (*env).FindClass("com/djm/testforprotectedandroid/MainActivity");
if (clazz == NULL) {
return result;
}
//注册native方法到Java
if ((*env).RegisterNatives(clazz,gMethods, sizeof(gMethods) / sizeof(gMethods[0]) < 0)){
return result;
}
return JNI_VERSION_1_4;
}
allowBackup属性
Android API 8及以上Android系统提供了为应用程序数据的备份和恢复功能,默认由AndroidManifest.xml中的allowBackup属性决定,该属性默认为true,当allowBackup属性为true时,用户可以通过adb backup 和 adb restore来对数据应用数据进行备份和恢复。
风险:
用户可以通过adb backup对应用数据备份,同时在另外的设备上安装同一个应用,并通过adb resotre恢复该备份数据,从而可以在另一个设备上恢复到被备份的应用程序的状态。
策略:
设置该属性值为false。
Android签名机制
基本概念
- 数据摘要 :对数据源进行操作之后得到一个摘要,即数据指纹。著名的算法有MD5和SHA系列算法。无论输入的消息长度多长,输出的长度都是固定,MD5-128位,SHA-1-160位。且具有不可逆性。
- 数字签名 :用私钥加密的数据摘要。
- 数字证书 :证书发布权威机构用自己的私钥对使用方的公钥进行加密生成数字证书,使用方将自己的数字证书发给另一方,另一方拿证书机构的公玥进行解密,如果发现内容不同,说明被篡改了。
签名流程
解压Android签名APK,可以得到一个文件夹META_INF,包含三个文件,如下:
CERT.RSA
CERT.SF
MANIFEST.MF
首先来看看第三个文件的内容:
Manifest-Version: 1.0
Name: AndroidManifest.xml
SHA1-Digest: BnuZbI6JNWcGmmaW2zKGB7dIyyk=
Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
第一二行指明了这是一个Manifest文件以及其版本。以下全是文件名和这个文件的SHA-1摘要值的base64表示值。
这里输入四个参数:java -jar signapk.jar xx.x5.9.pem xx.pk8 debug.apk debug.sign.apk
public static void main(String[] args) {
if (args.length != 4) {
System.err.println("Usage: signapk " +
"publickey.x509[.pem] privatekey.pk8 " +
"input.jar output.jar");
System.exit(2);
}
JarFile inputJar = null;
JarOutputStream outputJar = null;
try {
X509Certificate publicKey = readPublicKey(new File(args[0]));
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
PrivateKey privateKey = readPrivateKey(new File(args[1]));
inputJar = new JarFile(new File(args[2]), false); // Don't verify.
outputJar = new JarOutputStream(new FileOutputStream(args[3]));
outputJar.setLevel(9);
JarEntry je;
// MANIFEST.MF
**addDigestsToManifest的代码逻辑主要是:逐一遍历里面apk里面的所有条目,如果是一个文件就用SHA-1摘要提取该文件的摘要,然后进行base64位编码,作为“SHA-Digest”属性的值写入到MANIFEST.MF文件的一个块中,另外一个属性"Name"为该文件在apk包中的路径**
Manifest manifest = addDigestsToManifest(inputJar);
je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
// CERT.SF
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey);
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
**这里的工作就是将之前的MANIFEST.MF文件整个内容做一次SHA-1,放到Digest-Manifest字段中**
writeSignatureFile(manifest,
new SignatureOutputStream(outputJar, signature));
// CERT.RSA
je = new JarEntry(CERT_RSA_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(signature, publicKey, outputJar);
// Everything else
copyFiles(manifest, inputJar, outputJar, timestamp);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
} finally {
try {
if (inputJar != null) inputJar.close();
if (outputJar != null) outputJar.close();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
}
}
再来看看CERT.SF的内容:
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: lQ+7ZvCd0Q8xCRjEYYLjQyDUXaQ=
Name: AndroidManifest.xml
SHA1-Digest: UhETA8eTzLREWNZgdhkSKBXajuw=
Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: OPQCkzMXJVPQryHeMowVNZmfRMw=
Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: TSBGEIW1zN2n2sraHWcuRYSO8JU=
再看看代码:
private static void writeSignatureFile(Manifest manifest, OutputStream out)
throws IOException, GeneralSecurityException {
**对之前的MAINFEST>MF文件整个内容做一个SHA1放到SHA1-Digest-Manifest字段中。
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
BASE64Encoder base64 = new BASE64Encoder();
MessageDigest md = MessageDigest.getInstance("SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));
**遍历manifest变量的条目内容,然后进行SHA1算法计算,再用Base64计算一下。
其实就是对MAINFEST.MF文件中的每个条目内容做一次SHA,再保存一下即可。**
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));
sf.getEntries().put(entry.getKey(), sfAttr);
}
sf.write(out);
}
这里可以看到CERT.SF的工作主要如下:
- 计算MAINFSEST.SF文件的SHA1值,用Base64位编码记录在"SHA1-Digest-Manifest"的
属性值下; - 逐条计算MAINFEST.MF文件中的每一个块的SHA1,并经过Base64位编码后,记录在
CERT.SF同名块中,属性的名字是:“SHA1-Digest”
再看看CERT.RSA文件
3082 0437 0609 2a86 4886 f70d 0107 02a0
8204 2830 8204 2402 0101 310b 3009 0605
2b0e 0302 1a05 0030 0b06 092a 8648 86f7
0d01 0701 a082 02c1 3082 02bd 3082 01a5
a003 0201 0202 046f 2f5b b730 0d06 092a
...
上面是加密过的文件,用openssl命令可以查看具体的内容:
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text
具体内容如下:
Certificate:
Data:
Version: 3 (0x2) //版本号
Serial Number: 1865374647 (0x6f2f5bb7) //证书序列号
Signature Algorithm: sha256WithRSAEncryption //算法
Issuer: CN=Dudu //发行者名称
Validity
Not Before: Jun 26 07:09:51 2018 GMT //生效、截止日期
Not After : Jun 20 07:09:51 2043 GMT
Subject: CN=Dudu //主体名称
Subject Public Key Info:
Public Key Algorithm: rsaEncryption //算法
Public-Key: (2048 bit) //参数
Modulus: //密钥
00:9b:36:83:b6:30:a5:4e:e2:1a:c5:db:38:24:a3:
9e:85:0b:7b:08:cb:bc:ee:3c:8b:47:db:31:d6:4f:
...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
A6:4F:EB:85:61:CD:E3:ED:4F:37:0C:7A:E9:C7:98:28:69:4A:A8:D7
Signature Algorithm: sha256WithRSAEncryption //算法以及加密的hash值
91:e9:5a:e2:df:ee:af:c3:1b:99:eb:d7:8e:25:17:f6:88:99:
19:ff:51:c1:72:3e:83:89:f9:e0:d8:f9:a3:8a:09:0d:ce:c8:
...
这里会把之前生成的CERT.SF文件用私钥计算出签名,然后将签名以及包含公玥信息
的数字证书一同写入CERT.RSA保存。
代码如下:
private static void writeSignatureBlock(
Signature signature, X509Certificate publicKey, OutputStream out)
throws IOException, GeneralSecurityException {
SignerInfo signerInfo = new SignerInfo(
new X500Name(publicKey.getIssuerX500Principal().getName()),
publicKey.getSerialNumber(),
AlgorithmId.get("SHA1"),
AlgorithmId.get("RSA"),
signature.sign());
PKCS7 pkcs7 = new PKCS7(
new AlgorithmId[] { AlgorithmId.get("SHA1") },
new ContentInfo(ContentInfo.DATA_OID, null),
new X509Certificate[] { publicKey },
new SignerInfo[] { signerInfo });
pkcs7.encodeSignedData(out);
}
优化
发现CEET.SF和MAINFEST.MF这两个文件中的内容的那么字段都是资源的名字,所以如果资源名称很长的话,会导致这两个文件比较大,所以这里可以针对资源名称做一个优化。
Android 为何使用这种签名机制
1. 如果改变了任何apk的文件,那么在校验时就会发现与MAINFEST.MF的校验信息不同,验证失败,程序不能成功安装。
2.如果更改了文件之后,计算新的摘要值,替换MAINFEST.MF中对应的属性值,那与CERT.SF中计算的摘要值不一样,同样会验证失败;
3.如果计算改变CERT.SF中的摘要值,那么数字签名值也必定和CERT.RSA中的不同,同样会验证失败;
4.由于不知道数字证书的私钥,所以无法伪造数字签名,要让应用程序在Android设备上安装,只能重新签名。 通过签名校验可以发现伪造的apk。
dex文件格式
将apk解压之后可以看到如下几个文件:
AndroidManifest.xml
assets
classes.dex
lib
META-INF
res
resources.arsc:资源混淆的数据格式
dex的文件格式如图所示:
header(文件头) |
---|
string_ids(字符串的索引) |
type_ids(类型的索引) |
proto_ids(方法原型的索引) |
fields_ids(域的索引) |
method_ids(方法的索引) |
class_defs(类的定义区) |
data(数据区) |
link_data(链接数据区) |
Android应用加固原理
将源apk进行加密,并且和壳apk进行合并得到新的dex文件,最后替换掉壳apk的dex文件,得到新的apk。这个壳apk的主要工作就是解密源apk,并且加载apk让其正常运行。
如图所示:
先看看dex文件:
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
...
dex文件头部结构图:
address | name | size / byte | value |
---|---|---|---|
0 | Magic[8] | 8 | cafe babe 0000 0034 |
8 | checksum | 4 | |
C | Signature[20] | 20 | |
20 | file_size | 4 |
checkSum: 文件校验码
signature:唯一识别本文件
file_size:dex文件的大小。
合并时修改三者的值。
加固的大致流程:
加固所需的两个文件:
- 源Apk
- 脱壳Apk(解密源Apk和加载Apk)
第一步:
对源apk进行加密,并且和壳Apk的dex文件进行合并成新的dex,并且在最后追加4个字节用于存储源apk的长度,对应的修改文件头、SHA1头以及checkSum。
File payloadSrcFile = new File("myApk.apk"); //源apk
File unShellDexFile = new File("myApk.dex"); //源dex
byte[] payloadArray = encrypt(payloadSrcFile); //对源apk以二进制形式读出并且加密处理
byte[] unShellDexArray = readFile(unShellDexFile); //以二进制形式读出dex
int totalLen = payloadArray.length + unShellDexArray.length + 4 ; //多出四个字节用于存放源apk长度
byte[] newDex = new byte[totalLen];
System.arraycopy(unShellDexArray,0,newDex,0,unShellDexArray.length); //拷贝dex内容
System.arraycopy(payloadArray,0,newDex,unShellDexArray.length,payloadArray.length); //后面追加apk
System.arraycopy(intToByte(payloadArray.length),0,newDex,totalLen - 4,4);
fixFileSizeHeader(newDex); //修改dex file size的头
fixSHA1Header(newDex); //修改Dex SHA1文件头
fixCheckSumHeader(newDex); //修改Dex CheckSum文件头
第二步:
从壳apk的dex中获取源程序Apk进行解密,然后加载;
1.从脱壳Apk中找到源程序Apk,并且进行解密操作
//创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录
File odex = this.getDir("payload_odex", MODE_PRIVATE);
File libs = this.getDir("payload_lib", MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo", "apk size:"+dexFile.length());
if (!dexFile.exists())
{
dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk
// 读取程序classes.dex文件
byte[] dexdata = this.readDexFileFromApk();
// 分离出解壳后的apk文件已用于动态加载
this.splitPayLoadFromDex(dexdata);
2.加载解密之后的源程序Apk
//配置动态加载环境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//获取主线程对象
String packageName = this.getPackageName();//当前apk的包名
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)
DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,
libPath, (ClassLoader) RefInvoke.getFieldOjbect(
"android.app.LoadedApk", wr.get(), "mClassLoader"));
//base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?
//把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
Log.i("demo","classloader:"+dLoader);
try{
Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");
Log.i("demo", "actObj:"+actObj);
}catch(Exception e){
Log.i("demo", "activity:"+Log.getStackTraceString(e));
上面就是加固的大致流程,详细的过程更加复杂。
Android中常见漏洞分析
解压文件漏洞分析
场景
Android中经常会用到解压缩文件,比如动态加载,可能需要下载zip文件,然后在本地做解压工作,或者有些资源文件占据apk包的大小,就会放在服务端,需要时再去下载,本地做解压工作。Android中解压zip文件使用的是ZipInputStream和ZipEntry类,代码如下:
public void unZipFolder(String zipFileString,String outPathString){
ZipInputStream inZip;
FileOutputStream out;
try {
inZip = new ZipInputStream(new FileInputStream(zipFileString));
ZipEntry zipEntry;
String szName;
File outFile = new File(outPathString);
if(!outFile.exists()){
outFile.mkdir();
}
while ((zipEntry = inZip.getNextEntry()) != null){ //遍历zip文件中的所有zipEntry,zip文件中的子文件名格式没有格式要求,可以包含特殊字符。但是在PC中文件名是有限制的。
szName = zipEntry.getName();
if(zipEntry.isDirectory()){
szName = szName.substring(0,szName.length() - 1);
File folder = new File(outPathString + File.separator + szName);
folder.mkdirs();
}else{
//把zip中的文件解压到本地
File file = new File(outPathString + File.separator + szName);
file.createNewFile();
out = new FileOutputStream(file);
int len;
byte[] buffer = new byte[1024];
while ((len = inZip.read(buffer)) != -1){
out.write(buffer,0,len);
out.flush();
}
}
inZip.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
使用ZipOutputStream类进行压缩文件,对与文件名不做限制,压缩代码如下:
public void zipFolder(String srcPath,String zipFileName){
File file = new File(srcPath);
File zipFile = new File(zipFileName);
if(!file.exists()){
throw new RuntimeException(srcPath + "文件不存在");
}
try {
FileOutputStream outputStream = new FileOutputStream(zipFile);
CheckedOutputStream cos = new CheckedOutputStream(outputStream,new CRC32());
ZipOutputStream out = new ZipOutputStream(cos);
String baseDir = "";
BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file));
//这里的名称不受限制,这个名称就是zip文件中的文件名,如果zip包被拦截,然后进行修改,就可以把恶意文件写入到zip中。
ZipEntry entry = new ZipEntry(baseDir + "zip" + file.getName());
out.putNextEntry(entry);
int count;
byte[] data = new byte[1024];
while ((count = inputStream.read(data,0,1024)) != -1){
out.write(data,0,count);
}
inputStream.close();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
漏洞分析
因为文件名没有限制,如果攻击者把恶意文件名称命名为:…/…/…/data/data/xxx.xxx.x/hacker.dex,由于在Linux系统中,…/表示回到上一级目录,所以这里可以通过这个…/将恶意文件写入到应用内沙盒目录中。
风险
假如知道了应用的沙盒数据信息,一些隐私信息存放在SharePreferences.xml中,可以利用这个漏洞,将恶意文件名称改成:…/…/…/data/data/xxx.xxx.xx/shared_pre/info.xml,那么ZipEntry进行解压时,就会直接解压释放到本地,所以相当于替换了本应用的沙盒数据。
漏洞修复
解压时加上判断,不能有特殊字符,代码如下:
while ((zipEntry = inZip.getNextEntry()) != null){ //遍历zip文件中的所有zipEntry
szName = zipEntry.getName();
if(szName.contains("../")){
continue;
}
另外的防御方法就是下载时用HTTPS协议结合文件的MD5比对功能等措施。Google关于漏洞的修复提示:
Security note:Entry names can represent relative paths.foo/../bar or ../bar/baz,for example,if the entry name is being used to construct a filename or as a path component,it must be validated or sanitized to ensure that files are not written outside of the intended destination directory.
录屏授权漏洞
漏洞分析
Android5.0新增了一个API来进行屏幕录制,如果允许了恶意应用进行录屏,可能会在后台监听,当启动了金融或者银行类软件时,就去对屏幕进行录制,就可以记住用户的用户密码等信息。
漏洞场景
当需要录制视频时,会弹出默认的一个对话框,例如:xxx应用将开始截取你屏幕上显示的所有内容。如果xxx很长很长,那么会以滚动条的形式显示,如果用户没有滚动到底部直接授权,就会给恶意软件可乘之机。
漏洞修复
Google在6.0之后修复了这个问题,当内容很长时,采用省略号,把重要信息显示出来。但为了安全起见,我们还是需要在重要界面增加以下属性:
WindowManager.LayoutParams.FLAG_SECURE
可以防止屏幕被截图和录制。