99客服系统逆向实录

作者 Known Rabbit 日期 2016-08-30
99客服系统逆向实录

倒不是找不着准考证,不过成绩公布在假期里面就有点蛋疼了。[白眼]原来找不着准考证可以去一个神奇的网站,不过这两天想去查的时候,那个网站已经关停露出云服务商的404了。看那个项目的 issue 里面,似乎也是6月成绩放出来时候99ss顺便更新了,于是这个网站也不好使了。没办法自己下了99客服客户端,体验了一番,顺便搞掉这个充满铜臭味的软件做成网页版,方便普罗大众。

其实本来宝宝只想做一个诚实正直的好孩子,没想搞这个软件来着,不过因为虚拟机里面有个关不掉的 ProxyCap ,在99ss的黑名单里。 我想查个成绩竟然要被扫描电脑里安装的程序?你牛逼啊! 本意只是想抓个包看看请求就完事了,这简直逼人玩硬的么不是。

这个软件没有任何加密或者加壳处理,可以直接加载到 OD 里面。扫描字符串 “不兼容软件” 找到关键点,把几个 exit(1) nop 掉。

小彩蛋。我在想它为啥不让我用 ProxyCap 时候,顺手在字符串里面搜索了 proxy ,然后跟进 xref 看列表,反汇编结果如下。
得罪了一票友商啊。

// ...
v78 = "AdClean LSP Filter";
v50 = "AdClean LSP Filter";
v2 = 0;
v88 = a2;
// 杀毒软件
lpMultiByteStr = "Dr.COM";
// 水果同步
v75 = "Itunes";
// 虚拟机
v76 = "vmware";
// 杀毒软件
v77 = "AVSDA";
// 迅雷加速器
v28 = "XLacc";
// 迅雷加速器
v29 = "XLNetAcc";
// 迅游加速器
v30 = "xunyou";
// 网易UU (广告:个人觉得超好用)
v31 = "Netease UU";
// 国外加速器(?)
v32 = "GameCap";
v33 = "NGC-TcpFilter";
v34 = "NGC-UdpFilter";
// 唔,又是友商
v35 = "海豚加速器";
v36 = "xrush_socks";
// 应该是TX游戏助手
v37 = "QQVIPLSP";
v38 = "IERD_TGP_LSP";
v39 = "Easy2Game";
// 这也不行??
v40 = "XunLei Net Monitor";
v41 = "iKu Smart Network";
v42 = "ttslsp";
v43 = "SangforLSP";
v44 = "TGPLSP";
v45 = "VMCI sockets";
v46 = "MSAFD RfComm [Bluetooth]";
v47 = "FunAccelerator";
v48 = "MSAFD RSVP";
v49 = "MSAFD IPLAYER GAMELSP";
v51 = "HomeSafe LSPRAW";
v52 = "HomeSafe LSPTCP";
v53 = "HomeSafe LSPUDP";
v54 = "GameLSP";
v55 = "EvilLSP";
v56 = "RainSoft IP";
v57 = "rtwspi";
v58 = "Hyper-V RAW";
v59 = "GsLSP";
v60 = "LSP Pro";
v61 = "iKu Smart Network LSP";
// proxy
v62 = "PROXIFIER LSP";
v63 = "GameLSP IPLayer";
// proxy
v64 = "ASProxy";
v65 = "LavasoftLSP";
v66 = "TESTLSP";
v67 = "TenLSP";
// 也是迅雷
v68 = "XLNetAccLsp";
v69 = "SinforLSP";
v70 = "Network Tunnel Layered IP";
v71 = "XLNet";
// 唔
v72 = "Letv Network";
// 磨灭了一个输入法的野心
v73 = "SogouIpFilter";
// ...

呵呵。一个钦定的查四六级成绩的公司,非要把一个能在网页做的功能强行做成客户端,还捆绑一个垃圾加速器(减速器)模块,还不让人用这些别的软件。 隔着屏幕都能闻着铜臭的味道。 钦定的感觉就是不一样。

然后进到了主界面,随便查一个人,proxycap + mitmproxy 把请求拦下,看着看着发现请求超时了……
随手把超时改成了 0xFF ,然后把查询间隔的判断直接跳过了,都上阿里云了还计较这点访问压力,抠门不抠门?

逆向的时候最大难点就是如何判断哪个函数是做什么用的。这里要感谢一下 99ss 码农的姿势水平,程序简洁明了得就差给你源代码了。这里是关键部分的反汇编。

// sub_40D2C0
default:
AfxMessageBox("无法找到对应的准考证号,请联系客服", 0, 0);
stopconn(&sock);
break;
}
}
else
{
AfxMessageBox("服务器响应错误,请稍后尝试", 0, 0);
stopconn(&sock);
}
}
else
{
AfxMessageBox("连接服务器失败,请稍后尝试", 0, 0);
stopconn(&sock);
}
}
else
{
AfxMessageBox("分配内存失败,请重启尝试", 0, 0);
}
}

stopconn是通过里面调用了socket连接的winapi瞎猜的,那 else 是错的那 if 里的函数就是正确的处理咯!

[你知道吗] 99宿舍客服系统的码农,啊不程序员,个个都是精通 HTTP 网络协议的高手,甚至能用 nc 与服务器谈笑风生。他们需要联网的时候,手写一个个 HTTP 请求都是信手拈来,链接 raw socket ,sprintf 行云流水,去看他们的代码,你将受益匪浅!

//add computer macaddr to post parameters
strlen = (int)&strs[sprintf(&strs[(_DWORD)&params], "&m=%s", &macaddr)];
calcmagic((char *)&tmp2);
if ( encryptmessage((char *)&tmp2, strlen, &params) )
{
*(_DWORD *)ArgList = 17;
startconn((int)&sock);
LOBYTE(v84) = 6;
if ( postmessage((int)"http://find.cet.99sushe.com/search", &params, strlen, retstatus, (int *)ArgList) == 1 )
{
if ( *(_DWORD *)ArgList == 1 || *(_DWORD *)ArgList == 17 )
{
retstatus[*(_DWORD *)ArgList] = 0;
switch ( atoi(retstatus) )
{
case 2:
AfxMessageBox("参数错误,请联系客服", 0, 0);

跟进去以后果然, if 里面的函数 encryptmessage 带的参数是 calcmagic 函数生成的密码,这个函数的原理有点像密码学里面的看过的自动机,其实可以生成无限长的随机密码的。我写成了 py 版本,链接待补, 99cs 的码农复制这个函数,改吧改吧里面的参数就生成出一个新的8位 ascii 密码,然后美其名曰版本更新。呵呵,那以后我也改个参数咯。

到这里卡了好久,加密函数进去以后,把两个参数分别传给了三个函数,三个函数又调用了五六七八个子函数,里面的一串一串的位运算让我冷静下来开始思考人生,我就是想查一个考号啊亲……我……

然后我打开QQ问了我后桌的考号,然后随便找了个地方查到了成绩。

全剧终。

才怪。直到我翻代码时候不小心翻到了这个。

红红火火恍恍惚惚我似乎突然领悟了逆向的真谛。

搜索字符串啊亲!

到网上找来 openssl 1.0.0a 版本,2010年版本的,啧。看样子应该是静态链接进去了。 OD 识别是没指望了, IDA 有个 FLAIR(Fast Library Acquisition for Identification and Recognition) 可以自动生成一个库的特征文件,大概流程如下(粘贴自 README )

Typical scenario of a signature creation is:
- run a parser and create pattern (PAT) files
- run sigmake and get EXC file with collisions
- edit EXC file and resolve collisions
- run sigmake again and get SIG file
- repeat the above 2 steps till collisions exist
- run zipsig and get compressed SIG file

其实就是三个命令。

  • link 把lib里面的一个个obj模块全解压出来
  • plb 把模块的特征提取出来,取出一个个函数的名字
  • sigmake 从所有模块中去掉重复的函数/模块的特征,并打包成整个lib的signature

linux 下的 ar 命令似乎对 win 的 lib 不那么友好,不知道为啥,解压不出来。破 IDA 只能读取,能解压出来但不把文件给你,磨人的小妖精。没办法跑去下了个平时最讨厌的 VC++6.0 简体中文精简版 ,用它带的 link 命令一个模块一个模块解压。最后受不了了还是跑去找了个批量脚本。折腾一个sig加载了。世界瞬间明亮了。

加密函数部分:

char *__usercall encryptmessage@<eax>(char *key@<ecx>, size_t len@<edi>, char *text)
{
char *pkey; // ebx@1
char *rststr; // eax@1
char *pstrstr; // esi@1
char *v6; // ecx@2
int v7; // edx@2
void *v8; // ebx@2
unsigned int v9; // [sp+0h] [bp-A8h]@0
int v10; // [sp+Ch] [bp-9Ch]@2
void *ptxt; // [sp+10h] [bp-98h]@1
char v12; // [sp+14h] [bp-94h]@2
char *pkey_1; // [sp+98h] [bp-10h]@2
int v14; // [sp+9Ch] [bp-Ch]@2

pkey = key;
ptxt = text;
rststr = (char *)operator new[](v9);
pstrstr = rststr;
if ( rststr )
{
v6 = *(char **)pkey;
v7 = *((_DWORD *)pkey + 1);
v10 = 0;
pkey_1 = v6;
v14 = v7;
DES_set_odd_parity((int)&pkey_1);
DES_set_key_checked((int)&pkey_1, (int)&v12);
v8 = ptxt;
DES_cfb64_encrypt((char *)ptxt, pstrstr, len, (int)&v12, (int)&pkey_1, &v10, 1);
memcpy_0(v8, pstrstr, len);
operator delete(pstrstr);
rststr = (char *)1;
}
return rststr;
}

噢。好厉害。

那就一个 DES 加密呗。放狗搜了一下,CFB64 加密还是 libopenssl 的隐藏剧情!再一次印证了我的判断——99宿舍的程序员个个都是大神!

噢!好厉害!

大概分析完了,我又找到原来那个神奇网址的 GH 代码库看看能用多少。看了一遍发现,全能用。改个密码就行。

哦耶!

回头仔细看了看代码,想瞻仰一下无名大神是怎么写的加解密部分。结果发现,

if not libcrypto_path:
from ctypes.util import find_library
libcrypto_path = find_library('crypto')
if not libcrypto_path:
raise Exception('libcrypto(OpenSSL) not found')

self.libcrypto = CDLL(libcrypto_path)

噢。好厉害。

python,好厉害噢。

部署了 venv ,赶紧打开查一下自己的准考证……没有。好吧那我查我后桌的。查到了。可我后桌准考证不是5开头的啊……又点了一遍。唔,也不是8开头的啊……看来哪里不对。

用 mitmproxy 看了一下请求,是这样的。

POST /search HTTP/1.1
Host: find.cet.99sushe.com
Content-Length: 77
User-Agent: python-requests/2.7.0 CPython/2.7.10 Darwin/15.6.0
Connection: keep-alive
Accept: */*
Accept-Encoding: gzip, deflate

...

程序发的请求,是这样的。

POST /search HTTP/1.0
host: find.cet.99sushe.com
Accept: */*
Connection: keep-alive
Content-Length: 71

...

直觉判断是 User-Agent 的锅。尝试改成空 UA ,返回结果依然是随机数字。再用 mitmproxy 把 UA 字段整个去掉,返回结果正确。

于是又发现了一个奇妙的事实,python 的 Requests 库不能做到不包含 UA 的请求头。起因是, requests.utils.default_headers 里面定义了默认的头,会包含 requests 和版本号作为默认 UA 。这就麻烦了。要是不用 requests 呢,就要用 又臭又长 的 urllib2 重写请求部分,原来一句话的事要扩写成一个函数那么多?天啊。

唉,反正在 virtualenv 里面。把 default_user_agent 那里注释掉得了。懒[捂脸]

其实代码自己写出来也不是很费劲了啦,不过一个是放假,一个是学习的重点也不在代码上,(调用 libcrypto 这么奇葩的方法换我我也想不到)。才不是要玩 MC 呢。