Win10一个命令让你掌握笔记本/平板电池健康状况

使用笔记本以及平板设备的朋友应该对设备电池健康状况比较关心,Win10系统中就提供了可查看设备电池详细使用报告的方法,感兴趣的朋友可以尝试一下。其实这项功能在Win7中就已存在,在Win8中进一步完善,报告也更加详细。

• 以管理员身份运行CMD,输入下面的命令回车,系统就会在你指定的路径生成设备电池使用报告。

powercfg /batteryreport /output “C:\battery_report.html”

• 双击C:\battery_report.html会用你默认的浏览器打开该报告,一起来简单了解一下:

▲系统以及电池基本信息,包括电池设计容量、完全充电容量、充电周期

▲设备电池完全充电容量变化

▲最近三日电池使用情况记录,包括何时处于活动状态以及何时进入待机状态

▲最近三日电池使用变化曲线

▲电池续航预估值变化,这里的数据比你在任务栏电池图标上看到的要准确

这份报告会详细记录自系统安装后电池的使用详情,根据这些数据可掌握电池续航变化情况。

JS判断是否在微信浏览器打开的简单实例(推荐)

这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!

JavaScript 客户端脚本语言

Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。


下面小编就为大家带来一篇 JS 判断是否在微信浏览器打开的简单实例 (推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧

最近做很多 HTML5 的项目,很多页面会通过微信微博等 SNS 分享出去。在分享页面上提供公司 APP 的下载。但是在很多应用的浏览器中,点击下载链接无法下载应用。那么针对这些浏览器我们需要给用户提示从 safari 或者系统自带的浏览器打开分享页面。通过 js 就可以判断当前页面是在什么浏览器打开的。

以下是一段示例代码,注释中表明了通过 JS 如何判断是否在微信浏览器打开,是否在 QQ 空间浏览器,是否在新浪微博打开。当然可以做得更完善一点,再加上判断是在移动设备打开还是在 PC 端浏览器打开的,更加细分一点,可以判断是在安卓系统的浏览器打开的还是 IOS 系统浏览器打开的。

  1.  
  2. if (browser.versions.mobile) {//判断是否是移动设备打开。browser代码在下面
  3. var ua = navigator.userAgent.toLowerCase();//获取判断用的对象
  4. if (ua.match(/MicroMessenger/i) == “micromessenger”) {
  5. //在微信中打开
  6. }
  7. if (ua.match(/WeiBo/i) == “weibo”) {
  8. //在新浪微博客户端打开
  9. }
  10. if (ua.match(/QQ/i) == “qq”) {
  11. //在QQ空间打开
  12. }
  13. if (browser.versions.ios) {
  14. //是否在IOS浏览器打开
  15. }
  16. if(browser.versions.android){
  17. //是否在安卓浏览器打开
  18. }
  19. } else {
  20. //否则就是PC浏览器打开
  21. }

再附上 browser 的代码,通过以下方法可以判断很多浏览器。包括判断 IE 浏览器,Opera 浏览器,苹果浏览器,谷歌浏览器,火狐浏览器等。

  1. var browser = {
  2. versions: function() {
  3. var u = navigator.userAgent,
  4. app = navigator.appVersion;
  5. return { //移动终端浏览器版本信息
  6. trident: u.indexOf(‘Trident’) > 1,
  7. //IE内核
  8. presto: u.indexOf(‘Presto’) > 1,
  9. //opera内核
  10. webKit: u.indexOf(‘AppleWebKit’) > 1,
  11. //苹果、谷歌内核
  12. gecko: u.indexOf(‘Gecko’) > 1 && u.indexOf(‘KHTML’) == 1,
  13. //火狐内核
  14. mobile: !!u.match(/AppleWebKit.*Mobile.*/),
  15. //是否为移动终端
  16. ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
  17. //ios终端
  18. android: u.indexOf(‘Android’) > 1 || u.indexOf(‘Linux’) > 1,
  19. //android终端或uc浏览器
  20. iPhone: u.indexOf(‘iPhone’) > 1,
  21. //是否为iPhone或者QQHD浏览器
  22. iPad: u.indexOf(‘iPad’) > 1,
  23. //是否iPad
  24. webApp: u.indexOf(‘Safari’) == 1 //是否web应该程序,没有头部与底部
  25. };
  26. } (),
  27. language: (navigator.browserLanguage || navigator.language).toLowerCase()
  28. }

以上这篇 JS 判断是否在微信浏览器打开的简单实例 (推荐) 就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持 phperz。

基于JS判断iframe是否加载成功的方法(多种浏览器)

这篇文章主要介绍了基于 JS 判断 iframe 是否加载成功的方法【多种浏览器】的相关资料, 需要的朋友可以参考下

Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。

推荐阅读:

在项目中经常要动态添加 iframe,然后再对添加的 iframe 进行相关操作,而往往 iframe 还没添加完呢,后边的代码就已经执行完了,所以有些你写的东西根本没有显示出来。这时,我们就要考虑是否可以等 iframe 加载完后再执行后边的操作,当然,各种浏览器早就为我们考虑到啦,看下面:

ie 浏览器

IE 的每个 elem 节点都会拥有一个 onreadystatechange 事件,这个事件每次在 elem 内容发送变化的时候触发,比如内容正在载入 loading 会触发,内容载入完毕 loaded 会触发,内容载入成功 complete 会触发,这个函数还需要配合 readyState,这是 ie 上每个 elem 都拥有的属性,用来查看每次触发时候的状态。

  1. //先为iframe 添加一个 onreadystatechange
  2. iframe.attachEvent(“onreadystatechange”,
  3. function() {
  4. //此事件在内容没有被载入时候也会被触发,所以我们要判断状态
  5. //有时候会比较怪异 readyState状态会跳过 complete 所以我们loaded状态也要判断
  6. if (iframe.readyState === “complete” || iframe.readyState == “loaded”) {
  7. //代码能执行到这里说明已经载入成功完毕了
  8. //要清除掉事件
  9. iframe.detachEvent(“onreadystatechange”, arguments.callee);
  10. //这里是回调函数
  11. }
  12. });

其他浏览器(Firefox,Opera,chrome 等 )

在其他非 IE 的浏览器上 Firefox,Opera,chrome 等 iframe 都会拥有一个 onload 事件,此事件只要触发就说名内容已经加载完毕。

  1. iframe.addEventListener(“load”,
  2. function() {
  3. //代码能执行到这里说明已经载入成功完毕了
  4. this.removeEventListener(“load”, arguments.call, false);
  5. //这里是回调函数
  6. },
  7. false);

综合一下

  1. if (iframe.attachEvent) {
  2. iframe.attachEvent(“onreadystatechange”,
  3. function() {
  4. //此事件在内容没有被载入时候也会被触发,所以我们要判断状态
  5. //有时候会比较怪异 readyState状态会跳过 complete 所以我们loaded状态也要判断
  6. if (iframe.readyState === “complete” || iframe.readyState == “loaded”) {
  7. //代码能执行到这里说明已经载入成功完毕了
  8. //要清除掉事件
  9. iframe.detachEvent(“onreadystatechange”, arguments.callee);
  10. //这里是回调函数
  11. }
  12. });
  13. } else {
  14. iframe.addEventListener(“load”,
  15. function() {
  16. //代码能执行到这里说明已经载入成功完毕了
  17. this.removeEventListener(“load”, arguments.call, false);
  18. //这里是回调函数
  19. },
  20. false);
  21. }

注意:上面的函数必须放在 iframe 被 appendChild 到 body 后,否则不会被触发

以上内容是小编给大家介绍的 JS 判断 iframe 是否加载成功的方法,希望对大家有所帮助!

AndroidKiller:解决高版本APK编译错误

1、反编译卡死

卡死在这里,解决方法参考:
https://www.52pojie.cn/thread-658341-1-1.html

2、反编译成功,无法回编
>W: E:\移动安全\静态分析反编译\反编译工具\Androidkiller\projects\xiongdi\Project\AndroidManifest.xml:3: error: No resource identifier found for attribute ’roundIcon’ in package ‘android’
>W:
>brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1): [

android 7.1api level 25)有一个新特性,就是圆形桌面Icon,对应的是在AndroidManifest.xmlapplication节点配置:android:roundIcon=”@mipmap/ic_launcher_round” 。
反编译后,在AndroidManifest.xml文件中将此属性删除再次回编译即可。

3、apktool版本太旧
Exception in thread mainbrut.androlib.AndrolibException: Could not decode 这个问题,就是apktool.jar比较老旧的问题。
apktools.jar下载官网:https://ibotpeaches.github.io/Apktool/install/  对应平台下载,更新到apktool管理器,图片下方选择好默认的apktool版本。

逆向某直播app全程

  • 抓包

逆向原因不表。第一步当然是从抓包开始,电脑打开fiddler,设置手机wifi代理为电脑局域网ip,端口8888。打开app,抓到几个请求,其中获取直播列表请求如下:

  1. POST http://xxx.xxxx.cn/mapi/index.php HTTP/1.1
  2. Host: xxx.xxxx.cn
  3. Connection: keep-alive
  4. Accept-Encoding: gzip, deflate
  5. Accept: */*
  6. User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.1; E6683 Build/32.4.A.1.54)
  7. X-JSL-API-AUTH: sha1|1515724202|pRQCJVbTdAI6|e91cd7a62631b4426175b2ca4f77948f0f350fd9
  8. Cookie: client_ip=143.97.168.92; user_id=464890; __jsluid=c13ebd1234964c11ds9802a5b24sd6afc; nick_name=%e7%bb%ad%e5%91%bd; $Path=/; user_pwd=0091ea2a881c56fae1sece697510ec80; PHPSESSID2=9eirdal3p5q05ppdkj7uj767h5; PHPSESSID=5rdo1e11mk3bjctoaaeksmun63; $Domain=69.9vtao.cn
  9. Content-Length: 342
  10. Content-Type: application/x-www-form-urlencoded
  11. itype=&i_type=1&sdk_version_name=2.5.12&requestData=apwlYyV6t85HEc7wCgrb41qkcD0E%2F%2BMKEQcyaZzX0Xsob1xZKtUfh5P6PIGRHOPzocvGjqyDRcsW11TC7ZCJwRgLMmmew5%2FKTmYY9M3Pl8u2ohlBQRCkuR4YmL9%2FzTIQt2ieybEsLPh3Fa2ZC2EBmJwNqq0NtO3L5%2Fq2nxi%2FzX4L2xzFsL5dnJHMLD4jQxwFwWJs98OWS91ImakLf7MXqIhOoZEhslBzqHkVtFRq4TMHMhdgmQPyGaoRTXEXd%2BRR&act=index&ctl=index

出于隐私考虑,Host及Cookie值并非实际值,有修改。

响应(output字段太长,已省略。):

  1. HTTP/1.1 200 OK
  2. Date: Fri, 12 Jan 2018 02:25:12 GMT
  3. Content-Type: text/html; charset=utf-8
  4. Connection: keep-alive
  5. Vary: Accept-Encoding
  6. Vary: Accept-Encoding
  7. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  8. X-Cache: bypass
  9. Content-Length: 128941
  10. {"output":"5x2AZX2LDCzk2qFiaD1E4UmfLy1LzPnHKMuEHGkthendVDWGGuJZP16YLxvyhhIsKu7T\/4IRa0S1cc3pJRf0UZsiBO1aNHjw4P3eCYxH2AX0GQRfW39pmQhCdOj2klek..."}

很显然,请求和响应加密了,只能反编译app寻找加密方法。

  • 反编译

使用apktool解包apk,得到资源文件和classes.dex文件,使用dex2jar工具反编译得到jar包,使用jd-gui工具查看并导出jar包源码,使用编辑器或ide浏览源码。apk没有加固,顺利拿到源码。

jd-gui需要java8环境,java9会出错,真替java捉急。

尝试全局搜索请求参数关键字"requestData"(不加引号结果太多,加引号就只搜索到出现该字符串的代码了),找到AppHttpUtil.java关键部分代码如下,参数跟抓到的相符,说明发起请求的逻辑正是此处。

  1. for (localObject1 = localObject2;; localObject1 = AESUtil.encrypt(SDJsonUtil.object2Json(localObject1), ApkConstant.getAeskey()))
  2. {
  3. if (i != 0)
  4. {
  5. localRequestParams.addBodyParameter("requestData", (String)localObject1);
  6. localRequestParams.addBodyParameter("i_type", String.valueOf(i));
  7. localRequestParams.addBodyParameter("ctl", (String)localObject3);
  8. localRequestParams.addBodyParameter("act", str);
  9. localRequestParams.addBodyParameter("itype", AppRequestParams.getItypeParams());
  10. localRequestParams.addBodyParameter("sdk_version_name", SDPackageUtil.getVersionName());
  11. }
  12. // 省略
  13. }

AESUtil.encrypt显示这是一个AES加密,找到AESUtil.encrypt方法的代码:

查看AppHttpUtil.java文件顶部的import xxx.AESUtil行,按照import路径即可找到AESUtil.java文件。下面跳转到代码都用这个方法,如果没有import 这行,说明这个类跟当前类在同一个目录下。

  1. public static String encrypt(String paramString1, String paramString2)
  2. {
  3. Object localObject2 = null;
  4. Object localObject1 = null;
  5. try
  6. {
  7. paramString1 = paramString1.getBytes("UTF-8");
  8. paramString2 = new SecretKeySpec(paramString2.getBytes("UTF-8"), "AES");
  9. Cipher localCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  10. localCipher.init(1, paramString2);
  11. paramString1 = localCipher.doFinal(paramString1);
  12. paramString2 = (String)localObject1;
  13. if (paramString1 != null) {
  14. paramString2 = Base64.encodeToString(paramString1, 0);
  15. }
  16. return paramString2;
  17. }
  18. // 省略
  19. }

AES加密方式为AES/ECB/PKCS5Padding,再看上面的调用方式,加密key为ApkConstant.getAeskey(),找到该类:

  1. public static final String AES_KEY = SDResourcesUtil.getString(2131165252) + "000000";
  2. private static String AES_KEY_DYNAMIC = "";
  3. public static String getAeskey()
  4. {
  5. if (!TextUtils.isEmpty(AES_KEY_DYNAMIC)) {
  6. return AES_KEY_DYNAMIC;
  7. }
  8. return AES_KEY;
  9. }

这里使用了AES_KEY_DYNAMIC字段,如果没有则使用默认的AES_KEY字段,说明服务端可能会动态更新AES_KEY_DYNAMIC。我们先用默认的AES_KEY试试可不可行。

AES_KEY获取方式为SDResourcesUtil.getString(2131165252),根据名字猜测是获取资源文件值,2131165252是资源id,全局搜索一下这个id,在R.java中找到下面一行定义:

由于编译优化的原因,常量名会直接替换为本来的数值。方法的局部变量命名很奇怪也是如此,编译后不存在局部变量的名字,名字都是反编译工具生成的。

  1. public static final int app_id_tencent_live = 2131165252;

百度搜索一下R.java,出来的是安卓资源文件相关的结果,说明这个值在资源文件中定义。打开解包apk后的res文件夹,全局搜索app_id_tencent_live,在strings.xml文件中找到了该值:

  1. <string name="app_id_tencent_live">1400057185</string>

那么AES_KEY="1400057185" + "000000",也即AES加密KEY为1400057185000000

  • AES加密

找一个在线AES加密网站验证一下结果是否正确,设置参数如下:

  1. 加密模式:ECB
  2. 填充:pkcs5padding
  3. 密码:1400057185000000
  4. 字符集: utf8

粘贴上面抓包请求中的requestData字段值,注意上面是urlencode后的,可以直接从fiddler的WebFroms视图里复制原始值。点击解密,得到结果如下:

  1. {"screen_width": 1080, "sdk_type": "android", "sdk_version_name": "2.5.12", "sex": 0, "p": 1, "sdk_version": 2017122401, "cate_id": 0, "act": "index", "ctl": "index", "screen_height": 1776}

说明我们的猜测是对的,通信协议加密搞定。粘贴响应结果的output字段,点击解密,同样得到了原文(结果这里就不贴了,是一个json结构,返回了直播列表,包含房间id等信息)。

观察该请求的参数,并没有什么变量(screen_width跟设备相关,sdk_version跟app版本相关,app升级后可能会变),直接照抄参数即可模拟该请求。

python可使用pycrypto库实现AES加/解密,由于该库安装需要编译,而且不自动处理padding,比较麻烦,这里直接使用上面AES网站的API来做AES加/解密操作。fiddler抓包如下:

  1. Method:
  2. POST http://tool.chacuo.net/cryptaes
  3. WebForms:
  4. type: aes
  5. data: test
  6. arg: m=ecb_pad=pkcs5_block=128_p=1400057185000000_o=0_s=utf-8_t=0
  7. Returns:
  8. {"status":1,"info":"ok","data":["3KLa9xdRIRq4WVy7Sr3\/Ew=="]}

解密协议相同,只是arg参数变为以t=1结尾。python调用代码如下:

  1. def aes_encrypt(text):
  2. params = {
  3. 'data': text,
  4. 'type': 'aes',
  5. 'arg': 'm=ecb_pad=pkcs5_block=128_p=%s_i=_o=0_s=utf-8_t=0' % conf['aeskey']
  6. }
  7. r = requests.post('http://tool.chacuo.net/cryptaes', data=params)
  8. return r.json()['data'][0]
  9. def aes_decrypt(text):
  10. params = {
  11. 'data': text,
  12. 'type': 'aes',
  13. 'arg': 'm=ecb_pad=pkcs5_block=128_p=%s_i=_o=0_s=utf-8_t=1' % conf['aeskey']
  14. }
  15. r = requests.post('http://tool.chacuo.net/cryptaes', data=params)
  16. return r.json()['data'][0]
  • sign签名

接下来寻找获取房间信息的请求,先在手机上请求,fiddler抓包,报文跟直播列表请求结构相似,解密requestData得到原文:

  1. {"screen_width": 1080, "sdk_type": "android", "sdk_version_name": "2.5.12", "sign": "65c65aefb02040eebca7127576c47a2c", "is_vod": 0, "sdk_version": 2017122401, "room_id": 172084, "act": "get_video2", "ctl": "video", "screen_height": 1776}

解密响应包,内容是房间的具体信息,包含了一个flv链接的直播流,使用支持流媒体的播放器打开这个链接可直接观看。

注意多了一个sign字段,有签名验证,没有想象中顺利。尝试搜索请求参数get_video2(其他重复率可能比较低的参数也行,多尝试),找到CommonInterface.java代码如下:

  1. public static SDRequestHandler requestRoomInfo(int paramInt1, int paramInt2, String paramString, AppRequestCallback<App_get_videoActModel> paramAppRequestCallback)
  2. {
  3. AppRequestParams localAppRequestParams = new AppRequestParams();
  4. localAppRequestParams.putCtl("video");
  5. localAppRequestParams.putAct("get_video2");
  6. localAppRequestParams.put("room_id", Integer.valueOf(paramInt1));
  7. localAppRequestParams.put("is_vod", Integer.valueOf(paramInt2));
  8. localAppRequestParams.put("private_key", paramString);
  9. paramString = AppRuntimeWorker.getSdkappid();
  10. String str = AppRuntimeWorker.getLoginUserID();
  11. localAppRequestParams.put("sign", MD5Util.MD5(paramString + str + paramInt1));
  12. return AppHttpUtil.getInstance().post(localAppRequestParams, paramAppRequestCallback);
  13. }

可以看到sign参数为MD5Util.MD5(paramString + str + paramInt1)
strAppRuntimeWorker.getLoginUserID(),根据名字知道这是用户id,用户id当然在cookie中,查看最开始抓到的请求中,cookie头有user_id字段,就是该值。
结合localAppRequestParams.put("room_id", Integer.valueOf(paramInt1))这行,知道paramInt1为room_id,即房间id,在直播列表请求返回的结果中,有room_id字段。
paramStringAppRuntimeWorker.getSdkappid()

转到AppRuntimeWorker.getSdkappid方法代码:

  1. public static String getSdkappid()
  2. {
  3. InitActModel localInitActModel = InitActModelDao.query();
  4. if (localInitActModel == null) {
  5. return null;
  6. }
  7. return localInitActModel.getSdkappid();
  8. }

sdkappid是从InitActModel中读取的,找到InitActModel类,发现是一个纯数据类,sdkappid是外部设置的。尝试全局搜索setSdkappid方法,找到设置该值的地方,结果略多,没有找到有用的代码。

变换思路,查看InitActModelDao代码:

  1. public class InitActModelDao
  2. {
  3. public static void delete()
  4. {
  5. JsonDbModelDao.getInstance().delete(InitActModel.class);
  6. }
  7. public static boolean insertOrUpdate(InitActModel paramInitActModel)
  8. {
  9. boolean bool = JsonDbModelDao.getInstance().insertOrUpdate(paramInitActModel);
  10. HostManager.getInstance().saveActHost();
  11. return bool;
  12. }
  13. public static InitActModel query()
  14. {
  15. return (InitActModel)JsonDbModelDao.getInstance().query(InitActModel.class);
  16. }
  17. }

看上去是orm代码,继续查看JsonDbModelDao代码,从代码风格可以确定是orm了。代码不多,看到该行:

  1. DbManagerX.getDb().selector(JsonDbModel.class)

说明数据库初始化部分在DbManagerX类里,查看DbManagerX代码,发现下面这行:

  1. private static final DbManager.DaoConfig configDefault = new DbManager.DaoConfig().setDbName("fanwe.db")

数据库文件为fanwe.db,应该是一个sqlite数据库。可能是内置于apk里的,去apk解包后的文件夹里查找该文件,并没有找到。那就说明数据库文件是apk运行后创建的,那么sdkappid也可能是动态设置的,由服务端返回。

回到fiddler,查看之前抓到的请求,对每个请求结果都解密,查找sdkappid关键字。在一个包含ctl=app_init参数的请求中找到了该值"sdkappid": "1400057185",结果跟上面的aes_key是同一个值,what the fuck…

这里也可以直接去手机里找fanwe.db文件, 但是我用的手机并未root,无法查看app私有数据目录。实在找不到的情况下,可以用安卓虚拟机去运行app再找到数据库文件。

现在用于签名的三个参数都已经找到,我们使用上面的room_id 172084,结合userid和sdkappid,找个在线的md5网站,计算md5值,得到结果65c65aefb02040eebca7127576c47a2c,跟sign字段一致,签名验证搞定。本来还以为它的MD5Util.md5方法还要在加个什么key之类的…

现在房间信息请求也搞定了,然而我看到了请求头有一个X-JSL-API-AUTH的特殊的头,就知道事情并没有那么简单。

  • HTTP头

抓包请求中有如下一个http头,使用fiddler的Reissue and Edit功能重发请求,并编辑,去掉该头,发现返回了一个html页面,提示请求被拦截,说明该头是用来验证请求的有效性。

  1. X-JSL-API-AUTH: sha1|1515583942|EgloVUHapki7|e4abc467fdfb98d72c0c881329b1450c301b2d26

发现每次请求这个头的值都不一样,并且第二个参数是一串数字,看起来是时间戳,猜测这个头每次请求会生成,并且具有有效期。特意等待几分钟后,使用fiddler重新发起该请求,返回结果提示拦截,果然如此。

在源码中搜索X-JSL-API-AUTH关键字,找到关键代码:

  1. Object localObject1 = new StringBuilder(paramSDRequestParams.getUrl());
  2. localRequestParams.addHeader("X-JSL-API-AUTH", getToken(((StringBuilder)localObject1).toString()));
  3. //省略
  4. public static String getToken(String paramString)
  5. {
  6. String str = getStringRandom();
  7. long l = getSysTime();
  8. paramString = getStrToSHA("sha1|09969cd379a84835b6fece3c8d327bf1|" + l + "|" + str + "|" + paramString);
  9. return "sha1|" + l + "|" + str + "|" + paramString;
  10. }

实现逻辑比较简单,拼接字符串sha1|09969cd379a84835b6fece3c8d327bf1|、时间戳、一个随机字符串、要请求的url得到加密用原文,求sha1值,作为签名,然后拼接sha1、时间戳、随机字符串、以及签名即可得到该头部值。

python代码实现如下:

  1. def token():
  2. l = str(int(time.time()) + 300)
  3. string = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12))
  4. sign = sha1("sha1|09969cd379a84835b6fece3c8d327bf1|" + l + "|" + string + "|" + conf['url']).hexdigest()
  5. return "sha1|" + l + "|" + string + "|" + sign

事后发现,这里有个坑,脚本在我电脑上运行正常,在服务器上运行一直返回请求被拦截,开始以为是ip或cookie的问题,调试了好久发现,上面签名用的时间戳不是当前时间,而是失效时间。我自己电脑的时间比较快,所以坑了…解决方法也很简单,取当前时间加上5分钟就好。

猜测很可能会验证User-Agent头,试了下果然如此, 请求需要伪造User-Agent头。使用fiddler抓到的手机发出的请求的User-Agent即可。

  1. User-Agent: Dalvik/2.1.0 (Linux; U; Android 7.1.1; E6683 Build/32.4.A.1.54)
  • 模拟

至此,请求已分析完毕,可以编码模拟了。待填坑。。

Android逆向之旅—分析某直播App的协议加密原理以及调用加密方法进行协议参数构造

一、前言随着直播技术火爆之后,各家都出了直播app,早期直播app的各种请求协议的参数信息都没有做任何加密措施,但是慢慢的有人开始利用这个后门开始弄刷粉关注工具,可以让一个新生的小花旦分分钟变成网红。所以介于这个问题,直播App开始对网络请求参数做了加密措施。所以就是本文分析的重点。逆向领域不仅只有脱壳操作,一些加密解密操作也是很有研究的目的。
二、抓包查看加密协议本文就看一款直播app的协议加密原理,以及如何获取加密之后的信息,我们如何通过探针技术,去调用他的加密函数功能。首先这里找突破点,毋庸置疑,直接抓包即可,我们进入主播页面,点击关注之后,看Fiddler中的抓包数据:

我们会发现,请求参数中,有些重要信息,比如用户的id,设备的imei值等。不过最重要也是我们关心的就是s_sg字段了,因为这个字段就是请求参数信息的一个签名信息。也是服务端需要进行比对的信息。如果发现不对或者没这个字段,那么就认为这次请求操作是非法的。所以我们只要得到这个字段的正确值,才能模拟访问此次操作的网络请求。
三、分析加密流程找到突破口了,就是这个字段s_sg,用Jadx打开app的dex文件,打开dex文件之后,全局搜索s_sg字符串:

这里看到只有一处出现了这个字段值,我们点进去查看:

看到这个方法,很兴奋,感觉就是加密协议的功能,而且字段都能对上,我们把这个加密方法直接拷贝出来,写一个demo之后模拟加密之后的信息,悲伤的是发现数据是对不上的,而且看看这个方法所在的类:

尽然是一个和网页交互的类,说明应该不是这个地方进行请求加密了。这个突破口就断了。注意:这里说的是dex文件,不是apk文件,因为Jadx打开apk文件会解析资源文件,如果一个app有很多资源文件,那么Jadx打开就会卡死,所以很多同学问我为什么Jadx打开apk文件就出现卡死状态,主要是因为解析资源文件导致的。所以为了防止卡死,直接解压出dex文件,然后打开就不会卡死了。

我们继续上面的抓包信息,就是请求的url地址,再去Jadx全局搜索:

找到了,点进去查看:

然后全局搜索这个”USER_RELATION_FOLLOW”字符串:

搜到结果,点进去进行查看:

这里看到了,用了注解方式来构造请求信息,而这里的核心类就是InkeDefaultBuilder,全局搜索这个类:

可惜没搜到,因为这个app进行拆包操作,有多个dex文件:

所以我们需要用Jadx继续打开他的第二个dex文件进行搜索:

果然,在第二个dex中找到了这个类。注意:这里需要注意,对于Jadx打开dex或者apk文件之后,跟踪发现找不到一些搜索内容的时候,需要有如下猜想:第一、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。第二、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。第三、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。第四、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。
继续分析,点进去查看这个类的定义:

查看他的父类信息:

看到有一个url加密的方法,比较好奇。我们查看这个方法:

继续查看这个方法:


这里发现有一个网络请求,会发现一些信息,然后设置到一个地方。我们继续看方法:

看到d变量的定义类型,一般我们看到不可点击的可能这个类不在这个dex文件中了,所以我们需要去另外一个dex文件进行查找,而本文案例就是来回这么折腾查找信息操作的,去另外一个dex文件中进行搜索:

查找到了,点进去查看:

这里又看到是一个a变量,看看他的定义:

看到,这里这个类又不可以点击,说明这个类不在这个dex文件中,去另外一个dex文件中进行搜索:

发现这个类的定义了,点进去进行查看:

原来是一个native的工具类,内部有很多native方法,包括了设置信息的方法,加密解密url方法等。看看他加载的so是什么:

原来是这三个so文件,看到crypto和ssl,弄过加密的知道,这个是openssl加密的库文件,这里猜想他在native层用了这个加密算法了。先不管,我们用IDA打开这个so文件,因为我们知道libcrypto和libssl这两个是库文件,所以直接打开它自己的libsecret文件吧,然后Shift+F12查看他的字符串信息页面,在之前不是想看看那个加密字段,这里搜索看看结果:

的确找到了,那个加密的字段了,我们点击进入查看:

然后点击X键,查看调用地方,不过可惜的是,跳转过去之后发现,那个汇编代码不是一般的多。这里先不去看了,回过头来,看看那个加密url的函数:

点击进去,然后按F5查看对应的C代码:

这时候就要开始怀疑人生了,IDA卡死了。然后简单看一下这个函数的汇编代码,简直蒙圈。太长了。如果靠静态分析,我是没这个耐心和能力了。动态调试?我觉得也够呛,搞不好还有反调试,各种跳转。不知道调试到猴年马月。
四、获取加密内容那么到这里,我大致分析这个直播app的请求协议有一个加密签名的字段s_sg,这个值是在native层中进行加密操作的,采用openssl进行加密,但是加密函数非常长,分析难度加大。但是不能就这样放弃了。我们想要这个加密结果,用于我们自己构造参数之后获取正确的签名信息值。那么就需要转化思维了。我们或许只要结果,加密过程对我们来说并不那么重要。所以这里的一个思路:自己写一个程序调用它的so文件中的这个加密函数。
我们做过NDK开发的都知道,默认情况native方法在so中的JNI_OnLoad函数自动注册,但是native中的函数必须按照这种格式:Java_类名_方法名,类似这样:

那么我们就可以在自己的程序中,把app的那个Secret类拷贝过去,不过一定要注意包名一定不能变:

然后在代码中直接调用native方法:

不过可惜的是,调用的结果,没有加密字段信息。所以这里我们会发现应该还缺少什么设置。我们回过头看看java层的代码:

这里有一个set方法,在之前分析已经发现了,他的调用地方:

这里有一个SecretDataModel类,应该是从网上请求获取到的数据,然后解析构造出这个类,看看这个类定义:

看到这里,发现已经用了第三方的json解析包,注解直接解析字段值。但是我们全局搜索这个类的话,跟踪太麻烦了,这里就采用另外一种方式跟踪代码,那就是利用Xposed拦截这个类的构造方法,然后在内部打印堆栈信息来查看方法的调用路径,这种方法我在之前已经用过了。本文能够更好的体现:

因为应用是多dex文件,所以hook必须先hook他的Application类的attach方法拿到正确的类加载器,正确加载需要hook的类信息,不然就报错了。这个已经讲了很多遍了。然后就是利用自动抛异常来打印方法的调用堆栈信息,安装运行,重启生效看日志:

这里看到了,他用google的gson库解析json数据的,看到了json数据是从下面这两个方法中传递过来的,查看这个类的方法:

点进去进行查看:

为了看到返回的json数据,我们在拦截这个方法,把返回的json数据打印出来看看是啥:

运行模块,重启生效,看日志信息:

看到这段json数据了,这时候,在返回去看看SecretDataModel那个调用set方法:

看到这四个参数值,第一个是Context不解释了,第二个是serverTime字段值,第三个是startCode字段值,第四个是runCode字段值。这三个字段都是可以在上面打印的json中找到的,我们把json格式化看看:

有了这三个值和Context变量,直接调用Secret的native方法set进行设置,看看能否正确获取到加密之后的字段值:

运行demo程序看看日志信息:

看到了,设置成功了,而且获取加密字段也成功了。到这里我们就成功的获取到加密字段s_sg值了。
五、获取加密配置信息不过到这里还没有结束,因为我们发现那三个set值字段从网络获取,我们还需要知道是哪个url获取到的,这里从代码跟踪依然很困难,所以我们还需要利用hook来打印方法的堆栈信息来追踪代码,通过上面打印的获取json数据的堆栈信息:

看到是这个类访问获取json数据的,进去看看:

而传入的参数,在进行查看:

看到有一个getUrl方法,就是获取访问的url值,我们就可以这么来进行hook操作,打印这个url访问地址了:

运行模块,重启生效,看日志信息:

下面打印了那个我们想要的json数据,上面有几个url,我们在去Fiddler观察这几个url返回的数据,最后定位到这个是这个url返回的数据信息:

六、梳理加密流程到这里我们就分析完了直播app的协议加密流程,下面来总结一下:第一步:通过/user/account/token_v2接口获取加密前的配置信息第二步:通过native方法把第一步获取到的信息进行设置(set方法)。第三步:通过native方法将java层拼接参数的url值进行加密处理返回(encryUrl方法)那么我们可以这么做,自己协议demo程序,在Java层拼接好参数,然后调用so的native方法,返回还有加密字段的url,然后可以解析出这个字段就是加密信息了。我们就可以批量处理这种网络请求了。比如刷粉,观看直播,点赞等操作。当然有的同学会认为这次操作其实不是真正意义上的破解加密算法。的确不算,但是我们弄到我们想要的就好,过程其实没那么重要,而在这个操作过程中,我们又学习到了很多逆向技巧。
七、逆向技巧总结第一、在用Jadx打开apk卡死的时候,记得先解压出dex文件,在用Jadx打开dex文件即可。第二、对于多dex的应用,使用Xposed进行hook的时候,需要先拦截Application类的attach方法获取正确的类加载器。不然会报拦截失败。第三、当我们在使用Jadx进行全局搜索内容,发现没有搜索结果的时候,可能需要从以下几个方面考虑:1、是否包含多个dex文件,可以利用Jadx去打开其他dex文件进行搜索。2、是否存在动态加载插件功能,全局搜索DexClassLoader找到插件加载位置,获取插件功能包,在用Jadx打开插件包进行搜索。3、是否存在so文件中,可以利用IDA打开so文件,Shift+F12展示so中所有的字符串信息视图,然后进行搜索。4、是否信息来源于网络请求返回,比如一些字符串信息展示,有可能是服务器返回的信息。

第四、在我们用Jadx进行代码跟踪非常困难的时候,记得还可以利用Xposed拦截指定方法,打印堆栈信息来跟踪代码,也是一种高效方法。第五、当你在使用Jadx右键一个方法看看调用路径,发现没有结果的时候,那么看看这个方法是否是该类实现的一个接口中的方法或者是抽象方法。去父类或者接口中的那个方法在右键看看调用路径。第六、在不关心过程,只关心结果的场景下,可以构造一个app来调用程序的so,获取我们想要的结果,这种方式一定要记住,后面很多场景都会用到。可以用它来嗅探so中一些函数的功能。比如通过调用so中的一个方法,输入规律数据,看输出结果是否符合一定规律,通过规律来破解加密算法。
八、总结本文介绍的内容有点多,感谢该直播app开发者提供这么好的研究样本,当然最后需要说一句就是:安全防护策略不够,我们在本文可以看到我们利用调用他的so来获取加密信息,这个方法是可以用于很多app的,对于那些我们无需关心过程,只关心结果的内容,这种方法屡试不爽。那么作为开发者如何规避这种安全问题呢?很多人第一就想到了:在so的JNI_OnLoad方法中判断签名信息是否正确,不正确就直接退出。这个的确可以防范。但是如果用我之前介绍的kstools工具原理,直接hook系统的PMS服务拦截获取签名信息方法,返回正确的应用签名信息,这种防护策略就失效了。所以说:安全不息,逆向不止。两者都在进步。
严重声明本文介绍的内容只是为了逆向技术探讨,绝对不允许不法分子进行恶意用途。如果带来任何法律问题均与本文作者无关,由操作者本人承担,而涉及到安全隐患,技术交流可以加入编码美丽技术圈

adb远程安装APK

在电脑端命令窗口输入安装命令进行安装。

温馨提示:

网上使用的很多apk安装器,或者专用的apk安装应用程序,都是将adb脚本文件和adb调试命令做了批处理打包的简化操作,原理是一样的。
使用adb命令安装apk的方法适用于对Androidi,Lnux,Java较为熟悉的同事。

安卓apk反编译、修改、重新打包、签名全过程

首先明确,反编译别人apk是一件不厚道的事情。代码是程序员辛苦工作的成果,想通过这种手段不劳而获,是不对的。这也说明,代码混淆是非常重要的。本文抱着学习的态度,研究在一些特殊的情况下如果有需要,该怎么反编译apk。

工具简介

apktool,编译和反编译apk,从apk中提取图片和布局资源

dex2jar,将可运行文件classes.dex反编译为jar源码文件

jd-gui,查看jar源码文件

反编译

apktool安装

Windows系统:

1. 首先确保系统安装有Java

2. 下载apktool.bat脚本

3. 下载最新版本的apktool.jar,并且重命名为apktool.jar

4. 将apktool.bat和apktool.jar放在同一目录下,就可以在命令行窗口使用了。

5. 其他系统请参考链接

用法

可以直接在命令行执行apktool.bat查看帮助。这里介绍两个最常用的:

反编译

apktool.bat d [-s] -f <apkPath> -o <folderPath>

注:若不选择文件夹路径直接输 :apktool.bat d -f 1.apk -o 1 将默认生成在系统目录C:\Users\Administrator;

一. 这里需要用到另外两个工具,下载dex2jar并解压。下载jd-gui,这是一个带UI的应用程序。

二. 将需要反编译的apk的后缀名改为.zip或者.rar,然后解压到一个文件夹,得到其中的classes.dex文件。

三. 将classes.dex复制到解压后的dex2jar-2.0文件夹下。从命令行进入到该目录,执行

d2j-dex2jar.bat classes.dex

会生成由classes.dex反编译得到的jar文件,classes-dex2jar.jar。

四. 然后使用jd-gui打开classes-dex2jar.jar,就可以查看源码了。

如果apk在发布的时候加过混淆处理,那么我们也只能得到混淆后的版本。想通过阅读源码来破解别人的apk,难度较大,不过有兴趣可以网上去研究。

修改代码

如果只修改apk相应的资源,那么只要在res文件夹下找到相应的文件替换。

修改代码比较麻烦,因为反编译出来的结果中只有smali文件,即Java虚拟机支持的汇编语言。

如果确实需要修改代码,就得对照smali文件和从classes.dex反编译出来的源码了,按照smali的规范来改动即可。相当于写汇编,难度较大。

五.签名apk文件:

(如何查看签名信息:将签名后的apk文件后缀名改为zip,然后将里面的META-INF文件夹解压出来:输入命令:keytool -printcert –file <签名文件RSA的路径>)

签名文件需要用到keytool.exe和jarsigner.exe,这两个文件都在Java jdk的bin目录下:

1,打开命令行输入以下命令然后回车:

keytool -genkey -alias key.keystore -keyalg RSA -validity 30000 -keystore key.keystore

-genkey 产生证书文件
-alias 产生别名
-keystore 指定密钥库的.keystore文件中
-keyalg 指定密钥的算法,这里指定为RSA(非对称密钥算法)
-validity 为证书有效天数,这里我们写的是30000天

出现如下图所示随便照着填填

2,生成出来的keystore要与apk在同一目录下(一般都默认在系统目录没有修改路径的话C:\Users\Administrator)

命令行再输入以下命令然后回车:

jarsigner -verbose -keystore key.keystore -signedjar xxx-signed.apk xxx-unsigned.apk key.keystore

xxx-signed.apk 指签名后的apk文件名
xxx-unsigned.apk 原来的apk文件名
-verbose 指定生成详细输出 
-keystore 指定数字证书存储路径

这样,就完成了对一个apk的签名过程,然后就可以安装使用了。注意如果你的手机上原来就有这个apk,需先卸载,不然无法安装。

APK反编译之一:基础知识–smali文件阅读

用反编译工具apktool得到src为的smali文件,因此需要了解下smali的语法如下转载博客:

http://blog.csdn.net/lpohvbe/article/details/7981386

APK反编译之一:基础知识

本人接触不久,有错误望请各位神牛不吝赐教,仅仅希望把自己这段时间研究的东西分享一下,如果可以帮助到有需要的童鞋万感荣幸。欢迎评论转载,但请加上转载来源谢谢!请尊重开发者劳动成果!请勿用于非法用途!

作者:lpohvbe | http://blog.csdn.net/lpohvbe/article/details/7981386

     这部分涉及的内容比较多,我会尽量从最基础开始说起,但需要读者一定的android开发基础。但注意可能讲解详细得令人作呕,请根据个人理解程度斟酌。

APK、Dalvik字节码和smali文件

APK文件

大家都应该知道APK文件其实就是一个MIME为ZIP的压缩包,我们修改ZIP后缀名方式可以看到内部的文件结构,例如修改后缀后用RAR打开鳄鱼小顽皮APK能看到的是(Google Play下载的完整版版本):

Where’s My Water.zip\

  • asset\                        <资源目录1:asset和res都是资源目录但有所区别,见下面说明>
  • lib\                             <so库存放位置,一般由NDK编译得到,常见于使用游戏引擎或JNI native调用的工程中>
  • |—armeabi\                |—<so库文件分为不同的CPU架构>
  • |—armeabi-v7a\
  • META-INF\                  <存放工程一些属性文件,例如Manifest.MF>
  • res\                           <资源目录2:asset和res都是资源目录但有所区别,见下面说明>
  • |—drawable\               |—<图片和对应的xml资源>
  • |—layout\                   |—<定义布局的xml资源>
  • |—…
  • AndroidManifest.xml     <Android工程的基础配置属性文件>
  • classes.dex                 <Java代码编译得到的Dalvik VM能直接执行的文件,下面有介绍>
  • resources.arsc             <对res目录下的资源的一个索引文件,保存了原工程中strings.xml等文件内容>

 无关紧要地注:asset和res资源目录的不同在于:

1. res目录下的资源文件在编译时会自动生成索引文件(R.java),在Java代码中用R.xxx.yyy来引用;而asset目录下的资源文件不需要生成索引,在Java代码中需要用AssetManager来访问;

2. 一般来说,除了音频和视频资源(需要放在raw或asset下),使用Java开发的Android工程使用到的资源文件都会放在res下;使用C++游戏引擎(或使用Lua binding等)的资源文件均需要放在asset下。

因为Where’s My Water是使用迪斯尼公司自家的DMO游戏引擎开发,所以游戏中用到的所有资源文件都存放在asset下,除了应用图标这些资源仍需要放在res下。

Dalvik字节码

Dalvik是google专门为Android操作系统设计的一个虚拟机,经过深度的优化。虽然Android上的程序是使用java来开发的,但是Dalvik和标准的java虚拟机JVM还是两回事。Dalvik VM是基于寄存器的,而JVM是基于栈的;Dalvik有专属的文件执行格式dex(dalvik executable),而JVM则执行的是java字节码。Dalvik VM比JVM速度更快,占用空间更少。

通过Dalvik的字节码我们不能直接看到原来的逻辑代码,这时需要借助如Apktool或dex2jar+jd-gui工具来帮助查看。但是,注意的是最终我们修改APK需要操作的文件是.smali文件,而不是导出来的Java文件重新编译(况且这基本上不可能)。

smali文件

好了,对Dalvik有一定认识后,下面介绍重点:smali,及其语法。

简单的说,smali就是Dalvik VM内部执行的核心代码。它有自己的一套语法,下面即将介绍,如果有JNI开发经验的童鞋则能够很快明白。

      一、smali的数据类型

在smali中,数据类型和Android中的一样,只是对应的符号有变化:

  • B—byte
  • C—char
  • D—double
  • F—float
  • I—int
  • J—long
  • S—short
  • V—void
  • Z—boolean
  • [XXX—array
  • Lxxx/yyy—object

这里解析下最后两项,数组的表示方式是:在基本类型前加上前中括号“[”,例如int数组和float数组分别表示为:[I、[F;对象的表示则以L作为开头,格式是LpackageName/objectName;(注意必须有个分号跟在最后),例如String对象在smali中为:Ljava/lang/String;,其中java/lang对应java.lang包,String就是定义在该包中的一个对象。

或许有人问,既然类是用LpackageName/objectName;来表示,那类里面的内部类又如何在smali中引用呢?答案是:LpackageName/objectName$subObjectName;。也就是在内部类前加“$”符号,关于“$”符号更多的规则将在后面谈到。

     二、函数的定义

函数的定义一般为:

Func-Name (Para-Type1Para-Type2Para-Type3…)Return-Type

注意参数与参数之间没有任何分隔符,同样举几个例子就容易明白了:

     1. foo ()V

没错,这就是void foo()。

2. foo (III)Z

这个则是boolean foo(int, int, int)。

3. foo (Z[I[ILjava/lang/String;J)Ljava/lang/String;

看出来这是String foo (boolean, int[], int[], String, long了吗?

 

      三、smali文件内容具体介绍

下面开始进一步分析smali中的具体例子,取鳄鱼小顽皮中的WMWActivity.smali来分析(怎么获得请参考下一节的APK反编译之二:工具介绍,暂时先介绍smali语法),它的内容大概是这样子的:

[plain] view plaincopy

  1. .class public Lcom/disney/WMW/WMWActivity;
  2. .super Lcom/disney/common/BaseActivity;
  3. .source “WMWActivity.java”
  4. # interfaces
  5. .implements Lcom/burstly/lib/ui/IBurstlyAdListener;
  6. # annotations
  7. .annotation system Ldalvik/annotation/MemberClasses;
  8.     value = {
  9.         Lcom/disney/WMW/WMWActivity$MessageHandler;,
  10.         Lcom/disney/WMW/WMWActivity$FinishActivityArgs;
  11.     }
  12. .end annotation
  13. # static fields
  14. .field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = “installationId”
  15. //…
  16. # instance fields
  17. .field private _activityPackageName:Ljava/lang/String;
  18. //…
  19. # direct methods
  20. .method static constructor <clinit>()V
  21.     .locals 3
  22.     .prologue
  23.     //…
  24.     return-void
  25. .end method
  26. .method public constructor <init>()V
  27.     .locals 3
  28.     .prologue
  29.     //…
  30.     return-void
  31. .end method
  32. .method static synthetic access$100(Lcom/disney/WMW/WMWActivity;)V
  33.     .locals 0
  34.     .parameter “x0”
  35.     .prologue
  36.     .line 37
  37.     invoke-direct {p0}, Lcom/disney/WMW/WMWActivity;->initIap()V
  38.     return-void
  39. .end method
  40. .method static synthetic access$200(Lcom/disney/WMW/WMWActivity;)Lcom/disney/common/WMWView;
  41.     .locals 1
  42.     .parameter “x0”
  43.     .prologue
  44.     .line 37
  45.     iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;
  46.     return-object v0
  47. .end method
  48. //…
  49. #virtual methods
  50. .method public captureScreen()V
  51.     .locals 4
  52.     .prologue
  53.     //…
  54.     goto :goto_0
  55. .end method
  56. .method public didScreenCaptured()V
  57.     .locals 6
  58.     .prologue
  59.     //…
  60.     goto :goto_0
  61. .end method
      看得一头雾水的话那是正常的。现在我将逐一解析,理解这些符号的含义令你在后面注入代码的时候事半功倍

       1、smali中的继承、接口、包信息

      首先看看开头的几行:
  1] .class public Lcom/disney/WMW/WMWActivity;
  2] .super Lcom/disney/common/BaseActivity;
  3] .source “WMWActivity.java”
  4]
  5] # interfaces
  6] .implements Lcom/burstly/lib/ui/IBurstlyAdListener;
  7]
  8] # annotations
  9] .annotation system Ldalvik/annotation/MemberClasses;
10]     value = {
11]        Lcom/disney/WMW/WMWActivity$MessageHandler;,
12]        Lcom/disney/WMW/WMWActivity$FinishActivityArgs;
13]    }
14] .end annotation
      1-3行定义的是基本信息:这是一个由WMWActivity.java编译得到的smali文件(第3行),它是com.disney.WMW这个package下的一个类(第1行),继承自com.disney.common.BaseActivity(第2行)。
      5-6行定义的是接口信息:这个WMWActivity实现了一个com.burstly.lib.ui这个package下(一个广告SDK)的IBurstyAdListener接口。
      8-14行定义的则是内部类:它有两个成员内部类——MessageHandler和FinishActivityArgs,内部类将在后面小节中会有提及。
      分析完smali文件开头的这些信息,我们已经能在大脑中构造出一个大概这样的Java文件:
  1. class WMWActivity extends BaseActivity implements IBurstlyAdListener{
  2.     //…
  3.     class MessageHandler {
  4.         //…
  5.     }
  6.     class FinishActivityArgs{
  7.         //…
  8.     }
  9. }
      没错,这就是本来WMWActivity.java的大概框架了,成员变量和函数信息?别急,下面正要分析。
      在继续分析之前,有些东西需要先说明一下。前面说过,Dalvik VM与JVM的最大的区别之一就是Dalvik VM是基于寄存器的。基于寄存器是什么意思呢?也就是说,在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、v1、v2、…参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2、…特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this”,p1表示函数的第一个参数,p2代表函数中的第二个参数…而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)。本地寄存器没有限制,理论上是可以任意使用的,下面是例子:
[plain] view plaincopy

  1. const/4 v0, 0x0
  2. iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->isRunning:Z
      在上面的两句中,使用了v0本地寄存器,并把值0x0存到v0中,然后第二句用iput-boolean这个指令把v0中的值存放到com.disney.WMW.WMWActivity.isRunning这个成员变量中。即相当于:this.isRunning = false;(上面说过,在非static函数中p0代表的是“this”,在这里就是com.disney.WMW.WMWActivity实例)。关于这两句话的具体指令和含义暂可不用理会,先把Dalvik VM的机制弄明白就可以了,其实语法上和汇编语言非常相似,具体的指令会在后面逐一介绍。

        2、smali中的成员变量

      下面继续介绍有关成员变量的内容:
1 ] # static fields
2 ] .field private static final PREFS_INSTALLATION_ID:Ljava/lang/String; = “installationId”
3 ] //…
4 ]
5 ]
6 ] # instance fields
7 ] .field private _activityPackageName:Ljava/lang/String;
8 ] //…
      上面定义的static fields和instance fields均为成员变量,格式是:.field public/private [static] [final] varName:<类型>。然而static fields和instance fields还是有区别的,当然区别很明显,那就是static fields是static的,而instance则不是。根据这个区别来获取这些不同的成员变量时也有不同的指令。一般来说,获取的指令有:iget、sget、iget-boolean、sget-boolean、iget-object、sget-object等,操作的指令有:iput、sput、iput-boolean、sput-boolean、iput-object、sput-object等。没有“-object”后缀的表示操作的成员变量对象是基本数据类型,带“-object”表示操作的成员变量是对象类型,特别地,boolean类型则使用带“-boolean”的指令操作。
      (1)、获取static fields的指令类似是:
[plain] view plaincopy

  1. sget-object v0, Lcom/disney/WMW/WMWActivity;->PREFS_INSTALLATION_ID:Ljava/lang/String;
      sget-object就是用来获取变量值并保存到紧接着的参数的寄存器中,在这里,把上面出现的PREFS_INSTALLATION_ID这个String成员变量获取并放到v0这个寄存器中,注意:前面需要该变量所属的类的类型,后面需要加一个冒号和该成员变量的类型,中间是“->”表示所属关系
      (2)、获取instance fields的指令与static fields的基本一样,只是由于不是static变量,不能仅仅指出该变量所在类的类型,还需要该变量所在类的实例。看例子:
[plain] view plaincopy

  1. iget-object v0, p0, Lcom/disney/WMW/WMWActivity;->_view:Lcom/disney/common/WMWView;
      可以看到iget-object指令比sget-object多了一个参数,就是该变量所在类的实例,在这里就是p0即“this”。
      (3)、获取array的还有aget和aget-object,指令使用和上述类似,不细述。
      (4)、put指令的使用和get指令是统一的,直接看例子不解释:
[plain] view plaincopy

  1. const/4 v3, 0x0
  2. sput-object v3, Lcom/disney/WMW/WMWActivity;->globalIapHandler:Lcom/disney/config/GlobalPurchaseHandler;
      相当于:this.globalIapHandler = null;(null = 0x0)
[plain] view plaincopy

  1. .local v0, wait:Landroid/os/Message;
  2. const/4 v1, 0x2
  3. iput v1, v0, Landroid/os/Message;->what:I
      相当于:wait.what = 0x2;(wait是Message的实例)
 

        3、smali中的函数调用

smali中的函数和成员变量一样也分为两种类型,但是不同成员变量中的static和instance之分,而是direct和virtual之分。那么direct method和virtual method有什么区别呢?直白地讲,direct method就是private函数,其余的public和protected函数都属于virtual method。所以在调用函数时,有invoke-direct,invoke-virtual,另外还有invoke-static、invoke-super以及invoke-interface等几种不同的指令。当然其实还有invoke-XXX/range 指令的,这是参数多于4个的时候调用的指令,比较少见,了解下即可。

(1)、invoke-static:顾名思义就是调用static函数的,因为是static函数,所以比起其他调用少一个参数,例如:

[plain] view plaincopy

  1. invoke-static {}, Lcom/disney/WMW/UnlockHelper;->unlockCrankypack()Z

这里注意到invoke-static后面有一对大括号“{}”,其实是调用该方法的实例+参数列表,由于这个方法既不需参数也是static的,所以{}内为空,再看一个例子:

[plain] view plaincopy

  1. const-string v0, “fmodex”
  2. invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V<span style=”font-family: Verdana, sans-serif; “> </span>
      这个是调用static void System.loadLibrary(String)来加载NDK编译的so库用的方法,同样也是这里v0就是参数”fmodex”了。

(2)、invoke-super:调用父类方法用的指令,在onCreate、onDestroy等方法都能看到,略。

(3)、invoke-direct:调用private函数的,例如:

[plain] view plaincopy

  1. invoke-direct {p0}, Lcom/disney/WMW/WMWActivity;->getGlobalIapHandler()Lcom/disney/config/GlobalPurchaseHandler;

这里GlobalPurchaseHandler getGlobalIapHandler()就是定义在WMWActivity中的一个private函数,如果修改smali时错用invoke-virtual或invoke-static将在回编译后程序运行时引发一个常见的VerifyError(更多错误汇总可参照APK反编译之番外三:常见错误汇总)。

(4)、invoke-virtual:用于调用protected或public函数,同样注意修改smali时不要错用invoke-direct或invoke-static,例子:

[plain] view plaincopy

  1. sget-object v0, Lcom/disney/WMW/WMWActivity;->shareHandler:Landroid/os/Handler;
  2. invoke-virtual {v0, v3}, Landroid/os/Handler;->removeCallbacksAndMessages(Ljava/lang/Object;)V
      这里相信大家都已经明白了,主要搞清楚v0是shareHandler:Landroid/os/Handler,v3是传递给removeCallbackAndMessage方法的Ljava/lang/Object参数就可以了。
      (5)、invoke-xxxxx/range:当方法的参数多于5个时(含5个),不能直接使用以上的指令,而是在后面加上“/range”,使用方法也有所不同:
[plain] view plaincopy

  1. invoke-static/range {v0 .. v5}, Lcn/game189/sms/SMS;->checkFee(Ljava/lang/String;Landroid/app/Activity;Lcn/game189/sms/SMSListener;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z
      这个是电信SDK中的付费接口,需要传递6个参数,这时候大括号内的参数需要用省略形式,且需要连续(未求证是否需要从v0开始)。

 

有人也许注意到,刚才看到的例子都是“调用函数”这个操作而已,貌似没有取函数返回的结果的操作?

在Java代码中调用函数和返回函数结果是一条语句完成的,而在smali里则需要分开来完成,在使用上述指令后,如果调用的函数返回非void,那么还需要用到move-result(返回基本数据类型)和move-result-object(返回对象)指令:

[plain] view plaincopy

  1. const/4 v2, 0x0
  2. invoke-virtual {p0, v2}, Lcom/disney/WMW/WMWActivity;->getPreferences(I)Landroid/content/SharedPreferences;
  3. move-result-object v1
      v1保存的就是调用getPreferences(int)方法返回的SharedPreferences实例。
[plain] view plaincopy

  1. invoke-virtual {v2}, Ljava/lang/String;->length()I
  2. move-result v2

v2保存的则是调用String.length()返回的整型。

       4、smali中函数实体分析

下面开始介绍函数实体,其实没有什么特别的地方,只是在植入代码时有一点需要特别注意,举例说明:

[plain] view plaincopy

  1. .method protected onDestroy()V
  2.     .locals 0
  3.     .prologue
  4.     .line 277
  5.     invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V
  6.     .line 279
  7.     return-void
  8. .end method
      这是onDestroy()函数,它的作用大家都知道。首先看到函数内第一句:.local 0,这句话很重要,标明了你在这个函数中最少要用到的本地寄存器的个数。在这里,由于只需要调用一个父类的onDestroy()处理,所以只需要用到p0,所以使用到的本地寄存器数为0。如果不清楚这个规则,很容易在植入代码后忘记修改.local 的值,那么回编译后运行时将会得到一个VerifyError错误,而且极难发现问题所在。我正是被这个问题困扰了很多次,最后研究发现.local的值有这个规律,于是在文档查证了一下果然是这个问题。例如我往onDestroy()增加一句:this.existed = true;那么应该改为(注意修改.local的值为1——使用到了v0这一个本地寄存器):
[plain] view plaincopy

  1. .method protected onDestroy()V
  2.     .locals 1
  3.     .prologue
  4.     .line 277
  5.     const/4 v0, 0x1
  6.     iput-boolean v0, p0, Lcom/disney/WMW/WMWActivity;->exited:Z
  7.     invoke-super {p0}, Lcom/disney/common/BaseActivity;->onDestroy()V
  8.     .line 279
  9.     return-void
  10. .end method
      另外注意到.line这个标识,它是标注了该代码在原Java文件中的行数,它也很有用,想想使用eclipse开发时,遇到错误崩溃时,在catLog不是有提示哪个文件哪一行崩溃的么?Dalvik VM运行到.line XX时就将这个值存起来,如果在这一行运行时出错了,就往catLog输出这个值,这样我们就能看到具体是哪一行的问题了。jd-gui这个工具也是通过分析这些信息将smali代码还原成我们喜闻乐见的Java代码的。当然,它不是必须的,去掉也没有关系,只不过为了方便调试还是保留一下吧。
      以上一些smali语法规则可以参详这里