论如何再收一个新年解谜红包 – 2019 篇

注意:题目中出现的链接需要替换后才能访问 redpacket.kaaass.net=>redpacket.kaaass.net/archived/2019/。

嘛,和去年一样,今年我又发了个新年解谜红包(不知道去年红包的同学可以康:论如何正确收一个新年解谜红包)。这次的题目也非常简洁,只有一张图片(右键另存为下载)。今年的红包比起去年的流程更短,但是每一关的难度都不小(尤其是 Stage1)。不过,由于今年红包未领将会续两次,所以最长解谜时间可以是 3 天。那么接下来就是解谜全程啦~

Stage1 – 颜文字图片

Stage1 可以说是最难的,但是提示也是最多的。首先看图。

很多人一眼就看出,这个表情的眼睛是二维码的定位点。没错,而且这是一个非常关键的提示。而剩下的提示只能从文件本身入手了。随便丢进一个二进制文件查看器,在文件尾部发现另一个提示。

在 IEND 块后有一个单词,“TALLER”。除此之外,还有一个比较隐蔽的提示就是,部分图片浏览器打不开这张图片。于是很多人就此卡关了…… 其实解题的方向很明确,就是 PNG 文件结构(后来我也补了这个 hint)。

图片浏览器打不开的原因,其实是图片的第一个 IHDR CHUNK 的 CRC 校验失败了。

IHDR CHUNK 的数据段存放的其实是 PNG 图片的一些基本信息(长、宽、位深度等等),而结合 “TALLER” 的提示,很容易能想到是指图片的宽度,于是随便修改一个大一点的数值,然后一点点调整,就能看到之后的内容了。

可以看到,这是一个支离破碎的二维码。所以依照定位点的提示,简单的拼起来。

于是你就得到一个扫不出的二维码 23333。部分 dalao 看到扫不出来就放弃了这个思路,转而去思考如何用上另一只眼睛,于是就跑偏了。(然而无论从中间的内容还是定位点间隔来看,另一只眼睛都没办法组成一个二维码)

还是要在图上找答案。既然可以确定是二维码,那就来找一下定位图形好了。

可以看到,定位图形都是黑白的。而剩下区域的黑白点十分稀少,所以排除彩色块是杂色的可能。于是真正的答案就呼之欲出了 —— 这是三通道的三个二维码结合在一起!理由很简单,重复的区域(定位图形)是黑白的。于是打开 ps,把红、绿、蓝三个通道的二维码分开~

分别扫描这三个二维码,得到:

红: YW5ueWFvY3l1dS8=
绿: cmVkcGFja2V0Lmth
蓝: YWFzcy5uZXQvMjB0

没错,这是 base64。解码得到:“annyaocyuu/”、“redpacket.ka”、“aass.net/20t”。于是拼接得到下一关的链接:redpacket.kaaass.net/20tannyaocyuu/

有趣的事情

  • 之所以把二维码拆开来,是因为想不到不拆的话怎么隐藏其余部分
  • IEND 之后实际上增加了一个空字节,之后才是 TALLER。由于 IEND 的 CRC 校验的最后一字节是 82,所以增加了一个空字节以保证字母 T 能正常显示
  • 去年最后一关是图片(文件解谜),而今年第一关就是图片的原因是,去年由于过早暴露关卡地址,导致奇怪的请求较多,日志分析起来很麻烦

Stage2 – 寻找共鸣者

一打开页面,先闪过了一些字,然后变成了类似 “Not you, 37ece3d5410533901a1a40590f46d9a3.” 的内容。每次刷新时后面的内容都会变。查看源代码可以看到页面使用了 js 脚本 index.min.js,而且原本内容是:

Man, you should have javascript-supporting!

KAAAsS 留:这次出太难了,红包就直接丢在这吧 b547de608dd0f2bbd61919a854510263。如果继续玩的话,原来的最后一关我发了个 100 的。

这个红包过期且被领了,故只续其它红包啦~

另外,注意到网页的标题是 “Find the ECHOES!”。

简单版红包

由于第一个访问该页面的请求已经是题目发出的次日,且离续红包只有半小时了。于是,考虑到第一关已经很不简单了,我就临时把原定最后的红包发在这里,且最后一关的红包改发 100 的。不过这个红包也不是随便发的,也有一个简单的谜题。

b547de608dd0f2bbd61919a854510263 看着像 md5,实际上也就是红包码的 md5。当然我也是不可能直接让破一个 md5,那可太鬼畜了。真正的红包码藏在 COOKIE 里。而且每一次请求其实都会返回 Set-Cookie 头,所以要怪只能怪没用 Postman 之类的工具啦。

正常流程

打开调试工具发现,调试工具没办法正常使用。只要一打开调试工具,就会自动跳转到断点。

这就意味着没办法进行断点调试和 ajax 请求查看之类的操作的。事实上,这一步就是为了掩盖 ajax 请求。虽然可以用诸如油猴脚本的方式屏蔽反调试(具体见 “反调试浅析” 节),但是事实上也可以直接对 js 脚本进行静态分析。用自带调试工具 format 一下。

这种魔性的十六进制命名其实看着还是很头大的,而且 format 后文件有整整 383 行,从头阅读十分麻烦。于是,要从其他地方找突破口。这里有几种可能的方式。

  1. 如果通过抓包工具或其他手段发现了 ajax 请求,那么可以在代码中直接搜索 XMLHttpRequest。只出现于 340 行。
  2. 既然每次刷新页面,那串字符内容发生了变化,那么,那串字符要么是 ajax 请求来了,要么就是随机生成的。从 ajax 请求的分析可以看出,请求的 url 的参数是一串随机的字符串(比如 url:redpacket.kaaass.net/20tannyaocyuu/ronn.php?suttann=815abb03af71b),于是可以断定 js 中存在随机生成代码。于是搜索 Math 或 random,找到 316 行的函数_0x242600。

而且页面逻辑那么简单,肯定没有 300 来行,所以可以判断真正的逻辑就是 293-353 行。简单说明下逻辑:

  1. 调用_0xf07168 函数生成请求参数。
  2. 使用_0x4e01df 发送 ajax 请求。
  3. 将返回的 json 中,字段 “m” 的字符串,与随机字符串的 md5 进行比较。若相同,显示 “I think it’s you.”。若不同,显示 “Not you,  …….”。

于是我们来分析下 ajax 的返回。比如对于 http://redpacket.kaaass.net/20tannyaocyuu/ronn.php?suttann=815abb03af71b,将返回:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"t": "1549800860",
"i": "415c90a1be0659c317b6fc13f4",
"m": "280e4d25e5e203e5d5a1125b88612a27",
"e": "a67e6dd453bd1fa8ded097121b481308"
}
{ "t": "1549800860", "i": "415c90a1be0659c317b6fc13f4", "m": "280e4d25e5e203e5d5a1125b88612a27", "e": "a67e6dd453bd1fa8ded097121b481308" }
{
    "t": "1549800860",
    "i": "415c90a1be0659c317b6fc13f4",
    "m": "280e4d25e5e203e5d5a1125b88612a27",
    "e": "a67e6dd453bd1fa8ded097121b481308"
}

不难发现

  • t 是当前时间戳
  • i 是一个字符串,内容和传入的参数 suttann 有关
  • m 是 i 的 md5 值
  • e 是 i 的 md5 值

所以,实际上 js 部分的逻辑可以看作是在判断随机字符串(suttann)和 i 是否相同。另外,还有个提示。网页标题 “Find the ECHOES!”,ECHOES 是《不吉波普不笑》的共鸣者,只会重复别人说的话,ECHOES 是回声,即返回和输入相同。

所以我们要找到这样一个 “不动点” 字符串。事实上,suttann 是十六进制数。线索其实不少,结合 suttann 的生成代码:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 内容是"0123456789abcdef"
let _0x18f6e3 = _0x4a1f('0x14');
// 生成请求参数
let _0xf07168 = ()=>{
let _0x1f40ba = {}
, _0x2f85d0 = ''
, _0x387ebf = _0x4a1f('0x37'); // 内容是"hexof13length"
for (let _0x2a8809 in _0x387ebf) {
_0x2f85d0 += _0x18f6e3[_0x242600(0x0, 0xf)]; // _0x242600是生成随机数
}
_0x1f40ba[_0x4a1f('0x38')] = _0x2f85d0; // _0x4a1f('0x38') => "suttann"
return _0x1f40ba;
}
// 内容是"0123456789abcdef" let _0x18f6e3 = _0x4a1f('0x14'); // 生成请求参数 let _0xf07168 = ()=>{ let _0x1f40ba = {} , _0x2f85d0 = '' , _0x387ebf = _0x4a1f('0x37'); // 内容是"hexof13length" for (let _0x2a8809 in _0x387ebf) { _0x2f85d0 += _0x18f6e3[_0x242600(0x0, 0xf)]; // _0x242600是生成随机数 } _0x1f40ba[_0x4a1f('0x38')] = _0x2f85d0; // _0x4a1f('0x38') => "suttann" return _0x1f40ba; }
// 内容是"0123456789abcdef"
let _0x18f6e3 = _0x4a1f('0x14');
// 生成请求参数
let _0xf07168 = ()=>{
    let _0x1f40ba = {}
      , _0x2f85d0 = ''
      , _0x387ebf = _0x4a1f('0x37'); // 内容是"hexof13length"
    for (let _0x2a8809 in _0x387ebf) {
        _0x2f85d0 += _0x18f6e3[_0x242600(0x0, 0xf)]; // _0x242600是生成随机数
    }
    _0x1f40ba[_0x4a1f('0x38')] = _0x2f85d0; // _0x4a1f('0x38') => "suttann"
    return _0x1f40ba;

}
  • 生成字符串每一位只能是 0123456789abcdef
  • 生成时的 foreach 循环遍历的字符串为 hexof13length
  • 注意到 i 的长度和 suttann 有关,且 suttann 越大 i 越长。

理所当然,i 也可以猜测是数字。于是请求多组,转为十进制不难发现,

i=suttann2+suttanni = suttann^{2} + suttann

所以很简单,对于自然数 suttann,唯一使 suttann=i 的解既是 0。于是带上 0000000000000 请求,发现返回结果多了一个 seq。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
"t": "1549802094",
"i": "0000000000000",
"m": "4aad0d9ff11812ebdd5e376fdbef6222",
"e": "0f251631cda7e0d6fc2b5a3c75bd07ca",
"seq": "4836331"
}
{ "t": "1549802094", "i": "0000000000000", "m": "4aad0d9ff11812ebdd5e376fdbef6222", "e": "0f251631cda7e0d6fc2b5a3c75bd07ca", "seq": "4836331" }
{
    "t": "1549802094",
    "i": "0000000000000",
    "m": "4aad0d9ff11812ebdd5e376fdbef6222",
    "e": "0f251631cda7e0d6fc2b5a3c75bd07ca",
    "seq": "4836331"
}

多次请求发现,seq 的内容随时间变化。而且仔细看 tag,连起来是 “time seq”,即 “时间序列”。于是按时间顺序列出 seq 的内容:

4836282
4836283
4836286
4836291
4836298
redpacket.kaaass.net/kibounohana/?passwd=
4836318
4836331

……(循环)

其实就是一个很简单的数列找规律,得到下一关链接 redpacket.kaaass.net/kibounohana/?passwd=4836307

有趣的事情

  • 简单红包关,log 有二三十条带 b547de608dd0f2bbd61919a854510263 的请求
  • 原先返回的 i 是固定 13 位显示(长于 13 位就截断),然而由于随机字符串的值都很大,加上十六进制数的提示很隐蔽,所以难度特别高。于是我就改成返回完整数字了
  • stage2 总共收到 2679 次请求,来自 19 个不同 ip 地址。其中,光是 0000000000000 的就有 484 次
  • 当然少不了比如 suttann=2333333333333 这类的请求啦
  • 有位 dalao 劫持了 ajax,强行返回了一个一样的 md5,然后卡关了……???
  • 混淆使用的是 javascript-obfuscator/javascript-obfuscator,强烈推荐

反调试浅析

其实这种反调试是混淆工具 javascript-obfuscator/javascript-obfuscator 自带的功能之一。代码大致如下:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
(function() {
(function a() {
try {
(function b(i) {
if (('' + (i / i)).length !== 1 || i % 20 === 0) {
(function() {}).constructor('debugger')()
} else {
debugger
}
b(++i)
})(0)
} catch (e) {
setTimeout(a, 5000)
}
})()
})();
(function() { (function a() { try { (function b(i) { if (('' + (i / i)).length !== 1 || i % 20 === 0) { (function() {}).constructor('debugger')() } else { debugger } b(++i) })(0) } catch (e) { setTimeout(a, 5000) } })() })();
(function() {
    (function a() {
        try {
            (function b(i) {
                if (('' + (i / i)).length !== 1 || i % 20 === 0) {
                    (function() {}).constructor('debugger')()
                } else {
                    debugger
                }
                b(++i)
            })(0)
        } catch (e) {
            setTimeout(a, 5000)
        }
    })()
})();

主要是利用 debugger 触发断点调试。最简单的方法就是用油猴脚本替换掉 window.setTimeout 函数。

Stage3 – Partial Content

返回内容如下:

这不是 Brainfuck 嘛,随手找个在线编译器运行一下得到:60014489。正好八位!然而领取失败???Naive 了旁友!注意到 HTTP 状态码是 206 Partial Content,但是 Content-Range 却是 bytes 0-176/176。附加 Range 请求头也不会返回更多。其实这是一个提示,Partial Content 指的是你所看到的 Brainfuck 只是内容(Content)的一部分(Partial),选中这段文本就可以发现:

你可能是 Postman 的受害者。左 Postman,右 Notepad++

这 Brainfuck 里还夹带了私货!(还有一个提示就是 Brainfuck 的缩进)有经验的老司机应该看出来了。没错,这一段 Brainfuck 里穿插了 Whitespace,另一个魔性的语言。

但是把这一段东西丢到 Whitespace 编译器,却没有任何的反应。其实这段 Whitespace 还需要一个输入,那就是 Brainfuck 编码的 60014489。输入之后就返回了正确的红包码。至此,就是 2019 新年解谜红包的全部流程啦。

一些数据

算上追加,总共 3 处红包,总共被领取了 4 次。最欧的 33.36/50,最非的 4.21/100,甚至是同一个人。

Stage1 就询问我的人来看,很多人想到二维码拼接之后的处理方式。Stage2 有 19 个不同 IP 请求,Stage3 则是 4 个。Stage2 的大部分请求都是简单红包失效后,所以很可惜错过了那个红包。

隐藏红包

秉承去年的良好传统,今年的解谜红包也是附带隐藏红包的。不过今年的入口依旧魔性。和去年一样,有兴趣的 dalao 可以试着找找,解法隐藏回复可见~

后记

发解谜红包最主要还是想搞个有意思的活动,消磨一下无聊的假期。当然还有一点很重要,就是强调 “深究 => 查文档”,并加入一些实用的技巧。去年的 HTTP 状态码是鼓励查文档,今年的 PNG 文件格式、二维码也同样是鼓励深究这些平常经常接触事物的本质,然后查阅文档。实用的技巧上,去年有虾米音乐的 url 编码、异或运算的性质,今年有流行的混淆库和反调试手段。当然最主要的是希望大家玩的开心,同时祝大家新年快乐~

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片
  3. 没有图片
  4. 没有图片
  5. 没有图片

    2019.02.05

    新年快乐!

评论

  1. Paulzzh 2019.02.10 10:29 下午

    哈哈哈哈哈哈 “同一个人” 来报道了

    • KAAAsS 2019.02.11 1:06 上午

      噗,话说友链嘛

      • Paulzzh 2019.02.17 3:46 下午

        嗯,我已经安排上了。

  2. 豆豆 2019.02.10 10:35 下午

    在第一关就卡关的咸鱼表示无比害怕(捂脸)
    后面的内容能看懂已经很庆幸了 QWQ

  3. 某昨 P 2019.02.11 1:52 上午

    这次红包活动让我明白了
    Win10 自带的图片浏览器是不会校验 IHDR 的 CRC 的
    vscode 的图片浏览器是会校验的(逃

  4. 3DPinball 2019.02.11 2:03 下午

    第一关就卡了

  5. __Ressed__ 2019.02.17 5:44 下午

    咱来翻答案了 2333

  6. 膜拜大佬 2019.08.26 4:22 下午

    出来看神仙

在此评论中不能使用 HTML 标签。