分类目录归档:未分类

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)
  • 模拟

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

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语法规则可以参详这里

Android Studio配置文件路径修改

注意:新版的Android Studio可能已经无法使用下面的方法进行配置文件路径的迁移,下文仅供参考。

使用Android Studio进行Android开发已经成为趋势了,好的工具要用得称手也少不了好的调教,在Windows下更是如此。这里对Android Studio的相关配置文件的路径修改做下小结。

首先看下图:

安装运行后自动生成的几个配置文件夹

Android Studio安装好以后会在系统盘用户目录下产生这么几个文件夹:

  • .android 这个文件夹是Android SDK生成的AVD(Android Virtual Device Manager)即模拟器存放路径
  • .AndroidStudio 这个文件夹是Android Studio的配置文件夹,主要存放一些AndroidStudio设置和插件和项目的缓存信息
  • .gradle 这个文件夹是构建工具 Gradle的配置文件夹,也会存储一些项目的构建缓存信息

本来是用默认路径也没什么问题,不过在Windows环境下,什么东西都往系统盘里塞老是让人觉得不舒服,在一个这年头要个用个固态硬盘装系统,估计就剩不下多少地方了,因此把这些玩意儿扔到别处也是必要的。

注意,在进行以下修改前,建议先把之前的配置目录拷贝到新的路径,这样之前的配置就不会丢了。

1. .AndroidStudio文件夹的修改

进入Android Studio的安装目录,进入bin文件夹,用文本编辑软件打开idea.properties,去掉以下两项的注释符号#,修改对应的路径为新路径即可。

修改以上两项路径

2. .gradle文件夹的修改

这项比较简单,在Android Studio的配置选项中修改就行

修改gradle路径

3. .android文件夹的修改

这个文件夹是由Android SDK配置模拟器生成的,也是最占空间的一个。
首先,需要添加一个系统的环境变量ANDROID_SDK_HOME,如下图:

添加以上环境变量

变量名其实有些误导人,这个如果Google官方定义成AVD_HOME可能还好一些,其实应该是模拟器的默认路径。添加好环境变量后到新的路径下修改下相应的.ini文件内的路径信息,然后重启系统生效。

ini文件

完毕。

参考阅读:

  1. Android Studio 的安装和配置篇(Windows篇)
  2. How to config android studio avd path?

作者:显卡84du
链接:https://www.jianshu.com/p/7a58c5f154c5
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

旅行青蛙破解分析从内存到存档再到改包

最近朋友圈里出现了一款日本的游戏,十分火爆,于是忍不住想去破解看看。分析后发现这个游戏的破解并不难,但是可以多种思路进行,是个很好的学习样本,于是决定写一篇文章分享给初学者们。

本文分三个方向进行破解分析,分别为 内存修改,存档修改,apk修改。文章涉及的修改较为简单,主要目的是给大家提供多元的分析思路,接下来我们一个一个来进行具体分析。

所使用样本为 旅行青蛙 1.0.4版本(目前最新版本)。 链接:https://pan.baidu.com/s/1dSqHK6 密码: qmvg

目录

0x1.内存修改 → GG修改器修改数值,需root

0x2.存档修改 → 存档十六进制修改,无需root;原创apk用于修改存档,无需root

0x3.apk修改   → Unity3D游戏脚本修改,无需root

0x4.总结         → 文章整体思路和方向概况

正文

0x1.内存修改

思路:这个方式是用在已经root的手机上,也就是我们接触比较多的修改器通过搜索来确认关键数值的内存地址,然后将其修改,达到破解目的。

工具:GG修改器 / 需要ROOT权限

因为比较简单,这部分尽量简要讲。

打开GG修改器和游戏,进游戏后查看当前三叶草数量,GG修改器附加游戏进程,并搜索该数量。

    

附加后我们进行搜索,搜索37这个数值。

    

搜索结果比较多,我们需要筛选,回到游戏使用三叶草买东西,数值变化为27,然后我们搜索27来确认三叶草数量的内存地址。

    

修改最终搜索到的值为27000,回到游戏就可以看到三叶草数量已经变化。

    

其他物品及抽奖券等数量均可用该方式修改,请大家自己尝试。

这种方式非常方便,但是有个弊端就是需要我们有ROOT权限,对于目前大部分安卓手机来讲,ROOT权限的获取越来越难,接下来我们来分析不需要ROOT权限的两种修改方法。

0x2.存档修改

思路:通过存档文件分析和修改完成关键数值修改

工具:十六进制编辑器

单机游戏都会有存档,旅行青蛙当然也不例外,我们按照常规路径去找一下,发现游戏的存档都在Tabikaeru.sav文件中,路径请看图:

我们使用十六进制编辑器将其打开,编辑器可以用PC端的也可以用手机端的,自行选择。

打开后我们根据目前的三叶草数量27000进行搜索,27000的十六进制为0x6978,所以我们在十六进制文件中可以进行hex搜索,搜索 69 78 或 78 69。

(通常在十六进制中的数值都是倒序记录,比如0x6978会保存为 78 69,在旅行青蛙1.0.1版本的存档中就是这么保存的,不过在1.0.4版本的存档中,已经变为了正序,即69 78)

经过搜索我们找到了三叶草的数量,接下来我们将其修改验证一下,将69 78 修改为 FF FF,保存后放回手机中存档的文件夹中,重新启动,发现三叶草数量已经变更:

其他数值修改,比如抽奖券或者其他物品数量等,均可依照此方法进行,此处不再赘述,请大家自己尝试。另外还可以在每次数值有较明显变化后保存存档文件,进行对比分析,来找到更多物品的数值。

为了更简便的进行修改,我们做一个专用修改器apk用来在未root手机上专门完成此修改过程,源码如下 (完整project参考附件) :

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package com.example.frog;
import android.content.Context;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class MainActivity extends AppCompatActivity {
    private EditText editText;
    private EditText editText2;
    private Button button;
    private InputMethodManager inputMethodManager;
    private static final String FILE_PATH = Environment.getExternalStorageDirectory() + File.separator + "Android/data/jp.co.hit_point.tabikaeru/files/Tabikaeru.sav";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        editText = (EditText) findViewById(R.id.editText);
        editText2 = (EditText) findViewById(R.id.editText2);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (editText.getText().toString().equals("") || editText2.getText().toString().equals("")) {
                    return;
                }
                String cloverHex = String.format("%06X",  Integer.valueOf(editText.getText().toString()));
                String couponHex = String.format("%06X",  Integer.valueOf(editText2.getText().toString()));
                Log.d("123"" " + cloverHex);
                Log.d("123"" " + couponHex);
                writeToFile(cloverHex, couponHex);
            }
        });
    }
    public void writeToFile(String cloverHex, String couponHex) {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        File file = new File(FILE_PATH);
        File newFile = new File(FILE_PATH);
        byte[] cloverByteArray = hexStringToByte(cloverHex);
        byte[] couponByteArray = hexStringToByte(couponHex);
        if (!file.exists()) {
            Log.d("123""未找到文件Tabikaeru.sav");
            return;
        }
        try {
            fileInputStream = new FileInputStream(file);
            byte[] arrayOfByte = new byte[fileInputStream.available()];
            Log.d("123""文件大小" + arrayOfByte.length);
            fileInputStream.read(arrayOfByte);
            if (arrayOfByte.length > 29) {
                file.delete();
                Log.d("123""删除旧文件");
                createFile(newFile);
                //三叶草
                arrayOfByte[23] = cloverByteArray[0];//Byte.valueOf(cloverHex.substring(0, 2));
                arrayOfByte[24] = cloverByteArray[1];//Byte.valueOf(cloverHex.substring(2, 4));
                arrayOfByte[25] = cloverByteArray[2];//Byte.valueOf(cloverHex.substring(4, 6));
                //抽奖券
                arrayOfByte[27] = couponByteArray[0];//Byte.valueOf(couponHex.substring(0, 2));
                arrayOfByte[28] = couponByteArray[1];//Byte.valueOf(couponHex.substring(2, 4));
                arrayOfByte[29] = couponByteArray[2];//Byte.valueOf(couponHex.substring(4, 6));
                Log.d("123"" " + arrayOfByte.length);
                for (int i = 0; i <arrayOfByte.length; i++) {
                    Log.d("123"" " + arrayOfByte[i]);
                }
                fileOutputStream = new FileOutputStream(newFile);
                fileOutputStream.write(arrayOfByte);
            }
        catch (Exception e) {
            e.printStackTrace();
        finally {
            Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show();
            hideSoftInput();
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public void createFile(File file){
        try{
            file.getParentFile().mkdirs();
            file.createNewFile();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
    public void hideSoftInput(){
        if(inputMethodManager == null) {
            inputMethodManager = (InputMethodManager)this.getSystemService(Context.INPUT_METHOD_SERVICE);
        }
        inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), 0);
        editText.clearFocus();
        inputMethodManager.hideSoftInputFromWindow(editText2.getWindowToken(), 0);
        editText2.clearFocus();
    }
    /**
     * 把16进制字符串转换成字节数组
     */
    public static byte[] hexStringToByte(String hex) {
        int len = (hex.length() / 2);
        byte[] result = new byte[len];
        char[] achar = hex.toCharArray();
        for (int i = 0; i < len; i++) {
            int pos = i * 2;
            result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));
            if (result[i] == 0) {
                result[i] = 00;
            }
        }
        return result;
    }
    private static int toByte(char c) {
        byte b = (byte"0123456789ABCDEF".indexOf(c);
        return b;
    }
}

上述代码实现了存档的直接修改,界面如下,不需要ROOT权限:

输入数值后,点击修改即可完成三叶草及抽奖券的修改 ,更多物品修改请自行尝试 。

0x3.apk修改

思路:分析apk包,找到脚本文件,反编译后找到关键method进行修改,然后重新打包

工具:Android Killer,DnSpy

Android Killer相关操作这里不再赘述,反编译后我们发现这是一个mono框架的Unity3D游戏,Unity3D游戏的脚本文件都存放在Assembly-CSharp.dll或Assembly-CSharp-firstpass.dll文件中,很显然,旅行青蛙的脚本文件位于Assembly-CSharp.dll,我们使用Dnspy进行分析看看。

我们搜索三叶草的英文clover,发现getCloverPoint可能是我们需要找的关键method。

根据getCloverPoint的源码,我们发现这个method的功能是在三叶草数量发生变化时在三叶草数量进行增减运算,那么我们可以对函数内部增加数量的这句代码进行修改,修改为发生变化增加固定数量的三叶草,比如10000。

(抽奖券相关修改也在SuperGameMaster中可以找到,method名为getTicket,此处不作演示,请大家自己尝试修改)

修改后函数变更为:

保存后打包apk运行,只要三叶草数量发生变化(如收割三叶草或者购买物品),三叶草的数量就会增加10000。

0x4.总结

本文通过多种思路对旅行青蛙的修改进行了分析,内容较为简单,主要目的是分享游戏破解分析的思路,有兴趣的可以尝试更多物品数量的修改。