「Hackergame 2021」#4 Writup 持续破防篇 1

< #3

这里接上一篇,Writeup 的有:阵列恢复、马赛克、minecRaft、密码生成器
(其实 minecRaft 应该算“开局上分”篇,但是不好塞了,就放这里了)
剩下的就是做不上的了,看官方 Writeup 了(

阵列恢复大师

(这题整整做了我两天多,每天晚上都对着磁盘阵列……)

以下是两个压缩包,分别是一个 RAID 0 阵列的磁盘压缩包,和一个 RAID 5 阵列的磁盘压缩包,对应本题的两小问。你需要解析得到正确完整的磁盘阵列,挂载第一个分区后在该分区根目录下使用 Python 3.7 或以上版本执行 getflag.py 脚本以获取 flag。磁盘数据保证无损坏。

RAID 5

虽然 RAID 5 是第二问,而且分数高,但是更好做,而且做出的人也多。
因为数据保证无损坏,所以要做的仅仅是找出五个磁盘的顺序和块大小

顺序可以先简单地看看 strings *.img 输出的内容
逐个文件看,可以发现每个文件比较靠前的地方会有一段是 git 历史记录的一部分:

根据里面的时间可以推断出磁盘的顺序大致是:

Qj... -> 60... -> 3R... -> Ir... -> 3D...

只是,这个顺序应该是一个环,谁在第一还没区分出来

在看每个文件的头部,只有 60… 和 3R… 有 “EFI PART”:

所以应该是一个在开头,一个在结尾。所以最终的顺序是:

3R... -> Ir... -> 3D... -> Qj... -> 60...

然后需要找到块大小
直接丢到 DiskGenius 里组建虚拟 RAID,选左同步,然后可以试出来当块大小是 64k 的时候正好可以拼出完整磁盘
然后克隆磁盘生成 img 文件,再挂载,进入,执行 getflag.py 就得到了 flag

RAID 0

在做 RAID 5 的时候还发现了一个叫 Raid Reconstructor 的软件,可以爆破 RAID 阵列顺序和块大小
所以这问也就懒得看了,直接丢给 Raid Reconstructor 来爆破,得到最推荐的顺序:

wl. -> jC. -> 1G. -> 5q. -> d3. -> eR. -> RA. -> ID.

和块大小 128k

然后直接用 Raid Reconstructor 的 Copy 导出 img 文件,提取后又得到一个新的 img 文件
通过 file 可以看到结果的文件系统是 XFS

1
2
$ file MyDisk.img
MyDisk.img: SGI XFS filesystem data (blksz 4096, inosz 512, v2 dirs)

但是始终无法挂载(搞了一天)
可能是 Raid Reconstructor 的问题,所以又用 DiskGenius 试了下
因为 win 和 DiskGenius 读不了 XFS 文件系统,所以拼起来之后直接克隆出 img 文件
然后拖到 Kali Linux 里挂载,成功挂载后进入、运行 getflag.py 就得到了 flag


马赛克

(这道题已经做破防了,本以为是个青铜,结果是个王者……)
我做的肯定不是正解,利用二维码纠错能力勉强拿到了 flag,所以就不详细写 writeup 了,主要还是要看官方 wp(逃

大概步骤就是:

  1. 读图片
  2. 把已知的像素提取出来
  3. 把四个小定位块填上
  4. 挨个马赛克块寻找使还原的数据平均数与原马赛克值差的绝对值小于1的填补方法
    • 如果只有一种就填上,并且标注已经填好,以后不再搜寻
    • 如果有多种就先放下不填
  5. 重复4的过程,这是还会有唯一确定的填补方案。重复4次大概就不剩唯一解了
  6. 这时重复4,找出仅有2中填补方法的,选误差最小的填上
  7. 然后再重复4
  8. 然后重复6
  9. 然后重复4
  10. 这时可以看到已经还原得差不多了,剩下的不管直接扫码也可以扫出 flag 了

看,做法很烂对吧


minecRaft

kk 同学很喜欢玩 Minecraft,他最近收到了一张 MC 地图,地图里面有三盏灯,还有很多奇奇怪怪的压力板。

但他发现这些灯好像不太符合 MC 电磁学(Red stone),你能帮他把灯全部点亮吗?

注:本题解法与原版 Minecraft 游戏无关。

补充说明:flag 花括号内为让三盏灯全部点亮的最短的输入序列。例如,如果踩踏压力板输入的最短的序列为 abc,则答案为 flag{abc}。

还挺好玩的题,在网页中模拟了一个mc出来
看源码看到了引入了 flag.js 文件,所以可能就是要通过它来得到答案:

1
<script src="jsm/miscs/flag.js"></script>

也可以看到,最终判断是否正确是通过调用 gyflagh(input) 是否为 true 来判断,而 gyflagh 也在 flag.js 中,所以还是要看 flag.js

但是 flag.js 是经过简单混淆过的,还是要费点时间读一下

其中有四个转换 Str4 Base16 和 Long 的函数可以略掉不管
注意到了 _0x381b() 这个函数里有一个列表,而且比较简单,其实它返回的就是这个列表

1
['encrypt', '33MGcQht', '6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c', '14021KbbewD', 'charCodeAt', '808heYYJt', '5DlyrGX', '552oZzIQH', 'fromCharCode', '356IjESGA', '784713mdLTBv', '2529060PvKScd', '805548mjjthm', '844848vFCypf', '4bIkkcJ', '1356853149054377', 'length', 'slice', '1720848ZSQDkr']

但是在 Console 里面调用 _0x381b 得到的却是以 ‘slice’ 开头、’length’ 结尾的列表,将这个列表记为 lst 方便表述
而且源码中只有最开头的调用匿名函数里面有 ['push']['shift'],所以推测这个匿名函数就是将这个列表循环右移两个位置
那这个匿名函数也不用看了

再来看 _0x2c9e() 这个函数:

1
2
3
4
5
6
7
8
9
10
function _0x2c9e(_0x49e6ff, _0x310d40) {
const _0x381b4c = _0x381b();
return _0x2c9e = function(_0x2c9ec6, _0x2ec3bd) {
_0x2c9ec6 = _0x2c9ec6 - 0x1a6;
let _0x4769df = _0x381b4c[_0x2c9ec6];
return _0x4769df;
}
,
_0x2c9e(_0x49e6ff, _0x310d40);
}

其中 _0x381b4c 是刚刚说的那个列表 lst。然后 return 里面重新定义了 _0x2c9e,但是新的定义里第二个参数并没有用,然后调用返回,所以整个函数就相当于:

1
2
3
4
function _0x2c9e(_0x2c9ec6, ...) {
_0x2c9ec6 = _0x2c9ec6 - 0x1a6;
return lst[_0x2c9ec6];
}

0x1a6 是 422,所以整个函数也就相当于 function(x) { return lst[x - 422]; }
同时根据第一行,程序中所有 _0x22517d 也是这个函数

然后看判断答案的 gyflagh 函数

1
2
3
4
5
6
7
function gyflagh(_0x111955) {
const _0x50051f = _0x22517d;
let _0x3b790d = _0x111955[_0x50051f(0x1a8)](_0x50051f(0x1b7));
if (_0x3b790d === _0x50051f(0x1aa))
return !![];
return ![];
}

没啥特别的,结合 lst 可以得到:

1
2
3
4
5
6
function gyflagh(ans) {
if (ans["encrypt"]("1356853149054377") === "6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c") {
return true;
}
return false;
}

然后就可以结合 lst 中的值和索引,翻译出最重要的函数
再进行一些运算,用注释标注一下已知的值就可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String["prototype"]["encrypt"] = function(key) { // key = "1356853149054377"
const left = new Array(2);
const right = new Array(4);
let res = "";
ans = escape(this); // this := ans
right = [909456177, 825439544, 892352820, 926364468]
for (var i = 0; i < ans["length"]; i = i + 8) {
left[0] = Str4ToLong(ans["slice"](i, i + 4));
left[1] = Str4ToLong(ans["slice"](i + 4, i + 8));
code(left, right);
res = res + (LongToBase16(left[0]) + LongToBase16(left[1]));
}
return res; // 6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c
};

再来看 code 函数,根据 << 4、 ^、 >>> 5 可以大胆推测类似 TEA,然后解码就直接翻过来就好了:

1
2
3
4
5
6
7
8
function dec(left, right) {
for (var i = 2654435769 * 32; i != 0;) {
left[1] -= ((left[0] << 4 ^ left[0] >>> 5) + left[0] ^ i + right[i >>> 11 & 3]);
i -= 2654435769;
left[0] -= ((left[1] << 4 ^ left[1] >>> 5) + left[1] ^ i + right[i & 3]);
}
console.log(left);
}

最后把要得到的 res 分块,每 8 个一组:

1
6fbde674 819a59bf a1209256 5b4ca2a7 a11dc670 c678681d af4afb67 04b82f0c

然后从后往前,每两个执行 Base16ToLong,然后作为 left 传给 dec 函数解码,然后再 LongToStr4 得到四个字符:

拼起来就是 flag 了:flag{McWebRE_inMlnCrA1t_3a5y_1cIuop9i}


密码生成器

小 T 最近又写了一个 web 应用。

他发现很多用户都喜欢设置弱密码,于是决定让所有用户的密码都必须是 16 位长,并且各种符号都要有。为了让大家可以方便生成这样的密码,他还写了一个密码生成器,让用户可以生成符合规则的密码。

但这样果真安全吗?

(感觉这次 Hackergame 题的 tag 都很诡异。这题明明是 misc(general) 为什么打了 binary 的 tag)

看到 binary tag 直接先往 IDA 里面丢,然后报错了,大概是因为部分 winapi 导入不进去的问题(?)
然后就不会了…… 对着 IDA 干瞪眼

看题,题目给了一个网站,用来发布展板,看起来只有 admin 用户,而且没有注册系统,所以应该就是要搞到 admin 的密码了
再细看网站,特意提到 “网页显示时间”,而且展板后面都有发布时间,点进 admin 的用户页面发现也有注册时间,着实有些许诡异(
所以时间应该是一个提示

而写代码的时候设置随机数种子又常以当前时间作为种子,所以生成的密码可能是和时间有关系的
通过调系统时间,可以发现在同一秒点下生成,产生的密码是一样的
所以只需要把系统时间调到 admin 的注册时间左右,然后每秒生成密码,再挨个输进去爆破即可

最后得到 2021-09-22 23:10:53 时生成的密码 $Z=CBDL7TjHu~mEX 就是 admin 的密码
然后登录即可在“我的”里看到一条私密展板,内容是 flag

(其实这题搞得闹心的是每秒生成密码,像我这样的原始人只会反复调时间然后手动生成、复制粘贴,然后再复制粘贴检验密码)
(而且其实这个时间也试了很长时间,试了 23:11 的所有秒,15:11 的所有秒(考虑到了提到的时区问题))
(然后一共 120s 里也没有正确密码,就很闹心,最后的时间是 23:11 的前一分钟里的……我当时甚至想了,这些操作在一分钟之内都能完成,然后就没考虑前一分钟生成密码、后一分钟注册的问题……)
(于是就有了:


好了,我做上的题也就这些了,勉勉强强混了 4k2pt
没做上的题也好多:Amnesia2、赛博厨房23、灯、只读、一石二鸟、GPA、链上预言家、助记词2、Co-Program、外星人、befun、fzuu、wish、OI逆向
(草,好多qwq)
剩下的就看官方 Writeup 了(

Reference

  • RAID 相关的好多文章,没留作记录

「Hackergame 2021」#4 Writup 持续破防篇 1

https://blog.tonycrane.cc/p/d11ec8ed.html

作者

TonyCrane

发布于

2021-10-29

更新于

2021-10-30

许可协议

评论