前几天被别人安利了这个玩意儿 ((制作你的说明书), 打算看看到底是什么货色

其实被各种社交网站刮风搞得有点谜了, 但是看巨佬 @xmcp 有一段时间一刮风就去挖人家老底的操作还是很羡慕的, 所以这次就跟一波风批判一下搞出这个使用书生成器的网易云音乐

网址链接中间有一个随机 ID, 多半是用来检测是谁分享的链接用的, 现在把一个 sample 挂在这里:https://st.music.163.com/c/m2-spec/1abCDEFghi/index.html

万年不变的 PC 歧视

首先是各种防止电脑用户登陆, 不过这里可以 override 一个 User-Agent 直接 bypass 掉:Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Mobile Safari/537.36

深深地体会到了PC用户受到的歧视

(果然只是检测 User-Agent 呢)

(常规操作系列)

蜜汁响应式布局界面

这里多半是没考虑到窄屏用户 (没错, 说的就是你, 刘海屏) 或者宽屏用户的需求, 所以一旦把布局拉到立式宽屏之外的范围画面直接崩坏...... 当然, 有一个起码的响应式布局我们还是要称赞一发的 (雾)

宽屏用户不会受伤
刘海屏的字体大小果然有问题

另外顺便吐槽一下直接把文字嵌在图片里的操作 (不过这确实避免了不少布局问题)

Off-topic: 机智的 React 作者

vendor.ac146652.js 里面放的大概是 React 框架, 不过感觉 React 的开发组很皮 (不是我开玩笑, 是真的很皮)

// Lines 10442 - 10445
Vr = bn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,
zr = [],
Br = !0,
Wr = void 0,
// Lines 11355 - 11366
unstable_createPortal: yn,
unstable_batchedUpdates: Z,
unstable_deferredUpdates: ci.deferredUpdates,
flushSync: ci.flushSync,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    EventPluginHub: rr,
    EventPluginRegistry: Xn,
    EventPropagators: cr,
    ReactControlledComponent: Nr,
    ReactDOMComponentTree: ur,
    ReactDOMEventListener: Kr
}
// Lines 11655 - 11660
isValidElement: l,
version: "16.2.0",
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    ReactCurrentOwner: M,
    assign: m
}

用了这个参数就会被炒鱿鱼吗......

探究测试的机理

业务代码一共有 23336 行, 我数过了, 但是感觉都是机器生成的...... 大概

  1. vendor.ac146652.js
  2. vendor.ac146652.deobfs.js
  3. bundle.3f3bfd35.js
  4. bundle.3f3bfd35.deobfs.js

提交名称

// Lines 12245 - 12291
value: function () {
    var e = this.props,
        t = e.handleNameChange,
        A = e.handleNextCard,
        n = this.state.userName;
    if (!n) return void(0, h.ShowToast)({
            key: "empty",
            type: "center",
            isOk: !1,
            text: "请先输入名字",
            autoClose: !0
        });
    for (var r = 0, i = 0; i < n.length; i++) g.test(n[i]) ? r += 2 : r++;
    if (r > 16) return void(0, h.ShowToast)({
            key: "empty",
            type: "center",
            isOk: !1,
            text: "请不要超过8个字哦",
            autoClose: !0
        });
    var o = (0, d.
    default)("/api/activity/antispam/keyword/nickname/check", {
            method: "post",
            async: !1,
            data: {
                content: n
            }
        });
    try {
            o = JSON.parse(o.request.responseText)
        } catch (e) {
            console.log(e)
        }
    o && o.data ? (t(n), A()) : o && 0 == o.data || o && 407 == o.code ? (0, h.ShowToast)({
            key: "empty",
            type: "center",
            isOk: !1,
            text: "换个昵称试试?",
            autoClose: !0
        }) : (0, h.ShowToast)({
            key: "empty",
            type: "center",
            isOk: !1,
            text: "操作失败,请重试",
            autoClose: !0
        })
}

你的名称会被提交到 /api/activity/antispam/keyword/nickname/check 以检查是否为恶意行为. 我们发现, 将名字提交的过程中, 个人信息是通过一个密钥加密的, 所以基本上可以认为避开了中间人攻击 (关键其实在于 SSL).

笔者试了一个不文明用语名称然后 (按照预想地) 被提示换一个昵称 (捂脸)

在线 / 离线

在分析网络活动的过程中我们也发现, 所有的资源都被分配到了网易的 CDN 上.

不过既然姓名要提交服务器审核那干嘛不顺带把结果也收回去, 至少在大数据时代多收集点信息, 可以优化一下服务质量广告推送啊......

↑ 其实那个是应该签隐私协议的, 所以还不如不收集这个信息

事实上, 添加完昵称以后, 包括最后的图片生成在内的的整个流程全部是在本地做的.

判定规则

在第 14312 到第 15103 行之间的代码描述了所有可能输出的判定, 并给了相应的规则.

然后依据直觉, 我们在第 13232 行找到了祖传的麦斯 · 兰敦姆 Math. random () 大法, 成功通过这名 20 世纪末诞生的伟大数学家的大名找到了程序的奥妙所在 (弥天大雾)

随机大法好

可以看出, 这里的规则和判定输出规则还是比较复杂的.

先试图定位到在函数中经常出现的一个邪恶家伙 H:

// Lines 15171 - 15181
    t.
default = {
            spec: n,
            l1: r,
            l2: i,
            qss: a,
            foodie: o
        },
    e.exports = t.
default
},

这段代码将刚才的判定内容定义为 H.default.spec, 之后看到这一组关键字直接找到这里就可以了.

// Lines 13224 - 13226
n.forEach(function (e) {
        e.a === t[e.q - 1].decent ? r++ : e.a === t[e.q - 1].foodie && i++
    });

上段代码直接表示了判分规则, 然后在下面找一下选取规则, 就有了下文的结论:

获取题目

抓包完了很快就能定位到资源列表, 然后跑一波正则表达式

直接上爬虫 (人力 OCR 是真的累)

import re
import requests


with open('po.txt', 'r', encoding='utf-8') as f:
    ls = f.read().split('\n')
for l in ls:
    num, sel = re.findall(r'answer/(\d+)/(.)\.', l)[0]
    r = requests.get(l)
    with open('./out/%s-%s.png' % (num, sel), 'wb') as f:
        f.write(r.content)
    print(num, sel)

结论

题目

一共有 16 道题, 每道题有一个正确答案 (大概是判定情商用的吧) 和一个吃货答案 (吃货答案选的数量超过一个值以后就会认为你是一个吃货), 具体在下表中呈现, 其中各列分别代表:

  • index: 题目编号
  • decent: 正确答案, 留空代表没有 (没有也是醉了)
  • foodie: 吃货答案, 留空代表没有
  • A, B, C: 分别代表三个选项的内容
index A B C decent foodie
1 水烧开了 三鲜麻辣鸳鸯锅 老巫婆熬制魔药 A B
2 酷炫的高档跑车 不遵守交通规则 变形金刚, 变身
3 上台前的紧张 生命的活力 暗恋的人出现 A
4 温暖的篝火 危险的电火花 香喷喷的烤肉 B C
5 故乡的离别 沿途的风景 拥挤的人潮 A
6 定时炸弹 睡不着的夜晚 等待一个人 B
7 着急下班的员工 窃取机密的黑客 游戏中请勿打扰 A
8 雨中浪漫约会 为行程延误忧愁 鞋子湿掉的难受 B
9 此起彼伏的海浪 洗衣机内心独白 胃在翻腾要吐了 A
10 丛林里的流水声 谁水龙头忘关了 忍了很久的释放 A
11 树影斑驳的森林 清晨遛鸟的大爷 京中有善口技者 A
12 夏天的风 浪漫的幻想 无忧无虑的童年 A
13 在珠峰顶上许愿 海上迎接暴风雨 迷失在荒芜沙漠 C
14 在星际间冒险 藏着精灵的山谷 梦境中一片荒芜 C
15 天空有 UFO 飞过 孙悟空翻筋斗云 五毛特效
16 受伤心灵被治愈 一瞬间惊艳变身 考卷答案浮现 A

判分规则

小程序会给你计算两个参数, 分别代表 6 道题目中正确答案的数量和吃货答案的数量. 不妨设:

  • adec: 正确答案数量
  • afoo: 吃货答案数量

输出内容

下表为所有的判定依据, 其中各列分别代表:

  • index: 判定编号
  • val: 最后输出的内容, XXX 用他或她代替
  • level: 该类人群的情商级别
  • tag: 有些判定只适用于特殊人群, 值为空 / 吃货 / 幽默
  • fm: 选项适用的性别
  • not: 该项判定不能和编号为 \(\{x | x \in not\}\) 的同时存在
  • r, candi: 有 r 的概率第二项和第三项都会从 candi 里抽选
index val level tag fm not r candi
0 XXX 擅于发现美的事物, 并从中吸收能量, 变得越来越好看. 2 1 . 3
1 XXX 笑起来像个孩子, 把他带在身边, 能带来青春永驻的效果. 2 0 . 3
2 请多给予 XXX 一点睡眠时间. 2 0 42, 45, 65, 64 . 3
3 XXX 偶尔也有像天使一样温柔的时候. 2 0 . 3
4 XXX 喜欢自由, 适合经常带出去放养. 2 0 46 . 3
5 肚子饿了就会心情不好, 带 XXX 一起去吃美味的食物吧! 2 0 8, 37, 39, 43, 44, 51, 52, 13 . 3
6 XXX 常常给身边的人带来好运. 2 0 . 3
7 XXX 因为太可爱了, 会令人陷入沉迷, 属于正常现象. 2 1 11 . 3
8 XXX 遇到香喷喷的东西就会变圆. 2 0 5, 37, 39, 43, 44, 51, 52, 13 . 3
9 可能因为材质特殊, XXX 对外界的攻击有折射能力. 2 0 61, 40 1 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
10 XXX 虽然经过多次更新迭代, 但依然有颗纯真的心. 2 幽默 0 . 3
11 XXX 发光的时候, 可能会使对象产生短时眩晕, 使用时请注意. 2 0 7, 19 . 3
12 XXX 拥有迷之运气, 中乐透的实力异于常人. 2 0 . 3
13 只要缘分到了, XXX 自己就会瘦下来. 2 0 5, 8, 37, 39, 43, 44, 51, 52 . 3
14 XXX 拥有高性能大脑, 可快速运转, 但过度工作 (学习) 容易发热. 2 幽默 0 58 . 3
15 XXX 买买买时会释放出强大气场, 胆子也会变大. 2 0 . 3
16 XXX 常常因为无法体验人类的乐趣而神伤. 2 0 24, 30, 31, 20 . 3
17 XXX 是能带来元气和灵感的谜之吉祥物. 2 0 . 3
18 XXX 心心念念的梦, 会在某个不经意的时候实现. 2 0 . 3
19 XXX 的光芒太耀眼, 但也不至于会刺瞎别人. 2 0 11 . 3
20 XXX 历经严苛质检品控, 是市面上难得一遇的珍品. 2 0 30, 33, 62, 35, 16 . 3
21 只要对 XXX 倾注感情, 他就能成为最可靠的伙伴, 遇到危机可无限次召唤. 2 0 . 3
22 XXX 的出厂设定就是温暖的守护者, 也是周围人快乐和活力的源泉. 2 幽默 0 . 3
23 XXX 有极为罕见的感官知觉, 对他说谎可不是明智之举. 2 0 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
24 XXX 装备了精准的探测仪, 能发觉并把握住人生中真正的机会. 2 0 33, 62, 35, 16 . 3
25 XXX 能通过每次挑战完成升级, 获得更高的智慧和运气. 2 0 . 3
26 XXX 具备安全与稳定性, 是居家、旅行必备的小能手. 2 0 . 3
27 XXX 能快速拆卸烦恼, 重新制作出充满乐趣和创造力的产物. 2 幽默 0 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
28 变态 (萌) 是 XXX 的常态. 2 0 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
29 保温杯和泡脚桶, 可延长 XXX 的使用寿命. 2 幽默 0 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
30 XXX 偶尔会孤单, 但也能享受孤单, 是一款自由的产品. 2 幽默 0 33, 62, 35, 16 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
31 XXX 对时间的感知力弱, 所以不知不觉又加班了. 2 0 33, 62, 35, 16 1 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 24, 25, 26, 34
32 XXX 拥有读气氛的技巧和超高感知力, 自己却毫无察觉. 2 0 . 3
33 XXX 是怪人, 但接触后会很有趣, 一款来自外星的产品. 2 幽默 0 20, 30, 31, 24 . 3
34 XXX 听到喜爱的音乐, 就能触发运气开挂的效果. 2 0 . 3
35 XXX 还在努力融入人类世界, 拥有地球人不具备的思维方式. 1 幽默 0 24, 30, 31, 20 . 3
36 XXX 肯花费大量时间和精力, 超出预期的完成任务. 1 0 . 3
37 要定期给予 XXX 喂食, 他相当单纯. 1 0 5, 8, 39, 43, 44, 51, 52, 13 . 3
38 请用纤细的语调对 XXX 说话吧. 1 幽默 0 43, 44, 42, 30, 61, 47, 49, 55, 57, 45 . 3
39 如果做事开始慢腾腾, 及时给 XXX 一点吃的. 1 0 5, 8, 37, 43, 44, 51, 52, 13 . 3
40 XXX 如果发生故障, 就给他一个拥抱吧. 1 0 61, 9 . 3
41 XXX 喜欢被夸奖, 所以一天表扬他一次吧. 1 0 . 3
42 XXX 一困就会情绪低落, 请给予他适当的睡眠时间. 1 0 43, 44, 30, 61, 38, 47, 49, 55, 57, 45, 2, 65, 64 . 3
43 XXX 即使心情低落, 也会保持精神, 这时候给他送些点心吧. 1 0 44, 42, 30, 61, 38, 47, 49, 55, 57, 45, 5, 8, 37, 39, 51, 52, 13 . 3
44 XXX 心情不好的时候, 给他吃一点甜甜的东西吧! 1 0 42, 30, 61, 38, 47, 49, 55, 57, 45, 5, 8, 37, 39, 51, 52, 13, 43 . 3
45 XXX 最大的兴趣就是睡觉. 1 0 2, 42, 65, 43, 44, 42, 30, 61, 38, 47, 49, 55, 57, 64 . 3
46 XXX 能带给你更神奇的经历...... 当你带他出去玩的时候. 1 0 4 . 3
47 XXX 看着对方眼睛说话就会脸红. 1 0 43, 44, 42, 30, 61, 38, 49, 55, 57, 45 . 3
48 XXX 对气温变化的感知力很弱, 可能因为内心一直是暖的. 1 0 50 . 3
49 XXX 脑子里塞满天真的幻想, 能拍成一部高分动画片. 1 0 43, 44, 42, 30, 61, 38, 47, 55, 57, 45 . 3
50 XXX 遇到寒冷的温度就会变圆. 1 0 48, 5, 8, 37, 39, 43, 44, 51, 52, 13 . 3
51 XXX 吃越少, 越会变胖 1 0 5, 8, 37, 39, 43, 44, 52, 13 . 3
52 吃饭也是 XXX 缓解压力的一种办法 1 0 5, 8, 37, 39, 43, 44, 51, 13 . 3
53 XXX 易燃, 需要时刻为他准备好灭火器. 1 幽默 0 43, 44, 42, 30, 61, 38, 47, 49, 55, 57, 45 . 3
54 XXX 拥有巨大内存, 收到的关心 (和红包) 他都会记得. 1 幽默 0 . 3
55 如果丢开 XXX 不管, 要小心他自己溜走了! 1 幽默 0 43, 44, 42, 30, 61, 38, 47, 49, 57, 45 . 3
56 未来的道路上, XXX 会因为无敌而略感寂寞. 1 0 . 3
57 XXX 不擅长向别人求助. 如果有人察觉到的话请主动帮他一下. 1 幽默 0 43, 44, 42, 30, 61, 38, 47, 49, 55, 45 . 3
58 XXX 的脑部为 S 级原装货, 其他身体部分都是普通零件. 1 幽默 0 14 . 3
59 如果你觉得 XXX 发量开始减少, 那一定是出现了幻觉. 1 0 . 3
60 XXX 只要一出门就会变漂亮. 1 1 . 3
61 XXX 遇到刮痕损伤不会自己修理. 1 幽默 0 9, 40, 43, 44, 42, 30, 38, 47, 49, 55, 57, 45 . 3
62 XXX 对地球充满好奇, 喜欢人类 (不工作时) 的生活方式. 1 幽默 0 24, 30, 31, 20 . 3
63 XXX 只要开启降噪模式, 就能获得坚定追随内心和直觉的勇气. 1 0 . 3
64 XXX 早上起床时容易发生爆炸. 1 幽默 0 2, 42, 45, 65 . 3
65 XXX 按时睡觉的程序被设计的太复杂, 很难执行. 1 幽默 0 2, 42, 45, 64 . 3

以上的判定被分为三类 (有交集), 分别为:

index desc val
L1 所有 level 为 1 的 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65
L2 所有 level 为 2 的 0, 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
Foodie 标签为 5, 8, 37, 39, 43, 44, 51, 52, 13, 48, 50

生成流程

第一条: 程序会从 L2 中不断随机选取判定直到满足以下两条件之中至少一个:

  1. 当正确答案数 adec 不少于 5 个时, 判定标签为 “幽默” 的判定
  2. 该项 fm 值为 1 且用户性别为女的判定

第二条: 程序会以一定的概率分配从以下分组中选取:

  1. 若第一条的 candi 非空, 则有 r 的概率 (通常为 0. 3) 直接从 candi 中取
  2. 当吃货答案数 afoo 不少于 1 个时, 有 0. 7 的概率从 Foodie 中取
  3. 有 0. 2 的概率将从 L1 中取
  4. 剩下的一定取自 L2

随机取出的判定应满足以下判定:

  1. 和第一条天生不冲突 (互不在对方的 not 集合中), 也不相同的判定
  2. 当正确答案数 adec 不少于 5 个时, 判定标签为 “幽默” 的判定
  3. 该项 fm 值为 1 且用户性别为女的判定

第三条: 程序会以一定的概率分配从以下分组中选取:

  1. 若第一条的 candi 非空, 则有 r 的概率 (通常为 0. 3) 直接从 candi 中取 (该事件与第二条的事件相互独立)
  2. 有 0. 5 的概率将从 L1 中取
  3. 剩下的一定取自 L2

随机取出的判定规则基本与第二条的规则相似.

最终输出

随机几个背景颜色然后堆一个 canvas 出来, 由本地生成一个 data blob 显示到屏幕上. 其中还需要做一点小小的替换, 不过都无所谓了.

吐槽

不得不说这个小程序写得还是比较有意思的. 但是其实这类分享类应用基本套路都一样, 后面是不是有批量生成器都不是很好说.

通过我们严密的分析, 发现在前面 6 道题内做的选择与最后具体的结果几乎没有关系. 于是并不像我们所预想之程序员真正兢兢业业地把每个性格的特点写在代码里, 反而是偷懒地使用麦斯 · 兰敦姆 Math. random () 大法来掩盖自己没好好写代码的事实 (笑).

另外代码里各种神奇的操作, 比如用 !0 代替 true 一类的, 抑或是用 for 或者 lazy evaluation 强行缩行...... 或许这些都是业界常态但是我太 naive 没看到吧.

之前看 @xmcp 分析的另一个朋友圈刮风小程序, 大抵也是相同的套路. 考虑到这次作者读代码读到吐的情况, 下次也许对这类事情我就敬而远之了吧.

「氷菓」夕べには骸に

总之, 占卜这种玩意儿, 顶多是学园祭的特色, 若是在日常生活中出现, 大家便也娱乐一下就好, 切勿当真~