抽空研究了一下 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 框架复杂,初始验证过程不好模拟,且容易被服务器端的协议升级所反制,写一个手机协议的刷票软件仍然是十分费力且不讨好的事。
您好,能知道libcheckcode的md5方式么,应该是和传入的参数和另外一个key加起来md5的。现在我能想到的方法就是在android里调用libcheckcode.so。libcheckcode这部分应该不是那么容易更新的,如果更新这部分算法直接意味着老版本不能用。其他代码都有内建的更新流程。
调用过程的话直接在 js 里找找应该有收获。
具体实现的话,需要用反汇编分析,自己下一个 IDA Pro 然后针对 ARM 架构反汇编,就能看个大概。
麻烦的不是checkcode吧,
我看wl_authenticityRealm比checkcode麻烦得多。
确实是。所以用手机协议自己做 12306 的 App 这事不值当的。
我愿意出钱,请大神帮我弄出check_code的算法!
这里的 check_code 不是验证码,只是一个简单的针对传入参数的 MD5 Hash
我知道的,我就是不能算出这个东西,导致程序中断开发。加我QQ 详谈282122585
这个挺简单的,ios上直接可以找一个越狱机器,然后直接lldb上去,通过汇编设置断点,看里边的值就行了。可以加qq一起讨论1281494013
请问大家找到了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。
先看看 js 里 debug 版的 checkcode 函数吧,这里用的是 arm 汇编,还得找本手册看看,挺费事儿的。
你好,这个SO返回的string。是用来和服务器那边验证的吗?
假如说这个string只是取一个时间戳和设备ID,计算得到一个string。然后将这个string作为口令来和服务器交互的话。
那么,这个so里的算法完全不用分析 可以直接用这个so
有人想在 PC 上跑这个手机版的协议玩刷票,所以这个 ARM 架构的 SO 文件是没法放在 PC 上跑的。
而且即使是直接用,也要搞清楚 so 的导出函数,参数和返回值的类型什么的都必须清楚才行。