0 前言

四年大学生活马上就要结束了,想一想这四年在江安的生活还真是相当不错的;不过对于我来说,网络大概是江安学习生活中最烦人的东西了。从我2012年入学开始到现在的2016年,川大江安在宿舍所能使用的网络接入方式有三种:校园网、移动CMCC-EDU和电信宽带。其中校园网虽然免费,这四年来雷打不动的512kbps的带宽,基本是处于看图片都是煎熬的水平;CMCC-EDU我基本没用过,速度大概是4Mbps左右,可惜无线不稳定;剩下的电信宽带虽然非常贵(一个月79元,带宽6Mbps,但是可以抵扣话费,奈何我不用电信手机),但是矮子里面拔将军,是唯一比较好用的了。

但是,这是建立在电信不限制路由器的基础上的。其实在2012年下半年的时候,电信宽带是不限制路由器的,使用普通的pppoe拨号方式就可以上网了。但是这只是电信认证系统的bug而已(还有另一种阴谋论的说法,就不详细说了。另外江安这个认证系统bug极多,后面我会详细说说),2013年上半年的时候修复了,导致路由器失效,接着电信更新了拨号器(就是本文将要破解的),导致飞扬俱乐部的在线算号器失效(方法是粗暴的禁止Linux版拨号器,至今再没有可用的官方Linux拨号器),当时在学生中算是引起了很大的反弹,可是学校方面不知道出于什么原因一直避而不谈这件事(呵呵,这其中必然有肮脏的**交易),电信抬出什么公安部教育部要求大学生上网一人一号的文件(监控之心不死)来当挡箭牌,这件事后来也不了了之了。但是不能用路由器你让手机iPad怎么上网?许多同学选择通过电脑发射无线信号(其实这也是被拨号器所屏蔽的,不过比较好突破),但是这个方法非常繁琐,要保持电脑一直开机。除了这种方法以外就只能放弃电信宽带了,不过我是一个网速多快都不嫌快,慢一点就无法忍受的人,所以就只能自己动手来破解电信的拨号器了。

在此要特别感谢软件学院的HZY同学,在我破解的过程中提供了很多帮助。

我的宽带帐号是12年就有的,用户名是学号,跟后来的用户名是手机号的不一样,所以不保证对后来的账号有效。

完整代码与原版Mac拨号器程序请移步Github

1 2.21版Mac协同拨号器拨号流程

我在13年的时候破解的是2.21版的Mac拨号器,Windows版的拨号器的程序逻辑要相对复杂许多,这个版本的算法直到目前(16年6月)都还是可用的。PPPoE的介绍和普通的PPPoE拨号流程可以参考Wikipedia:https://www.wikiwand.com/zh-cn/PPPoE和RFC文档:https://tools.ietf.org/html/rfc2516。计算机自带的PPPoE拨号器无法连接电信宽带的原因主要出在电信使用一种“二次验证”的机制,即计算机第一次拨号的时候是必然失败的,同时认证服务器返回一个字符串,拨号器根据这个字符串通过算法生成一个新的用户名,再次进行拨号才能成功。其中第一次拨号的用户名也是通过算法生成的,不过与服务器无关。

通过抓包就可以比较直观的看到拨号器拨号的整个流程,我已经很久没有装过协同拨号器了,在这里只贴一下我的路由器模拟的拨号过程。

这是第一次拨号的过程,可以看到CHAP认证失败并且返回16进制字符串:37f13ef44a72。

1
2
3
4
5
6
7
8
9
10
11
12
Jan  1 00:34:12 PandoraBox daemon.info pppd[2435]: Plugin rp-pppoe.so loaded.
Jan 1 00:34:12 PandoraBox daemon.info pppd[2435]: RP-PPPoE plugin version 3.8p compiled against pppd 2.4.5
Jan 1 00:34:12 PandoraBox daemon.notice pppd[2435]: pppd 2.4.5 started by root, uid 0
Jan 1 00:34:12 PandoraBox daemon.info pppd[2435]: PPP session is 30874
Jan 1 00:34:12 PandoraBox daemon.warn pppd[2435]: Connected to 00:25:9e:08:b8:3e via interface eth2.2
Jan 1 00:34:12 PandoraBox daemon.info pppd[2435]: Using interface pppoe-wan
Jan 1 00:34:12 PandoraBox daemon.notice pppd[2435]: Connect: pppoe-wan <--> eth2.2
Jan 1 00:34:15 PandoraBox daemon.info pppd[2435]: syncppp not active
Jan 1 00:34:15 PandoraBox daemon.info pppd[2435]: CHAP authentication failed: 37f13ef44a72
Jan 1 00:34:15 PandoraBox daemon.err pppd[2435]: CHAP authentication failed
Jan 1 00:34:15 PandoraBox daemon.notice pppd[2435]: Connection terminated.
Jan 1 00:34:15 PandoraBox daemon.info pppd[2435]: Exit.

这是第二次的拨号流程,可以看到认证成功并分配了IP地址等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Jan  1 00:34:23 PandoraBox daemon.info pppd[2447]: Plugin rp-pppoe.so loaded.
Jan 1 00:34:23 PandoraBox daemon.info pppd[2447]: RP-PPPoE plugin version 3.8p compiled against pppd 2.4.5
Jan 1 00:34:23 PandoraBox daemon.notice pppd[2447]: pppd 2.4.5 started by root, uid 0
Jan 1 00:34:23 PandoraBox daemon.info pppd[2447]: PPP session is 19277
Jan 1 00:34:23 PandoraBox daemon.warn pppd[2447]: Connected to 00:25:9e:08:b8:3e via interface eth2.2
Jan 1 00:34:23 PandoraBox daemon.info pppd[2447]: Using interface pppoe-wan
Jan 1 00:34:23 PandoraBox daemon.notice pppd[2447]: Connect: pppoe-wan <--> eth2.2
Jan 1 00:34:26 PandoraBox daemon.info pppd[2447]: syncppp not active
Jan 1 00:34:26 PandoraBox daemon.info pppd[2447]: CHAP authentication succeeded: Authentication success,Welcome!
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: CHAP authentication succeeded
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: peer from calling number 00:25:9E:08:B8:3E authorized
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: local IP address 220.167.43.208
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: remote IP address 220.167.40.1
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: primary DNS address 61.139.2.69
Jan 1 00:34:26 PandoraBox daemon.notice pppd[2447]: secondary DNS address 202.98.96.68
Jan 1 00:34:26 PandoraBox daemon.notice netifd: Interface 'wan' is now up

3 关于如何破解

主要使用IDA作为反汇编工具,最关键的就是阅读反汇编拨号器出来的汇编代码和IDA生成的反编译代码。编写自己的拨号器的使用的是C语言,当然也可以自由选择其他的语言。使用libpcap在程序内进行抓包。我在破解的最后阶段还用了GDB在命令行模式下直接调试官方拨号器,在没有调试信息的情况下跟踪程序运行真是很酸爽。

这个破解程序是差不多三年前写的了,当时接触编程和C语言不到一年,代码写的很烂,但是因为一直能用就没有再做什么改动了。

4 生成第一次拨号的用户名

这个生成第一次拨号用户名的算法是完全模拟的协同拨号器的工作方式。首先获取系统时间,再和一个特殊字符串、用户名、密码拼接成一个新字符串:

1
2
3
time1=time(NULL);
sprintf((char*)strTime,"%08x",time1);
sprintf((char*)first,"%s%s%s%s",strTime,"m2o=crE54nyNUht[",username,psw);

对该字符串计算MD5值并转换成16进制表示的字符串:

1
2
3
4
5
6
7
8
9
10
MD5_CTX md5;
MD5Init(&md5);
MD5Update(&md5,first,strlen((char*)first));
MD5Final(&md5,md5Result);
//把md5转换成字符串
for(int j=0;j<16;j++)
{
sprintf((char*)pMd5Str,"%02x",md5Result[j]);
pMd5Str+=2;
}

然后取该字符串的前19个字符,再与其他字符串一起拼接成最终的用户名:

1
2
char md5Str19[20]={0};
memcpy(md5Str19,md5Str,19); sprintf((char*)userName,"%s%s%s%s%s",strTime,"M","2021",md5Str19,username);

5 进行拨号

获得了第一个用户名之后就可以进行拨号了。这个拨号的方法是根据不同的平台而不同,我在Windows和Linux系统(主要是路由器运行的OpenWRT)上都移植过这个程序,可以说唯一需要修改的地方就是这里了。我在这里放一下我在路由器上运行的版本的拨号函数:

1
2
3
4
5
6
7
8
9
10
int PPPoeDial(char *user,char *pwd,char *name,char *device)
{
char pppoe_cmd[1024];
char ifname[40]="pppoe-";
sprintf(pppoe_cmd,"/usr/sbin/pppd nodetach ipparam %s ifname %s nodefaultroute usepeerdns persist maxfail 1 user %s password %s ip-up-script /lib/netifd/ppp-up ipv6-up-script /lib/netifd/ppp-up ip-down-script /lib/netifd/ppp-down ipv6-down-script /lib/netifd/ppp-down mtu 1492 mru 1492 plugin rp-pppoe.so nic-%s &",\
name,strcat(ifname,name),user,pwd,device);
system(pppoe_cmd);
puts("拨号中。。。");
return 1;
}

这里的pppoe_cmd的内容要根据对应的平台调整。Windows平台本身就提供了拨号函数,不需要运行pppd之类的命令了。

6 获取服务返回的参数

在我的程序中是通过使用libpcap库进行抓包来获取服务器返回的字符串的。libpcap库相关的信息请访问官网http://www.tcpdump.org。对每个数据包的回调函数是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
if(valid && pkt_data[22]==0x04&&pkt_data[23]==0x01)
{
pcap_breakloop(adhandle);
valid = 0;
char seed[10]={0};
for(int i=0;i<8;i++)
{
sprintf(&seed[i],"%c",pkt_data[30+i]);
}
printf("seed:%s\n",seed);
int time2=((unsigned int)rand())<<16|rand();
char result[50]={0};
getSecondUserName(result,seed,g_userName,g_pwd,time1,time2);
printf("%s\n",result);
sleep(8);
PPPoeDial(result,g_pwd,wan,d->name);
}
}

其中pkt_data[22]==0x04&&pkt_data[23]==0x01是表示这个数据包是服务器返回的认证失败的数据包,从中取得字符串seed,然后产生一个随机数time2,作为产生第二个用户名的参数。sleep(8)的目的是等待第一次拨号的进程完全退出以免造成冲突。 ​

7 生成第二次的用户名

生成第二次用户名的函数比较复杂,具体还是参见代码中的实现吧。主要思路仍然是对字符串进行处理后计算MD5值再进行拼接。

8 运行平台

在破解出这个算法后,我写过Windows和OpenWRT版本的拨号器,我相信大多数同学都是想使用路由器的,使用路由器很简单,只需要一个刷了OpenWRT的路由器就可以了,下载相应的工具链之后进行交叉编译,把得到的可执行文件上传到路由器上,再添加启动脚本就可以实现开机自动连接宽带了。

9 后记与扯扯淡

江安校区的电信宽带真的是相当奇葩,贵而难用,而且BUG多多,单单我知道的就有:把用户名最后几位去掉就可以使用无上限的带宽(取决于你寝室的出口物理带宽);同样是去掉几位就可以进行无限制的多拨(带宽叠加);绑定的手机号注销之后宽带帐号还在等等。靠着前两个bug我还是享受了将近两年的50M以上宽带的,虽然到现在这些bug已经修复了,但是说不定还是有其他bug,这就交给学弟学妹们来开发啦。

有了OpenWRT路由器之后可玩的东西就变得挺多了,完全可以当一个24h运行小服务器来用。比如以前我还在用多拨叠加宽带的时候因为有一定概率拨号失败(挺低的),所以就写了一个shell脚本,拨号完成后发邮件通知我有没有没连上的连接。或者写一个桥接CMCC信号并且自动登陆的脚本等等。