Tuesday, January 24, 2012

12306刷票记

新blog:sskaje.me
本文地址:http://sskaje.me/index.php/2012/01/12306bot/

我也记不清啥时候动了写bot刷票这个念头的。原因很简单,我一直认为作为一个以代码谋生的不合格程序员,只有把生产工具用好,才能增加自己存在的价值。

首先说明一下主要开发环境:Windows 7,PHP 5.3,php_curl。

翻到了 第一条关于刷票的微博,附了图

很不低调地炫耀。

要刷票,首先自然得熟悉目标系统,所谓踩点。firefox+firebug,抓了一个标准流程的请求:登录、查票、订票。确认订单一开始没敢点,怕会有什么影响,后来去注册了几个测试号,然后尝试了确认订单的操作。流程本身不复杂,但是提交参数有点太多,一步一步来。

回到图1,登录,其实核心在验证码。

1 验证码识别
登录的验证码处理起来很简单,图在 https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=lrand,提供一个示例
这个验证码结构一直没变,字体比较规则,无变形,直接就想到了tesseract,上一次用这个的时候是2010年12月初为了刷顺丰快递的订单追踪。

流程很简单:
1 下载图片;
2 转tiff(使用了ImageMagick的convert);
3 tesseract识别;
4 读取识别文件;
5 简单判断识别输出是否合法;
6 拼登录请求。

代码里留了个 decaptcha_valid()的方法,不过只判断了是不是4位数字+字母。
写完第一版代码后,清理了一些个人信息,把代码发给了@也云,不过当时毕竟比较早,他没怎么看,但是后期验证码识别这块提供了一些改进,包括:
1 replace识别输出里的空字符,因为由于字符间距不确定,tesseract偶尔会识别出空格;
2 训练tesseract,优化识别效果;
3 这次tesseract用的是v3,已经支持多种图片格式,而我还是按上次使用的经验convert jpeg 2 tiff,这次直接省去这个过程;
4 最关键的,登录验证码在登录请求提交后并不会删除上次访问验证码图片生成的session字段,也就是说,只需要识别一次验证码,后续只需要暴力提交即可

按4调整了验证码识别方法,增加了人肉输入的支持。使用system('out.jpg')直接利用Windows 7 CMD的高级功能,同时发现了Win7自带的图片查看器system执行后程序可以继续往下走而不必关掉图片查看器,这样可以alt+tab切换cmd和图片查看器,确认人肉识别的输入是否正确。

上述逻辑在1月19号之前也同样适用于确认订单的验证码,那天早起发现这块儿验证码需要每次请求了,幸好之前人肉识别的改造增加了配置开关,很轻松地就完成了改动(虽然19号刷票无一成功,但是20号回家客车上刷5张票轻松到手)。

2 查票
查票的过程其实没啥特别可以说的,简单拼个请求,查票就是了。不过返回值是需要处理的,处理的依据,从页面看,就是查票结果输出里的“预订”按钮。

预订的参数就是页面输出里的getSelect()这个js调用的参数,最初的结构我从另外一条微博里找到了例子。
查票输出
正如页面上所示,结构是“车次号#历时(分钟单位)#发车时间#某个ID#始发站编号#目标站编号”,这个参数由页面解析后带入实际的预订请求。 不过后来某一天,这个参数变了,一个输出示例是“Z67#11:26#20:06#2400000Z6705#BXP#NCG#07:32#北京西#南昌#10175000003030800001404860000160895000001017503001”。同样按上边的解释,结构是“车次号#历时#发车时间#某个ID#始发站编号#目标站编号#到站时间#某个带入ypInfoDetail字段的参数”,@也云同学研究了一下最后这个字段,认定是“余票信息”的意思,对我们没有特别的意义,可无视(后续也证实了)。

这里可能有用的信息包括上述的“某个ID”。而查票的时候也需要输出里的始发站和目标站编号,这个编号可以从 这个地址里找到

3 订票 && 提交订单
订票使用了查票的输出参数,提交url是 https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=submutOrderRequest。明显有个Typo,而且这里名字是“提交订单”。不管那么多,说技术细节。

这个使用查票参数构造了一个POST请求,如果提交成功,服务器端会发一个302,重定向到一个新的页面 https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=init。

流程很简单,curl实现只需要开 CURLOPT_FOLLOWLOCATION 即可。但是这个地方卡了我很久,直到12月初的某天才查出来为什么每次提交在这里总是出现 HTTP 500,虽然还是不知道原因,但是补上了几个http头,一切ok了(具体看代码)。

订票输出的页面,会有formtoken,参数名是 org.apache.struts.taglib.html.TOKEN。有点Web编程经验就应该能知道是干啥的。不多说,参数请求必须带上,所以正则匹配,无技术含量。
确认订单页只是拼参数,补上验证码识别就ok,无技术含量。

第一个细节,这个页面是填身份证的页面,入口有两个,订票 和 确认订单失败(后期确认订单失败不再直接回到该页面,不过验证码错误的应该还是会回来),所以确认订单的错误处理是可以考虑增加的。这个一开始代码就加上了,不过后来@也云改的lite版把这个流程改为 提交订单 + 确认订单,两步走,确认失败直接重订票。

第二个细节,验证码URL不一样了,https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=randp,不过输出还是一样的。

第三个细节,最关键的,某个晚上睡前突然意识到的问题。回到form token上,我们可以观察到这个系统很多地方都会有form token,包括但不限于确认订单、支付订单、撤销订单等等。但是我意识到一个问题,提交订单这个过程可能会很慢,慢到一个不能忍的状态,尤其是高峰期。而,用户中心的表单里,也有form token,而且参数名完全一样,那么,这两个token会是同一个session字段吗?
接下来做的事情是改造form token的获取方法,改从用户中心的页面匹配,仅作测试。测试结果很乐观,从用户中心匹配到的token能作为确认订单token,而且这个请求自然会比从提交订单拿到的页面匹配要快,于是匹配token的方法可以优化了。(但是16号早上,订单系统似乎出问题了,各种慢,未支付订单页面刷不出来,支付订单也失败了,3张票扣了我1.3k RMB,没出票,极度想粗口!!!)

说到第三细节,如果你顺着读下来,可能会觉得有疑问:不是有很多参数要从页面匹配吗?如果不去拿提交订单后的页面,匹配变量,怎么确认订单?
答案是:你所需要的参数从你拿到查票输出的时候就已经足够了。你所需要的车次相关的信息,查票的输出已经能满足所有字段的需求;你所需要的订票人信息,以及所需坐席,你自己是已经知道了的。而,经过观察,查票输出里除了余票信息字段会变,其他的都不变!!!所以,你只需要在确定要刷票的前一天晚上,做一下准备工作,查查你要刷票的车次的信息,第二天拿着刷票便是了!!!
具体不多说,看代码便知。

到此,基本能说的都说了。

后边还有一些其他的细节优化。
支付
最初的支付表单是在一个独立的页面上,从未完成订单里选择订单,点支付,弹出的一个新页面匹配几个参数再submit,输出页面只有一个简单的form,body onload的时候触发了submit操作。也就是说,只需要拿下这个页面完成支付即可,这个最早@也云实验成功了,后来我有几次支付也用这个方法完成。过了几天,支付的表单在未完成订单点支付后的页面里直接写死了,于是新的lite版bot直接匹配了页面表单,存了个独立页面。
完后,弹出IE,支付。
本来想做招行的手机支付自动化的,后来实在懒了,自己订票的需求满足了,没心做这种优化。

登录
登录是件苦力活,经常就人满了,频率高了IP也会被封。所以登录成功一次得好好珍惜。
做法是: CURLOPT_COOKIEFILE + CURLOPT_COOKIEJAR。
脚本执行结束后,手工改 cookie jar文件,把expiration时间改长点。
但是由于经常性在登录完成后ctrl+c,没法触发__destruct()的调用,按@也云的建议,改成了登录完成后触发一次curl close操作并改expiration,执行结束后的继续保留。


源码
/bot.php
/config/test.php

执行参数 php bot.php 0 test 2>err.txt
只保证win下可用,soff改过一个mac下可用的版本,没找他要。
test 是 config/ 下的 test.php
0 是 test.php 里的tickets_info字段的索引,其实叫train_info更合适

然后为了骚扰自己,加了个vbs文件,内容很简单,取名messagebox.vbs,这段内容感谢@linxinsnow
' Message box Script Set objArgs = WScript.Arguments For I = 0 to objArgs.Count - 1 msgbox objArgs(I) Next

写在最后
虽然我经常在微博XY,但是一直没公开源码。最终可用的代码也只有也云和soff拿到了。
一直不开源或者不发布的原因很简单,我不希望我的代码成为别人系统“有压力”的理由。我也看到了几个网上放出来的刷票工具或者js解决方案,但是,越多人知道这个,就意味着越多人会用你的“刀”“杀”铁道部。
或许我想的有点多。

感谢也云同学对本年度刷票的支持。感谢soff提供的验证码识别优化方案,虽然效果还不如我们的。

Saturday, December 31, 2011

纪事:2010-2011


工作第三年过半,如果从实习开始算,也已经满了三年了。
2009年底,正式工作刚半年,在BITUNION上替版主掺和了年终总结的活动,然后2010年。今年,不知觉忘了这事,前不久去联盟职场版看了眼,efni已经开了新的活动。没曾想,这一无聊之举也成了版面的小传统了。
翻看了一眼09年写的总结,时间过得真快。10年,才到新公司半年,犹豫了一下最后没写。于是这回得从2010年开始回忆了。
2010年初,继续以尴尬的身份在前公司混着,先是0911月在微博产品的正式上线后离开,然后在那层好几个产品线转了一圈,**产品的准上线,**产品的夭折,然后经历了人生中的第一次项目被砍、部门被砍,最后尴尬地回到了微博。紧接着,离职潮,虽然总共就4个人,整个组就这样先后离开了前公司。
决定离开之前,犹豫了很久。很迷茫,不知道自己该做什么,该学什么,不知道微博适不适合我,也不知道下一份工作在哪儿。只知道,4个人的小团队散了,很难找到一个理由一个人留下;微博或许很好但是不适合我,找不到归属感。6月初甚至更早的时候,Sunshow同学一次电话一次bg欲拉我入伙,犹豫着没给答复,只是说6月底给结论。soff那儿当时也有可以做的项目,没心思做。只是在那儿纠结着。直到有一天xkl同学告诉我她确认要离职了,纠结了好一会儿然后在msn上跟她说出了我的打算,顿时心里轻松了不少。
那时学校里的事情总算有了个结论,压在心里的几块石头少了一块最大的。然后给几个领导简单地写了个邮件说了自己想离职的事情,紧接着被耗子找着聊了两次。依然记得当时说了,趁自己年轻,还有机会犯错,再过几年就彻底没机会了。然后没多久,就离职了。(感谢高成同学替我折腾了好久离职的手续。)
然后7月第二个周一,换了个新的环境,从零开始。
开始的半年,完全站在旁观者的位置以学习的状态看待要做的内容,因为真的不知道要做什么,模模糊糊的有个目标却不知道有什么需求。搭积木似的堆需求、堆模块,堆了小半年,新项目的上线时间也从一开始的8月变成10月,12月,第二年……最后被告知农历年后要上线,这才开始上粘合剂,把项目搭了起来。
2010年后半年,还在中关村,除了加班还是加班。小team初成型,公司也从10多个人变成20个,30个;挤着一个2M的破铁通宽带,经历过一次QQ群被屏蔽;无数次吃饭时被紧急电话告知线上系统挂了;为了躲酒翘了第一次大FB;集体加班叫小豆外卖,中午的午饭上楼,免费饮料,定期的毛毛虫,还有偶尔出现的地铁口的外卖烤肉串,有双夹,以及各种抱怨午饭难吃然后下楼买的炒饼;有午间的魔兽,有PESCS;还有那6点准点关掉和周末从来不开的空调,天热的时候真想抱着电脑去电梯里干活……
2010年,偶然的机会,开始尝试接触抗战史,于是开始读书学习,加班之余释放压力。2010年,无数次折腾,用了两年的nokia终于告急,找萧剑风借了个HTC Magic凑合了半年,然后自己买了个iphone,也第一次有了信用卡。2010年,用了两年半的笔记本被变成了台式机,然后换了个新的,也是2010年,买了台5k的联想本带回去送给了爹娘……
然后2010年结束了,2011110号搬到了亮马桥,开始从每天100米变成每天地铁+公交,也开始了寻觅午餐的痛苦生活。
年前,百般无奈买了全价机票,准备从南昌再折腾一次,然后也云同学告诉我弄到了两张软卧,于是happy地省了小1k回家了。
年后回来,开始紧张的上线前准备工作。上线从2011022102时开始,翻看着当时的微博来了段流水账般的回忆:
20110220日凌晨2点,新系统上线前的预演。等待是漫长的,从19日中午开始。翻看着那时候的微博,似乎当时正在替28同学干活,折腾一个LFI的东西,未果,然后只能去睡觉。那个时候可能是qatang吧,不知道拿啥放着哈狗帮的东西,本来椅子就难受,然后耳旁还各种噪声,怎么着都没睡着。无聊,刷微博,看球,然后3点多看球睡着了,直到5点半,才撤回去睡觉。
20日睡到中午,吃完午饭,买了无数红牛,备着晚上上线前用。看着当时的微博,完全不知道自己在干啥,只知道喝了三瓶红牛,还困得不行睡着了。
21日凌晨2点开始执行上线流程,一切都很顺利,出乎意料的顺利。白天没回去。上午在公司趴桌上睡觉,没睡好,但是也没人骚扰说系统不可用,直到下午,充值系统各种掉单,然后线上查bug,查得死去活来,终于搞定。晚上11点,去雍和宫的金鼎轩吃了夜宵,也许是晚饭。
22日下午才去的公司,结果晚上临下班系统出问题,被迫把一个op cache的组件下了,然后又发现另外的问题,陪着俩人熬到4点多,中间又出去买了红牛,继续喝红牛睡觉。
…………
然后后来,工作似乎开始进入了新的篇章,稳定而又不平淡。开始踢球了,开始跟着一群人喝酒了,各种吃,各种玩。见证着公司的发展,也很享受现在这种团队的氛围。
2011年,有久等不至的302731,也有难见空车的十号线;有令人发愁的午餐,也有高喊着减肥各处海吃的晚饭;有兰特伯爵的德国啤酒,也有门口的山西面;有科荟路的脆骨,也有簋街的烤串;有每周一次的欢乐足球,也有不再掺和的香山;有雨后天晴的秦皇岛,也有青山绿水的雾灵山;有无数新面孔的加入,也有不少老面孔的难再见。
也许真是太过怀念2010,总觉得2011没那么丰富多彩,但是亲眼见证着公司的成长,产品的上线,这一年,值得的。

--------------------------------
拖了好几天,最终决定舍弃一些不该说的话,凑合了这篇不疼不痒的两年回忆,权当总结这两年吧。
不善言辞亦不习惯从言语上表达感谢,但是最后,感谢本文提及和未提及的所有人。

Saturday, October 23, 2010

live essential 2011 messenger msgsres.dll ad-free beta

这个纯粹是为了凑发博数量:P

上一篇记录的东西大体没问题,一开始用的时候的确广告都没了,但是突然我发现聊天窗口的广告又出现了。。。于是各种囧。。。

这次简单地复了一遍流程,改一点测一点。

log如下:
1195 
 找到 SSConstrainer,layoutpos=top -> layoutpos=none
  主窗体广告消失。


1192
 adbannercont, layout=FillLayout() -> layoutpos=none

  聊天窗口动画广告还在,关掉动画后的文字广告已经不在了

 找到 adbannerregiontrident,删除 layout=FillLayout()


下载地址:http://sinaurl.cn/h40Mhk
有问题请sina微博 @sskaje
感谢 Messenger Plus!,Resource Hacker,微盘/SAE 的存储

Thursday, October 21, 2010

Windows Live Essential 2011 Messenger 去广告

上篇相关的文章:http://blog.sskaje.name/2009/09/msn.html

等了好久的uib decoder,终于在Messenger Plus! 4.90.0.392 中拿到了(获取前边已经有了,但是一直没敢再次尝试恐怖的live essential)。

操作流程差不多,区别如下:
这次有3个bin文件,920 4010 "Data_1192.bin",923 4010 "Data_1195.bin",925 4010 "Data_1197.bin"。

1195 修改:找到 SSConstrainer,layoutpos=top -> layoutpos=none
1192和1197大体相同都作三个修改:
1) adbannercont,该行 layout=FillLayout() -> layoutpos=none
2) 上边 layoutpos=none
3) 跟 2 同一行,去掉 layout=FillLayout()

保存,替换就行。
这次语言都是1033,直接替换就好。二进制文件可以 mail 我 sskaje (at) gmail dot com

================
这次在1192/1197上折腾死了,唉,唉

Tuesday, August 31, 2010

Problems with tracing location in callback-ed functions/methods

Problems with tracing location in callback-ed functions/methods

Exceptions thown in a function/method been called via 'call_user_func' or 'call_user_func_array', have key 'file' and 'line' disappeared in their first element (array[0]) from Exception::getTrace().
getTrace());
}

function callback() {
 call_user_func('test');
}

try {
 callback();
} catch (Exception $e) {
 var_dump($e->getTrace());
}
?>

G:\wwwroot\Test\Exception>php trace_func.php
#0  test() called at [G:\wwwroot\Test\Exception\trace_func.php:8]
array(1) {
  [0]=>
  array(4) {
    ["file"]=>
    string(40) "G:\wwwroot\Test\Exception\trace_func.php"
    ["line"]=>
    int(8)
    ["function"]=>
    string(4) "test"
    ["args"]=>
    array(0) {
    }
  }
}
#0  test()
#1  call_user_func(test) called at [G:\wwwroot\Test\Exception\trace_func.php:14]
#2  callback() called at [G:\wwwroot\Test\Exception\trace_func.php:18]
array(3) {
  [0]=>
  array(2) {
    ["function"]=>
    string(4) "test"
    ["args"]=>
    array(0) {
    }
  }
  [1]=>
  array(4) {
    ["file"]=>
    string(40) "G:\wwwroot\Test\Exception\trace_func.php"
    ["line"]=>
    int(14)
    ["function"]=>
    string(14) "call_user_func"
    ["args"]=>
    array(1) {
      [0]=>
      string(4) "test"
    }
  }
  [2]=>
  array(4) {
    ["file"]=>
    string(40) "G:\wwwroot\Test\Exception\trace_func.php"
    ["line"]=>
    int(18)
    ["function"]=>
    string(8) "callback"
    ["args"]=>
    array(0) {
    }
  }
}
Similar result given by debug_print_backtrace(), is this a bug or an unexpected feature ? XD

Wednesday, August 25, 2010

foreach 赋值引用 + refcount 引发的一个问题

第一个问题,封装了 mysqli_stmt 的bind_param方法,为了实现 $stmt->execute(array(typelist, var1, var2, ...)); 的操作。
<?php
  $values[0] = $info[0];
  foreach ($info[1] as $k=>$v) {
   $fields[] = "`{$k}`=?";
   $values[] = & $v;
  }

这个操作的结果显然是错误的。于是有了个下边的测试:
<?php
$array = array(
1,2,3,4,5
);

$to0 = array();
foreach ($array as $k=>$v) {
 $to0[$k] = & $v;
}
var_dump($to0);


$to1 = array();
foreach ($array as $k=>$v) {
 $to1[$k] = & $v;
 unset($v);
}
var_dump($to1);


$to2 = array();
foreach ($array as $k=>&$v) {
 $to2[$k] = & $v;
}
var_dump($to2);

/*
Output:

G:\wwwroot\Test>php reference3.php
array(5) {
  [0]=>
  &int(5)
  [1]=>
  &int(5)
  [2]=>
  &int(5)
  [3]=>
  &int(5)
  [4]=>
  &int(5)
}
array(5) {
  [0]=>
  &int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
  [3]=>
  int(4)
  [4]=>
  int(5)
}
array(5) {
  [0]=>
  &int(1)
  [1]=>
  &int(2)
  [2]=>
  &int(3)
  [3]=>
  &int(4)
  [4]=>
  &int(5)
}

*/


第一个问题结束
---------------------------------------------------------

第二个问题,
最上边代码后来的版本是这样的:
<?php
  $values[0] = $info[0];
  foreach ($info[1] as $k=>&$v) {
   $fields[] = "`{$k}`=?";
   $values[] = & $v;
  }
  unset($info);

  $stmt = $db->prepare($query);
  $ret = $stmt->execute($values);

结果传递多于一个字段时,execute报错。
假设foreach 循环结束后,$values 内容是:
array(5) {
  [0]=>
  &int(1)
  [1]=>
  &int(2)
  [2]=>
  &int(3)
  [3]=>
  &int(4)
  [4]=>
  &int(5)
}
而在execute()的时候,却变成了
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
  [3]=>
  int(4)
  [4]=>
  &int(5)
}

除了最后一个元素外,其他的引用都消失了。。。
在几个点放上debug_zval_dump,一看便明白了。

foreach 循环结束的时候,$values 数组涉及到引用的元素,除了最后一个 refcount 为3,其余均为2. 因为对于其他的元素,$info 和 $values 两个数组的元素都对目标进行了引用,所以refcount为2,而最后一个,因为 foreach 的临时变量 $v 还对这个有引用,所以 refcount 是3.
当执行了 unset($info) 的操作后,所有的refcount均 -1,于是变成了 11112。
这个时候var_dump,自然会发现只有最后一个元素还处于引用的状态。

Monday, August 16, 2010

第一次完整流程体验wep破解

一个白天都在折腾bt4,直到下午才把usb persistent live 做好。
大致步骤:
live cd/dvd 启动 bt4;
1个4GB U盘,fdisk 分区成 2+2,fat32+ext3,激活1;
mount -o loop bt4 dvd iso, 复制内容到fat分区;
grub-install --no-floppy --root-directory=/PATH/TO/FIRST/PARTITION /dev/DEVICENAME;
done;
印象中就这几步,具体可以看看bt4官网的tutorials

破解过程其实都还简单,手头没教程,就只能自己折腾。
一开始遇到问题,所以后来先把 realtek的驱动装了,不过后来想想 iw scan时 nl80211 not found 的问题,应该不用装驱动;
iwlist wlan0 scanning,找到bssid,因为目标是在windows下就确定了的,所以根据essid,找到bssid,也就是mac,记下来就行;
airodump-ng 。。。。。 参数不记得了= =, --ivs --write test ..... 反正有一个 filter 用是 bssid,需要指定interface(最后一个参数),第一次抓了11466个包,不成,后来重搞,抓了20000+个,搞定了;
aircrack-ng 。。。。。 参数也不记得了,有个 -a 1 指定wep,有个指定ivs文件,最后结果大致入下:

                                                 Aircrack-ng 1.1 r1738


                                  [00:00:07] Tested 108182 keys (got 20290 IVs)

   KB    depth   byte(vote)
    0    1/ 33   13(25648) 4F(25392) 43(25280) 25(24912) D0(24836) 47(24800) AE(24696) 76(24544)
    1    0/ 11   57(27220) C4(26432) F8(26416) E3(25616) FB(24620) F6(24588) 04(24472) AC(24292)
    2    0/  1   92(29236) B8(25916) 84(25172) 8D(25092) 4E(24980) E6(24684) B1(24476) 41(23960)
    3    6/ 33   46(24548) 7C(24512) A6(24364) 72(24216) EA(24152) 14(23968) 27(23884) 64(23448)
    4    8/ 10   08(24404) C2(24332) 1E(24284) 11(24256) A7(24176) 30(24108) FC(24072) 02(23888)

                         KEY FOUND! [ 13:57:92:46:80 ]
        Decrypted correctly: 100%