USTC Hackergame 2022 个人题解

USTC Hackergame 2022 个人题解

panedioic
2022-10-29 / 0 评论 / 20 阅读 / 正在检测是否收录...

前言

算下来,今年已经是我第四次参加 hackergame 了,前几次参加做出来的题都不多,这次总算是勉强上榜了。所以来写个题解,混个脸熟(
另:这次上榜后感谢各位大佬没有把我挤下去。能够上榜,填列其中,倍感荣幸()
2210301234

题解(做出来的)

签到

解法一

众所周知,签到题是一道手速题。
只要我的手速够快,就可以在cpu反映过过来之前,在屏幕上写下 2022 四个数字,然后提交得到 flag。

解法二

然而很遗憾,我并没有那样的手速。
不过没关系,我们在点击提交后可以看见我们写下的数字是通过 get 传参的,
image
因此,只要把 result=2022 加在后面提交就可以获得 flag 了。

http://202.38.93.111:12022/?result=2022

猫咪问答喵

Q1. 中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?

搜索 NEBULA 战队(USTC NEBULA) 可以找到这么一个网站:
image-1667037158735

点进去就可以看见成立日期为 2017-03
image-1667037189629

Q2. 2022 年 9 月,中国科学技术大学学生 Linux 用户协会(LUG @ USTC)在科大校内承办了软件自由日活动。除了专注于自由撸猫的主会场之外,还有一些和技术相关的分会场(如闪电演讲 Lightning Talk)。其中在第一个闪电演讲主题里,主讲人于 slides 中展示了一张在 GNOME Wayland 下使用 Wayland 后端会出现显示问题的 KDE 程序截图,请问这个 KDE 程序的名字是什么?

搜索 Linux 用户协会(LUG @ USTC) 可以找到 自由软件日的介绍页面 ,继续浏览就可以看到题中 闪电演讲的 slides

根据第15张ppt配图中的 Configure Kdenlive 字样可以得出本体的答案就是 Kdenlive
image-1667047466326

Q3. 22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?

提示:格式为 2 位数字的整数。

搜索词条 forefox win2000 可以找到这样一篇文章:
Firefox向Windows 2000, Windows XP RTM和SP1说再见 -36氪

点进去就可以看到:

Dotzler表示,今年6月5日的Firefox 12发布日期是Firefox支持这些老旧系统的最后期限。如果用户还不升级他们的系统,Opera将是Firefox的最佳替代方案。

可以得知本题的答案是 12

Q4. 你知道 PwnKit(CVE-2021-4034)喵?据可靠谣传,出题组的某位同学本来想出这样一道类似的题,但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵!那么,请找出在 Linux 内核 master 分支(torvalds/linux.git)下,首个变动此行为的 commit 的 hash 吧喵!

提示:格式为 40 个字符长的 commit 的 SHA1 哈希值,字母小写,注意不是 merge commit。

搜索关键词 linux kernel,可以找到 Linux 内核的 git 页面。之后在右上角的 log 搜索关键词 argc ,在得出的结果中进一步 ctrl+f 搜索 arg ,可以看到这样一条 commit:
exec: Force single empty string when argv is empty
image-1667048165271
看名字就知道可能是符合题意的答案。因此本题的答案就是 dcd46d897adb70d63e025f175a00a89797d31a43

Q6. 中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的?

搜索关键词 中科大 网络通 可以找到 中国科学技术大学校园网用户服务页面

进一步点开 常见问题列表 后,可以看到:
image-1667048685699

但提交后发现 2011年1月1日并不是本题的答案,因此推测在更早就已经是这个价格了。在 使用 Web Archive 无果后,直接进入 中科大网络信息中心的新闻公告页面。终于在第18页翻到了 关于实行新的网络费用分担办法的通知 这篇文章。
点进去后看到:

各单位:
    为充分发挥网络资源在教学、科研和管理工作中的作用,根据中国科学院、中国教育和科研计算机网有关网络费用分担的原则和办法,参照学校与有关通信公司签订的协议和兄弟院校的做法,结合我校具体情况,决定自2011年1月1日起实行新的网络运行及通信费用分担办法。总的原则是降低费率、鼓励使用、合理分担、促进发展。同时网字〔2003〕1号《关于实行新的网络费用分担办法的通知》终止实行。
    特此通知。

然后再搜索 〔2003〕1号《关于实行新的网络费用分担办法的通知》 ,找到 这个页面
image-1667049083102

由此得到本题的答案是 2003-01-01

### Q5. 通过监视猫咪在键盘上看似乱踩的故意行为,不出所料发现其秘密连上了一个 ssh 服务器,终端显示 ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce.,你知道猫咪在连接什么域名吗?
提示:填写形如 example.com 的二级域名,答案中不同的字母有 6 个。

这题不会了QAQ。看了下题解的确是自己的搜索姿势不对没能想到还有用公钥反查服务器的网站了。
自己甚至为此写了个脚本把所有的开着22端口的3个字母的域名都扫了一遍,可惜脚本出了bug,导致没能扫到本题的答案,因此这道题就只拿了一半的分。

家目录里的秘密

第一问

首先,把题中的压缩包下下来解压后,直接全文搜索 flag ,就可以找到 user/.config/Code/User/History/2f23f721/DUGV.c 这个文件。打开后就可以看到文件的开头:

//
// ramdisk that uses the disk image loaded by qemu -initrd fs.img
//

// flag{finding_everything_through_vscode_config_file_932rjdakd}

从而拿到flag。

第二问

第二问,既然题目提及了是 Rclone 相关,那就找与 Rclone 相关的文件就行了。然后把目录翻了一遍,发现只有 user/.config/rclone/rclone.conf 这个文件比较可以,打开看看:

[flag2]
type = ftp
host = ftp.example.com
user = user
pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ

flag 的字样都贴到脸上了捏。
ftp.example.com 显然是没什么用了,而 user 这个用户名我犹豫了一会,最后发现也没什么用。所以答案应该就在 pass 里了。

自己装了个Rclone,然后添加了个 ftp 服务器,发现 pass 就是对输入密码的加密。然后搜索关键字 rclone password retrive 可以找到 Is it possible to retrieve my Crypt passwords from the rclone.conf file? 这个网页。根据下面回答的提示,可以找到 rclone 密码的加密解密的算法

我的本地虽然没有 go 的运行环境,不过只要随便找个 在线 go 语言运行环境 就可以了。

简单修改一下原来的源码,再运行,就可以得到flag了。

package main

import (
    "fmt"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
)

func main() {
    fmt.Println(Reveal("tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ"))
}

var (
    cryptKey = []byte{
        0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
        0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
        0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
        0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
    }
    cryptBlock cipher.Block
    cryptRand  = rand.Reader
)

func crypt(out, in, iv []byte) error {
    if cryptBlock == nil {
        var err error
        cryptBlock, err = aes.NewCipher(cryptKey)
        if err != nil {
            return err
        }
    }
    stream := cipher.NewCTR(cryptBlock, iv)
    stream.XORKeyStream(out, in)
    return nil
}

func Reveal(x string) (string) {
    ciphertext, err := base64.RawURLEncoding.DecodeString(x)
    buf := ciphertext[aes.BlockSize:]
    iv := ciphertext[:aes.BlockSize]
    crypt(buf, buf, iv)
    _ = err
    return string(buf)
}

HeiLang

解法一

直接使用 来自 Heicore 社区的新一代编程语言 HeiLang 的解释器运行源码,得到 flag。

解法二

可以我没有 来自 Heicore 社区的新一代编程语言 HeiLang,所以得换其他的方法了。
于是,我们对原来的脚本进行一些修改。

首先,我们把中间的数组操作部分全部转换成字符串:

a = [0] * 10000
b = '''
a[1225 | 2381 | 2956 | 3380 | 3441 | 4073 | 4090 | 4439 | 5883 | 6253 | 7683 | 8231 | 9933] = 978
......
a[92 | 377 | 384 | 493 | 1237 | 2479 | 4299 | 6702 | 6819 | 7761 | 7822 | 8777 | 8779] = 581
'''

之后,再把 main 部分改成:

import re
def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)]
if __name__ == "__main__":
    c = b.strip().split('\n')
    for line in c:
        numlist = s2n(line)
        val = numlist[-1]
        numlist.pop()
        for pos in numlist:
            a[pos] = val
    get_flag(a)

运行,即可得到 flag。

Xcaptcha

解法一

找一个真正的 robot ,为我们完成验证。

解法二

然而可惜我并没有这么一个机器人朋友。不管了,作为突击队中唯一的黑客,全村人民最后的希望,开冲!

首先点开 f12,阻塞住自动请求,观察一下页面:
image-1667054083580

发现题目都在页面中的 3 个 form-group 里。因此,我们可以编写下面的脚本:

let a = document.getElementsByClassName("form-group");
let b = Array.prototype.slice.call(a);
b.forEach((c)=>{
    d = c.innerHTML.match(/(\d*\+\d*)/)[0].split('+');
    d[0] += 'n';
    d[1] += 'n';
    e = eval(d[0] + '+' + d[1]).toString();
    c.childNodes[3].value = e;
});
document.getElementById('submit').click();

之后点开 f12 的 console,点击验证,再以顺雷不及掩耳盗铃之势,把上面的脚本粘贴在 f12 内,并按下回车,得到 flag。
image-1667054414880

旅行照片 2.0

照片分析

随便百度一个 EXIF 查看工具,把照片粘贴进去,就能得到所有需要的答案。

社工实践

在照片中能够看见一个圆形的标志建筑物。继续放大,能依稀看见 zozostdium 字样。
image-1667054673794
bing搜索这两个关键词,就可以知道,这是日本千叶县的一个体育管。
image-1667054761236

这里有一个小坑,当时我以为拍照的地方离这个体育馆应该不远,所以邮政编码应该也一样,但实际上他们的邮政编码是不一样的,还得要去查。(

邮编

从照片可以看出,作者是在一个高处去俯拍这招照片的,所以猜测作者应该在这附近的一个酒店内。继续查找附近的酒店,可以看见附近有这些酒店:
image-1667055548600

进一步排除,可以知道应该是在这个酒店,继而得到邮编。
image-1667055587302

手机分辨率

重新查看这张照片的 EXIF 信息,可以得知拍摄手机的 soc 为 sm6115,搜索这个 soc,可以得到他是 高通骁龙662,这个 soc。然后继续搜索,可以搜到 红米 Note9 4G 版,骁龙662,不打游戏,只社交看视频刷短视频看新闻日常使用,能流畅用3年吗?

根据下面答主的附图,可以看到这个手机和照片中反射出的手机长得非常像,可以确定应该大概就是这个型号了。
再去 搜索这个型号的手机,就可以得到这个手机的屏幕分辨率了。

航班信息

根据前面的分析,我们能知道照片中的航班飞行的时间和地点。根据这些信息,我们就能在 FR24 这么个网站中查询某时某刻在某地飞行的航班的信息了。

在白嫖了个 FR24 的账号后,我们就可以开始查询了。

需要注意的是,照片中 EXIF 中的时间是日本的当地时间,而 FR24 中的时间线是 UTC0 的时间。日本的时差是+9,因此把照片里的时间减去9个小时,就得到了在 FR24 中的时间。

而对于位置,从照片中我们能看出,在拍照的时候,作者所在的酒店,照片中的体育馆和飞机大体是在一条直线上的。结合照片中的方向,我们就能知道要找到航班大概是在地图中体育馆正西方位往正北飞行的航班。

根据这些信息,我们就能筛选出这题的答案了。
image-1667056481246

小彩蛋

原本这道题我虽然有了大概的思路,但航班信息我即不知道要去哪里收也不想花钱买数据库。直到我看见了。。。
image-1667056735563
感谢这位大佬的提示,我也是看了他的id后打起鼓起去 FR24 注册了账号然后解出了这道题。

另:各位读者若有跟我一样解法的也别忘了取消下订阅,一个月几美元也还是挺贵的。

猜数字

解法一

题目要求猜一个0到1的数,精确到6位小数。因此我们只需要随便输一个数字。平均下来只需要猜1000000次就可以一次猜中啦!

解法二

但很遗憾,像我这种抽卡都抽了7,80抽还要大保底才能出货的人,解法一实在是不适合我。看来还是得另寻他法。

查看题目的源码,可以看到题目中判断有没有输对并返回flag的地方是在这里:

         // </talented>
         if (this.previous.isPresent()) {
            // <guess>
            var previous = this.previous.getAsDouble();

            var isLess = previous < this.number - 1e-6 / 2;
            var isMore = previous > this.number + 1e-6 / 2;

            writer.writeStartElement("guess");
            writer.writeAttribute("less", Boolean.toString(isLess));
            writer.writeAttribute("more", Boolean.toString(isMore));
            writer.writeCharacters(Double.toString(previous));
            writer.writeEndElement();
            // </guess>
         }
         if (this.talented > 0) {
            // <flag>
            writer.writeStartElement("flag");
            writer.writeCharacters(this.token.flag());
            writer.writeEndElement();
            // </flag>
         }
         writer.writeEndElement();
         // </state>

可以看到源码的基本逻辑是判断答案是否比下限小和答案是否比上限大。如果两个都是 False,则返回 flag。

然后这题的解题点就在于,并非我们只有填入比下限大的数才能得到 False。在不少语言中,如果在比较的时候传入了两个不同类型的参数,也会返回一个和 False 差不多的东西。因此,我们只需要随便给他传个不是数字的东西,他就能得到 False,然后得到答案。
但是网页中的输入框我们只能输入数字,因此就需要做一些修改了。

点开 F12,可以看到网页的 DOM:
image-1667057593127

我们把第一个 input 框的 type 改为 type="text,然后把她的 oninput 删掉。再把第二个 input 框的 disable 删去。

再之后,我们只需要脸滚键盘,然后点提交,就可以得到 flag 了。
image-1667057779086

LaTeX 机器人

纯文本

下载题目中的附件,可以知道这道题是用 TexLive 这个工具进行 latex 的渲染的。虽然我并没有使用过 texlive,不过 latex 的语法我还是懂一些的。

首先,题目给出了 flag 文件的储存位置,因此猜测 latex 语法中应该存在类似读取文件的函数的东西。在进行一番搜索后,果然发现 latex 里有 \input{} 可以读取本地文件。因此可以构造以下的 payload:

\input{/flag1}

得到 flag。

当然,要是觉得花体字不得看的话,也可以使用下面的 payload,可以看的更清楚一些。

$$\input{/flag1}$$

只要记得提交 flag 的时候别忘了加上 {} 就行了。

特殊字符混入

第二问和第一问基本差不多,不过不同的是第二问的 flag 中有一些特殊字符。而 latex 中的 \input{} 宏是类似于 c 语言中的 #define,做的是类似于字符串替换的工作,因此遇到 _# 这样的特殊字符就会报错。

在 latex 中, # 这个字符的作用是在宏定义中作为形参使用。所以我最开始的想法就是自己定义一个函数,然后拿flag中的 # 和后面的字符当作形参,然后传入不同的参数,观察字符串替换的情况来反推flag。
不过很可惜的是,在 latex 中定义函数时最多只能有9个形参,以在 # 后加数字 1-9 来表示,而本题中的 flag 显然是不满足这个要求的,因此只能换一种方法。

继续搜索,我发现了 \@ifnextchar 这个宏。他能够在使用的时候对后面以恶搞字符进行判断。根据后面字符的不同,可以输出不同的内容。因此想到可以用这种办法把 flag 中的 # 换成其他的内容,然后再来确定flag。
不过很遗憾的是,这种方法也是不能奏效的。在 latex 中, # 是一个特殊的字符,是不可以直接用作参数的。除非在使用的时候在前面加上 \。但如果这样的话,在 flag 中也比如遇到 \# 这样的形式才能进行替换。因此这种方法也不可行。

不过虽然这种方法也不可行。不过在观察 \@ifnextchar 的使用的时候可以发现,在 latex 中 @ 也是一个特殊的字符,是不可以用作宏的名称的。而在我们定义的时候,会在前面加上一句 \makeatother。这个宏可以临时的把 @ 这个字符转换成一个普通的字符,从而可以被定义为宏的名称。
那么继续思考,我们能否把题目中的 _# 也转换成普通的字符从而使他们可以被输出呢?

答案是肯定的。
通过进一步的搜索,我们能在 wiki 中找到 关于 catcode 的介绍。里面介绍了 latex 中各种字符的 catcode 和他们的作用。并且给出了改变 @ 这个字符的 catcode 的示例:

\catcode`\@=11

事实上,前面的 \makeatother 也是和这句话等价的。因此我们就可以通过类似的方法,改变 _# 的 catcode,从而使他们能够被输出:

\catcode`\_=11 \catcode`\#=11 \input{/flag2}

点击提交,就可以看到flag了。

Flag 的痕迹

这道题,观察题目的描述,就可以知道应该是要我们去寻找 dokuwiki 的漏洞了。

简单尝试一下,就可以看见题目关闭了 reversion 的功能。继续尝试也想不出来解法后,我就自己也搭了一个 dokuwiki ,试试能不能复现一下。

image-1667094835602

在我自己搭建的 dokuwiki 中,能看见 reversion 功能是能够正常使用的。不过如果直接把后面的 get 的参数传给题目中的话,还是会报错。

不过继续观察,我们能看见 dokuwiki 还有比较两个版本的差异的功能。点进去后还能在右上角看见 Link to this comparison view 这样的字样。
这样的话提示就比较明显了。我们把链接复制下来:

https://xxx/doku/dokuwiki/doku.php?id=start&do=diff&rev2%5B0%5D=1666694430&rev2%5B1%5D=1666694440&difftype=sidebyside

可以看到逻辑还是比较明显的。id 是页面的名称,do 就是我们的比较差异功能,difftype 就是把两个版本列到两边,进行比较,而两个 rev 自然就是要比较的两个版本的发布时间了。

链接要求我们输入的时间是一个精确到秒的 unix 时间戳,而在题目页面右下角只要一个精确到分钟的修改时间。我原本还以为这里得暴力遍历一下所有的可能,不过后来发现想多了, rev 的地方随便传参都可以的。

image-1667095310156

即使我传了一个不存在的参数,他也只是没有正文,我们还是可以通过左上角选择对应的条目,获取 flag。
image-1667095364932

安全的在线测评

无法 AC 的题目

解法一

虽然题目有说

当然,因为目前 OJ 只能运行 C 语言代码,即使请来一位少年班学院的天才恐怕也无济于事。

但是这就能难得住我们吗?
我们可以直接找到这样一个少年班学院的天才,通过意识上传,把他的大脑思维用 c 语言写出来,然后上传,就可以让他帮我解题了(

解法二

咳咳咳,

通过观察题目给出的源码我们可以发现,这道题的判题逻辑就是接收我们给出的 c 语言代码,然后执行,在通过和本地数据的对比,判断是否正确。而全程中并没有对我们的输入做任何限制。
因此我们只需要上传一个能读取文件的程序把标准答案读取并输出就行了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

int main() {
    const char* filename = "./data/static.out";
    FILE* input_file = fopen(filename, "r");
    
    struct stat sb;
    stat(filename, &sb);
    char* file_contents = malloc(sb.st_size);
    
    fread(file_contents, sb.st_size, 1, input_file);
    printf("%s\n", file_contents);
    fclose(input_file);
    free(file_contents);
    
    return 0;
}

粘贴并回车 即可拿到 flag1。

线路板

这道题,下载下来题目中的附件后,可以看到,,,嗯,又是个我不懂的文件格式呢(

(于是我甚至为此下了个 altium designer 来作题,但做到最后发现根本不需要)

我们可以继续观察压缩包里的文件。其中有个很大的 ebaz_sdr-F_Cu.gbr,通过观察,可以确定我们要找到 flag 应该就在这个文件里了。

然后就是,直接用记事本打开这个文件,就可以看到,这个文件格式其实就是用一些文字来描述最后的形状的文件。有点类似矢量图的感觉。
因此,我们就可以一行一行的把里面的字全删掉。

直到我把他删的只剩400行左右的时候,终于能在 al 里看见 flag 了。
image-1667131388761

Flag 自动机

然后是这么一道逆向题。打开题目的附件,会发现鼠标一挪到 狠心夺取 键上时,按钮就会乱跑。无奈,我只能拿出我尘封已久的 ida,重新去学一下这玩意要怎么用了。

不过好在,这道题对逆向知识的要求并不高。打开 IDA,打开刚才的文件,然后按 F5 反编译,就可以看到 IDA 已经把这个文件的逻辑比较清楚的展现出来了。
image-1667132115907

进一步查看就可以看到 pfnSubclass 这个非常可疑的函数了。点开一看就很清楚了。虽然我并不了解 Win32 API,但这个代码肯定都能看出来就是当鼠标滑过按钮的时候自动重设按钮的位置的逻辑了。而那个 512 应该就是代指着鼠标滑过按钮的事件了。因此,直接把他修改为其他的值:
image-1667132437188
512 的16进制为 0x200。因此,虽然还不太清楚这里是大端排序还是小端排序,但把这个 20 改掉总是没问题的。我们直接把他改为 0xff,然后Patch 掉。

不过即使如此,我再打开文件之后,虽然这次按钮不会乱跑了,他还是报错:
image-1667132624006

怎会如此!难道这还需要我的管理员权限吗?
我国在我之后多次尝试用管理员权限打开或者修改文件的权限后,我发现我可能还是想多了。接着看代码吧。

还是回到之前的反汇编界面,这次我们重点看 WndClass.lpfnWndProc = sub_401510; 的这个函数。
image-1667132772808
然后可以看到,这 flag 的字符串都出现了,所以不出意外的话一ing该就是这里了。
之后看到第19行,他会要求 lParam 这个参数等于 114514,否则就不会给我们 flag。然而这个程序如果正常运行,怎么看也穿不进来这么臭的参数。因此决定把这个地方也 patch 掉。

这里回到汇编界面,可以看到下图绿色的部分就是刚才的判断那里的逻辑了。这里为了方便,我就直接把 jz 改成了 jnz,就相当于把 if 改成了 if not。
image-1667132987257

然后再 Patch,运行!
image-1667133102798
就可以拿到 flag 了。

微积分计算小练习

这道题,初看是一道微积分的计算题。不过 当我把微积分题目都做完后,才发现事实并没有那么简单。
做题,提交后:
image-1667096738001
(此处应有掀桌表情包)我的 Flag 呢?!

只能去翻源码了。看完源码后才发现题目根本没想把 flag 输出给我。
不过,继续看源码,就会发现这样一段代码:

print(' Putting secret flag...')
driver.execute_script(f'document.cookie="flag={FLAG}"')
time.sleep(1)

这样的代码就显得很做作啊,没事把 flag 往 cookie 里放干什么呢?这不明摆着是要我们 XSS 嘛。因此,可以构造这样一个名称:

<script>setTimeout(()=>{document.querySelector('#score').textContent+=document.cookie;},1000);</script>

为了防止网页没加载完成,我还特意让他等了1秒再执行。

不过,执行之后会发现,和之前相比,并没有什么变化。
这是因为,现在的浏览器一般都有一些防 XSS 攻击的手段。在成绩页面中,是这么展示用户的名称和成绩的:

const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const result = urlParams.get('result');
const b64decode = atob(result);
const colon = b64decode.indexOf(":");
const score = b64decode.substring(0, colon);
const username = b64decode.substring(colon + 1);

document.querySelector("#greeting").innerHTML = "您好," + username + "!";
document.querySelector("#score").innerHTML = "您在练习中获得的分数为 <b>" + score + "</b>/100。";

而在浏览器中,通过修改 innerHTML 加载的 js 代码是不会被执行的。
不过,继续查阅资料,得知我们还有其他的方法可以达成同样的效果。比如使用 img 标签的 onerror 方法:

<img src="x" onerror="setTimeout(()=>{document.querySelector('#score').textContent+=document.cookie;},1000);">

提交后得到查询成绩的 URL,然后再进行提交,就可以拿到 flag 了。
image-1667097480437

蒙特卡罗轮盘赌

这道题其实和上面的猜数字差不多。因此解法也差不多,都得从源码上入手。当然,要是你是一个欧皇,直接上去硬猜也不是不可以()

srand((unsigned)time(0) + clock());

通过观察源码可以发现,题目中的随机数就来自于这个种子的设定了。只要知道种子,后面各个问题的答案就都可以被反推出来。
而代码中的 time(0) 是一个获取当前的 unix 时间戳的函数。而后面的 clock() 则是一个返回当前的CPU周期数的一个函数,这个就不太好猜了。不过没关系,我们直接打印一下试试看:

printf("%d\n", clock());
srand((unsigned)time(0) + clock());
printf("%d\n", clock());

虽然代码的编译过程可能会遇到一点小报错,但这并不影响我们发现,毕竟这个 clock() 的执行比较早,所以他也就是个1000不到的数量级。在这个数量级下,想要穷举也不是什么特别难的事情:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

double rand01()
{
    return (double)rand() / RAND_MAX;
}

int main()
{
    // disable buffering
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    srand((unsigned)time(0) + clock());
    int nowtime = time(0);
    for(int i = 0; i < 1000; i += 1){
        srand(nowtime+i);
        printf("cnt: %d. ", nowtime+i);
        for (int j = 0; j < 4; j += 1) {
            int M = 0;
            int N = 400000;
            for (int k = 0; k < N; k++) {
                double x = rand01();
                double y = rand01();
                if (x*x + y*y < 1) M++;
            }
            double pi = (double)M / N * 4;
            printf("%1.5f ", pi);
        }
        printf("\n");
    }
    return 0;
}

用这种方法穷举出来一定范围内所有随机数种子对应的答案。然后进入题目,第一次猜测随便输入答案即可。在打错并知道第一次的正确答案后,就可以在刚才的程序中寻找第一次的答案是刚才的输出的那行,然后再依次输入后面的答案,就可以拿到 flag 了。

惜字如金

惜字如金化标准

这道题还是,先根据题目要求把对应的脚本下载下来。
在经过简单的 debug 改掉不能正常运行的函数之后,就可以看出题目的意图了。观察可以发现这道题最主要的地方就是下面这几句了:

# import secret
secret = b'ustc.edu.cn'
check_equals(len(secret), 39)
# check secret hash
secret_sha384 = 'ec18f9dbc4aba825c7d4f9c726db1cb0d0babf47f' +\
                'a170f33d53bc62074271866a4e4d1325dc27f644fdad'
check_equals(sha384(secret).hexdigest(), secret_sha384)
# generat th signatur
return digest(secret, f.read(), sha384)

从上面的代码片段不难看出,我们需要找到一个 secret 使他的 hash 的结果正好是下面给出的结果。
然而无论是代码中的 secret,还是 secret_sha384,都是经过惜字如金化的,所以不能直接使用。我们得想办法把他们还原。

对于 secret,由题目中的题意可以得知:

  • 第一原则(又称 creat 原则):如单词最后一个字母为「e」或「E」,且该字母的上一个字母为辅音字母,则该字母予以删除。
  • 第二原则(又称 referer 原则):如单词中存在一串全部由完全相同(忽略大小写)的辅音字母组成的子串,则该子串仅保留第一个字母。

因此,要想穷举可能的原字符串,也可以:

    1. 对于每个元音字母和非字母字符,其后面不可以添加新的字符
    1. 对于每个辅音字母,其后面可以添加任意数量个自己以及其中一个元音字母或什么都不加。

根据这两条规则,就可以写出在题目中给出的只剩下11个字符的 secret 的每个位置的所有可能增加字符的可能性:

secret = ['u','s','t','c','.','e','d','u','.','c','n']
    fu = ['', 'a', 'e', 'i', 'o', 'u']
    p = [
        [], # u
        [['s'] * i for i in range(29)], # s
        [['t'] * i for i in range(29)], # t
        [(['c'] * i) + [fu[j]] for i in range(29) for j in range(6)], # aeiou
        [], #  .
        [], # e
        [['d'] * i for i in range(29)], # d
        [], # u
        [], # .
        [['c'] * i for i in range(29)], # c
        [(['n'] * i) + [fu[j]] for i in range(29) for j in range(6)], # aeiou
    ]

然后就可以进行遍历:

for i1 in p[1]:
        for i2 in p[2]:
            for i3 in p[3]:
                for i6 in p[6]:
                    for i10 in p[10]:
                        # for i10 in p[10]
                        i9len = 39 - len(''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + ['n'] + i10))
                        if(i9len < 0):
                            continue
                        # do something

对于下面的 do something ,自然就是求出 hash 并进行比较了。
不过,题目中给出的 hash 显然也是经过过惜字如金化的,而他可能的原始情况就太多了,难以一一遍历。
不过,我们其实也没必要对他进行遍历。通过简单的计算,不难得出,上面我们遍历的所有 secret 的可能,也就 一亿种左右。而对于 hash 值,我们只需要让每种求出的结果的 hash 与目标 hash 的前6位相等就可以了。两个字符串 hash 前6位相等的可能性是 2 的 24 次方分之一,这么算下来也就只有几十种最后的可能性了。所以我们险些代码进行一次初筛:

maylist = []
    ph = ['ec18f9', 'ec18ff', 'ec18fa', 'ec18fe', 'ecc18f', 'eca18f', 'ece18f', 'eccc18', 'ecca18', 'ecce18', 'ecccc1', 'eccca1', 'eccce1', 'eccccc', 'ecccca', 'ecccce']
    for i1 in p[1]:
        for i2 in p[2]:
            for i3 in p[3]:
                for i6 in p[6]:
                    for i10 in p[10]:
                        i9len = 39 - len(''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + ['n'] + i10))
                        if(i9len < 0):
                            continue
                        i9 = p[9][i9len]
                        s = ''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + i9 + ['n'] + i10)
                        hash = sha384(s.encode()).hexdigest()[0:6]
                        if(hash in ph):
                            maylist.append(s)

而计算的结果其实只有6个secret是满足要求的。把他们的 hash 都计算出来,用眼睛比较下,就不难找到最后的结果了。

最后,修改下题目的代码,把 secret 改成我们刚求出的 secret,然后根据提示的方法,对题目中的 3 个文件进行签名,就可以拿到 flag 了。

小彩蛋(?)

在我提交签名后,网页给了我这么一个回复,不知道是不是正常的(
image-1667099411920

置换魔群

U1S1,这道题我觉得是我做出来的题目中最有意思的一个了,反正思考的过程中挺好玩的,还学到了写东西。

首先,这道题虽然名为 置换魔群 ,但感觉和群的关系不是特别大。下面的做题中更多的还是关注于这个置换群中的某一个具体的元素。

通过对本题的分析可以得知:对于一个置换群 $A_n$ 中的一个具体的元素(置换矩阵)$A$,都可以在标准化后被分为若干个子矩阵。每个子矩阵中,都可以通过以自身的值为下一个元素的下标的方式完整的遍历。那么对于每个子矩阵而言,若是以他们为置换矩阵对别的矩阵进行若干次变换的话,都最多只能变幻出他的长度种情况。

这个结论不难证明。因为对于每个变换矩阵而言,其每个元素在变换之后都只有唯一的去处。所以当任意一个元素经过变换回到自己原来的位置之后,就开始下一轮的循环了。而前面已经得出其中的每个元素都可以通过遍历经过所有的位置,于是就没经过自身的长度次变换后就会开始下一轮的循环,因此就会只有自身的长度种不同的状态。

而对于每一个变换矩阵 A 而言,他都是由若干个不同的子矩阵组成的。使用矩阵A进行任意次变换后,就相当于用他的每个子矩阵进行同样次数的变换。因此就能够得出,当使用变换矩阵 A 对某一矩阵进行他的所有子矩阵的长度的最小公倍数次变换后,才能使矩阵恢复原来的样子。

因此当我们拿到一个变换矩阵后,一个必要的步骤就是计算能使变换恢复的最小次数,即找出他所有的子矩阵,并计算他们的最小公倍数。找左右的子矩阵可以通过遍历的方式去寻找,而计算最小公倍数的代码也比较简单。对应的代码可以随便一搜就有了。

题外话

当我得出上面的结论之后,我就自己动手实现了相关的逻辑。而当我做完两问之后我才发现,原来题目已经有相关逻辑的实现了。不过写都写了,就看着用吧。

置换群上的 RSA

观察源代码,就可以发现,这道题的题意就是,随机生成一个变换矩阵,用它自己进行 e 次变换后,求原来的矩阵。

题目中给出 e 的值为 65537,是一个质数。因此就能得知矩阵在进行 e 次变换后是不会回到原来的状态的。

我们可以假设原矩阵为 1,而他的所有可能的情况为 n,这样子的话在经过 e 此变化得到的结果就可以记作:

$$ e \ mod\ n $$

因此题意就变成了,知道 e 和 e mod n,求原数字。因此不难得知,我们只需先求出n,再求出 (e mod n) 对于 n 的逆元,然后在对其取这么多次方,就可以得到本体的答案了。

本题解题脚本如下:

import math
from permutation_group import permutation_element, permutation_group
import re
from pwn import *
def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)]

def exgcd(a, b):
    if b == 0:
        return 1, 0, a
    else:
        x, y, q = exgcd(b, a % b)
        x, y = y, (x - (a // b) * y)
        return x, y, q

def ModReverse(a, p):
    x, y, q = exgcd(a, p)
    if q != 1:
        raise Exception("No solution")
    else:
        return (x + p) % p

r = remote('202.38.93.111', 10114)
r.sendlineafter(b'Please input your token: ', '<token>'.encode())
r.recvuntil(b'> your choice: ')
r.sendline(b'1')
r.recvline() # Since the order of the permutation group can be computed easily, the RSA cryptography is not safe in this gruop.
r.recvline() # Anyway, I decide to give this flag to you for free. Just get it!

for _ in range(15):
    r.recvline() # [+] RSA public key: n = {n}, e = {e}
    r.recvline() # [+] my encrypted secret is here: 
    ele_str = r.recvline() # [1, 2, ... n]
    r.recvline() # [+] Prove that you own the secret (a list like [1,2,3]):
    r.recvuntil(b"> your answer: ")
    ele_list = s2n(ele_str.decode().strip())
    ele = permutation_element(len(ele_list), ele_list)
    r.sendline(str(ele ** ModReverse(65537 % (ele.order()), ele.order())).encode())
    result = r.recvline()
    print(result)

print(r.recvline())

置换群上的 DH

DH 的题意就和 RSA 的题意正好相反了。这次是随机生成一个 secret,给出一个随机的变换矩阵,以及他的 secret 次方,反求 secret。

这道题的思路也还是比较简单的。对于公钥,g,我们可以求出他的所有的子矩阵。然后对每个子矩阵进行遍历,就可以知道 secret 对于每个子矩阵的取模的结果。由此,我们就能用扩展中国剩余定理反推出最小的符合条件的n。

本题的解题脚本如下:

import math
from permutation_group import permutation_element, permutation_group
import re
from pwn import *
def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)]

def get_cnt(li):
    listlen = len(li)
    checked = [0] * listlen
    checking = []
    for i in range(listlen):
        if(checked[i] > 0):
            continue
        if(li[i] == i + 1):
            checked[i] = 1
            continue
        checking.append(i)
        next = li[i] - 1
        checking.append(next)
        while(li[next] != i + 1):
            next = li[next] - 1
            checking.append(next)
        for j in checking:
            checked[j] = len(checking)
        checking = []
    return checked

def getModList(g, y):
    listlen = g.p
    tp = permutation_element(g.p, list(range(1, g.p+1)))
    c = [0] * listlen
    cnt = 0
    orderlist = get_cnt(g.permutation_list)
    for i in range(1, listlen):
        tp = g * tp
        for j in range(y.p):
            if(c[j] == 0 and tp.permutation_list[j] == y.permutation_list[j]):
                c[j] = i
                cnt += 1
        if(cnt == g.p):
            break
    res = set()
    for i in range(y.p):
        res.add((orderlist[i], c[i] % orderlist[i]))
    return res

def exgcd(a, b):
    if b == 0:
        return 1, 0, a
    else:
        x, y, q = exgcd(b, a % b)
        x, y = y, (x - (a // b) * y)
        return x, y, q

def excrt(am):
    n = len(am)
    a=[]
    m=[]
    for e in am:
        m.append(e[0])
        a.append(e[1])
    if n == 1 :
        if m[0] > a[0]:
            return a[0]
        else:
            return -1
    for i in range(n):
        if m[i] <= a[i] :
            return -1
        x, y, d = exgcd(m[0], m[i])
        if (a[i] - a[0]) % d != 0:
            return -1   
        t = m[i] // d
        x = (a[i] - a[0]) // d * x % t
        a[0] = x * m[0] + a[0]
        m[0] = m[0] * m[i] // d
        a[0] = (a[0] % m[0] + m[0]) % m[0]
    return a[0]

r = remote('202.38.93.111', 10114)
r.sendlineafter(b'Please input your token: ', b'2468:MEYCIQCS81iLuK1V2PdML6emg5MjteL014mivUkH1jvTjh3/2wIhAItngXa2L3vgDJUJzhnGq1uP+Aws6t0ShzoTD9WKut3y')
r.recvuntil(b'> your choice: ')
r.sendline(b'2')
r.recvline() # Since permutation group's order is super large, I believe the discrete logarithm problem is hard to solve in this group.
r.recvline() # Therefore I plan to implement the DH protocol in this magic group.
r.recvline() # Now, go and crack my private key!

for _ in range(15):
    pubkey = r.recvline() # [+] DH public key: n = {n}, g = {g}
    secmsg = r.recvline() # [+] my public key = {y} 
    r.recvline() # [+] Prove that you own the secret (a list like [1,2,3]):
    r.recvuntil(b"> your answer: ")
    g1 = s2n(pubkey.decode().strip())[1:]
    y1 = s2n(secmsg.decode().strip())
    g2 = permutation_element(len(g1), g1)
    y2 = permutation_element(len(y1), y1)
    modlist = getModList(g2, y2)
    r.sendline(str(excrt(modlist)).encode())
    result = r.recvline()
    print(result)

print(r.recvline())

光与影

打开题目的链接,看到”
image-1667099557201

可以看到,我们已经能看见 flag 了,但后面的内容被挡住了。所以肯定需要我们干些什么把雾给去掉。不出意外的话,我们又得对源码下手了。

image-1667116238950
点开 f12,可以发现这个网页其实并没有多少个文件。其中 index 很显然就是主界面了,其中也没有什么有价值的信息。render-main.jswebgl-utils.js 感觉都是没有经过修改的官方开源包,不用去看。而 vertex-shader 也是一个很标准,没怎么修改的样子。所以,我们的目标应该就在 fragment-shader.js 里了。

点开可以看到里面是一段非常标准的 glsl 代码。这样就非常方便了,我们可以使用 shadertoy 对这段代码进行调试。

打开 shadertoy,然后点新建,再把刚才复制的代码粘进去。还有一点报错,于是把下面的 main() 注释掉,再把上面的几个 uniform 注释掉,就可以正常编译了。

之后查看她的代码。很容易注意到这个函数:

float sceneSDF(vec3 p, out vec3 pColor) {
    pColor = vec3(1.0, 1.0, 1.0);
    
    vec4 pH = mk_homo(p);
    vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH;
    
    float t1 = t1SDF(pTO.xyz);
    float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
    float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
    float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
    float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);
    
    float tmin = min(min(min(min(t1, t2), t3), t4), t5);
    return tmin;
}

直接盲猜 t1-t5 应该都是在做光线的碰撞检测之类的东西。尝试着把他们一个个注释掉试试。
最终,我是直接在 t5SDF 这个函数一开始直接加了一句让他返回,最后变成了这个样子:

float t5SDF(vec3 p, vec3 b, float r) {
    return 999999.9;
    vec3 q = abs(p) - b;
    return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
}

然后再让他编译运行,就能看见 flag 了。
image-1667116982029

片上系统

打开题目附件,果然又是一个自己没见过的文件格式捏\~( ̄▽ ̄)\~*

不过好在,打开压缩包下的 metadata 文件,可以看见:

[global]
sigrok version=0.5.2

于是可以确定,这个文件可以使用 sigork 打开。

下载 sigork 并安装,然后打开这个文件,就可以看到如下的界面了。
image-1667119439902

放大可以看见,从中间的一部分开始,就可以看见在 D1 处由很多个这种 8 个这种东西的循环,而在 D3 处有一些不规则的信号。
image-1667119614535

由于一个字节是 8bit,因此猜测这里应该是在用 spi 之类的协议传输数据。因此在下方添加一条 SPI 通道,就可以解码这些数据了。
image-1667119495415

再根据题目的提示:

听说,第一个 flag 藏在 SD 卡第一个扇区的末尾。你能找到它吗?

于是,在这里面找到可能是第一扇区结尾的地方(比如这里):
image-1667119755698

之后对这段信息进行解码:
image-1667119848805
就能拿到 flag 了。

企鹅拼盘

这么简单我闭眼都可以!

第一问就比较白送了。因为一共只需要输入4个二进制数字,只有16种可能性。只要手动把所有可能性都试一便,就能拿到 flag 了。

大力当然出奇迹啦~

第二问比第一问复杂不少,不过题目倒是给了相当明显的提示。第二问要求输入16个二进制数字,共有 65536 种可能性。因此想要穷举也并非不可能。

我是写了下面这样的代码。因为数据量不是特别大,就没怎么优化。最终跑了半个小时左右,得到了答案。


import json

class Board:
    def __init__(self):
        self.b = [[i*4+j for j in range(4)] for i in range(4)]

    def _blkpos(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] == 15:
                    return (i, j)

    def reset(self):
        for i in range(4):
            for j in range(4):
                self.b[i][j] = i*4 + j

    def move(self, moves):
        for m in moves:
            i, j = self._blkpos()
            if m == 'L':
                self.b[i][j] = self.b[i][j-1]
                self.b[i][j-1] = 15
            elif m == 'R':
                self.b[i][j] = self.b[i][j+1]
                self.b[i][j+1] = 15
            elif m == 'U':
                self.b[i][j] = self.b[i-1][j]
                self.b[i-1][j] = 15
            else:
                self.b[i][j] = self.b[i+1][j]
                self.b[i+1][j] = 15

    def __bool__(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] != i*4 + j:
                    return True
        return False

def chal(bitlength, obf):
    filename = f'chals/b{bitlength}{"_obf" if obf else ""}.json'
    with open(filename) as f:
        branches = json.load(f)
    
    b = Board()
    for i in range(1<<bitlength):
        b.reset()
        bitseq = [(1 if (i & (1<<res) > 0) else 0)  for res in range(16)]
        #print(bitseq)
        for pat in branches:
            pos = pat[0]
            leftorright = bitseq[pos]
            movepattern = pat[2 - leftorright]
            b.move(movepattern)
            
        if(b):
            print(bitseq)

chal(16, True)

后记

这次参加 hg 也是又学到了好多东西,随然还是有很多东西不会的,但最后居然能进前100名我也算是心满意足了。

孩子玩的很开心,明年还来玩()

参加 Hackergame 喵,参加 Hackergame 谢谢喵。

0

评论 (0)

取消