12306手机版App逆向研究

抽空研究了一下 12306 放出的 Android 版手机 App,本想挖一挖有没有手机版协议可供调用,不过就结果来看意义已经不大。以下简单记一下过程。

12306 的 apk 安装包可以用 WinRAR 打开,解压出 classes.dex,然后用 dex2jar 转换成 jar 包,就可以放到 jd-gui 里看 java 源代码了,源代码未经混淆,看起来十分方便。

在 jd-gui 里可以看到有好几个 java package,cn.domob 这个包是多盟的广告联盟代码,12306 App 的界面上未见到有广告,估计是用来做统计分析的。有 com.worklight 这个包,可以证明是用了 IBM Worklight 这个框架。真正实打实有内容的就是 com.MobileTicket 这一个包,其它的都是引入的库,并非 12306 相关的程序代码。阅读入口类 MobileTicket.class 的代码,可以发现其调用了本地的浏览器显示一个网页。也就是说:12306 App 的主界面其实是由网页组成的,整个程序的逻辑实现也都放在网页里了,java 源代码的干货其实很少。

  protected void bindBrowser(CordovaWebView paramCordovaWebView, boolean paramBoolean)
  {
    super.bindBrowser(paramCordovaWebView, paramBoolean);
    mDService = new DService(this, "56OJyf1IuNPVnYbzz4", "16TLwHboApIGwNU-ypi0ThBk");
    mDViewManager = new DViewManager(this, mDService);
    mDViewManager.doAppStartReport();
    paramCordovaWebView.pluginManager.addService("WebResourcesDownloader", "com.worklight.androidgap.plugin.SSLWebResourcesDownloaderPlugin");
    paramCordovaWebView.pluginManager.addService("NativeBusyIndicator", "com.worklight.androidgap.plugin.MyBusyIndicator");
  }

  public void init()
  {
    SSLWLWebView localSSLWLWebView = new SSLWLWebView(this);
    if (Build.VERSION.SDK_INT < 11) {}
    for (Object localObject = new CordovaWebViewClient(this, localSSLWLWebView);; localObject = new IceCreamCordovaWebViewClient(this, localSSLWLWebView))
    {
      super.init(localSSLWLWebView, (CordovaWebViewClient)localObject, new CordovaChromeClient(this, localSSLWLWebView));
      return;
    }
  }

知道是怎么回事,就得提取网页资源了。apk 安装包中的 assets/www 目录里有两个 resources.zip 文件,但似乎经过特殊处理,直接用 WinRAR 解压缩无法打开。另辟奚径,把手机程序安装到 Android 手机里(模拟器也行),然后用手机文件管理器打开 /data/data/com.MobileTicket 这个数据目录,就能看到程序使用的关键数据了。网页就存在 files/www/default 这里。

看了看代码,是基于 worklight 框架用 MVC 的方式实现的,view 都是 html 网页,js 来实现 model 和 controller,12306 的程序猿很厚道,js 都没有混淆,连中文注释都在,读起来十分方便,嘿嘿。比较关键的有 MobileTicket.js util.js 还有 controller 目录里的各个 js,看看文件名就知道是用来干什么的很好读。

这里有一个比较关键的 CheckCodePlugin(此 check code 并非我们看到的四位验证码,而是请求里自带的一个参数),通过网络抓包也会发现每次都会提交一个请求的参数是check_code,而这个东东实现上貌似是由 libcheckcode.so 这个 native 库来实现的(CheckCodePlugin.js 中有一份调试版本,但未可知调试版本与实现版本有何不同)。把 libcheckcode.so 放到 IDA Pro 里进行反汇编分析,在导出表中能看到 main, MD5_Init, MD5_Update 等几个模块,看了看汇编代码,也就是简单地做了个 MD5 Hash,看来 libcheckcode.so 与 js 的行为应该是一致的。很好奇为什么做 MD5 这活儿非要用一个 native 的库来完成,为了故弄玄虚?也许是为了保密或者准备以后换成更复杂的 checkcode 算法。

关于抓包,12306 App 需要读取本地网页,单纯给 Android 设置系统级代理然后在 PC 上用 Fiddler 抓包的话,打开程序便会跳出错误,因为无法读取本地的 file:/// 开头的 URL (其实就是本地网页)。如此,需要用 ProxyDroid 这样的代理 App,使之忽略掉 localhost 和 intranet address,然后在 Fiddler 中启用 HTTPS 分析,就可以轻松抓到 12306 手机 App 的协议。

协议中最开始有 IBM Worklight 框架自带的验证过程,由于此验证过程由 Worklight 框架实现,分析起来较为复杂。App 里用到了 wl_antiXSRFRealm,wl_deviceNoProvisioningRealm 和 wl_authenticityRealm,这些实现都可以在 apk java 源代码的 com.worklight.wlclient.challengehandler 包中找到。

以上是主要的研究心得。最开始以为 libcheckcode.so 是用来生成图片验证码的,这样 12306 App 实际上不需要从服务器取得验证码,而是在本地完成,没想到这个猜测是错误的。实际上 App 仍然要从服务器请求验证码,这一流程在 orderManager.js 中可以看到 refreshCaptcha() 即是。总结一下,手机协议的优势是验证码简单容易破解,但是代价是 worklight 框架复杂,初始验证过程不好模拟,且容易被服务器端的协议升级所反制,写一个手机协议的刷票软件仍然是十分费力且不讨好的事。

“12306手机版App逆向研究”的12个回复

  1. 您好,能知道libcheckcode的md5方式么,应该是和传入的参数和另外一个key加起来md5的。现在我能想到的方法就是在android里调用libcheckcode.so。libcheckcode这部分应该不是那么容易更新的,如果更新这部分算法直接意味着老版本不能用。其他代码都有内建的更新流程。

    1. 调用过程的话直接在 js 里找找应该有收获。

      具体实现的话,需要用反汇编分析,自己下一个 IDA Pro 然后针对 ARM 架构反汇编,就能看个大概。

        1. 这个挺简单的,ios上直接可以找一个越狱机器,然后直接lldb上去,通过汇编设置断点,看里边的值就行了。可以加qq一起讨论1281494013

  2. 请问大家找到了checkcode 的算法了吗,我看见别人反汇编的代码
    ; CheckCodePlugin – (void)getcheckcode:(id) withDict:(id)
    ; Attributes: bp-based frame

    ; void __cdecl -[CheckCodePlugin getcheckcode:withDict:](struct CheckCodePlugin *self, SEL, id, id)
    __CheckCodePlugin_getcheckcode_withDict__

    var_68= -0x68
    var_5C= -0x5C
    var_58= -0x58
    var_54= -0x54
    var_50= -0x50
    var_4C= -0x4C
    var_48= -0x48
    var_34= -0x34
    var_30= -0x30
    var_2C= -0x2C
    var_28= -0x28
    var_24= -0x24
    var_18= -0x18

    PUSH {R4-R7,LR}
    ADD R7, SP, #0xC
    PUSH.W {R8,R10,R11}
    SUB.W R4, SP, #0x40
    BIC.W R4, R4, #0xF
    MOV SP, R4
    VST1.64 {D8-D11}, [R4@128]!
    VST1.64 {D12-D15}, [R4@128]
    SUB SP, SP, #0x50
    MOVW R1, #(:lower16:(selRef_pop – 0x4A80))
    MOV R4, R2
    MOVT.W R1, #(:upper16:(selRef_pop – 0x4A80))
    STR R0, [SP,#0x68+var_54]
    ADD R1, PC ; selRef_pop
    MOV R0, R4
    LDR R1, [R1] ; “pop”
    BLX _objc_msgSend
    MOVW R3, #(:lower16:(___objc_personality_v0_ptr – 0x4A98))
    LDR R1, =(unk_220394 – 0x4A9A)
    MOVT.W R3, #(:upper16:(___objc_personality_v0_ptr – 0x4A98))
    MOVW R2, #(:lower16:(selRef_objectAtIndex_ – 0x4AA4))
    ADD R3, PC ; ___objc_personality_v0_ptr
    ADD R1, PC ; unk_220394
    MOVT.W R2, #(:upper16:(selRef_objectAtIndex_ – 0x4AA4))
    STR R0, [SP,#0x68+var_50]
    LDR R3, [R3] ; ___objc_personality_v0
    ADD R2, PC ; selRef_objectAtIndex_
    STR R3, [SP,#0x68+var_34]
    ADD R0, SP, #0x68+var_4C
    STR R1, [SP,#0x68+var_30]
    LDR R1, =0x134
    STR R7, [SP,#0x68+var_2C]
    ORR.W R1, R1, #1
    STR.W SP, [SP,#0x68+var_24]
    ADD R1, PC
    STR R1, [SP,#0x68+var_28]
    MOVS R1, #1
    LDR R5, [R2] ; “objectAtIndex:”
    STR R1, [SP,#0x68+var_48]
    BLX __Unwind_SjLj_Register
    MOV R0, R4
    MOV R1, R5
    MOVS R2, #0
    BLX _objc_msgSend
    STR R0, [SP,#0x68+var_58]
    MOV R0, #(selRef_stringWithFormat_ – 0x4AEA) ; selRef_stringWithFormat_
    MOV R5, #(classRef_NSString – 0x4AEC) ; classRef_NSString
    MOV R2, #(stru_254554 – 0x4AF2) ; “%@%@”
    ADD R0, PC ; selRef_stringWithFormat_
    ADD R5, PC ; classRef_NSString
    MOVW R3, #(:lower16:(cfstr_Fqn1 – 0x4AFE)) ; “1”
    ADD R2, PC ; “%@%@”
    MOVT.W R3, #(:upper16:(cfstr_Fqn1 – 0x4AFE)) ; “1”
    LDR R1, [R0] ; “stringWithFormat:”
    MOVS R0, #2
    LDR R5, [R5] ; _OBJC_CLASS_$_NSString
    ADD R3, PC ; “1”
    STR R5, [SP,#0x68+var_5C]
    STR R0, [SP,#0x68+var_48]
    LDR R0, [SP,#0x68+var_58]
    STR R0, [SP,#0x68+var_68]
    MOV R0, R5
    BLX _objc_msgSend
    MOV R2, R0
    MOV R0, #(selRef_md5_ – 0x4B18) ; selRef_md5_
    ADD R0, PC ; selRef_md5_
    LDR R1, [R0] ; “md5:”
    MOVS R0, #3
    STR R0, [SP,#0x68+var_48]
    LDR R0, [SP,#0x68+var_5C]
    BLX _objc_msgSend
    STR R0, [SP,#0x68+var_5C]
    LDR R0, [SP,#0x68+var_58]
    CBZ R0, loc_4B7C
    可是真心看不懂。大神能帮忙看看吗
    还有这句,window.CheckCodePlugin.getCheckCode(
    onCheckcodeSuccess, onCheckcodeFailure,
    (time_str + common[‘baseDTO.device_no’]));
    好像是设备id 和time_str 作为参数传到libcheckcode.so。

    1. 先看看 js 里 debug 版的 checkcode 函数吧,这里用的是 arm 汇编,还得找本手册看看,挺费事儿的。

  3. 你好,这个SO返回的string。是用来和服务器那边验证的吗?
    假如说这个string只是取一个时间戳和设备ID,计算得到一个string。然后将这个string作为口令来和服务器交互的话。
    那么,这个so里的算法完全不用分析 可以直接用这个so

    1. 有人想在 PC 上跑这个手机版的协议玩刷票,所以这个 ARM 架构的 SO 文件是没法放在 PC 上跑的。

      而且即使是直接用,也要搞清楚 so 的导出函数,参数和返回值的类型什么的都必须清楚才行。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注