Android反编译看看手Q口令红包的实现原理

首篇作为开始,先讲讲简单的反编译。反编译通常有几种目的:互相学习、借来用用、嘿嘿(干你,又分为小干干类似微信红包,和大干干改别人的apk帮他上架)。

因为没带kvm回来,mbpr屏幕太小,所以下文环境为windows。

一、反编译
让我们从实战开始,先实践一下怎么去反编译一个apk,看看某些功能的实现。毕竟没有实践的原理都是耍流氓。
这里我们保留互相学习的心态,所以是友善的第一种目的,嘻嘻。

1、准备
工具

  • Apktool
  • jadx(新一代反编译大杀器)

安装包

手机QQ 6.2.3 (目标就设定为看看口令红包是怎么做的吧)
2、Apktool的使用

首先确保你安装了java 7或以上,并能直接在命令行调用java。

  • 下载 windows用wrapper脚本 (mac使用这个)。
  • 下载最新的apktool。
  • 重命名上面下载的apktool jar文件为apktool.jar。
  • 把apktool.bat和apktool.jar放在同一个目录下,并加入PATH环境变量。
  • 现在你可以直接通过命令行调用apktool并查看使用方式了。
Apktool v2.0.3 - a tool for reengineering Android apk files
with smali v2.1.0 and baksmali v2.1.0

usage: apktool
 -advance,--advanced  prints advance information.
 -version,--version  prints the version then exits

usage: apktool if|install-framework [options] <framework.apk>
 -p,--frame-path <dir>  Stores framework files into <dir>.
 -t,--tag <tag>     Tag frameworks using <tag>.

usage: apktool d[ecode] [options] <file_apk>
 -f,--force       Force delete destination directory.
 -o,--output <dir>    The name of folder that gets written. Default is apk.out
 -p,--frame-path <dir>  Uses framework files located in <dir>.
 -r,--no-res       Do not decode resources.
 -s,--no-src       Do not decode sources.
 -t,--frame-tag <tag>  Uses framework files tagged by <tag>.

usage: apktool b[uild] [options] <app_path>
 -f,--force-all     Skip changes detection and build all files.
 -o,--output <dir>    The name of apk that gets written. Default is dist/name.apk
 -p,--frame-path <dir>  Uses framework files located in <dir>.

3、jadx的使用

  • 下载jadx。
  • 运行gradlew dist编译。
  • jadx\jadx-gui\build\install\jadx-gui\bin下有可运行的gui
  • jadx\jadx-cli\build\install\jadx\bin是命令行程序
  • 可以都加入PATH环境变量,以便直接命令行调用。

4、分析APK文件
First Try

虽然我们可以用jadx直接打开apk傻瓜式地去查看源代码,但是为了更理解反编译的过程和工作原理,以便以后在碰到一些问题(比如加壳)的时候可以自己解决,这里我们先装逼一下,使用Apktool去进行分析。

D:\dev\reverse>apktool d -o qq mobileqq_android_6.2.3.apk
I: Using Apktool 2.0.3 on mobileqq_android_6.2.3.apk
I: Loading resource table...
Exception in thread "main" brut.androlib.AndrolibException: Multiple res specs: attr/name
    at brut.androlib.res.data.ResTypeSpec.addResSpec(ResTypeSpec.java:78)
    at brut.androlib.res.decoder.ARSCDecoder.readEntry(ARSCDecoder.java:248)
    at brut.androlib.res.decoder.ARSCDecoder.readTableType(ARSCDecoder.java:212)
    at brut.androlib.res.decoder.ARSCDecoder.readTableTypeSpec(ARSCDecoder.java:154)
    at brut.androlib.res.decoder.ARSCDecoder.readTablePackage(ARSCDecoder.java:116)
    at brut.androlib.res.decoder.ARSCDecoder.readTableHeader(ARSCDecoder.java:78)
    at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:47)
    at brut.androlib.res.AndrolibResources.getResPackagesFromApk(AndrolibResources.java:544)
    at brut.androlib.res.AndrolibResources.loadMainPkg(AndrolibResources.java:63)
    at brut.androlib.res.AndrolibResources.getResTable(AndrolibResources.java:55)
    at brut.androlib.Androlib.getResTable(Androlib.java:66)
    at brut.androlib.ApkDecoder.setTargetSdkVersion(ApkDecoder.java:198)
    at brut.androlib.ApkDecoder.decode(ApkDecoder.java:96)
    at brut.apktool.Main.cmdDecode(Main.java:165)
    at brut.apktool.Main.main(Main.java:81)

竟然报错了,Multiple res specs: attr/name,在网上找了找资料,应该是腾讯利用Apktool的bug去进行了加壳,除了添加同名id外还做了若干加固,好,你狠,我们下篇文章针对腾讯的壳来分析并修改Apktool,这次先用jadx来试试。

Second Try

如果直接用jadx-gui打开QQ的apk,你会发现,卡死了。不错,就是卡死了,因为太大了…

我们打开jadx-gui文件(其实就是个启动的script),加上:

set JAVA_OPTS=-server -Xms1024m -Xmx8192m -XX:PermSize=256m -XX:MaxPermSize=1024m

就跟我们加速as/idea的原理差不多,多给点内存,这样就能顺利地打开了(可能会需要比较久的时间)。

5、字符串大法

为了找到我们的目标,红包,我们首先尝试用字符串搜索大法:在Resources -> resources.arsc -> res -> values -> strings.xml找到口令红包对应的

<string name="qb_hbdetail_command_word">口令红包</string>

然后Crtl+Shift+F进行Text Search,结果…没找到。

我们再使用资源id大法,直接在resources.arsc找到

0x7f0a0e5a (2131365466) = string.qb_hbdetail_command_word: 口令红包

再搜,好,你狠。。。还是没有。是在下输了。

6、类/函数名大法

我们再祭出第二大杀器,类/函数/变量名大法搜索大法。

通常类名符合的范围更小,所以先只使用Class。
试试看红包的英语:RedPacket(类名命名所以R和P大写)

OK,我们找到了十几条,开始逐一排查,第一条RedPacketInfo点进去一看就是个包含了各种field的ui用的vo类,跳过,再看下一个,从包名com.tencent.mobileqq.data看上去,似乎有戏,QQWalletRedPacketMsg:

package com.tencent.mobileqq.data;

import android.text.TextUtils;
import com.tencent.mobileqq.hotpatch.NotVerifyClass;
import cooperation.qzone.util.WiFiDash;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import tencent.im.msg.im_msg_body.QQWalletAioBody;

/* compiled from: ProGuard */
public class QQWalletRedPacketMsg {
  public String authkey;
  private int channelId;
  public int conftype;
  public QQWalletTransferMsgElem elem;
  public String envelopeName;
  public int envelopeid;
  public boolean isOpened;
  public int msgFrom;
  public String redPacketId;
  public int redtype;
  private int resend;
  public int templateId;

...串行化、读写、构建方法等,可以无视。
从field名来看,这里还是比较可疑的,猜测redtype是不是描述红包类型的。

我们再次使用关键词redtype进行搜索,这次选择Code,只进行代码内搜索,结果却发现貌似不对,找到相关的字符串是”查看详情”,貌似是描述红包领取状态的。

不放弃,继续抓住QQWalletRedPacketMsg这个类进行搜索,看看是不是有外面包着这个类的Class,搜索QQWalletRedPacketMsg,范围使用Field,排除掉类本身外,只有唯一的结果:MessageForQQWalletMsg:

public class MessageForQQWalletMsg extends ChatMessage {
  // 哦哦?COMMAND_REDPACKET?口令红包
  public static final int MSG_TYPE_COMMAND_REDPACKET = 6;
  public static final int MSG_TYPE_COMMON_REDPACKET = 2;
  public static final int MSG_TYPE_COMMON_THEME_REDPACKET = 4;
  public static final int MSG_TYPE_INDIVIDUAL_REDPACKET = 2001;
  public static final int MSG_TYPE_LUCY_REDPACKET = 3;
  public static final int MSG_TYPE_LUCY_THEME_REDPACKET = 5;
  public static final int MSG_TYPE_PUBLIC_ACCOUNT_REDPACKET = 2002;
  public static final int MSG_TYPE_TRANSFER = 1;
  ...

我们找到了一个常量字段,目测就是这个描述了是否是口令红包了。在该类搜索此字段还找到

public static boolean isCommandRedPacketMsg(MessageRecord messageRecord) {
  if (messageRecord != null && (messageRecord instanceof MessageForQQWalletMsg) && ((MessageForQQWalletMsg) messageRecord).messageType == MSG_TYPE_COMMAND_REDPACKET) {
    return true;
  }
  return false;
}

果然,我们再接着分别查找MSG_TYPE_COMMAND_REDPACKET和isCommandRedPacketMsg,结果只在TroopMessageManager里面找到了一段没成功反编译的代码中对方法isCommandRedPacketMsg的引用:

L_0x0100:
  r2 = com.tencent.mobileqq.data.MessageForQQWalletMsg.isCommandRedPacketMsg(r25);
  if (r2 == 0) goto L_0x011e;

这里如果是口令红包会继续走下去,而如果不是则会跳到L_0x011e。

而从类的名字来看,TroopMessageManager应该是指群消息管理者,应该没错,毕竟红包也是群消息的一种。

于是我们只能耐心地看下去这段神奇的充满goto的代码。晕着看完后大概看到就是各种逻辑判断和调用MsgProxyUtils.java去处理消息处理逻辑和缓存。然后就没了…好,你屌,是在下输了。我再试试别的。

7、常量大法

常量大法其实也可以算是字符串搜索的一种,只是不去搜索xml里的,而是使用中文转化为unicode后的字符串去进行查找。自行搜索Unicode编码转化可以找到online convertor。

口令红包对应的是”\u53e3\u4ee4\u7ea2\u5305”:

找到2个类共3处代码引用。

最后那个类的起名有点耐人寻味,PasswdRedBagManager,密码红包管理器,有点意思:

public void b(String str) {
  ((TroopTipsMsgMgr) this.f2203a.getManager(80)).a(str, "\u533f\u540d\u4e0d\u80fd\u62a2\u53e3\u4ee4\u7ea2\u5305\u54e6", NetConnInfoCenter.getServerTime(), BaseConstants.DEFAULT_QUICK_HEARTBEAT_TIMEOUT, f);
}

这串Unicode转换成中文后是”匿名不能抢口令红包哦”,原来还有这种逻辑,产品经理你真是够了。

这里我们重新从该类的上面看下来,大致扫一扫,发现onDestroy下面有一个方法打的log很神奇:

public long[] m883a(SessionInfo sessionInfo, String str) {
  if (QLog.isColorLevel()) {
    QLog.d(f2197a, (int) h, "openPasswdRedBagByPassword, passwd = " + str);
  }
  long[] jArr = new long[]{0, 0};
  if (sessionInfo == null) {
    return jArr;
  }
  if (TextUtils.isEmpty(str)) {
    return jArr;
  }
  c();
  List<String> list = (List) this.f2206a.get(str);
  if (list == null || list.isEmpty()) {
    return jArr;
  }
  PasswdRedBagInfo passwdRedBagInfo;
  String str2 = a(sessionInfo.a) + "_" + sessionInfo.f1757a;
  for (String str3 : list) {
    HashMap hashMap = (HashMap) this.f2209b.get(str3);
    if (hashMap != null) {
      passwdRedBagInfo = (PasswdRedBagInfo) hashMap.get(str2);
      if (!(passwdRedBagInfo == null || a(str3))) {
        jArr[g] = passwdRedBagInfo.a.uint64_creator_uin.get();
        if (!b(str3)) {
          if (!c(str3)) {
            hashMap.put(str2, passwdRedBagInfo);
            jArr[f] = 1;
            break;
          }
          jArr[f] = 3;
        } else {
          jArr[f] = 2;
        }
      }
    }
  }
  passwdRedBagInfo = null;
  if (passwdRedBagInfo == null) {
    return jArr;
  }
  b(sessionInfo.a, sessionInfo.f1757a, passwdRedBagInfo.a.string_redbag_id.get().toStringUtf8());
  a(sessionInfo, passwdRedBagInfo);
  return jArr;
}

isColorLevel目测是某种debug用的tag,可能某些环境下部分用户会打开,而从log结合我们平时打log习惯来看,这个方法应该就叫openPasswdRedBagByPassword了,第二个参数就是password。终于找到了。看一下逻辑大致是从外面load进来所有红包信息到本类的各种hashmap和list(有一个tag,只会加载第一次,本类多个方法都会调用这个方法),然后根据password从里面找到对应passwdRedBagInfo,设置result tag,然后调用了

b(sessionInfo.a, sessionInfo.f1757a, passwdRedBagInfo.a.string_redbag_id.get().toStringUtf8());
a(sessionInfo, passwdRedBagInfo);

我们先不急看这两个方法是做什么的。再往下看下一个方法,直接就有:

public long[] b(SessionInfo sessionInfo, String str) {
  if (QLog.isColorLevel()) {
    QLog.d(f2197a, (int) h, "openPasswdRedBagById, id = " + str);
  }

openPasswdRedBagById用id打开红包,猜测该id就是我们最早看到的结构里的redPacketId字段。

而该方法同样调用了

b(sessionInfo.a, sessionInfo.f1757a, str);
a(sessionInfo, passwdRedBagInfo);

看看这两个方法:

public void a(SessionInfo sessionInfo, PasswdRedBagInfo passwdRedBagInfo) {
  if (sessionInfo != null && passwdRedBagInfo != null) {
    Object obj = (sessionInfo.a == 0 || sessionInfo.a == h || sessionInfo.a == Action.ACTION_REGISTNEWACCOUNT_COMMITSMS || sessionInfo.a == Action.ACTION_LOGIN) ? g : null;
    String str = sessionInfo.f1757a;
    String valueOf = String.valueOf(passwdRedBagInfo.a.uint64_creator_uin.get());
    if (obj != null) {
      str = valueOf.equals(this.f2213d) ? sessionInfo.f1757a : this.f2213d;
    }
    JSONObject a = QQWalletMsgItemBuilder.a(this.f2203a, sessionInfo, passwdRedBagInfo.a.string_redbag_id.get().toStringUtf8(), passwdRedBagInfo.a.string_authkey.get().toStringUtf8(), str, "appid#1344242394|bargainor_id#1000030201|channel#msg", "graphb", null);
    Bundle bundle = new Bundle();
    bundle.putString("json", a.toString());
    bundle.putString("callbackSn", jbi.a);
    Intent intent = new Intent(this.f2200a, PayBridgeActivity.class);
    intent.putExtras(bundle);
    intent.addFlags(268435456);
    intent.putExtra("pay_requestcode", 5);
    this.f2200a.startActivity(intent);
  }
}

public void b(int i, String str, String str2) {
  if (!TextUtils.isEmpty(str2)) {
    HashMap hashMap = (HashMap) this.f2209b.get(str2);
    if (hashMap != null) {
      PasswdRedBagInfo passwdRedBagInfo = (PasswdRedBagInfo) hashMap.get(a(i) + "_" + str);
      if (passwdRedBagInfo != null && !passwdRedBagInfo.f4810a) {
        passwdRedBagInfo.f4810a = true;
        ThreadManager.a(new kmr(this, str2), h, null, true);
      }
    }
  }
}

发现第一个方法似乎就直接发请求了,看来只要调用到这里,就是可以领红包了。那最初又是如何来这里的呢?我们搜索对PasswdRedBagManager内这两个方法的引用找到BaseChatPie.java:

public PasswdRedBagManager f25190a;
...
public class EnterForSend implements OnKeyListener, OnEditorActionListener {
  ...
  // 这里从方法名判断是每次输入点击发送后调用
  public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
    if (i != BaseChatPie.dr) {
      return false;
    }
    String obj = this.a.f25220a.getText().toString();
    if (obj.length() > 0) {
      // 调用了外部类的下述方法
      long[] a = this.a.a(obj);
      // 再进行消息发送
      SendMsgParams sendMsgParams = new SendMsgParams();
      sendMsgParams.b = this.a.dL;
      sendMsgParams.a = this.a.dJ;
      sendMsgParams.c = this.a.dN;
      sendMsgParams.f26863c = this.a.dL;
      ...
    }
    return true;
  }
}

// 这里调用了那2个openPasswdRedBagxxx方法
public long[] m5613a(String str) {
  long[] jArr = null;
  // 非匿名模式才会继续尝试匹配口令红包,原来里里外外都做了判断
  if (!AnonymousChatHelper.a().a(this.f25174a.a)) {
    if (TextUtils.isEmpty(this.f25269d) || !str.equals(this.f25278e)) {
      // 使用密码打开
      jArr = this.f25190a.a(this.f25174a, str);
    } else {
      // 使用redPacketId直接打开
      jArr = this.f25190a.b(this.f25174a, this.f25269d);
    }
    // 无意义的打log打点啥的,华丽丽地无视吧
    if (jArr != null && jArr[s] == 1) {
      this.f25269d = QunUppUploadTask.QunUppAppId;
      this.f25278e = QunUppUploadTask.QunUppAppId;
      this.f25228a.sendEmptyMessage(dz);
      if (QLog.isColorLevel()) {
        QLog.d(PasswdRedBagManager.a, u, "passwdredbags result[0]=" + jArr[s] + ",result[1]=" + jArr[t] + ",send str=" + str);
      }
    }
  } else if (QLog.isColorLevel()) {
    QLog.d(PasswdRedBagManager.a, u, "current is in Anonymous, dont search passwdredbags");
  }
  return jArr;
}

可见每次我们输入消息发送时,都发生了判断,会去查询是不是红包口令,如果是则直接发请求拿红包然后继续,否则直接当做普通消息继续发送。所以如果想要做自动抢红包的话,其实只要直接在收到消息时,调用PasswdRedBagManager的open方法即可,连模拟UI、生成请求、发送消息都不用了,我们再也不用昧着良心说口令了。顺便我们还看到了手机QQ确实喜欢用Activity,这里的红包弹框也是一个单独的Activity,而且请求是发送到手Q红包那边去的,看来还分业务线。

到此为止我们的目的告一段落,其实继续下去,还可以尝试dump当前Activity,用Activity名字去查找,或者用hierarchy view看看view id试试。

经过上文的折腾,我们成功反编译了手机QQ,并追溯到手机QQ红包的数据结构和判断流程。期间经历过数次无用功,但逆向工程正是这么一回事,尤其是静态分析,如果不及时找其他的路,而一路钻牛角尖从一个线索一路去看,很可能会越陷越深,本文的跟踪流程正是不断在坑还小的时候钻出来,然后去找其他的路径,最后才快速地找到了想看的东西。

以上就是本文的全部内容,希望对大家的学习有所帮助。

(0)

相关推荐

  • Java实现仿微信红包分配规则

    最近过年发红包拜年成为一种新的潮流,作为程序猿对算法的好奇远远要大于对红包的好奇,这里介绍一种自己想到的一种随机红包分配策略,还请大家多多指教. 算法介绍 一.红包金额限制 对于微信红包,我们知道没人随机的最小红包是1分,最大金额是200元,这里我们同样来设置红包的范围,下面代码我们统一金钱的单位为分. //最小红包额度 private static final int MINMONEY = 1; //最大红包额度 private static final int MAXMONEY = 200

  • 微信随机生成红包金额算法java版

    最近几年玩得最疯狂的应该是发红包了,尤其是过年的时候特别受欢迎,下面写了红包的随机算法,其实挺简单的,仅是提供一种思路,希望可以给大家一些启发. public class WxAlgorithm{ /** * @param moneySum 输入总金额 * @param redNum 输入红包数量 */ private static void wxAlgorithm(double moneySum, int redNum) { // 设置最小的金额 double moneyMin = 0.01;

  • Android仿百度福袋红包界面

    首先来看一下效果图: 1.编程思路 看看界面,不难发现,其就是一个放入九张图片的容器,绘制其实可以在其上面另创建一个透明View负责绘制线与圆圈.下面我们将介绍一下实现过程. ㈠自定义ViewGroup 我们知道,自定义ViewGroup一定需要实现其onLayout()方法.该方法是设置子View位置与尺寸的时候调用.还有一个onMeasure()方法,该方法是测量view及其内容来确定view的宽度和高度. ㈡存储其点与圆的位置及绘制参数 当重回界面的时候,是不会保存上一次绘制界面的内容,必

  • Android实现QQ抢红包插件

    又想到快要过年了,到时候还不知道群里要发好多红包,所以我将之前在网上宕的一份微信抢红包的代码修改了一下,实现了QQ抢红包!可以支持抢QQ拼手气红包,普通红包,口令红包,现在再也不怕20年单身手速的人跟我抢红包了! 先看测试效果图: 1.抢QQ口令红包  可以看见,只要红包一发出,自动填写口令并发出,帮你将红包抢到手! 2.抢QQ拼手气红包 拼手气红包也是一样,只要红包一发出,自动帮你把红包抢到手,是不是很爽的感觉? 3.抢QQ好友发送的红包 只要好友或者群里的人把红包一发出,就会第一时间让你抢到

  • java开发微信公众号支付

    最近做了微信公众号支付的开发,由于是第一次做也摸索了几天的时间,也只是达到了实现功能的水平,并没有太多考虑到性能问题,所以这篇文章比较适合初学者. 微信公众号支付的总体其实很简单,大致就分为三步.第一步需要获取用户授权:第二步调用统一下单接口获取预支付id:第三步H5调起微信支付的内置的js.下面介绍具体每一步的开发流程. 一    首先要明确微信公众号支付属于网页版支付,所以相较于app的直接调取微信支付要多一步微信授权.也就是需要获取用户的openid.微信公众号使用的交易类型是JSAPI,

  • 微信java开发之实现微信主动推送消息

    1.拉取access_token2.拉取用户信息3.主动推送消息4.接口貌似要申请权限5.依赖httpclient4.2.3 和jackson 2.2.1 复制代码 代码如下: public class WeixinAPIHelper { /**  * 获取token接口  */ private String getTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=

  • 分享Android微信红包插件

    本文实例为大家分享了Android微信红包插件,供大家参考,具体内容如下 效果图: 具体代码 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void handleWindowChange(AccessibilityNodeInfo nodeInfo) { switch (Stage.getInstance().getCurrentStage()) { case Stage.OPENING_STAGE: // 调试信息,打印TTL // Lo

  • java实现微信公众平台自定义菜单的创建示例

    复制代码 代码如下: import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL; import org.json.JSONObject; public class MenuUtil { /**  * 获得ACC

  • 微信公众平台开发实战Java版之微信获取用户基本信息

    在关注者与公众号产生消息交互后,公众号可获得关注者的OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的.对于不同公众号,同一用户的openid不同). 公众号可通过本接口来根据OpenID获取用户基本信息,包括昵称.头像.性别.所在城市.语言和关注时间. 开发者可通过OpenID来获取用户基本信息.请使用https协议. 我们可以看看官方的文档:获取用户的基本信息. 接口调用请求说明 http请求方式: GET https://api.weixin.qq.com/cgi-b

  • java微信开发API第一步 服务器接入

    微信开发API如何接入服务器,下面就为大家进行介绍 一.说明 * 本示例根据微信开发文档:http://mp.weixin.qq.com/wiki/home/index.html最新版(4/3/2016 5:34:36 PM )进行开发演示. * 编辑平台:myeclipse10.7+win32+jdk1.7+tomcat7.0  * 服务器:阿里云 windows server 2008 64bits * 平台要求:servlet使用注解方式,平台要求:j2ee6.0+.jdk6.0+.tom

随机推荐