如何“调教”隔壁胖虎

胖虎的入住

合租屋原来隔壁的文静小伙搬走了,换来了一个胖胖的男生。也许是胖子天生对胖子的认同,我对胖子的印象都是非常不拘小节,且随和、好说话的那种。

而这位呢?确实不拘小节,但是实在太任性了。每天打游戏也就罢了,边打还边语音聊天,大叫大嚷个不停。白天如此,晚上12点过后还是如此。

同住的室友好言相劝过,也演变成指责谩骂过,但他的反应都是不言不语,依旧我行我素。依然不顾他人感受,还经常敞着房门,那叫喊声简直响彻屋顶。

最讨厌的是,打CF每每打到关键时刻,被爆头了还爱甩锅给队友!甩锅的同时还吐各种脏话侮辱队友,他队友还能忍他这么久,这我就不能忍了!

本着先礼后兵、先君子后小人的原则,既然劝阻无效,我决定采取一个措施。

限速

因为路由器的管理员密码是默认的“123456”,所以很容易就登录进管理页面。每当晚上要睡觉的时候,给他个限速。游戏打的不爽?早点儿睡吧。

顺便把路由器密码也改了,这样防止他再给自己解限速。

然而胖虎毕竟打了这么多年游戏了,并不傻,很快就发现被限速了。

于是开始敲门问其他住客,打电话问房东,到底密码是啥?!但并没有人知道……

某一天,胖虎终于忍不了了,重置了路由器,恢复了默认密码,SSID改成和原来的一样。

简直机智。

新的对策

又恢复了夜晚的嘶吼,实在是令人沮丧。

这回我和女票一起开始商量新一轮的制裁了。我们有以下三个前提:

  1. 直接限速是不可能的了,这边改密码,那边就重置,根本不是办法。
  2. 在不影响其他人正常使用的情况下,给他限速或断网。
  3. 不能被他发现是人为因素导致的。

ARP欺骗

女票说:“既然在同一个网关下,又能ping通,就可以ARP欺骗呀。

伪造ARP包,自己在路由器这边伪装被攻击的主机,在主机那头伪装成路由器,两头欺骗。”

但我们很快发现这也不是可行的方案,因为ARP欺骗是很容易检测到的,如果他电脑装了360的话,360是会有弹框报警的。

给路由器刷入openWRT

给路由器换个系统,相当于把路由器完全掌控在手中。不仅想限速就限速,还可以对外显示虚假的管理页面。

理论上可行,然而我们都没有这方面经验,而且要动手实现工作量有点忒大,属于杀鸡用牛刀了。

建一个同SSID名的热点蜜罐

设备碰到以前连过的wifi会自动连接,靠的是wifi的SSID(就是wifi的名称啦)。只要你开一个同名的wifi热点,他的设备就会选择信号最强的一个连接。

这时你只需要偷天换日,把路由器的wifi断一会儿,他就连上你的“蜜罐”啦,这时他的网速命运就掌握在你的手里了。

可是这个办法也行不通……因为胖虎这家伙,根本不用wifi。为了保证打游戏稳定,他是直接拉了根网线到自己屋子里的!

用脚本发送禁网请求,实现针对胖虎的间歇性禁网

最后这个方案是可行且成本最小的。

简单说就是,既然我们可以登录路由器管理页,就可以写个脚本直接发请求,实现管理页的所有管理操作。但如果永久禁网或限速的话,无疑像以前一样会被发现。所以既要给他断网,又不要太长时间,这样间歇性地断网。

开工

这个路由器是 TP-link 的,型号是 TL-WR842N。

1. 找到禁网的请求

首先进入管理页面:

打开 chrome,点开 network 可以看到这个页面每隔0.1秒就发送一个请求去获取每个设备当前的网速。

然后点击一下禁用按钮,会发送一个更上图类似的 POST 请求:

对比这个请求和上一个请求,有以下几个要点:

  1. url参数 code=0
  2. url参数 id 好像是个 token 值,隔一段时间就会变化一次。
  3. POST 的内容是设备的 mac地址+设备名称+上传限速+下载限速+是否禁网

这其中(1)和(3)都不是问题,如果我要禁某个设备的网,这两部分都是固定的。

不好搞的是这个 id 值,每几秒就会变化一次,而这个 id 值是怎么得到的呢?在所有请求当中都完全找不到影子,在 HTML当中也找不到。看来它是通过某种算法算出来的。

抓包到这一步没有什么好办法了,只能靠看前端 js 源码了。

2. 找到点击“禁网”按钮的事件代码

照我以前的尿性,就是拿起js代码就开始看了,代码很多的时候要摸索很久。

这时女票教了我一个新姿势,啊不,是新知识。查看页面元素的事件:

简单说就是找到目标元素,在 Elements 里选择 Event Listeners 然后点开相应的事件,就能找到绑定的函数了~ 如下图

这个过程中还有点儿小插曲:

前面说过了,这页面每隔0.1秒就会发送一个获取所有设备当前网速的请求,所以正中间的那个列表的 DOM 元素在不停地变化。当你点击“禁网按钮”所在元素的时候,因为 DOM 每0.1秒就变化一次,所以你根本就选不中目标元素。

这怎么办呢?很简单,只要在 Sources 当中点击右边的暂停按钮,当前正在执行的 js 就会全部暂停。这样 DOM 元素就不会变化了,你就能顺利选中你想选中的元素啦~

3. 找到token生成算法

找到元素绑定的函数后,顺藤摸瓜,找到了生成token的相关函数,直接贴代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
	
/* 认证请求 */
this.auth = function(data)
{
var pwd = data;
var url = this.domainUrl + "?code="+ TDDP_AUTH +"&asyn=0";

/* 密码格式错误 */
if (data == undefined || 0 == pwd.length)
{
this.result.errorno = EUNAUTH;
return this.result;
}

data = undefined;
this.initResult();
//这里的 securityEncode 是token的加密算法
this.session = this.securityEncode(authInfo[3], pwd, authInfo[4]);
url += ("&id=" + this.encodePara(this.session));

if ((false == this.local) || (this.routerAlive))
{
this.externLoading(true);
this.request(url, data, "post", this.ajaxSyn);
this.externLoading(false);
}

/* 解析数据 */
this.parseAuthRlt();

if (ENONE == this.result.errorno)
{
this.setLgPwd(pwd);
}

return this.result;
};

剥茧抽丝,所有问题都转化为 this.session是如何生成的。

代码审计到这一步,有三条路线可以走:

  1. 继续看 securityEncode 函数是怎么实现的,以及传入它的三个参数的来源。
  2. 不看 securityEncode 内部的复杂代码,用 Python 写发请求的部分,到了需要用到token的时候,调用 PyV8的JS引擎,直接把securityEncode函数丢进去,直接算出结果。
  3. 根本不用审计任何代码,使用 PantomJs、CasperJs、chrome插件等方式,直接在浏览器的环境里模拟点击按钮。

这三条路都行得通,有点像 脱离浏览器环境->半浏览器环境->全浏览器环境 的感觉。

女票的意思是直接用(3),但我还是喜欢刨根问底,于是女票就把写脚本的这个任务就交给我了……

4. 找到参数来源,移植加密算法

  1. authInfo[3]
  2. pwd
  3. authInfo[4]

上面是 securityEncode 的三个入参,要找到它们的来源。

(2)很好找,是个固定的字符串,就是管理员密码“123456”的固定映射。

找了半天(1)和(2),终于找到了……只能说写这个路由器管理页面的程序员想法挺有意思的——

前文反复说到,这个页面会每隔0.1秒发送一次获取当前网速的请求。这些请求大多数情况都是成功的,返回码都是200。但是过几十秒钟的某次就会返回401 Unauthorized

这个401的请求会跟200返回的结果不一样:

200返回的结果就是每个设备的 IP、MAC、网速等等信息:

而401返回的结果是这样的:

这样就清晰了,authInfo[3]authInfo[4]就是这个响应结果的第三行和第四行。

接下来就是把 securityEncode 函数的代码翻译成 Python 了。

还好,securityEncode函数的内容不长,算法也不复杂,而且没有混淆代码。否则的话只能用 PyV8了。

直接贴代码:

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
this.securityEncode = function(input1, input2, input3)
{
var dictionary = input3;
var output = "";
var len, len1, len2, lenDict;
var cl = 0xBB, cr = 0xBB;

len1 = input1.length;
len2 = input2.length;
lenDict = dictionary.length;
len = len1 > len2 ? len1 : len2;

for (var index = 0; index < len; index++)
{
cl = 0xBB;
cr = 0xBB;

if (index >= len1)
{
cr = input2.charCodeAt(index);
}
else if (index >= len2)
{
cl = input1.charCodeAt(index);
}
else
{
cl = input1.charCodeAt(index);
cr = input2.charCodeAt(index);
}

output += dictionary.charAt((cl ^ cr)%lenDict);
}

return output;
};

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def securityEncode(self, input1, input2, input3):
dictionary = input3
output = ""
cl = 0xBB
cr = 0xBB

len1 = len(input1)
len2 = len(input2)
lenDict = len(dictionary)
length = len1 if len1 > len2 else len2

for index in xrange(length):
cl = 0xBB
cr = 0xBB

if index >= len1:
cr = ord(input2[index])

elif index >= len2:
cl = ord(input1[index])
else:
cl = ord(input1[index])
cr = ord(input2[index])
output += dictionary[(cl ^ cr) % lenDict]

return output

5. 写脚本,开始“制裁”

很快Python脚本写好了,每隔 N 秒钟,就给他断一次网,每次断 M 秒。谁会受得了游戏不停地掉线呢?

禁网的 M 秒期间,是登不上路由器管理页面的。

当他登上去的时候,又解禁了,所以看不到任何的痕迹。

为了做到万无一失,禁网的几秒钟里会把他的手机和电脑一起禁掉,这样他就没有任何设备可以登陆管理页看到这是为什么了。

疗效

每天晚上要睡觉的时候,觉得太吵了,就把脚本一开……

一开始,断网的时候就听见胖虎在那儿叫:

“艹!掉线了!”

“卧槽!!又掉线了!”

“啊啊?怎么又 TM 掉线了?!”

“……”

到后来就再也听不到他的怒吼了……

如今,到了晚上12点,胖虎打游戏基本不会发出太大的声音,如果听见声音太大的话,我就直接开脚本。如果还不起作用,就把掉线时间间隔调小一点,断网时长调长一点。

胖虎依旧天天打游戏,但渐渐开始早睡了,相信他这个“掉线小王子”再也没有理由甩锅给队友了。

感觉又为这个世界的美好安详做出了小小的贡献,真好……