存档

作者存档

360随身WiFi试用报告

2013年6月20日 没有评论

这两天我厂公开了的名为 360 随身WiFi 的小东西,着实火了一把。某有幸在6月初的时候拿到了内测版,并且端午节假期休假期间正好好好用了一番,遂写个简单的试用报告。

这个小东西的内测发生在儿童节前一天晚上9点,某正在辛勤加班中,突然一封全员邮件出现,说有儿童节神秘礼物,限量200个,最先回邮件的200人有。本着不抢白不抢的原则,回邮件。过了两天就要求去领了,竟然要先签署NDA,然后加飞信群,最后领回来的是一个小白盒子(因为有NDA,所以我今天才能发这个)。

开箱

盒子(我把东西已经拿出来了)

盒子颇有苹果范儿,但这是内测版,据说现在公开发售的版本盒子改了包装了,会更好看一点。

盒内东西

拆开盒子是一个黑色的小指大小的设备,一根略显山寨的挂绳和一个耳机塞。把挂绳和耳机塞连在一起费了些功夫,内测群里面也是一片吐槽,据说正式版已经改掉了。照片是我已经拴好的。里面还有一张薄薄的说明书,是个四页折纸,内测版很简陋,估计正式版已经改了,就不上图了。

挂在iPhone上的效果

插在手机上是这个效果,顺了同事的一个白iPhone拍了一下,这个应该配白色版的好看。这个黑的配我的傻大笨粗的Galaxy Note 正好。因为傻大笨粗,就不上照片了。

试用

用这个功能当然需要安装360,内侧的时候只有公司的内部企业版才支持,过了两天最新的Beta版也支持了,现在正式发售了之后,应该360正式版也支持了(不确定啊,如果正式版不行请安装最新的Beta版)。我们内测群里面有人说识别不了,装了最新版的360就好了。

小东西插入电脑之后,托盘立即出现气泡,说是需要2-3分钟,应该是在下载驱动吧,随后出现一个比较炫的动画正在准备,速度也挺快。

准备中

我的网速比较好,过了十几秒之后就出现了一个窗口提示WiFi已经可用:

创建成功

这时候用手机连接,输入密码,成功连接!

WiFi成功连接

整个过程非常简单,完全就是插上就搞定,连一键都没有用到。

关了窗口之后,悬浮窗还在,鼠标悬停在悬浮窗上,也能看到WiFi信息。

悬浮窗

悬浮窗信息

真正的使用

端午节期间,正好有机会去青岛休息了几天。这几天住在7天酒店,一共住了4晚。7天的网络还算好的,客房大部分都有WiFi,但通常是连不上的。而且是开放式WiFi+网页登录的那种,安全性没有保证。而酒店的有线网络相对稳定,网速也还可以,因此这几天基本上就靠这个小东西上网了。

因为都是晚上在酒店,所以小东西是一直插在电脑上的,电脑也是整晚开着,稳定性一直很好,4天当中都没有出现过断线的情况。只是长时间插着的话,发热量有点多,摸起来热热的,大约40-50度的样子,不知道更长期这样的温度会不会损坏硬件。

不用的时候,都是直接拔掉的,后续再插上也能正常识别,不需要什么拔出硬件之类的操作,稳定性挺好。

总之几天当中,小东西的使用非常让人满意。至少我现在比较敢给完全不懂电脑的父母买一个插在老家的台式机上了。

其他的技术碎碎念

另外刚拿到硬件的时候,曾经简单看了一下硬件配置,因为是个USB设备,直接挂到Linux上面,lsusb一下:

idVendor 0x148f Ralink Technology, Corp.
idProduct 0x5370 RT5370 Wireless Adapter

厂商是Ralink,据说现在已经被山寨机之王,我厂的邻居友厂联发科技(MTK)收购了,芯片是RT5370,一个比较实惠的芯片。这个裸片在淘宝上大概卖18块钱一颗(见下图)。从这个角度上来说,我厂的19.9的价格还真是良心价了,毕竟还多了个壳(不知道对面的友厂是否有给个更良心的芯片进价)。

RT5370裸片

搜RT5370的的时候,发现有人把这个放在 Raspberry Pi 上面做一个无线网卡的,看来这个芯片完全可以用作一个无线网卡接收端使用(也就是作为笔记本的第二块无线网卡)。加上这个19.9的厚道的价钱,对于喜欢折腾点东西玩的兄弟们,其实也是个不错的选择。

 

标签:

【搬运】我们在360如何使用Python – virtualenv 篇

2012年11月13日 没有评论

原文地址:http://blogs.360.cn/blog/how-360-uses-python-1-virtualenv/

搬运文章,这里只贴摘要。

这个系列的前面一部分,我们简单介绍了我们的项目的基本结构和开发流程。整个开发流程都是基于 distribute 的。关于我们如何使用distribute,会在后面一部分当中介绍。基于 distribute,我们具有了从代码构建到发布,测试和部署的基本框架。但是在实际的开发当中,我们还有一些问题需要解决,最重要的一点就是开发和运行环境的隔离和低权限。 所谓环境的隔离,是指在同一台机器上,能够并行(side-by-side)地部署多个python环境,每个环境之间互相独立,拥有自己的python程序,库和可执行程序。这样,我们既可以在一台机器上为多名开发者提供互不影响的开发环境,也能在同一台服务器上为多个应用提供互不影响的执行环境。同时,为了能够让多名开发者或多个应用实现真正意义上的隔离,还需要让每个环境能够在各自的非root且不能sudo的用户手里进行管理,包括升级包,安装新的包等等,否则需要为每个人都提供sudo权限,会破坏隔离性。。

标签:

【搬运】我们在360如何使用Python – 引言

2012年11月13日 没有评论

原文地址:http://blogs.360.cn/blog/how-360-uses-python-0/

这里只贴一下摘要。

打算写这样一个系列,说说我们在360里面,如何使用Python。在360,除非是需要包含在360客户端软件当中的功能,技术人员使用什么语言进行开发,更多的是一种个人,最多是项目团队的决定。因此我们的同事会使用 php写页面,用python的工具,用c/c++写模块,甚至用易语言写界面。 也正是因为这个原因,虽然这个标题写得很大,但其实我也只能写一下我所在的团队如何使用Python。我们目前有Python代码约6万行,程序运行在Linux下,使用 Python2.5 和 Python 2.7 环境。 这6万行Python代码被被分成80余个项目进行组织。每个项目提供一个或一组完整的功能集合,每个项目都有自己的 setup.py 文件用来将项目代码打包成 Python 发布包(Distribution),部分项目还有自动文档生成,我们使用的是 Sphinx 和 reST格式的文本。打包好的Python包被发布到我们自己搭建的内网的与 pypi.python.org 兼容的私有 pypi 服务器上,而文档保存在内网的类似于 readthedocs 的服务器上

标签:

27岁生日礼物

2012年2月10日 5 条评论

2月7日算是27岁生日,2727多好的一个组合。这个生日和家人一起过的,有蛋糕吃有长寿面吃,很不错。不过外部的问候除了Facebook上,就是信用卡公司、中国移动等几家盯着钱包的主发了一些短信了,除了临近一天结束的时候,一个意外的礼物,来自某著名技术青年。

先截图:

鉴于主题是乱码,加上当时GFW抽风,将附件图片block掉了(附件图片预览都是在 googleusercontent.com 域名下,上面有很多不和谐的东东,所以很容易被墙掉),没有看到图片,再加上前段时技青的VPS刚刚被渗透过一次,第一反应是技青*又*被搞了,有木马冒用身份发邮件。谨慎起见,翻看了一下邮件原文,查Received Header,发现确实来自技青常用的客户端IP地址,鉴于技青对自己的机器近乎洁癖的使用方式,暂且认为这封邮件确实是他发的了。

不过为啥发这么一封邮件给我?不管怎么着先Base64解码一下看看吧。

base64_decode('ZGwuZHJvcGJveC5jb20vdS82ODgwMTMyL1YgSiByLiA+IG4gdS5odG1sCg==') 
    = 'dl.dropbox.com/u/6880132/V J r. > n u.html\n'

前面部分应该是没问题的吧,后面看起来像是某种加密。简单研究了一下,因为密文实在太短,无法猜出加密方式。秉承我的一贯作风,能找到当事人用非技术手段搞定加密的,绝对不花时间自己去破解。于是给技青回了一个邮件问了一下。邮件大意就是:你发的这是个啥?

技青很敬业的回了一封长长的邮件,虽然其中99%的部分都是空行,以便保证我在一屏上面只能看到一行字,详细解释了这封诡异邮件的原理和逻辑。当我看到第一行的时候,我就知道我输了。

第一行大意是这样的:漫画当中的这个角色名叫 Pig-pen。

虽然说我看到这个漫画也不知道他叫Pig-pen,但是我至少会Google Image Search吧。问题是,GFW把他给党掉了。哭。

带着深深的挫败感,往下翻了一下,看到第二行: Pigpen也是一种加密方法。

都说到这个份上了,再看提示已经对不起天地良心了。后面流程就很简单了,Pigpen算法是很容易找到的:

pigpen加密算法

正如这个图所描述的这样,pigpen是一种符号加密方法,自然的,就将技青发过来的字符串作为象形文字来解释一下吧:

pigpen解密过程

也就是说,最后的URL应该是:

dl.dropbox.com/u/6880132/sarthb.html

访问一下,看到了!

sarthb

相当相当有创意的一个礼物,够Geek,感谢技青。

最后吐槽一下:

  1. 这个URL为什么没有m呢? smarthb = smart happy birthday 不是更好。据技青解释,M的pigpen符号无法使用象形文字来表达。这个解释不足以说服我,键盘上P向右两个键位是什么?
  2. 为什么base64之后的URL最后有个\n呢?一定是技青echo的时候忘了 -e 了,一定是的。
  3. 这个图上的动物是个什么东东?尤其是,他的右手为什么抓着一只箭?鉴于技青一贯作风,没有典故的动物他是不会用的,所以,求典故。
标签:

搭建私有的 python 包发布中心 pypi

2010年12月12日 4 条评论

项目组现在使用Python越来越多,大部分老逻辑都已经迁移到了Python上,相当数量的新逻辑都是Python写的。经过之前的一段时间的分享,团队已经开始使用 virtualenv 和 setuptools 来进行Python代码的开发和打包和发布了。

但是现在的问题是,随着项目规模的变大,以及几个子项目的启动,代码复用开始成为一个问题。有很多代码在多个库当中被使用,应该被抽取出来成为单独的模块。但是这些模块和模块的依赖关系会比较复杂,比如一个应用可能需要依赖pypi上开源的库,同时需要依赖一个内部的库,而内部的库又依赖开源的库。如何解决这种情况下的依赖管理和自动包管理呢?

实际上setuptools 和 easy_install 已经提供了完善的依赖管理,在setup.py当中写上所有依赖的模块名已经在团队内形成了共识,那么内部模块是否可以用同样的方式进行管理呢?

答案是肯定的。easy_install 本质是 pypi 服务的客户端,它依赖的web服务称为 pypi,或者叫Cheese Shop。是 http://pypi.python.org/simple/ 。这个页面下面就是一系列的 index.html 文件,指向各个版本的包文件。这个index.html本身没有严格的格式规定,只是其中应该包含<a>标签,指向每个版本。easy_install 负责抽取出这些标签,形成一个文件列表,比较版本,下载指定版本或者最近版本,并安装到系统上。

而且,easy_install支持一个命令行参数 –index-url,或者短参数 -i ,可以指定兼容于 pypi çš„ pypi 索引。这个索引只要满足pypi的规定就是可以的,自然,这个是可以自己搭建的。

但是easy_install有一个限制,就是只能指定一个index URL。对于多个index的问题,PEP381明确说了,这是客户端的问题。easy_install选择不解决这个问题,也是一种解决方案吧……

但是easy_install不解决,我们就要想办法自己解决。去除这个限制有两个方法,一个是让自己的私有pypi在发现私有包里面没有匹配的包名字的时候重定向到pypi.python.org/simple,另一种方法就是使用 easy_install 的替代品 pip。在翻看了众多的部署脚本之后,我们决定,还是使用前面一个策略。

除了常用的easy_install来安装包,pypi还需要一个功能就是支持 distutils 兼容的协议上传包到服务器。对于使用disutils或者setuptools建立的setup.py文件,开发者可以使用 python setup.py register 将项目名注册到 pypi index,也可以通过 python setup.py bdist_egg upload 上传打包好的文件。这个协议很简单,很容易即可实现,只是其中需要的用户管理方面,稍微复杂和体力活一些。

也正是因为简单,搭建私有的pypi服务器的开源程序有很多,PEP 381当中有两个推荐,分别是PloneSoftwareCenter 和 EggBasket。PloneSoftwareCenter是一个恐龙级别的东西,它是一个完整的CMS,pypi只是其中一个小小的功能。为了这样一个简单的功能需要安装一大堆Plone的东西,实在是难以接受,而且它的文档简直是个杯具……,唉。

EggBasket稍好,但是也要安装一堆东西,包括一只小恐龙TurboGears。所幸TurboGears只是一只小恐龙,而且EggBasket本身的文档比较清楚,一步步照着做即可。由于一些安全方面的限制,EggBasket单独的服务器端口在我们的服务器上是不能访问到的,因此我们用apache的mod_proxy做了一个反向代理。

很快,基于EggBasket和apache mod_proxy反向代理的私有pypi就搭建起来,问题随即而来:EggBasket不支持我们上面要求的自动重定向。所幸源代码也不多,简单修改了一下之后,做了一个patch。需要的可以下载下来自己apply。我已经联系了EggBasket的作者,希望能够将这个patch合并进官方代码,但是作者表示,他现在正在休假。【update @2010-12-12 自从这个patch发送过去已经接近半年了,还没有响应,好吧,我放弃了】

经过这些patch,我们的pypi服务器就成功搭建起来了。项目组使用它的方式是:

开发机:
(dev) zhangc@dev-01:pypismpl$ python setup.py register -r http://pypi-server/pypi
We need to know who you are, so please choose either:
1. use your existing login,
2. register as a new user,
3. have the server generate a new password for you (and email it to you), or
4. quit
Your selection [default 1]: 1
Username: zhangc
Password: ********
Server response (200): OK
I can store your PyPI login so future submissions will be faster.
(the login will be stored in /home/zhangc/.pypirc)
Save your login (y/N)? y

这步操作执行一次即可,如果最后一步选择了save login,则后面不再需要每次都register。

在程序新版本稳定了之后,执行:
(dev) zhangc@dev-01:pypismpl$ python setup.py bdist_egg upload -r http://pypi-server/pypi/upload

即可把新版本的程序打包成egg并且上传到服务器。

这时候如果通过浏览器访问pypi服务器,会发现新版本的pypismpl程序已经在页面上列出了。

然后在生产机上:
(dev) zhangc@production-01:~$ sudo -u appuser -E /usr/app/env/bin/python -i http://pypi-server/pypi -U pypismpl

新版本的程序就会自动部署了。

标签:

谈一个Kernel32当中的ANSI到Unicode转换的问题

2010年6月17日 4 条评论

【这篇文章就是我在twitter上说过的那篇文章,早已写好,但由于一些原因压到现在才发表,深表歉意】

众所周知,Windows的几乎所有带有字符串参数的API都是有W和A两个版本,分别对应于Unicode和ANSI版本。同时,Windows内部是使用Unicode的,因此所有的A版本的API函数都是实际上调用了一次ANSI到Unicode的字符集转换之后,再调用Unicode版本的函数。

你真的清楚这中间的这一步转换吗?普通的工程师确实不需要了解太深刻,但是我们是做安全软件的,安全软件的一个基本实现机制就是Hook,就是在原本的API调用路径上面拐一个弯,改变原本的API执行流程,以便在其中插入安全检查等逻辑。

这当中,如果没有对于所要Hook的函数的深刻理解,也许会有很多看似灵异的事件出现。在我们的代码当中,我们就使用了Hook的机制,然后很幸运的(很不幸?),遇到了这样一个灵异问题:

在测试当中,发现某个软件一旦被我们Hook,就总是崩溃。必现的崩溃总是很容易解决的定位的,很快,我们发现崩溃来源于下面一个调用序列:

HMODULE hMod = LoadLibraryA("Kernel32");
PROC pfxIsWow64Process = GetProcAddress(hMod, "IsWow64Process");

对LoadLibrary的调用返回没有检查返回值,进而继续调用GetProcAddress。那么,当hMod = NULL的时候,GetProcAddress并不检查hMod是否是0,而直接试图去寻找其中的导出表,于是导致了非法的内存访问。

但是,这里的代码当中,调用的是LoadLibraryA(“kernel32”),kernel32.dll作为系统的核心链接库,通常情况下几乎必然是已经加载到了进程空间当中,即使没有,加载此DLL也不应该失败。

但是调试的结果显示,这个返回值的确是NULL,看起来相当灵异。但是我们相信,计算机没有灵异事件。为了排错,我们祭出WinDbg和IDAPro,从汇编层面一步步跟踪LoadLibraryA函数,看看中间究竟发生了什么。经过漫长的错误定位工作,在此略过不表(关于整个排查过程可以写一篇专门的文章),我们终于发现了问题的所在。

没错,加载了Hook就会造成崩溃,因此崩溃的原因在于Hook。

我们使用Inline Hook技术修改了LoadLibrary函数的执行流程,使得在进行真正的DLL加载之前进行某些校验和检查的工作。LoadLibrary函数其实是一个函数族,包括LoadLibraryA,LoadLibraryW,LoadLibraryExA,和LoadLibraryExW。它们之间的调用关系如下图:

我们选择了在整个调用依赖树的最底端的LoadLibraryExW的入口点作为Hook点,当一个函数调用LoadLibraryExW的时候,其实是调用到我们的Hook函数,Hook函数再调用真正的LoadLibraryExW,经过Hook,一个典型的LoadLibraryA的调用流程如下图:

问题就出在了这个CheckLibrary函数里面。在这个函数里面,我们调用了一个公共模块当中的日志函数,这个日志函数的逻辑是,以追加模式打开一个日志文件,写入然后关闭。且不论这个日志函数这样做的合理性,先来看看这段代码:

void Log(const char* msg) {
FILE* pf = fopen(_g_log_file, "a");
fprintf(pf, "%s\n", msg);
fclose(pf);
}

非常直观的程序,怎么会带来问题呢?我们来继续分析fopen函数。fopen函数是c库提供的文件IO函数,最终调用到Windows API CreateFileA,然后CreateFileA会调用到CreateFileW。把这个调用关系加上,调用序列如下图所示:

在这个函数调用序列当中,存在两次ANSI 到 Unicode 的转换,即图中标记有五角星的两个地方。那么我们来看看两个转换的代码是什么样子的:

LoadLibraryExA:

CreateFileA

注意到其中高亮的部分了么?两个函数在进入之后,都调用了一个公共的函数 _Base8bitStringToStaticUnicodeString,从名字就可以看出来,这段代码负责将输入的ANSI字符串转换为Unicode。那么这个函数做了什么呢?跟进去看看(这个使用了Hex-Rays的反编译功能,看得更清楚一点):

再仔细看给RtlAntiStringToUnicodeString这个函数传入的参数地址pUnicodeString,来自*MK_FP(__FS__, 24) + 3064,这是Hex-Rays的表示法,其实对应下面一串汇编序列(由于编译器的优化导致的乱序,重点看红线标出来的指令):

large fs:18h 这句魔法一样的指令,其实是 TEB 的地址。这个地址对于同一个线程来说是一样的,因此,如果在一个线程当中调用这个函数,总是会引用到同一个地址,这个地址就是当前线程的TEB当中的一个静态缓冲区(StaticUnicodeString & StaticUnicodeBuffer)。

很明显,这个静态缓冲区的作用是性能优化,对于ANSI到Unicode的转换,复用一个thread-specific的缓冲区来接受目标Unicode字符串。对于不太长的函数参数字符串,这个优化可以避免一次堆分配的过程,对性能的提升是很明显的。正常情况下这个优化是合理而且有效的。但是在存在Hook的情况下,事情就不是这么简单了。考虑一下上面那张图当中的LoadLibrary的调用序列:

解释一下:

  1. 在第1次ANSI到Unicode转换的时候, LoadLibraryA将目标Unicode字符串 L”kernel32″ 存入 StaticUnicodeBuffer,然后将这个字符串的首地址传递给LoadLibraryW
  2. LoadLibraryW在调用到LoadLibraryExW之前,先调用到了Hook函数
  3. Hook函数调用到了CreateFileA,传入日志文件名(ANSI)作为函数参数
  4. CreateFileA进行第二次ANSI到Unicode转换,将目标Unicode字符串(日志文件名)存入了StaticUnicodeBuffer,然后将这个字符串首地址传给CreateFileW
  5. CreateFileW返回,
  6. CreateFileA返回,
  7. Hook函数返回,
  8. 调用到实际的LoadLibraryExW。此时LoadLibraryExW从参数指针当中取出字符串试图进行加载动态库的操作,但是,它取出的是被在第4步当中覆盖掉的字符串,也就是Unicode版本的日志文件名!试图加载日志文件,自然会收到LoadLibrary失败的返回。‘
  9. LoadLibraryExW返回NULL,LoadLibraryW返回NULL,LoadLibraryA返回NULL。

然后,没有检查LoadLibraryA返回值的程序,在调用GetProcAddress的时候,biu~ 掉了。

好了,知道了为什么,怎么解决也就很容易了,在CheckLibrary当中,先备份传入的参数的内容,然后继续走流程,等到需要调用原本函数的时候,将备份的参数传给原本函数。

这就是Hook爱好者们的义务:当你试图改变某个执行流程的时候,请务必清楚地了解关于这个流程的一切。

当心一种利用手机和银行转账汇款的火车票诈骗

2010年4月27日 7 条评论

今天同事去买车票的时候遇到的。虽然最终我们并没有上当,但是还是把整个流程在这里写出来,以便更多的人识别骗局。

首先把这个骗局场景给大家讲述一下,碰到类似的情况,请对号入座,看看自己是不是受害人或者受害人的朋友。为了讲述方便,我们把这个事情当中的角色用三个名字来说明:买票人、黄牛、以及受害人的朋友(简称朋友,是付款人)。其中黄牛就是骗子。在我碰到的这个案例当中,同事的角色是买票人,我的角色是付款人。

这个骗局当中,买票人在黄牛的指引下,引导买票人通过手机和一个朋友联系,并精心设计流程得手。这个过程如下:

  1. 黄牛事先知道某车次的票被售罄,然后以此车次的票为诱饵在网上发帖转让;或者笼统的说自己可以搞到票,等待受害者上钩。
  2. 买票人需要去购买火车票,但是很不幸,他要买的的火车票没有了;
  3. 买票人通过某这方式找到黄牛,并且联系上了他;
  4. 黄牛告诉买票人,他可以搞到票,但是由于某种原因(原因五花八门,但通常黄牛声称是内部人士,需要规避风险),他不能进行现金交易,而需要通过下面的步骤来进行:
    1. 买票人需要用自己的手机联系一位朋友,并且这样告诉他的朋友:“我在买票,有人能搞到票,但是需要你帮忙把钱汇到某个账户,然后他会给我票。”,并且把下面的步骤向朋友解释;
    2. 黄牛用自己的手机联系买票人的朋友,重复说明该流程,并提供一个银行帐户给买票人的朋友;
    3. 黄牛将票给买票人,买票人将自己的手机给黄牛;
    4. 黄牛使用买票人的手机联系买票人的朋友,这说明第3步已经完成,买票人已经拿到了票,而自己正拿着买票人的手机,朋友需要转账给提供的账户来让买票者赎回手机(在给买票者及其朋友讲述这个流程的时候,骗子强调,由于票已经在给了买票人,只有当他拿到了票款,才能把手机还回去,防止买票人拿回手机之后不付钱);
    5. 朋友将钱汇到黄牛提供的帐号;
    6. 黄牛确认钱已经收到之后,将手机还给买票人;
    7. 买票人带走车票,回去之后将票款还给朋友。
  5. 买票人按要求执行 4.1 当中的操作,给一个朋友打电话并解释;
  6. 黄牛执行 4.2 当中的操作,给该朋友打电话并解释,同时提供帐号;
  7. 黄牛和买票人执行 4.3 当中的操作,用手机交换票;
  8. 黄牛用买票人的手机打电话给他的朋友,要求汇款;
  9. 朋友汇款;
  10. 黄牛确认款项收到之后,将手机还给买票人;
  11. 交易完成

这个过程有什么问题么?看起来无懈可击,通过使用手机和票的抵押,交易双方的利益都得到了保证。而依靠手机号码的识别性,朋友也能够验证买票人的身份。

但是整个流程当中仍然有几个可疑的地方,就是上面的这些红色标记的地方。

首先,买票人和黄牛一定是面对面的,因为两人可以互相交换手机和票,因此,没有理由两人不可以交换人民币。而且,火车票之类的东西并非非常高价,通常数百元,现金并不多。当然,在我同事遇到的这个例子当中,黄牛宣称他和我的同事之间隔着一块玻璃墙,因为他提供的是“内部票”,但是这个借口仍然经不起推敲。隔着一个玻璃墙递送手机,比递送人民币更加引人注目。而且,根据我对火车站流程的了解,他们不存在所谓的“内部票”一说。

第二,如果当我接到电话,是黄牛在说话,但是是我同事的手机号的时候,能够证明黄牛正在使用我同事的手机吗?很多人也许认为是可以的,但是当你知道这个世界上存在一种叫做手机改号软件(提供商)的时候,你就能够知道其实这个什么都不能证明。改号软件利用的是电信系统来电显示的漏洞,通过非法的电话网关接入运营商的网络,并且伪造拨打方的号码。通过此类软件,骗子实际上可以假冒任何人的身份进行通信。作为可能的受害者的朋友,应对此类系统的策略其实很简单,一种方式是要求和朋友讲话(这也是很多警匪片里面侦探确认人质仍然活着的方式);另一种方式是在收到朋友的手机号打来的电话请求时,不是接听而是回拨。第一种方式在我遇到的这个例子当中已经被骗子废掉了,因为骗子强调,为了保护他的权益,他不能把手机还给买票人,而正如他之前所说,他和买票人之间隔着一堵 玻璃墙,因为他拿“内部票”是需要到售票窗里面去的,因此我也不可能听见同事的声音。但是第二种方式仍然是可用的,因为手机改号软件本身的缺陷,回拨的请求会真的发送到同事的手机上,而不是骗子伪造的通话端。因此,通过接到电话回拨的方式,一定程度上可以完成一次相对可靠的认证,从而避免上当受骗。

你知道了改号软件,那么就很容易想到,在上面的步骤当中,在第四步完成之后,买票人和黄牛分别为买票人的朋友解释了流程之后,很可能的过程是:

  1. 骗子借口离开买票人(这个很可能是通过说“我去拿票”);
  2. 骗子使用号码伪造软件,伪造买票人的手机号码向买票人的朋友通话,要求转账;
  3. 朋友转账;
  4. 骗子消失,朋友和买票人受骗。

随便Google了一下,这种诈骗手段还真是比较新的,大部分都是最近的帖子:
案例1
,案例2,案例3,案例4

我和同事很幸运,没有成为那个上当的一对通信者。回顾整个过程,讲讲我的判断过程,也希望能够给其他人防范未知诈骗提供一些帮助。

在同事给我打电话讲述流程的时候,我问了他两个问题:1. 你是否看到这个人了?2. 你是否看到票了。同事对第一个回答是是,第二个却是否。这个是第一个引起我怀疑的地方,甚至,我重新检查了来电号码,仔细辨认了同事的声音,以便确定电话的确是他打来的,同时仔细听了背景声音,确定他的确在火车站。但是我还是建议他先看到票,然后再说交易。无论什么交易,先看到货,再谈交易是基本原则,除非有一个足够强势的第三方担保,例如淘宝。

在同事挂掉电话之后,我正在琢磨整个流程的时候,我接到了骗子的电话。同事之前完全没有告诉我骗子会给我打电话。而骗子在电话接通之后就非常流利的开始讲解整个流程,坦率地讲,这个骗子是一个优秀的社会工程学专家,在整个过程当中他语速一致,条例清楚,伪装也很巧妙,尤其是通过“内部人士”的角色,成功规避了“要求听到同事的声音”这种请求。但是我还是从对话当中发现了他的一些疑点:

  1. 此人能够给我打电话,讲述这样一个复杂的流程,却不能接受现金交易,而且借口是为了逃避监管。而实际上,为了逃避监管,现金交易是首选,转帐反而会留下证据(大家不知道发现金的那些单位么?),这是疑点一。
  2. 第二,骗子一直在强调,他和同事是“隔着玻璃”的,但是同事说,他们在一起。
  3. 骗子坚持要求我使用ATM转帐而不是专业版网银,而且只透漏帐号,拒绝透漏帐户名(ATM转帐不需要户名,户名是在输入帐号之后显示的,但是专业版转帐是需要先提供户名,由系统进行匹配的),目的也许是为了让我能够远离互联网,我于是忽悠他说我已经在ATM机旁边了,对方明显松了一口气,然后转而告诉我,别着急,等他电话。

这些疑点足够我产生怀疑了。接下来就是搜索。使用“火车票”,“转帐”,“手机”等关键字组合搜索之后,很快就发现了大量的此类案例,然后立即拨同事的电话,告知他一切。而这个时候,同事正在和他一起走向某个地方的路上。同事听明白我讲的话之后,挂掉电话。

不知道他和骗子说了什么,骗子马上打来了一个电话,是他的号码,我还没有接,他就挂掉了,也许是做贼心虚了。

公开骗子的电话和帐号:手机 18773861321,帐号 6225 8878 3931 1236。大家可以搜索一下,这个号码在很多网站都留了联系方式。

PS: 推荐《欺骗的艺术》(又译《入侵的艺术》)一书,我所学习的识别骗子的技巧,相当一部分来自此书。【下载】

标签: ,

CALL指令有多少种写法

2010年4月4日 7 条评论

最近有一个需求,给你个地址,看看这个地址前面是不是一个CALL指令(请同学们自行联想该需求的来源)。作为团队的救火队员+炮灰,这个简单的事情自然落在了我的头上。

这个事情很简单,作为一个善于站在别人肩膀上的程序员我们可以考虑使用 libdisasm;如果要考虑x64,就试试udis86;如果需要用Python,就有Python包装好的 pydasm。不过这两个400KB+的库,显然不值得为了一个CALL指令导入到编译出来大小仅仅100K不到的项目代码里面。

那么就自己抽一个CALL指令解码逻辑出来好了。这个逻辑的复杂性在于,你无法知道前面一个CALL指令有多长。因此,首先需要枚举出所有的CALL指令格式。

Intel有公开的指令集格式文档,你需要的是第二卷的上半部分,指令集从A到M。这篇文档的难度超出一般人想象,里面有众多晦涩的标识、与硬件紧密相关的介绍,拿到这后,即使直接翻到目录的CALL 指令一节,也不见得能够弄清楚。不相信?我们就翻到那里看看:

CALL指令格式一览表

CALLæŒ‡ä»¤æ ¼å¼ä¸€è§ˆè¡¨

虽然很明确的列出,第一列是指令的二进制形式,第二列是指令的汇编形式,但是面对着 E8 cw, FF/2这样的标识,一样不知道究竟对应的二进制格式是什么样的。

那好,我们就从理解这些标识开始。文档向前翻,有一个专门的节(3.1.1 Instruction Format)讲述这些标识的含义。这里抽出其中两个用得着的翻译一下:

表格中的“Opcode”列列出了所有的所有可能的指令对应的二进制格式。有可能的话,指令代码使用十六进制显示它们在内存当中的字节。除了这些16进制代码之外的部分使用下面的标记:

cb, cw, cd, cp, co, ct — opcode后面跟着的一个1字节(cb),2字节(cw),4字节(cd),6字节 (cp),8字节(co) 或者 10字节(ct) 的值。这个值用来表示代码偏移地址,有可能的话还包括代码段寄存器的值。

/digit — digit为0到7之间的数字,表示指令的 ModR/M byte 只使用 r/m字段作为操作数,而其reg字段作为opcode的一部分,使用digit指定的数字。

红字部分不知道什么含义?没关系,我们先不看它。对于cb/cw之类的,基本上能够简单看明白其中的一些指令含义了:

E8 cw 的含义是:字节 0xE8 后面跟着一个2字节操作数表示要跳转到的地址与当前地址的偏移量。
E8 cd 的含义是:字节 0xE8 后面跟着一个4字节的操作数表示要跳转的地址与当前地址的偏移量。
9A cd 的含义是:字节 0x9A 后面跟着一个6字节的操作数表示要跳转的地址和代码段寄存器的值。

那么,同样的0xE8开头的指令,CPU如何区分后面的操作数是2字节还是4字节?答案是和CPU的模式有关,在实模式下,0xE8接受2字节操作数,而32位保护模式下接受4个字节,64位保护模式下同样接受4字节,同时需要对该操作数进行带符号扩展。

因此,CALL指令的前两种格式是:E8 xx xx xx xx,和 9A xx xx xx xx xx xx。一个是5字节长,一个是7字节长。其实E8 那种,就是我们在汇编指令里面写 CALL lable之后产生的,最常见的CALL指令。

然后是下面的FF /2。这个是0xFF字节后面跟上一个blablabla的东西。这个blablabla的东西是什么呢?要解释这个,首先需要知道红字标出来的部分,即ModR/M是什么东西。

这个要先回到最基本的一个问题:IA32的指令格式。

IA32,64指令格式

IA-32,Intel 64æŒ‡ä»¤æ ¼å¼

其中每个部分是什么含义呢?

首先是指令前缀。有印象的应该记得当年学习微机原理的时候提到过得循环前缀 repnz/repne,这个前缀就是被编码在指令的前面部分的。每个前缀最多一个字节,一条指令最多4个前缀。

然后是指令代码(opcode),这部分标识了指令是什么。这个是指令当中唯一必需的部分。前面例子当中的 0xE8,0xFF都是opcode。

再后面就是我们要重点关心的 ModR/M字段了,还有和它密切相关的SIB字节。手册2.1.3当中有对于它们的详细描述。

许多指令需要引用到一个在内存当中的值作为操作数,这种指令需要一个称为寻址模式标识字节(addressing-form specifier byte),或者叫做ModR/M字节紧跟在主opcode后面。ModR/M字节包含下面三个部分的信息:

  • mod(模式)域,连同r/m(寄存器/内存)域共同构成了32个可能的值:8个寄存器和24个寻址模式。
  • reg/opcode(寄存器/操作数)域指定了8个寄存器或者额外的3个字节的opcode。究竟这三个字节用来做什么由主opcode指定。
  • r/m(寄存器/内存)域可以指定一个寄存器作为操作数,或者可以和mod域联合用来指定寻址模式。有时候,它和mod域一起用来为某些指令指定额外的信息。

这一段有些晦涩。其意思解释一下是这样的:一个指令往往需要引用一个在内存当中的值,典型的就是如mov:

MOV eax, dword ptr [123456]
MOV eax, dword ptr [esi]

这其中的 123456 或者 esi 就是 MOV 指令引用的内存地址,而MOV关心的是这个地址当中的内容。这个时候,需要某种方式来为指令指定这个操作数的类型:是一个立即数表示的地址,还是一个存放在寄存器当中的地址,或者,就是寄存器本身。

这个用来区分操作数类型的指令字节就是 ModR/M,确切的说是其中的5个位,即mod和r/m域。剩下的三个位,可能用来做额外的指令字节。因为,IA32的指令个数已经远超过一个字节所能表示的256个了。因此,有的指令就要复用第一个字节,然后依据ModR/M当中的reg/opcode域进行区分。

现在回头看前面的红字标识的部分,能不能理解 /digit 这种表示法了?

对于SIB的介绍,我们先忽略,看看对于CALL指令的枚举我们已经能做什么了。

CALL指令的表示法:FF /2,是 0xFF 后面跟着一个 /digit 表示的东西。就是说,0xFF后面需要跟一个 ModR/M 字节,ModR/M字节使用 reg/opcode 域 = 2 。那么,reg/opcode = 2 的字节有32个,正如ModR/M的解释,这32个值代表了32种不同的寻址方式。是哪32种呢?手册上面有张表:

32字节寻址模式下的ModR/M字节

32字节寻址模式下的ModR/M字节

非常复杂的一张表。现在就看看这张表怎么读。

首先是列的定义。由于 reg/opcode 域可以用来表示opcode,也可以用来表示reg,因此同一个值在不同的指令当中可能代表不同的含义。在表当中,就表现为每一列的表头都有很多个不同的表示。我们需要关心的就是 opcode 这一个。注意看我用红圈圈出来的部分,这一列就是 opcode=2 的一列。而我们需要的 CALL 指令,也就是在这一列当中,0xFF后面需要跟着的内容。

行的定义就是不同的寻址模式。正如手册所说,mod + R/M域,共5个字节,定义了32种寻址模式。0x10 – 0x17 对应于寄存器寻址。例如指令 CALL dword ptr [eax] :[eax]寻址对应的是 0x10,因此,该指令对应的二进制就是 FF 10。同理, CALL dword ptr [ebx] 是 FF 13,CALL dword ptr [esi] 是 FF 16,这些指令都是2个字节。有人也许问 CALL word ptr [eax] 是什么?抱歉,这不是一个合法的32位指令。

0x50-0x57部分需要带一个 disp8,即 8bit 立即数,也就是一个字节。这个是基地址+8位偏移量的寻址模式。例如 CALL dword ptr [eax+10] 就是 FF 50 10 。注意虽然表当中写的是 [eax] + disp8 这种形式,但是并不表示是取得 eax 指向的地址当中的值再加上 disp8,而是在eax上加上disp8再进行寻址。因此写成 [eax+disp8] 更不容易引起误解。后面的disp32也是一样的。这个类型指令是3个字节。

0x90 – 0x97部分需要带 disp32,即4字节立即数。这个是基地址+32位偏移量。例如 CALL dword ptr [eax+12345] 就是 FF 90 00 01 23 45。有趣的是, CALL dword ptr [eax+10] 也可以写成 FF 90 00 00 00 10。至于汇编成哪个二进制形式,这是汇编器的选择。这个类型的指令是6个字节。

0xD0 – 0xD7部分则直接是寄存器。这边引用的寄存器的类型有很多,但是在CALL指令当中只能引用通用寄存器,因此 CALL eax 就是 FF D0,臭名昭著的 CALL esp 就是 FF D4。注意 CALL eax å’Œ CALL [eax] 是不一样的。这些指令也是2个字节。

仔细的人也许主要到了,在表当中,0x14, 0x15, 0x54和0x94是不一样的。0x15比较简单,这个要求 ModR/M后面跟上一个32位立即数作为地址。即常见的 CALL dword ptr [004F778e] 这种格式的,直接跳转到一个固定内存地址处存放的值,常见于调用Windows的导出表。对应的二进制是 FF 15 00 4F 77 8E ,有6个字节。

0x14,0x54,0x94部分是最复杂的,因为这个时候,ModR/M不足以指定寻址方式,而是需要一个额外的字节,这个字节就是指令当中的第4个字节,SIB。同样在手册的2.1.3,紧跟着ModR/M的定义:

某些特定的ModR/M字节需要一个后续字节,称为SIB字节。32位指令的基地址+偏移量,以及 比例*偏移量 的形式的寻址方式需要SIB字节。 SIB字节包括下列信息:

  • scale(比例)域指定了放大的比例。
  • index(偏移)域指定了用来存放偏移量 的寄存器。
  • base (基地址)域用来标识存放基地址的寄存器。

0x14, 0x54, 0x94就是这里所说的“特定的ModR/M字节。这个字节后面跟着的SIB表示了一个复杂的寻址方式,典型的见于虚函数调用:

CALL dword ptr [ecx+4*eax]

就是调用ecx指向的虚表当中的第eax个虚函数。这个指令当中,因为没有立即数,因此FF后面的字节就是0x14,而 [ecx+4*eax] 就需要用SIB字节来表示。在这个指令当中,ecx就是 Base,4是Scale,eax是Index。

那么,Base, Scale和Index是如何确定的呢?手册上同样有一张表(又是巨大的表):

32位寻址模式当中的SIB字节

32位寻址模式当中的SIB字节

列是Base,行是Index*Scale,例如[ecx+4*eax] 就是0x81。

根据这张表,CALL dword ptr [ecx+4*eax] 就是 FF 14 81 。由此可见,对于 0x14系列的来说,CALL指令就是 3个字节。
而 0x54 带 8bit 立即数,就是对应于 CALL指令:CALL dword ptr [ecx+4*eax+xx],这个指令就是 FF 54 81 xx,是4个字节。
同理,0x94带32位立即数,对应于CALL指令:CALL dword ptr [ecx+4*eax+xxxxxxxx],这个指令就是 FF 94 81 xx xx xx xx,是7个字节。

OK,截止到目前,我们基本上能够列出常见的CALL指令的格式了:

指令 二进制形式
CALL rel32 E8 xx xx xx xx
CALL dword ptr [EAX] FF 10
CALL dword ptr [ECX] FF 11
CALL dword ptr [EDX] FF 12
CALL dword ptr [EBX] FF 13
CALL dword ptr [REG*SCALE+BASE] FF 14 xx
CALL dword ptr [abs32] FF 15 xx xx xx xx
CALL dword ptr [ESI] FF 16
CALL dword ptr [EDI] FF 17
CALL dword ptr [EAX+xx] FF 50 xx
CALL dword ptr [ECX+xx] FF 51 xx
CALL dword ptr [EDX+xx] FF 52 xx
CALL dword ptr [EBX+xx] FF 53 xx
CALL dword ptr [REG*SCALE+BASE+off8] FF 54 xx xx
CALL dword ptr [EBP+xx] FF 55 xx
CALL dword ptr [ESI+xx] FF 56 xx
CALL dword ptr [EDI+xx] FF 57 xx
CALL dword ptr [EAX+xxxxxxxx] FF 90 xx xx xx xx
CALL dword ptr [ECX+xxxxxxxx] FF 91 xx xx xx xx
CALL dword ptr [EDX+xxxxxxxx] FF 92 xx xx xx xx
CALL dword ptr [EBX+xxxxxxxx] FF 93 xx xx xx xx
CALL dword ptr [REG*SCALE+BASE+off32] FF 94 xx xx xx xx xx
CALL dword ptr [EBP+xxxxxxxx] FF 95 xx xx xx xx
CALL dword ptr [ESI+xxxxxxxx] FF 96 xx xx xx xx
CALL dword ptr [EDI+xxxxxxxx] FF 97 xx xx xx xx
CALL EAX FF D0
CALL ECX FF D1
CALL EDX FF D2
CALL EBX FF D3
CALL ESP FF D4
CALL EBP FF D5
CALL ESI FF D6
CALL EDI FF D7
CALL FAR seg16:abs32 9A xx xx xx xx xx xx

有了这个列表,写一段代码来完成最初我们的需求也就不难了。

标签: ,

Sergey Brin 的童年

2010年3月14日 1 条评论

最近因为Google的退出事件,越来越多的新闻开始对这个行为进行深挖,越来越多的消息开始指向Google的创始人之一的Sergey Brin。Sergey Brin被称为Google的良心,而Google的“不作恶”当中的“作恶”的定义也就是“Brin说是恶的,那就是恶的”。这个激起了我了解Sergey Brin的经历的好奇心。正好wikipedia上面记录了Sergey Brin的童年介绍,看后很受触动。

Sergey Brin在6岁以前在苏联度过,他的父亲在他6岁那年决定离开苏联移居美国。我可以理解他的父亲对于苏联和苏联式的社会主义的失望,也许还有恐惧,否则他也不会做出这样的决定。1979年,正是那一个社会主义实体第60年的时候,正是一个一直怀揣梦想的人第一次接触到西方社会之后的一年之后,一切都和现在的我如此相似。如果我做了和米歇尔一样的事情的话,将来,我的孩子,会对着我的眼睛说出和Sergey一样的话么。

— 翻译开始 —

1979年, 当布林6岁的时候,他的家庭感到不得不移居美国。在和《The Google Story》作者Mark Malseed的一次采访当中,谢尔盖的父亲解释他如何“在进入大学之前就被扼杀了他成为天文学家的梦想。根据官方说法,反犹太主义在苏联是不存在的,但事实上,共产党禁止犹太人进入大学任教,尤其被禁止进入物理系……”。米歇尔.布林(谢尔盖的父亲)因此不得不将其专业换成了数学,在那里他几乎门门得到A。但是,“几乎没有人认为我可以进入研究生院,因为我是犹太人”。布林全家住在莫斯科中心的一个30平方米的3室的公寓里,而且和他的祖母同住。谢尔盖告诉Malseed:“我很久之前就知道父亲不能去追逐他的职业梦想”,但是谢尔盖只记得到达美国之后的事情的详细情况了。他记起,1977年,当他父亲从波兰华沙的一次数学会议上回来的时候,他宣布是时候考虑全家移民了。“我们不能再在这里待下去了”,他告诉妻子和母亲。在那次会议上,他能够“自由地和来自美国、法国、英国和德国的同行们交流,发现他西方世界当中的同类们并‘不是怪物’”,他还说,“我是家里唯一感到离开是如此重要的事情的人”。

谢尔盖的母亲不想离开他们在莫斯科的家,她在哪里度过了一生。Malseed写道:“对于伊吉尼亚来说,决定权完全落在了谢尔盖的头上。而她的丈夫承认他考虑他自己的未来的同时也在考虑谢尔盖的未来,而且,80%都是在考虑谢尔盖”。他们1978年9月申请了离境签证,随后他的父亲被“立即辞退”,因为相关的原因,他们的母亲也被迫离开工作岗位。在剩下的8个月里,没有固定收入,他们不得不在等待的时间里去做一些临时工作,而且不知道他们的申请是否能够获得批准。在这段时间里,谢尔盖的父母轮流照顾他,他的父亲学会了计算机编程。1979年5月,他们拿到了离境签证,被允许离开这个国家。

在这个2000年10月的采访当中,谢尔盖说到:“我知道我的父母那段时间艰难,我感谢他们把我带到了美国”。10年前,1990年的夏天,谢尔盖17岁生日前的几周,他的父亲带着一群有数学天赋的高中生,包括谢尔盖,到苏联参加一个为期两周的交换学生项目。 根据谢尔盖的回忆,这次旅行“唤醒了他童年对于威权的恐惧”,他还记得,他第一次对被苏维埃压迫感到冲动,并且向警车扔石头。Malseed补充道:活动的第二天,当他们游览到莫斯科郊外的一个疗养院的时候,谢尔盖走到他父亲的旁边,望着他的眼睛说:“谢谢你把我们都带出了苏联”。

[翻译] [RabbitMQ+Python入门经典] 兔子和兔子窝

2010年3月14日 16 条评论

RabbitMQ作为一个工业级的消息队列服务器,在其客户端手册列表的Python段当中推荐了一篇blog,作为RabbitMQ+Python的入门手册再合适不过了。不过,正如其标题Rabbit and Warrens(兔子和养兔场)一样,这篇英文写的相当俏皮,以至于对于我等非英文读者来说不像一般的技术文档那么好懂,所以,翻译一下吧。翻译过了,希望其他人可以少用一些时间。翻译水平有限,不可能像原文一样俏皮,部分地方可能就意译了,希望以容易懂为准。想看看老外的幽默的,推荐去看原文,其实,也不是那么难理解……

原文:http://blogs.digitar.com/jjww/2009/01/rabbits-and-warrens/

兔子和兔子窝

当时我们的动机很简单:从生产环境的电子邮件处理流程当中分支出一个特定的离线分析流程。我们开始用的MySQL,将要处理的东西放在表里面,另一个程序从中取。不过很快,这种设计的丑陋之处就显现出来了…… 你想要多个程序从一个队列当中取数据来处理?没问题,我们硬编码程序的个数好了……什么?还要能够允许程序动态地增加和减少的时候动态进行压力分配?

是的,当年我们想的简单的东西(做一个分支处理)逐渐变成了一个棘手的问题。以前拿着锤子(MySQL)看所有东西都是钉子(表)的年代是多么美好……

在搜索了一下之后,我们走进了消息队列(message queue)的大门。不不,我们当然知道消息队列是什么,我们可是以做电子邮件程序谋生的。我们实现过各种各样的专业的,高速的内存队列用来做电子邮件处理。我们不知道的是那一大类现成的、通用的消息队列(MQ)服务器——无论是用什么语言写出的,不需要复杂的装配的,可以自然的在网络上的应用程序之间传送数据的一类程序。不用我们自己写?看看再说。

让大家看看你们的Queue吧……

过去的4年里,人们写了有好多好多的开源的MQ服务器啊。其中大多数都是某公司例如LiveJournal写出来用来解决特定问题的。它们的确不关心上面跑的是什么类型的消息,不过他们的设计思想通常是和创建者息息相关的(消息的持久化,崩溃恢复等通常不在他们考虑范围内)。不过,有三个专门设计用来做及其灵活的消息队列的程序值得关注:

Apache ActiveMQ 曝光率最高,不过看起来它有些问题,可能会造成丢消息。不可接受,下一个。

ZeroMQ 和 RabbitMQ 都支持一个开源的消息协议,成为AMQP。AMQP的一个优点是它是一个灵活和开放的协议,以便和另外两个商业化的Message Queue (IBM和Tibco)竞争,很好。不过ZeroMQ不支持消息持久化和崩溃恢复,不太好。剩下的只有RabbitMQ了。如果你不在意消息持久化和崩溃恢复,试试ZeroMQ吧,延迟很低,而且支持灵活的拓扑。

剩下的只有这个吃胡萝卜的家伙了……

当我读到它是用Erlang写的时候,RabbitMQ震了我一下。Erlang 是爱立信开发的高度并行的语言,用来跑在电话交换机上。是的,那些要求6个9的在线时间的东西。在Erlang当中,充斥着大量轻量进程,它们之间用消息传递来通信。听起来思路和我们用消息队列的思路是一样的,不是么?

而且,RabbitMQ支持持久化。是的,如果RabbitMQ死掉了,消息并不会丢失,当队列重启,一切都会回来。而且,正如在DigiTar(注:原文作者的公司)做事情期望的那样,它可以和Python无缝结合。除此之外,RabbitMQ的文档相当的……恐怖。如果你懂AMQP,这些文档还好,但是有多少人懂AMQP?这些文档就像MySQL的文档假设你已经懂了SQL一样……不过没关系啦。

好了,废话少说。这里是花了一周时间阅读关于AMQP和关于它如何在RabbitMQ上工作的文档之后的一个总结,还有,怎么在Python当中使用。

开始吧

AMQP当中有四个概念非常重要:虚拟主机(virtual host),交换机(exchange),队列(queue)和绑定(binding)。一个虚拟主机持有一组交换机、队列和绑定。为什么需要多个虚拟主机呢?很简单,RabbitMQ当中,用户只能在虚拟主机的粒度进行权限控制。因此,如果需要禁止A组访问B组的交换机/队列/绑定,必须为A和B分别创建一个虚拟主机。每一个RabbitMQ服务器都有一个默认的虚拟主机“/”。如果这就够了,那现在就可以开始了。

交换机,队列,还有绑定……天哪!

刚开始我思维的列车就是在这里脱轨的…… 这些鬼东西怎么结合起来的?

队列(Queues)是你的消息(messages)的终点,可以理解成装消息的容器。消息就一直在里面,直到有客户端(也就是消费者,Consumer)连接到这个队列并且将其取走为止。不过。你可以将一个队列配置成这样的:一旦消息进入这个队列,biu~,它就烟消云散了。这个有点跑题了……

需要记住的是,队列是由消费者(Consumer)通过程序建立的,不是通过配置文件或者命令行工具。这没什么问题,如果一个消费者试图创建一个已经存在的队列,RabbitMQ就会起来拍拍他的脑袋,笑一笑,然后忽略这个请求。因此你可以将消息队列的配置写在应用程序的代码里面。这个概念不错。

OK,你已经创建并且连接到了你的队列,你的消费者程序正在百无聊赖的敲着手指等待消息的到来,敲啊,敲啊…… 没有消息。发生了什么?你当然需要先把一个消息放进队列才行。不过要做这个,你需要一个交换机(Exchange)……

交换机可以理解成具有路由表的路由程序,仅此而已。每个消息都有一个称为路由键(routing key)的属性,就是一个简单的字符串。交换机当中有一系列的绑定(binding),即路由规则(routes),例如,指明具有路由键 “X” 的消息要到名为timbuku的队列当中去。先不讨论这个,我们有点超前了。

你的消费者程序要负责创建你的交换机们(复数)。啥?你是说你可以有多个交换机?是的,这个可以有,不过为啥?很简单,每个交换机在自己独立的进程当中执行,因此增加多个交换机就是增加多个进程,可以充分利用服务器上的CPU核以便达到更高的效率。例如,在一个8核的服务器上,可以创建5个交换机来用5个核,另外3个核留下来做消息处理。类似的,在RabbitMQ的集群当中,你可以用类似的思路来扩展交换机一边获取更高的吞吐量。

OK,你已经创建了一个交换机。但是他并不知道要把消息送到哪个队列。你需要路由规则,即绑定(binding)。一个绑定就是一个类似这样的规则:将交换机“desert(沙漠)”当中具有路由键“阿里巴巴”的消息送到队列“hideout(山洞)”里面去。换句话说,一个绑定就是一个基于路由键将交换机和队列连接起来的路由规则。例如,具有路由键“audit”的消息需要被送到两个队列,“log-forever”和“alert-the-big-dude”。要做到这个,就需要创建两个绑定,每个都连接一个交换机和一个队列,两者都是由“audit”路由键触发。在这种情况下,交换机会复制一份消息并且把它们分别发送到两个队列当中。交换机不过就是一个由绑定构成的路由表。

现在复杂的东西来了:交换机有多种类型。他们都是做路由的,不过接受不同类型的绑定。为什么不创建一种交换机来处理所有类型的路由规则呢?因为每种规则用来做匹配分子的CPU开销是不同的。例如,一个“topic”类型的交换机试图将消息的路由键与类似“dogs.*”的模式进行匹配。匹配这种末端的通配符比直接将路由键与“dogs”比较(“direct”类型的交换机)要消耗更多的CPU。如果你不需要“topic”类型的交换机带来的灵活性,你可以通过使用“direct”类型的交换机获取更高的处理效率。那么有哪些类型,他们又是怎么处理的呢?

Fanout Exchange – 不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。

Direct Exchange – 处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。

Topic Exchange – 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。我在RedHat的朋友做了一张不错的图,来表明topic交换机是如何工作的:

Source: Red Hat Messaging Tutorial: 1.3 Topic Exchange

持久化这些小东西们

你花了大量的时间来创建队列、交换机和绑定,然后,砰~服务器程序挂了。你的队列、交换机和绑定怎么样了?还有,放在队列里面但是尚未处理的消息们呢?

放松~如果你是用默认参数构造的这一切的话,那么,他们,都,biu~,灰飞烟灭了。是的,RabbitMQ重启之后会干净的像个新生儿。你必须重做所有的一切,亡羊补牢,如何避免将来再度发生此类杯具?

队列和交换机有一个创建时候指定的标志durable,直译叫做坚固的。durable的唯一含义就是具有这个标志的队列和交换机会在重启之后重新建立,它不表示说在队列当中的消息会在重启后恢复。那么如何才能做到不只是队列和交换机,还有消息都是持久的呢?

但是首先一个问题是,你真的需要消息是持久的吗?对于一个需要在重启之后回复的消息来说,它需要被写入到磁盘上,而即使是最简单的磁盘操作也是要消耗时间的。如果和消息的内容相比,你更看重的是消息处理的速度,那么不要使用持久化的消息。不过对于我们@DigiTar来说,持久化很重要。

当你将消息发布到交换机的时候,可以指定一个标志“Delivery Mode”(投递模式)。根据你使用的AMQP的库不同,指定这个标志的方法可能不太一样(我们后面会讨论如何用Python搞定)。简单的说,就是将Delivery Mode设置成2,也就是持久的(persistent)即可。一般的AMQP库都是将Delivery Mode设置成1,也就是非持久的。所以要持久化消息的步骤如下:

  1. 将交换机设成 durable。
  2. 将队列设成 durable。
  3. 将消息的 Delivery Mode 设置成2 。

就这样,不是很复杂,起码没有造火箭复杂,不过也有可能犯点小错误。

下面还要罗嗦一个东西……绑定(Bindings)怎么办?我们无法在创建绑定的时候设置成durable。没问题,如果你绑定了一个durable的队列和一个durable的交换机,RabbitMQ会自动保留这个绑定。类似的,如果删除了某个队列或交换机(无论是不是durable),依赖它的绑定都会自动删除。

注意两点:

  • RabbitMQ 不允许你绑定一个非坚固(non-durable)的交换机和一个durable的队列。反之亦然。要想成功必须队列和交换机都是durable的。
  • 一旦创建了队列和交换机,就不能修改其标志了。例如,如果创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重现创建。因此,最好仔细检查创建的标志。

开始喂蛇了~

【译注】说喂蛇是因为Python的图标是条蛇。

AMQP的一个空白地带是如何在Python当中使用。对于其他语言有一大坨材料。

但是对Python老兄来说,你需要花点时间来挖掘一下。所以我写了这个,这样别的家伙们就不需要经历我这种抓狂的过程了。

首先,我们需要一个Python的AMQP库。有两个可选:

  • py-amqplib – 通用的AMQP
  • txAMQP – 使用 Twisted 框架的AMQP库,因此允许异步I/O。

根据你的需求,py-amqplib或者txAMQP都是可以的。因为是基于Twisted的,txAMQP可以保证用异步IO构建超高性能的AMQP程序。但是Twisted编程本身就是一个很大的主题……因此清晰起见,我们打算用 py-amqplib。更新:请参见Esteve Fernandez关于txAMQP的使用和代码样例的回复。

AMQP支持在一个TCP连接上启用多个MQ通信channel,每个channel都可以被应用作为通信流。每个AMQP程序至少要有一个连接和一个channel。

from amqplib import client_0_8 as amqp
conn = amqp.Connection(host="localhost:5672 ", userid="guest",
password="guest", virtual_host="/", insist=False)
chan = conn.channel()

每个channel都被分配了一个整数标识,自动由Connection()类的.channel()方法维护。或者,你可以使用.channel(x)来指定channel标识,其中x是你想要使用的channel标识。通常情况下,推荐使用.channel()方法来自动分配channel标识,以便防止冲突。

现在我们已经有了一个可以用的连接和channel。现在,我们的代码将分成两个应用,生产者(producer)和消费者(consumer)。我们先创建一个消费者程序,他会创建一个叫做“po_box”的队列和一个叫“sorting_room”的交换机:

chan.queue_declare(queue="po_box", durable=True,
exclusive=False, auto_delete=False)
chan.exchange_declare(exchange="sorting_room", type="direct", durable=True,
auto_delete=False,)

这段代码干了啥?首先,它创建了一个名叫“po_box”的队列,它是durable的(重启之后会重新建立),并且最后一个消费者断开的时候不会自动删除(auto_delete=False)。在创建durable的队列(或者交换机)的时候,将auto_delete设置成false是很重要的,否则队列将会在最后一个消费者断开的时候消失,与durable与否无关。如果将durable和auto_delete都设置成True,只有尚有消费者活动的队列可以在RabbitMQ意外崩溃的时候自动恢复。

(你可以注意到了另一个标志,称为“exclusive”。如果设置成True,只有创建这个队列的消费者程序才允许连接到该队列。这种队列对于这个消费者程序是私有的)。

还有另一个交换机声明,创建了一个名字叫“sorting_room”的交换机。auto_delete和durable的含义和队列是一样的。但是,.excange_declare() 还有另外一个参数叫做type,用来指定要创建的交换机的类型(如前面列出的): fanout, direct 和 topic.

到此为止,你已经有了一个可以接收消息的队列和一个可以发送消息的交换机。不过我们需要创建一个绑定,把它们连接起来。

chan.queue_bind(queue=”po_box”, exchange=”sorting_room”,
routing_key=”jason”)

这个绑定的过程非常直接。任何送到交换机“sorting_room”的具有路由键“jason” 的消息都被路由到名为“po_box” 的队列。

现在,你有两种方法从队列当中取出消息。第一个是调用chan.basic_get(),主动从队列当中拉出下一个消息(如果队列当中没有消息,chan.basic_get()会返回None, 因此下面代码当中print msg.body 会在没有消息的时候崩掉):

msg = chan.basic_get("po_box")
print msg.body
chan.basic_ack(msg.delivery_tag)

但是如果你想要应用程序在消息到达的时候立即得到通知怎么办?这种情况下不能使用chan.basic_get(),你需要用chan.basic_consume()注册一个新消息到达的回调。

def recv_callback(msg):
    print 'Received: ' + msg.body
chan.basic_consume(queue='po_box', no_ack=True,
callback=recv_callback, consumer_tag="testtag")
while True:
    chan.wait()
chan.basic_cancel("testtag")

chan.wait() 放在一个无限循环里面,这个函数会等待在队列上,直到下一个消息到达队列。chan.basic_cancel() 用来注销该回调函数。参数consumer_tag 当中指定的字符串和chan.basic_consume() 注册的一直。在这个例子当中chan.basic_cancel() 不会被调用到,因为上面是个无限循环…… 不过你需要知道这个调用,所以我把它放在了代码里。

需要注意的另一个东西是no_ack参数。这个参数可以传给chan.basic_get()和chan.basic_consume(),默认是false。当从队列当中取出一个消息的时候,RabbitMQ需要应用显式地回馈说已经获取到了该消息。如果一段时间内不回馈,RabbitMQ会将该消息重新分配给另外一个绑定在该队列上的消费者。另一种情况是消费者断开连接,但是获取到的消息没有回馈,则RabbitMQ同样重新分配。如果将no_ack 参数设置为true,则py-amqplib会为下一个AMQP请求添加一个no_ack属性,告诉AMQP服务器不需要等待回馈。但是,大多数时候,你也许想要自己手工发送回馈,例如,需要在回馈之前将消息存入数据库。回馈通常是通过调用chan.basic_ack()方法,使用消息的delivery_tag属性作为参数。参见chan.basic_get() 的实例代码。

好了,这就是消费者的全部代码。(下载:amqp_consumer.py)

不过没有人发送消息的话,要消费者何用?所以需要一个生产者。下面的代码示例表明如何将一个简单消息发送到交换区“sorting_room”,并且标记为路由键“jason” :

msg = amqp.Message("Test message!")
msg.properties["delivery_mode"] = 2
chan.basic_publish(msg,exchange="sorting_room",routing_key="jason")

你也许注意到我们设置消息的delivery_mode属性为2,因为队列和交换机都设置为durable的,这个设置将保证消息能够持久化,也就是说,当它还没有送达消费者之前如果RabbitMQ重启则它能够被恢复。

剩下的最后一件事情(生产者和消费者都需要调用的)是关闭channel和连接:

chan.close()
conn.close()

很简单吧。(下载:amqp_publisher.py)

来真实地跑一下吧……

现在我们已经写好了生产者和消费者,让他们跑起来吧。假设你的RabbitMQ在localhost上安装并且运行。

打开一个终端,执行python ./amqp_consumer.py让消费者运行,并且创建队列、交换机和绑定。

然后在另一个终端运行python ./amqp_publisher.py “AMQP rocks.” 。如果一切良好,你应该能够在第一个终端看到输出的消息。

付诸使用吧

我知道这个教程是非常粗浅的关于AMQP/RabbitMQ和如何使用Python访问的教程。希望这个可以说明所有的概念如何在Python当中被组合起来。如果你发现任何错误,请联系原作者(williamsjj@digitar.com) 【译注:如果是翻译问题请联系译者】。同时,我很高兴回答我知道的问题。【译注:译者也是一样的】。接下来是,集群化(clustering)!不过我需要先把它弄懂再说。

注:关于RabbitMQ的知识我主要来自这些来源,推荐阅读:

–完–