题解吗 那无敌了

锅里捞面

灵感来自 CCBC15 大海捞针,题目名取自“大海捞针”中的一个 hint。传奇电报员半夜拉四个小时窗帘令人动容,以此为灵感出了道烂题嗯蹭啊嗯。

设计上算是视频隐写,实际上也没多隐,原意是考察下蟒蛇语言的使用,再加点简单编码。

分了视频和音频两部分。视频隐写就随机挑几帧塞信息进去,音频也差不多。最大难点在于视频时长。

视频方面,想到可以用条形码贴合标题,所以按时间顺序切成“面条”塞进去。音频方面也就简单地塞了个摩斯密码。

此外文件名取了“代号128”作为提示,有致敬传奇电报员之意,主要指 Code128(条形码规范),Google 一下第一个就是(甚至百度第一个也是)然而似乎没什么人用上。

组合两部分信息的方法用的是 base36,短学期简单讲过 base 系列,然而没想到网上搜到的大多数是转换成数字的(虽然也就差一步)。

以上是出题的思路。解题的话,取出视频的特殊帧(可以用最大差异取或者其他一堆办法),提取出三个 Code128 条形码得到 base36 的符号表,再解码音频的摩斯密码,做 base36 解码即可。

然而现实是,发现视频和音频的蹊跷这一步还是卡住了不少选手,盯帧最有用的一集。

可能是打 puzzle hunt 打的,似乎还是太谜语了,滑跪。

小/中/大 A 口算

还在嗯蹭啊嗯。

三题用的同一份代码,三个难度分别拿 flag。

小学单身汉

小 A 口算比较简单,打榜超过第一名,凭借你超人的反应力或者写点自动化就可以了。

中学大师

部分源代码:

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
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
void push_question(question *queue, int *tail) {
question q;
long long a, b;
a = rand();
b = rand();
q.a = (a << 31) | (rand() ^ b);
q.b = ((rand() ^ a) << 31) | b;
if (q.a > q.b) {
q.ans = '>';
} else if (q.a < q.b) {
q.ans = '<';
} else {
q.ans = '=';
}
queue[*tail] = q;
*tail = (*tail + 1) % SHOW_SIZE;
}

void pop_question(question *queue, int *head) {
*head = (*head + 1) % SHOW_SIZE;
}

void print_question(question *queue, int head, int tail) {
printf("Question Set:\n");
for (int i = head; i != tail; i = (i + 1) % SHOW_SIZE) {
printf("%lld ? %lld\n", queue[i].a, queue[i].b);
}
printf("\nNow input your answers (like '><<<>>=<<>'), or input 'submit' to submit your score:\n");
}

void master() {
char ans[0x1000];
question queue[SHOW_SIZE];
int head = 0, tail = 0;
printf("%s", master_banner);
while (1) {
while ((tail + 1) % SHOW_SIZE != head) {
push_question(queue, &tail);
}
print_question(queue, head, tail);
memset(ans, 0, 0x1000);
read(0, ans, 0x1000);
printf("\nchecking answers...\n");
sleep(1);
for (int i = 0; ans[i] || head == tail; i++) {
if (!strncmp(ans + i, "submit", 6)) {
printf("\nsubmitting...\n");
return;
}
if (ans[i] != '<' && ans[i] != '>' && ans[i] != '=') {
continue;
}
if (ans[i] == queue[head].ans) {
score++;
} else {
score--;
}
pop_question(queue, &head);
push_question(queue, &tail);
}
printf("\nOK! Now your score is %lld\n\n", score);
}
}

中 A 口算主要考察伪随机数生成,代码原理是用循环队列显示题目,但是每批改一题就生成下一题,所以能读多少答案就能改多少题,一口气发 4096 个答案就不会被 sleep(1) 卡住了。然后问题是怎么猜到没显示的题目。所以只要得到随机数种子就好了。此时你可能发现了,代码里的随机数种子用的是 time(0),也就是当前(以秒为单位)的时间戳,那理论上是非常好得到的,在本地也能非常轻松的得到验证。

但是跟远程交互的时候发现寄了。怎么会是呢?

在大 A 口算里我加了个提示,暗示远程的时间是不一样的,在很久很久之前。(事实上因为容器的原因,改时间的操作是在程序里把变量减掉然后把下发的程序 patch 掉实现的……)。为什么中 A 不给提示呢?因为理论上你可以轻松地反爆出种子然后发现这点……虽然发现的人也不多。

中 A 口算里生成的题目是可以算出每次 rand() 函数的结果的,而 libc 的随机数生成器是 LCG 实现的,破解肥肠简单(可以出门左转看看 X 老师出的困难版),或者实在不想学数学,可以直接枚举所有种子,没几个小时也能爆出来。然后爆出种子就好办了。多试几次应该能发现远程时间与现在的偏移是不变的(事实上恰好是 14 年前,那时候 GDK 是个纯种小学生),然后就更好办了……如果没猜出来,至少也可以靠 LCG 先做两套题然后解出种子……总之办法不止一种,总有一种适合你(x)。

高中医生

部分源代码:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
question gen_question() {
question q;
q.a = rand() % 20 + 1;
q.b = rand() % 20 + 1;
if (q.a > q.b) {
q.ans = '>';
} else if (q.a < q.b) {
q.ans = '<';
} else {
q.ans = '=';
}
return q;
}

void doctor() {
char ans[0x100];
int len = -1;
printf("%s", doctor_banner);
while (1) {
printf("\nNow guess the answer, I'll correct your answer:\n");
memset(ans, 0, 0x100);
if (len == -1) {
read(0, ans, 0x100);
} else {
read(0, ans, len);
}
len = strlen(ans);
printf("\nYou set the size of question to %d...\nchecking answers...\n", len - 1);
sleep(1);
int cnt = 0;
for (int i = 0; i < len; i++) {
if (!strncmp(ans + i, "submit", 6)) {
printf("\nsubmitting...\n");
return;
}
if (ans[i] != '<' && ans[i] != '>' && ans[i] != '=') {
continue;
}
question q = gen_question();
if (ans[i] == q.ans) {
++cnt;
} else {
ans[i] = q.ans;
}
}
if (ans[len - 1] != '\n') {
ans[len] = '\n';
}
printf("\nOK! %d answer(s) correct!\n\n", cnt);
if (cnt * 2 < len) {
printf("\nNot enough correct answers, s00ry :(.\n");
return;
}
printf("And the corrected answers:\n%s", ans);
}
}

大 A 口算事实是中 A 口算 + trivial rop(包比 easy rop 简单的),让你猜题并好心地帮你批改答案,但是正确个数不到一半会把你踢了。这里的算数都对 20 取了模,所以直接爆出种子大概是有点难度的。如果前面猜到了偏移那么这里猜题目答案就不难了。

那前面的偏移没猜到呢?☝️🤓欸,我们的选手有很多的创意(意思是从某选手那里学到的):
可以同时连接两个实例并且一个选中 A 难度,一个选大 A 难度,两者的种子一样,就可以用中 A 算出种子然后拿来猜大 A 的了。

总之是解决第一个问题了。那怎么 pwn 呢?可以看到,这段代码写的一拖四,看到用变量作为读取长度的 read,当场应激了。这里用的长度变量在答案每一次被读取后都会被设为 strlen,而每次程序都会在字符串末尾加一个 \n\0 都帮你填上了,还给回显,感激。所以直接送最大长度(256)的答案,后面的 \n 补上后就变成 257,然后就可以送更长的答案……OK,这条栈现在归你了。偷到 canary 和返回地址就可以直接干到输出 flag 的地方了,你想的话也可以把三个 flag 全偷了,虽然这题都做了前两题也就没啥难度了。

综上,这三题还是 puzzle 题,太有 misc 了。