这一批题都是我不会的,只能把官方write-up放在这里了
1、FLXG 的秘密
------------------------------------------------------------------------------------------------------------
公元 0xFB2 年, FLXG 正当其道.
没错, 在 CWK 的伟大倡导之下, 年份采用了更为先进的 16 进制表示. 中国滑稽大学也因为率先提出了 FLXG 的理论, 其世界超一流的院校的地位已经不可动摇. 而在肥宅路 98 号某个废弃的角落里 -- 实际上是两千年前一时风光无二, CWK 口中以考试分数为唯一目标的分院 -- 几名幸存的理论物理系跑男 (旧指为 GPA 而四处奔波的分院学生) 在饥寒交迫中, 企图谋划着最后的反抗.
实际上, 他们已经成功了. 多少代物理系的先辈们忍辱负重, 转入 CS, 就是为了制造出量子计算机, 试图攻破 FLXG 这个天衣无缝的理论. 这个计划已经实施了两千年, 而现在终于结成正果了. 世界上仅存的几位分院跑男, 他们已经掌握了 FLXG 最核心的秘密, 那是除了创始人 CWK 无人知晓的秘密, 那是失传千年的, 整个 FLXG 的唯一漏洞. 当年的 Nature, Science, 如今的 Engineer 期刊上不断有人试图找出这个纰漏, 然而所有人都失败了. (可惜也没人能够证明 FLXG 的绝对完美性) 因此 FLXG 有一段时间被认为是最接近真实的假设 -- 当然, 这是落后的理科思想所形成的结论. 所以, 正如你看到的这样, FLXG 已经成为了金科玉律一般的存在. 然而, 它的唯一漏洞, 在这一年, 已经这几名跑男找到了.
但是, 他们也是失败的. 分院和物院, 已经在滑稽大学的 FLXG 改革中消失, 所有留存的痕迹, 也成为校史馆中的笑料和反面教材. "什么? 你还跑过去找老师要分? 怕不是分院余孽.." 之前时时还能听到这样的嘲讽, 如今嘲讽的对象也越来越少, "分院跑男" 这种词已经被新版的 CWK 词典移到了附录里, 以后估计会被删掉的吧. 就算有人找到 FLXG 的秘密又怎么样呢, 再也不会有人去读物理了.
当然, 有唯一的一条出路, 就是设法把这条秘密发送到两千年前. 这样大家就能在 FLXG 的实施之前, 看到它的漏洞了, 也许就可以拯救分院和物院的命运了.
如今的技术发展, 虽然能够在一定程度上控制时空, 但是要把消息传回两千年前, 的确是不太靠谱. 何况两千年前的人类根本无法做出应答. 当然更关键的, 就是因果律的影响了. 传递消息的做法, 必须要瞒过因果律, 否则只会在过去的时间长河中开出一条小小的支流, 对于这个平行宇宙来说并无意义.
为了做到这一点, 他们几人把秘密用许多随机生成的锁保护起来, 最后连接成一个可以自动计算出秘密的程序 (他们为了存活也转行做 CS 了), 而这个程序运行起来需要 2000 年甚至更久. 之后, 他们再以四千年前的伏羲先天六十四卦将程序编码, 以此试图骗过因果律, 逆流而上, 成前人未有之壮举.
然而, 由于时间长河的冲刷, 这份信息仍然受到了损毁. 在 0x7E2 年的你, 能够解出 FLXG 的秘密吗?
------------------------------------------------------------------------------------------------------------
来自未来的漂流瓶
TL;DR
六十四卦那些卦象,你们看着不觉得就像二进制吗..。
详解
下下来直接打开发现乱码。不知道为啥编辑器不觉得这是 UTF-8。切换到 UTF-8 的编码,发现果然一堆六十四卦的名词。
无论怎么编码的,第一步肯定是把不同卦分离开来。六十四卦的名称比较杂乱,是变长的 CISC 架构,代码里面的六十四卦卦名要老老实实写出来。
接下来就是脑洞时间了。六十四卦每一卦都有 6 个 bits 的信息,而一般的数据都是以 8 个 bits 为单位。因此,我们看一下长度,发现可以被 8 整除,这进一步验证了我们的猜想。接下来,就是考虑如何把一个卦象转化为 6 个 bits。
有三类显而易见的情况:
每个卦象自下而上,阴阳对应 0 和 1,这就是两种可能
每个卦象自上而下,阴阳对应 0 和 1,这又是两种可能
卦象以先天六十四卦顺序,也是 Unicode 字符集中的顺序编码
写出来脚本跑一跑,发现第二种情况能产生一个 gzip 的文件。解压时提示文件损坏,查看文件末尾即可得到 flxg。
难以参悟的秘密
TL;DR
Merkle Hellman Knapsack Cryptosystem
详解
本题是一道逆向。
解压得到一个可执行文件和一堆动态链接库。拖进 IDA 里发现,程序会读取 passkey.txt 的内容,然后通过调用动态链接库的函数进行校验。最后经过一番处理,每 8 行变成一个大整数。然后根据一个 128bit 的数的某一位进行求和。最后判断是否结果等于最后一个大整数。这实际上是一个 Merkle Hellman Knapsack Cryptosystem。
二进制里重要函数都已经标注出来。一旦我们弄清楚程序的意图,就可以继续做下去了。思路非常清晰: 先要恢复 passkey.txt 的内容,进而得到每个大整数。然后求解 flxg。
第一步,需要选手批量处理动态链接库中的代码。动态链接库里面的验证基本可以总结为 kx+b=x,通过将 k 和 b 提取出来,可以求解 x。而 k 和 b 两个数字在 lock 函数中偏移固定,可以通过能处理 ELF 的 python 库或二进制分析框架提取出来。
第二步,我们得到了 passkey.txt 应有的内容。现在我们需要还原 128 个大整数。大致有两种思路,一种是动态运行程序,通过修改程序的代码或者 Hook 或者 DBI 或者调试器脚本的方法,可以得到这些大整数。另一种是通过逆向,自己重现相应的算法。这道题里面使用了一个不常见的 Hash 算法 -- JH,很难识别出来。并且代码中大量使用 SSE,很难手动实现。但是可以通过将可执行文件转为动态链接库的方法导出相关函数,从而直接运行程序的算法。
第三步,Low Density attack。这实际上是 1984 年的攻击了。需要找到论文简单复现一下即可。比如 http://www4.ncsu.edu/~smsulli2/MA437_Fall2017/knapsack.pdf。其核心思想是构造出一个 Lattice,这些向量加起来和为 0。然后就变成了格点规约问题了。通过 LLL 算法很容易求出解。
(此处省略丑陋的 Mathematica 代码)
本题代码见 flxg.c 与 lock.c。代码实际上不能直接编译,因为缺少 jh.h (只是一个 hash 算法的实现) 和 lock.h (由脚本生成)。不过大致流程比较清晰。可参看。
2、C 语言作业
------------------------------------------------------------------------------------------------------------
今天 C 语言课程的作业是写一个简单的计算器程序,我在 linux 上面只花了几分钟就写完了,大家可以下载我编译好的程序来使用。如果不想下载的话,我还专门提供了一个网络服务,大家可以在 linux 上面使用
nc 202.38.95.46 12008
这条命令来连接。什么?你说你看到我服务器的家目录里有一个 flag 文件?这怎么可能?
------------------------------------------------------------------------------------------------------------
这道题的思路源自我之前玩过的一个 Wargame:Smash the Stack IO Level 2。
逆向这个二进制,发现它就是再平常不过的计算器程序了,而且考虑了除以 0 的情况。
仔细观察,在程序初始化的时候(main 函数执行之前),程序注册了几个 signal 的处理函数。这个实际上是用 gcc 的 __attribute__((constructor)) 实现的。
SIGILL、SIGABRT、SIGFPE、SIGSEGV 几个信号被注册到了 __err 函数上面,也就是说发生这几种异常的时候 __err 函数会被执行。__err 函数会让你输入一个字符串,不能包含 sh,然后用 execlp 来执行这个字符串代表的程序。值得一提的是,execlp 限制了我们只能不带参数地执行程序。
根据 signal 的 man page,SIGILL、SIGABRT、SIGSEGV 在本程序中看起来应该不会发生,我们关注一下 SIGFPE:
According to POSIX, the behavior of a process is undefined after it ignores a SIGFPE, SIGILL, or SIGSEGV signal that was not generated by kill(2) or raise(3). Integer division by zero has undefined result. On some architectures it will generate a SIGFPE signal. (Also dividing the most negative integer by -1 may generate SIGFPE.) Ignoring this signal might lead to an end‐ less loop.
惊讶!不仅除以 0 可能会触发 SIGFPE,最小的 INT 除以 -1 也可能会触发!也就是说,写一个整数计算器,只考虑除以 0 的异常是不够的,最小的 INT 除以 -1 也可能会让程序崩掉。
所以第一步输入 -2147483648/-1 即可。
然后,我们需要找到一个程序,不带参数地运行可以帮我们拿到 shell,我能想到的是 vim,当然也可能有其他解法。
注:我本想模拟一个新安装的 Ubuntu 环境,但是 Ubuntu 的 docker 镜像里面什么都没有,ed、vi 等命令也应该安装一下的,我对此表示抱歉。(有人提到 python,emmmm)
第二步输入 vim ,然后进入了一个没有 tty 的 vim,很难受,不过也可以执行命令。我们在 vim 内输入 !cat flag 即可读取 flag。
然而 flag 告诉我们这个是假的,真的 flag 在一个叫 - 的文件里,我们执行 !cat - 就可以了。然后什么都没看到!
实际上,cat - 中的 - 表示标准输入,cat 会试图从你的标准输入中读取,并不能看到 - 文件的内容。绕过的办法有多种,最简单的就是 cat ./-。
至于 5 秒的限时嘛……复制粘贴都是来得及的,反正进入 vim 之后就没有了。
我们也可以把上面的过程写成一个 shell 脚本:
#!/bin/bash
echo -e "-2147483648/-1\nvim\n:!ls\n:!cat flag\n:!cat ./-" | nc 202.38.95.46 12008 | grep flag
------------------------------------------------------------------------------------------------------------
提示:本题中使用的 base64 编码,采用已被广泛应用于各种场合的 RFC 4648 §5 标准。
小赵听到自己成为了信息安全大赛的创始人后感到非常吃惊:“我一个少院学生会的干事,怎么就成信息安全大赛的创始人了呢?”这也难怪,毕竟小赵后来成为了物理学院的学生。物理和信息安全,通常情况下可都是八杆子打不着的呢。
当然了,小赵作为物理学院的学生,和其他物理学院的学生一样,身上的浮躁劲儿可一点都不少,常常因为一点小成就而沾沾自喜。这不,因为个人安全上的不重视,小赵的某个学弟小郑,很快从小赵暗恋的女孩子手里拿到了小赵和她交流的加密算法的程序。小赵在得知此事后反而没有尽可能地息事宁人,反而公开宣称,由于解密算法目前没有公开,所以拿到了加密算法也没有什么用。看来小赵对于现代密码学,根本没什么全面深入的了解啊。
不过,即使小赵使用的是对称加密算法,分析出解密算法也并非易事——小赵对程序进行了混淆,而混淆的方法是使用 BrainFuck 虚拟机——这也正是小赵的底气所在。现在的任务是分析并读懂一段 BrainFuck 程序,从而将一段密文还原。
现在小郑将这一任务交给了跃跃欲试的你。快来挖掘小赵的黑历史吧!
更多信息请下载题目文件
------------------------------------------------------------------------------------------------------------
小赵听到自己还要给自己出的题目写 write up 感到十分震惊——明明那么简单的题目,write up 还要出题人来写。不过,小赵还是硬着头皮把 write up 写完了。
寻找规律
这道题涉及的 BrainFuck 代码看似复杂,实际上如果仔细分析,可以发现大部分代码都是有规律可循的。
我们首先得知 BrainFuck 虚拟机元素的初始值都是零:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ...
我们打开encrypt.bf,将整段代码按循环(一对匹配的[]对及其中间的部分)分段:
,
[->>+++++++>>+++++>>+++>>++>>++++>++++++<<+++<<+++++++++<<++++++++<<++++++<<++++++++<]
>
[->>+++++>>+++++++++>>+++++++++>>++>>++++<+++++++++<<++++<<++++<<++<<++<]
<,
[->>+++++>>++++++++>>++++++>>+++++++>>+++++++>++++++<<++++++<<++++++++<<+++<<+++<<++++++++<]
>
[->>++++++++>>+++>>+++++++>>++++>>+++++++++<++++++<<+++++++++<<++<<+++++++++<<++++++++<]
<,
etc
然后我们从第一段开始:
,
这段只有一个字符的代码读入一个值,我们不妨设它为a1。现在 BrainFuck 虚拟机情况如下:
a1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ...
然后我们进入循环:
[
-
>>
+++++++
>>
+++++
>>
+++
>>
++
>>
++++
>
++++++
<<
+++
<<
+++++++++
<<
++++++++
<<
++++++
<<
++++++++
<
]
经过一次循环后的虚拟机情况如下:
a1-1 8 7 6 5 8 3 9 2 3 4 6 ...
指针回到a1-1处,我们可以得出这个循环还要进行a1-1次,共a1次,最后虚拟机情况如下:
0 8*a1 7*a1 6*a1 5*a1 8*a1 3*a1 9*a1 2*a1 3*a1 4*a1 6*a1 0 0 0 ...
然后下一段:
>
指针向右一格,停在8*a1处。然后进入下一个循环:
[
-
>>
+++++
>>
+++++++++
>>
+++++++++
>>
++
>>
++++
<
+++++++++
<<
++++
<<
++++
<<
++
<<
++
<
]
我们可以得知这个循环将会进行8*a1次,和上面的循环一样,我们将增量代入,最后结果如下:
0 0 23*a1 46*a1 21*a1 80*a1 35*a1 81*a1 34*a1 19*a1 76*a1 38*a1 0 0 0 ...
接下来的指令是:
<,
首先将指针向左一格到最左点,然后读入一个字符,不妨设为a2:
a2 0 23*a1 46*a1 21*a1 80*a1 35*a1 81*a1 34*a1 19*a1 76*a1 38*a1 0 0 0 ...
然后接着进行上面的两组循环。这样的操作一共进行十轮,共二十次循环。
最后的代码段将每个元素加上一个固定的值输出:
>++.
>++++++.
>++++++++.
>++++++++.
>+++.
>+++++.
>+++++.
>+++++++.
>++++.
>+++++++++.
通过分析我们实际上可以发现,这就是对a1...a10十个数组成的一次线性变换,当然如果算上最后加上一个固定的值输出的话就是仿射变换。
如果我们设输出为b1...b10的话,我们的目标是找到一个十一维的矩阵A_(11x11)满足:
[b1, b2, ..., b10, 1] = A_(11x11) * [a1, a2, ..., a10, 1]
实际上每一轮操作除了+的数量不同,代码形式是一模一样的,我们只需要将代码中连续的+序列分离并分组,然后依次处理就可以了。
提取数据
我们编写一段 Python 脚本提取出整个矩阵(为方便后续运算,运算结果为A_(11x11)的转置):
import re
def to_matrix(code):
i = re.compile("[+]+").finditer(code.replace("\n", ""))
m = [[0 for k in range(11)] for j in range(11)]
for j in range(10):
for k in [0, 2, 4, 6, 8, 9, 7, 5, 3, 1]:
m[j][k] += len(next(i).group(0))
factor = len(next(i).group(0))
for k in [1, 3, 5, 7, 9, 8, 6, 4, 2, 0]:
m[j][k] += factor * len(next(i).group(0))
for k in range(10):
m[10][k] += len(next(i).group(0))
m[10][10] = 1
return m
以下是输出:
[[23, 46, 21, 80, 35, 81, 34, 19, 76, 38, 0],
[69, 67, 80, 27, 22, 64, 79, 38, 55, 78, 0],
[40, 40, 63, 69, 66, 51, 74, 52, 41, 43, 0],
[61, 54, 33, 53, 43, 46, 52, 72, 68, 59, 0],
[47, 31, 60, 37, 68, 37, 27, 49, 39, 55, 0],
[21, 23, 26, 81, 36, 44, 19, 71, 62, 74, 0],
[62, 54, 39, 24, 67, 75, 38, 36, 48, 50, 0],
[73, 75, 32, 61, 22, 77, 79, 40, 65, 18, 0],
[18, 64, 48, 23, 58, 71, 30, 60, 21, 36, 0],
[81, 69, 39, 50, 37, 18, 68, 45, 66, 77, 0],
[ 2, 6, 8, 8, 3, 5, 5, 7, 4, 9, 1]]
整理算法
有经验的参赛成员应该能够发现这正是希尔密码(Hill Cipher)的一个变种。当然了,如果没有经验,此时也应能想到下一步是求这个矩阵的逆。不过,在题目中也有说明,矩阵的运算结果需要对 64 取余。因此这个矩阵不是定义在实数域(R)上的,而是定义在整数模 64 环(可记为Z_64)上的。
求矩阵的逆的直接方法无非求解伴随矩阵,再除以矩阵的行列式共两步。当然一些库是可以直接对某个整数模 n 环(可记为Z_n)求逆的(比方说 SymPy 的inv_mod)。
以下是待求矩阵的逆(同样经过转置):
[[ 2, 17, 34, 57, 17, 45, 15, 61, 45, 24, 0],
[ 0, 20, 38, 43, 56, 29, 34, 41, 29, 61, 0],
[29, 3, 25, 25, 32, 57, 22, 4, 32, 52, 0],
[41, 63, 22, 19, 49, 32, 4, 22, 40, 18, 0],
[27, 24, 11, 11, 2, 15, 57, 9, 36, 4, 0],
[26, 55, 52, 10, 10, 54, 32, 37, 52, 28, 0],
[14, 31, 12, 60, 47, 44, 53, 41, 19, 13, 0],
[ 3, 53, 16, 51, 53, 11, 48, 35, 49, 28, 0],
[25, 26, 8, 57, 51, 30, 62, 17, 53, 0, 0],
[25, 37, 46, 19, 52, 53, 24, 18, 31, 12, 0],
[25, 56, 17, 57, 16, 55, 18, 4, 39, 41, 1]]
求解答案
最后 transform 一下输入输出就大功告成了。以下是本人编写的 Python 代码:
import sympy def decrypt(matrix): base64_mapping = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" def to_output(output): transformed_output = output[:4, :10] return "".join([base64_mapping[o % 64] for o in transformed_output]) def from_input(input): transformed_input = [base64_mapping.index(i) for i in input] return sympy.Matrix(4, 10, transformed_input).row_join(sympy.ones(4, 1)) inv_matrix = sympy.Matrix(matrix).inv_mod(64) return lambda input: to_output(from_input(input).multiply(inv_matrix))
整个 Python 代码位于encryption_and_decryption_solution.py中。
彩蛋
其实这道题的加解密函数存在一个刻意加入 (但却毫无意义) 的不动点,请参阅附带的 Python 代码(^_^)
4、王的特权
------------------------------------------------------------------------------------------------------------
小王同学刚刚学会了 Rust 语言,今天拿着自己写的 BUG 来找老张同志。
老张:哟,小子学会写 Rust 了?
小王:没错,刚出炉的程序,而且只有我能运行!
老张:只有你能运行?我不信。
小王:不信你就试试呗!
小王放心地把程序交给了老张,并声称可以找任何人来帮忙。作为老张多年的好友,你能帮他破开「王的特权」的秘诀吗?
注意:
做这道题,你可能需要准备 64 位 Linux 环境。
做题时请保证网络畅通。但这是为了避免直接拿到 flag 而给你的小小考验,预期解法与网络无关,请不要在这方面下工夫。
------------------------------------------------------------------------------------------------------------
这是一道 Rust 逆向科普题。Rust 官网的 介绍是:Rust 是一种系统编程语言。 它有着惊人的运行速度,能够防止段错 误,并保证线程安全。
源代码
会 Rust 的选手看到代码应该什么都明白了吧!
use std::env; use std::net::{TcpStream, Ipv4Addr}; use std::io::{Write, Read}; const KEY: u64 = 19260817; const ADDR: [u8; 4] = [0, 0, 0, 0]; // Not relevant MASKED const PORT: u16 = 0; // NOT relevant MASKED fn main() { let args = env::args().collect::<Vec<String>>();// 本题的预期解法args[0].find("sudo").expect("Permission denied");// 下面代码的目的只是:// 1. 不要把文件拖到 IDA 里面就看到 flag// 2. 不要被随便看到服务器 IP 和端口号……let local_key = args[0].as_bytes()[0] as u8;let addr = Ipv4Addr::new(ADDR[0] ^ KEY as u8, ADDR[1] ^ KEY as u8, ADDR[2] ^ KEY as u8, ADDR[3] ^ KEY as u8);let port = PORT ^ KEY as u16;let mut stream = TcpStream::connect((addr, port)).unwrap();stream.write(&[local_key]).expect("write");let mut buf = Vec::new();stream.read_to_end(&mut buf).expect("read");for c in &mut buf {*c ^= local_key;}println!("{}", String::from_utf8(buf).expect("decode"));}
题目中的粗体字「不是网络题」其实就是明示,不要尝试手动构造网络请求。
思路
首先,把 b 跑一下:
thread ‘main‘ panicked at ‘Permission denied‘, libcore/option.rs:1010:5note: Run with `RUST_BACKTRACE=1` for a backtrace.
给不熟悉 Rust 的选手的注释:这句话来自于代码的第二行的 .expect 调用。 熟悉 Rust 的选手可能直接就会发现这个 Permission denied 的 panic 来自于 Option panic 的而不是真正的没有权限。而且是默认的输出。那就按它说的, 运行一下:
$ RUST_BACKTRACE=1 ./b thread ‘main‘ panicked at ‘Permission denied‘, libcore/option.rs:1010:5 stack backtrace: 0: std::sys::unix::backtrace::tracing::imp::unwind_backtrace at libstd/sys/unix/backtrace/tracing/gcc_s.rs:49 1: std::sys_common::backtrace::print at libstd/sys_common/backtrace.rs:71 at libstd/sys_common/backtrace.rs:59 2: std::panicking::default_hook::{{closure}} at libstd/panicking.rs:211 3: std::panicking::default_hook at libstd/panicking.rs:227 4: std::panicking::rust_panic_with_hook at libstd/panicking.rs:477 5: std::panicking::continue_panic_fmt at libstd/panicking.rs:391 6: rust_begin_unwind at libstd/panicking.rs:326 7: core::panicking::panic_fmt at libcore/panicking.rs:77 8: core::option::expect_failed at libcore/option.rs:1010 9: <core::option::Option<T>>::expect 10: b::main 11: std::rt::lang_start::{{closure}} 12: std::panicking::try::do_call at libstd/rt.rs:59 at libstd/panicking.rs:310 13: __rust_maybe_catch_panic at libpanic_unwind/lib.rs:102 14: std::rt::lang_start_internal at libstd/panicking.rs:289 at libstd/panic.rs:392 at libstd/rt.rs:58 15: std::rt::lang_start 16: main 17: __libc_start_main 18: _start
为了降低题目难度,没有使用优化编译,也没有去除调试符号,所以你可以轻松 看到完整的 backtrace。注意到第 10 行的 b::main,这意味着 panic 就发 生在 b::main 函数中。
那么直接看反汇编喽。(建议首先使用 rustfilt 工具去除命名修饰。)
; 在 b::main 中
10b27: e8 14 12 00 00 callq 11d40 <std::env::args>
; 这个函数可以收集程序选项参数,类似于 C 语言里的 argv
10b4d: e8 ee 97 ff ff callq a340 <core::iter::iterator::Iterator::collect>
; 注意到这个 collect 和周围的一些代码,可以推测源代码就是把 args 收集到 Vec<String>
10b5e: e8 bd c0 ff ff callq cc20 <<alloc::vec::Vec<T> as core::ops::index::Index<I>>::index>
; 那么这里显然是对 vec 进行取下标操作(不难发现就是 argv[0])
10b75: e8 c6 9b ff ff callq a740 <<alloc::string::String as core::ops::deref::Deref>::deref>
; String deref 到 &char,不懂的话其实可以略过
10b9b: 48 8d 15 cf a3 04 00 lea 0x4a3cf(%rip),%rdx # 5af71 <str.1+0x21>
; 下面来到这里…… 于是你找到了一个字符串常 ‘sudo‘
10bb7: e8 a4 f3 ff ff callq ff60 <core::str::<impl str>::find>
; 在 argv[0] 里面找这个 ‘sudo‘
下面就先不看了,直接试试把文件名改成 sudo 或者 bsudo 或者 bbbsudofhdksj 然后跑一下看看效果。
$ ./abcdefghijklmnopqrsuvwxyzsudoxiaowang
flag{CA11m30xidiz3r}
好的,你拿到了 flag,去交吧!
Flag
flag{CA11m30xidiz3r}
里面的文本是 call me oxidizer(叫我氧化剂),暗示 rust(铁锈)是被氧化 剂氧化的产物。
花絮
这道题原本是一道签到题,想在 argv[0] 上做文章而已,只不过顺手用了 Rust 而没有用 C,然后发现其实没那么签到,因为有很多选手对 Rust 并不熟 悉。第一版时里面还使用了多线程和 MPSC channel,后来发现不太容易就去掉 了这个设定,感兴趣的选手可以自己试试。然后,剩下的就成为了历史。
5、她的礼物
------------------------------------------------------------------------------------------------------------
(在做这道题之前,你可能需要先完成题目「她的诗」的一部分。)
小 T 的生日就要到了,在一天前,他收到了她发的电子邮件。
「祝你生日快乐!……」庆生邮件的开头大概都是这个样子的。自从小 T 加入校文学社以来,她可没少给他带去惊吓。这家伙邮件里面又会写什么东西?
出乎他意料的是,这封邮件看起来挺正常的,除了结尾之外。
「另外呢,我写了一个小小的程序送给你,在里面藏了一些东西,不过运行它是需要密码的,密码我想想……哦,还记得上次我给你的那首诗吗?就是那首诗,用你朋友的脚本解密之后的第 10 行,然后啊我还要去赶路呢,就先写到这里吧,拜拜~」
附件看起来像是一个 Linux 下的可执行文件。理论上讲,把密码作为参数启动程序,就能看到她想要告诉小 T 的字符串了。不过……这家伙不会藏了什么 rm -rf / --no-preserve-root 这种命令(注:请勿在自己的机器上执行此命令!)在里面吧?但小 T 又想,她不可能会做出这种事情的。
------------------------------------------------------------------------------------------------------------
(承接“她的诗”)
这道题做出来的人数比我预想的少……难道都是被混淆吓跑的吗?实话讲,混淆确实是我故意弄上去的,为了劝退一般的逆向方法。万恶之源在这。为了不让你轻易发现这是个魔改 clang,我特意在二进制文件里把 LLVM 的版本信息改成别的了。
打开程序,可以倾听到悦耳的蜂鸣器声(取决于环境),欣赏到美妙的歌词,只是运行了 10 秒,这个程序就会自己退出。难道大家真的没有一种想要让这个程序正常一点的冲动吗……
正解其实是 hook(或者 patch)这个二进制,把所有烦人的函数都搞掉。使用 LD_PRELOAD hook(动态编译的)Linux 二进制文件的相关内容,可以参考 [1], [2] 等资料。
Hook 的话至少需要把 alarm()、sleep() 和 system() hook 掉,要加速的话输出部分也可以处理一下。最后我自己写的「库」的代码如下:
#include <stdarg.h> #include <stdio.h> int system(const char *command) { return 0; // no beeping } unsigned int alarm(unsigned int seconds) { return 0; } unsigned int sleep(unsigned int seconds) { return 0; } int puts(const char *s) { return 0; // speed up } int printf(const char *format, ...) { if (format[0] == ‘f‘) { // is flag va_list arg_ptr; va_start(arg_ptr, format); vprintf(format, arg_ptr); return 0; } return 0; }
用对应的编译参数编译,然后改 LD_PRELOAD 环境变量,等待一小段时间之后程序就乖乖把 flag 吐出来了。
打 patch 也是同理,只是注意不要 patch 太过火:有人问我为什么他 patch 了之后结果不对,我看到他把 main() 里面所有的循环都 nop 掉了,很想跟他说他的思路基本是正确的,但是根据规定,又只能回复「无可奉告」,我也很无奈。
花絮:
其实原来这个程序是静态编译的,后来为了降低难度(毕竟是定向新生的比赛),改成了动态编译。不知道有没有人真的用硬碰硬的办法获得 flag 的……其实我也很希望能看到用其他(我)想象不到的方式做出这道题的题解。
没人觉得,「她」所在的文学社似乎不是很文学吗?? 其实在编第一道题「她的诗」的时候,「她」的原型是某个游戏的真女主角,但是随着后面题面修改、加题目,就看起来不太像了。
------------------------------------------------------------------------------------------------------------
在民间流传着这样一个传说,智慧的老者有一个神秘的魔镜,每当有人向老者提出自己的疑惑,老者将轻轻抚摸镜面,此时魔镜发出了古老的声音,诉说着做法的对错。无数的年轻人通过老者向魔镜请教,他们留下了一个个不朽的神话。过了很久很久……人们进入了工业时代,又过了很久进入了电器时代,随着人们对于自然理解,人们进入了原子能时代。人们似乎忘记了久远的传说。但是智者却以其他形式的方式再次出现在了人们的生活之中。小冉同学像是天之娇子,他在互联网上发现了一个这样的神秘程序,输入一段文字,神奇的程序会告诉你它是否正确。或许这就是21世纪的魔镜。魔镜里到底隐藏着多少不为人知的秘密,冉同学能发现这魔镜里的秘密吗?
------------------------------------------------------------------------------------------------------------
我们先把程序拖入 IDA,等 IDA 分析完毕后,我们先从所有字符串下手! ?
拖入 IDA,一般来说都先从字符串入手,有一个像是生成 base64 的字符串,还有一个像是逆序的 base64 字符串,解出来:flxg{I_am_A_fake_flag_23333}。恩,好吧,那我们看看程序别的信息,我们会发现有一个进入异常的 string。可能这道题目和 C++ 异常有关,看到那么多函数,瞬间有点懵,没事慢慢来,先看哪些系统函数被调用了:
我们看到如此多的函数,我们找到结尾为 498F 的这个函数,里面运用了 strrev 来翻转字符串。恩,这应该就是解密函数。 我们来分析这个解密函数。 qmemcpy((void *)(a1 + 800), &unk_1400054D8, 0x39ui64);
此句将一串怪异的字符串复制到了一个内存地址。我们跟随一下这个地址里面的内容
00000001400054D0 0A 00 00 00 00 00 00 00 39 65 45 54 77 5F 34 5F ........9eETw_4_
00000001400054E0 64 5F 66 68 3C 34 58 55 7F 43 21 4B 7F 20 43 76 d_fh<4XU.C!K. Cv
00000001400054F0 5F 20 4C 4D 7A 53 70 7D 56 4D 65 47 4C 5D 71 43 _ LMzSp}VMeGL]qC
0000000140005500 18 6F 47 48 42 18 1C 4D 74 45 01 69 00 4D 5B 6D .oGHB..MtE.i.M[m
我们接着分析经过 sub_140001590() 函数后将一块内存地址初始化为 0。
我们继续分析
do
{
*(_BYTE *)(a1 + 32) = **(_BYTE **)(a1 + 80);
**(_BYTE **)(a1 + 48) = *(_BYTE *)(a1 + 32);
++*(_QWORD *)(a1 + 80);
++*(_QWORD *)(a1 + 48);
}
这一段代码类似于 strcpy()
接着我们看到了调用了 strrev 函数,这个函数翻转了字符串的内容。
接着我们往下看
*(_BYTE *)(a1 + *(signed int *)(a1 + 36) + 592) = *(_BYTE *)(a1 + 36) ^ *(_BYTE *)(a1+ *(signed int *)(a1 + 36) + 176);
这是在 for 循环里的 xor 操作,*(_BYTE *)(a1 + 36)就像我们的 i 循环变量。
之后就没有什么加密的操作了。
我们将我们发现的那一段字符串整理出来。
我们跟据我们的分析来写解密程序:
#!/usr/bin/env python3 import base64 gotlist = "39 65 45 54 77 5F 34 5F 64 5F 66 68 3C 34 58 55 7F 43 21 4B 7F 20 43 76 5F 20 4C 4D 7A 53 70 7D 56 4D 65 47 4C 5D 71 43 18 6F 47 48 42 18 1C 4D 74 45 01 69 00 4D 5B 6D".split() flxg = "" ccc = 0 for i in gotlist: flxg = flxg + str(chr(int("0x"+i,base=16)^ccc)) ccc = ccc + 1 print("We got xor:" + flxg) flxg = flxg[::-1] print("We reverse it, we get:" + flxg) flxg = base64.b64decode(bytes(flxg,‘utf-8‘)) print("We will get the flag:" + str(flxg))
运行后我们就得到了我们的 flxg:
7、CWK 的试炼
------------------------------------------------------------------------------------------------------------
提示:
1. 本题两个 flag 均由远程服务器提供.
2. 本题两个 flag 均为有意义的字符串.
3. 与服务器交互时请使用 UNIX 换行符.
4. 这不是 HTTP 协议啊喂 (╯‵□′)╯︵┻━┻, nc 命令请了解一下.
The CWK History Symposium 会议上的一篇论文 On the missing heritages of CWK 里写道, “…CWK 并没有将他的修为与财富留给子嗣, 因为当时的 FLXG 并没有被世人所理解. 在无人知晓的时候, CWK 远游四海, 于一个孤岛上独自一人建立了一座巍峨宏伟的神庙, 并将有关 FLXG 的宝藏全部埋葬于此. 然后他又利用不为人知的技术, 使这座荒岛看起来平平无奇, 并且还能避开如今的 FLXG 雷达的探测. 根据当时联合国粮食与农业组织 (FAO) 的记录, 那几年太平洋西部海岸洋葱产量锐减. 我们由此推测 CWK 应该使用了当时比较冷门的一个西方魔法, 可以从洋葱中提取能量从而隐藏海域…”
当我偶然翻到这篇 paper 的时候, 脑子里电光火石般想起来, 自己曾经在滑稽大学图书馆中借走的一本 信息安全导论 中夹着的那一张羊皮纸. 这一瞬间, 我感觉自己的内心里充满了 flag… 没错, 一定是这样的. 这张古老的羊皮纸就是 CWK 留给滑稽大学最宝贵的遗产, 前往 FLXG 神庙的地图!
不愧是 CCF (China CWK Federation) 推荐的 A 类会议, 我一边想, 一边往下读, “…CWK 在神庙里设计了试炼, 只有通过的人才有资格成为他的继承人…据说神庙的设计图被 CWK 用法力嵌入到了指向神庙的地图里. 关于 CWK 的其他许多传闻逐渐都得到了验证, 而这张宝贵的地图却依旧只是传闻, 在历史上从未出现过…”
读至此处, 我心中热血沸腾, 恨不得立马出发, 去 FLXG 神庙一探究竟. 可是转念一想, CWK 出的题目往往都很坑, 又有些踌躇不前. 正好, 最近似乎滑稽大学要举办一个啥比赛, 不若投石问路, 让那些好奇的选手们先去探个险, 看看他们能不能从神庙中站着出来…
------------------------------------------------------------------------------------------------------------
神庙设计图,Get!
TL;DR
Tor,nc with proxy,LSB,抠图
详解
得到一张webp格式的图片。使用官方工具分析得知此 webp 图片为无损压缩。使用 dwebp 转换为 png 格式。注意,如果有选手使用第三方工具转换而导致后续步骤无法进行,请不要抱怨题目有非预期的错误,相反,您应该给这些第三方工具提 issue。
得到 png 后,使用 stegsolve 查看通道。发现绿色通道的 LSB 有明显的隐写痕迹。右下角有一个洋葱地址,中间的神庙区域有明显规律性条纹。实际上这个条纹是因为出题人故意使用 Base64 编码一遍,使得原来二进制中的规律部分更为明显。
我不知道为什么很多人都把这个地址和端口号当作 HTTP 协议。没有任何说明的情况下,一个端口并不应该默认为使用 HTTP 协议。这又不是 80 或者 8080 端口..。正确做法应该是使用 nc 连上去,会发现这实际上是一个类似于 pwnable 的一个交互方式。
至于如何使用洋葱,这里不再详述,请参考官网教程。一般的洋葱客户端会提供一个 9150 或者 9050 端口的 SOCKS 5 代理服务。使用 nc 的参数或者 proxychains-ng 均可接入。
连入后发现提示输入 CRC32。如果输入 webp 图片的 CRC32 会进一步提示设计图纸被藏在图片里。
所以我们转向绿色通道中的神庙区域。思路应该比较明显,需要把这片区域的像素抠出来。
为降低难度,这片区域已经用纯黑色的边框包围,并且保证了区域内没有纯黑色的像素点。一个最简单的做法是,使用 Photoshop 手动选择一小部分,选区 -> 扩大选区,可以将这片区域内的像素点全部选中。查看一下统计信息,可以发现这片区域内有 249024 个像素点。很明显可以被 8 整除。这是一个正面的提示。然后将这片区域粘贴到一个新的全黑色的背景图片上。保存。然后就可以写 python 脚本处理绿色通道的 LSB 了。
(此处省略处理脚本)
脚本得到的是一个 Base64 的字符串。解码后是一个 ELF 文件。在远程输入 Base64 字符串或者 ELF 的 CRC32 均可得到第一个 flxg。
此小技耳
TL;DR
https://gist.github.com/pzread/2ae0bb3aa5fe0dc69fcf3257c41db944 ,bit flipping attack
详解
这道题其实是出题人学习去年 HTICON 里一个技巧的成果 (话说马上又要 HITCON 了)。有两个人做出来有点出人意料(可能有非预期了),不过因为控制好了 SECCOMP,再怎么非预期也不会造成预期之外的危害 23333333
这道题功能很简单。首先输入用户名,判断不能为 root,拼接上 hash 后再使用随机的密钥和 IV 做 AES CBC 加密。另一个函数需要输入结果,然后通过密钥和 IV 解密,再比对 hash 正确性。之后判断用户名是否为 root,如果是 root 就直接给 flag。
这题有几个漏洞:
首先有整数溢出,溢出的后果是 double free。控制 free 的整数只有 8 位,所以 free 被拒绝 128 次后就可以随便 free 了。
其次内存拷贝用的是 strcpy,这个会造成越界。
然后就是密码学上的,bit flipping attack。通过更改 IV 可以更改解密后第一个分组的内容。
最后就是 HITCON 的奇技*巧,二进制中的 memcmp 实际上是 strcmp。
double free 不是用来利用的。实际上要注意到 init 里面会调用 mallopt,设置了这个函数会将 free 的 buffer 填充为 0xAA。而这次 malloc 的 buffer 很小,使用了 tcache 后所以需要 free 七次才行。
然后通过 strcpy,将 0xAA 复制到目标数组。通过 bit flipping attack 得到 root 的用户名和第一个 0xAA。然后需要绕过 hash 检测。所以我们不停的尝试,直到 hash 以 0x00 开头。这样 strcmp 比较两个空字符串会直接返回 0。
exp 见 poc.py,代码见 trial.c
关于如何做到将 memcmp 偷天换日到 strcmp,可以看 HITCON 的那篇 gist。基本原理就是内核计算 PHDR 的偏移错误,所以可以放上两个 PHDR,真 PHDR 中的 PT_DYNAMIC 中 DT_SYMTAB 被修改了。所以 ld.so 解析函数的时候会使用后面的 DT_SYMTAB,而一般的反汇编工具会使用 ELF Spec 下的 DT_SYMTAB。
更详细一点的介绍在这里,http://h3ysatan.blogspot.com/2018/02/quick-notes-hitcon-ctf-2017-qual-elf.html。
------------------------------------------------------------------------------------------------------------
坊间传言 Jeff Dean 在面试 Google 的时候,曾根据公钥心算出 Google 私钥。当然大多数人都把这事当作笑话对待,但只有 Z 同学知道,这是真的。
根据国家计生委未公开的大数据统计,平均每 66666666 位少年少女中就有一位这样的天才,心算能力极为恐怖,可以在线性复杂度内分解任意长度的大整数,因为时间瓶颈主要在于把结果用笔写出来。中国科学技术大学少年班学院成立的主要目的,其实就是为了网罗这样的神童。每当这样的一位天才出现,国家就会将其秘密保护起来,送到国家高性能计算中心作为 ALU,而其同学却只是被告知出国。实际上,曾排 TOP500 榜单第一名的超算“神威-太湖之光”的主要算力其实是由两位这样的天才少年提供,剩余的 10649600 个核心则负责将通用的计算程序规约到大整数分解,以及处理输入输出等等外围工作。
后来,少年班学院的“冰球”,Z 同学的一个好朋友,在离开科大去北大做相关秘密研究之际,告诉了 Z 同学这个消息。Z 同学马上意识到 RSA 实际上并不安全,惊慌失措,立刻删掉了自己所有服务器上面的 RSA 公钥,大喊:“不能再公开 RSA 中的 n 了!我们必须立即以一种新的方式使用 RSA!”。在连续几个通宵的苦思冥想后,Z 同学写了一段 python 代码,用他改进过的 RSA 算法加密了一段消息。新的算法并没有透露 n,只给定了两个大整数:(p*q)^(p+q) 和 (p*q)^(p-q),其中 ^ 是按位异或运算。
“终于能睡个安稳觉了”,Z 同学如是想。在上床之前,Z 同学告诉你,除非这个新的算法有漏洞,否则不要打扰他的休眠。而喜欢恶作剧的你,能找到正当理由叫醒------------------------------------------------------------------------------------------------------------
正在熟睡的 Z 同学吗?
我出这道题的灵感来自于某个国际比赛的一道 RSA 题目,那道题目的解法也是逐位爆破,具体的题目找不到了,欢迎知道的同学告诉我是哪题。
RSA 基本知识
请参见 RSA 基本介绍 - CTF Wiki
分析
题目代码很简单,除去空行,连 10 行都不到
import sympy p = sympy.randprime(2 ** 1023, 2 ** 1024) q = sympy.randprime(2 ** 1023, 2 ** 1024) a = (p * q) ^ (p + q) b = (p * q) ^ (p - q) flag = open(‘flag.txt‘, ‘rb‘).read() m = int.from_bytes(flag, ‘big‘) print(a, b, pow(m, 65537, p * q))
代码中随机生成两个大素数 p 和 q,然后计算出 a = (p * q) ^ (p + q) 和 b = (p * q) ^ (p - q),其中 ^ 是按位异或运算。然后,读取 flag 文件的内容并且转换成一个大整数 m,再输出 a、b 和用参数 n = p * q, e = 65537 的 RSA 加密后的 flag。
要解密 flag,我们只能求出 p 和 q,然后算出 RSA 的私钥。可是,因为异或运算的存在,我们从 a 和 b 很难用数学推导的方法来解出 p 和 q。
解答
解法 1
根据题目描述
根据国家计生委未公开的大数据统计,平均每 66666666 位少年少女中就有一位这样的天才,心算能力极为恐怖,可以在线性复杂度内分解任意长度的大整数,因为时间瓶颈主要在于把结果用笔写出来。中国科学技术大学少年班学院成立的主要目的,其实就是为了网罗这样的神童。
我们去少年班学院找出一位这样的天才,然后让他/她心算求解即可得到 flag。
解法 2
我们定义 f1(x, y) = (x * y) ^ (x + y) 和 f2(x, y) = (x * y) ^ (x - y),我们发现这两个函数都有一个共同的性质,就是函数值的最低 n 个二进制位只和 x、y 的最低 n 个二进制位有关。也就是说,我们可以用 a 和 b 的最低 n 位来判断 p 和 q 的最低 n 位是否可能正确。如果它们的最低 n 位满足 f1 和 f2 函数,那么它们就是 p 和 q 低位的候选答案;如果不满足,它们就根本不可能是真正 p 和 q 的低位。
所以我们可以从一个二进制位(n=1)开始,每次增加一位。每增加一位时,我们把原来满足条件的 p 和 q 低位的每种可能情况分别在前面加上 0 或 1,这样每种情况就变成了 4 种新的情况,然后对所有新的情况用 f1 和 f2 函数提供的约束条件进行过滤,只保留满足条件的情况。当跑到 1024 位的时候,就只会剩下真正满足条件的 p 和 q 了。
然后,我们根据 RSA 的原理,在 mod (p-1)*(q-1) 的意义下对 e 求逆元,得到私钥 d,计算 pow(c, d, p * q) 即可得到 flag 的大整数表示。
这个问题的巧妙之处在于,每增加一位,候选答案的数量会变成 4 倍,但同时根据数学期望,正好会有 1/4 的候选答案被过滤后保留下来,所以程序的运行时间不会指数爆炸。
求解脚本如下:
#!/usr/bin/env python3 import gmpy a, b, c = [int(s) for s in open(‘output.txt‘).read().split()] f1 = lambda p, q: (p * q) ^ (p + q) f2 = lambda p, q: (p * q) ^ (p - q) candidates = {(0, 0)} for m in range(1025): print(m, len(candidates)) candidates_ = set() mask = (2 << m) - 1 for x, y in candidates: if f1(x, y) == a and f2(x, y) == b: p, q = x, y d = gmpy.invert(65537, (p - 1) * (q - 1)) m = pow(c, d, p * q) print(bytes.fromhex(hex(m)[2:])) exit() for bx in range(2): for by in range(2): xx = x + (bx << m) yy = y + (by << m) if f1(xx, yy) & mask != a & mask: continue if f2(xx, yy) & mask != b & mask: continue candidates_.add((xx, yy)) candidates = candidates_
最后吐槽一下 github 的 markdown 不能加 latex 数学公式。
彩蛋
题目中的 p 和 q 真的是用随机数生成出来的吗?Z 同学会不会在里面隐藏了什么信息?不要问我,我不知道
------------------------------------------------------------------------------------------------------------
作为一个专业的HiFi发烧友,你不仅要会欣赏音乐,还要有扎实的数理基础,精通声学、电子、程序设计。请用你专业的HiFi知识解读题目所给的图片并得到flag。
------------------------------------------------------------------------------------------------------------
根据题目所给 exe 的文件名显然可以看出是一个隐写程序,尝试执行程序,得到信息:
Usage: stegan in.wav in.bmp out.bmp
于是猜测是将音频隐写于所给的图片中. 用 IDA 打开 stegan.exe,F5 反编译,发现四个导出了符号的函数:put_bit, put_uint32, linear_resample, delta_sigma_modulate.Google 搜索 Delta-Sigma Modulation,进入 Wikipedia 页面后发现一张和所给图片中电路一样的电路图,于是猜测 delta_sigma_modulate 函数为对该电路的模拟,并且发现图片中所给标志 DSD 是一种利用 Delta-Sigma Modulation 编码的文件格式. 查阅资料后发现 DSD 是把信号重采样 64 倍后经过 Delta-Sigma Modulation 得到比特流,与 stegan.exe 的行为相符. 再看 put_bit 和 put_uint32 的代码,发现 put_bit 是在一个字节的 LSB 隐写一个 bit, 而 put_uint32 是把一个整数连续隐写到 32 个字节中. 于是可以得知数据隐写在位图的 LSB 中. 又根据 BMP 文件头可以得知,程序会跳过 BMP 的文件头,从实际的图像数据 (偏移量为 BMP 文件头的 bfOffBits) 开始隐写,并且先写入 DSD 比特流的长度,再写入比特流. Google 搜索 DSF File Specification 可以搜到 DSF(DSD Stream File)的标准 DSF File Specification,据此可写出提取 DSF 文件的代码.
#include <stdio.h> #include <math.h> #include <string.h> #include <stdlib.h> #include <stddef.h> #include <inttypes.h> struct DSDHeader { char chunk_header[4]; uint64_t chunk_size; uint64_t file_size; uint64_t pointer_metadata; } __attribute__ ((packed)); struct FMTHeader { char chunk_header[4]; uint64_t chunk_size; uint32_t format_version; uint32_t format_id; uint32_t channel_type; uint32_t channel_num; uint32_t sample_rate; uint32_t bits_per_sample; uint64_t sample_count; uint32_t block_size_per_channel; uint32_t reserved; } __attribute__ ((packed)); struct DataHeader { char chunk_header[4]; uint64_t chunk_size; } __attribute__ ((packed)); void write_dsd(size_t length, const uint8_t *bitstream, const char *filename) { FILE *file = fopen(filename, "wb"); size_t total_file_size = 52 + 28 + length / 8 + 12; struct DSDHeader dsdheader = { {‘D‘, ‘S‘, ‘D‘, ‘ ‘}, 28, total_file_size, 0 }; fwrite((const void *)&dsdheader, sizeof(struct DSDHeader), 1, file); struct FMTHeader fmtheader = { {‘f‘, ‘m‘, ‘t‘, ‘ ‘}, 52, 1, 0, 1, 1, 2822400, 1, length, 4096, 0 }; fwrite((const void *)&fmtheader, sizeof(struct FMTHeader), 1, file); struct DataHeader dataheader = { {‘d‘, ‘a‘, ‘t‘, ‘a‘}, 12 + length / 8 }; fwrite((const void *)&dataheader, sizeof(struct DataHeader), 1, file); fwrite((const void*)bitstream, 1, length / 8, file); fclose(file); } struct BMPFileHeader { uint16_t type; uint32_t size; uint16_t reserved1; uint16_t reserved2; uint32_t offset; } __attribute__ ((packed)); uint8_t get_bit(const uint8_t *buffer, size_t pos) { return buffer[pos] & 1; } uint32_t get_uint32(const uint8_t *buffer, size_t pos) { uint32_t res = 0; for (int i = 0; i < 32; ++i) { res += get_bit(buffer, pos + i) * (1 << i); } return res; } int main(int argc, char **argv) { const char *bmp_filename = argv[1]; const char *out_filename = argv[2]; FILE *bmp_file = fopen(bmp_filename, "rb"); if (!bmp_file) { printf("Cannot open bitmap file.\n"); return 0; } struct BMPFileHeader bmp_header; fread(&bmp_header, sizeof(struct BMPFileHeader), 1, bmp_file); size_t data_size = bmp_header.size - bmp_header.offset; uint8_t *bmp_data = (uint8_t *)malloc(sizeof(uint8_t) * data_size); fseek(bmp_file, bmp_header.offset, SEEK_SET); fread(bmp_data, sizeof(uint8_t), data_size, bmp_file); size_t length = get_uint32(bmp_data, 0); size_t bytes = length / 8; printf("%d\n", length); uint8_t *bitstream = (uint8_t *)malloc(sizeof(uint8_t) * bytes); for (size_t i = 0; i < bytes; ++i) { bitstream[i] = get_bit(bmp_data, 32 + i * 8) << 7; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 1) << 6; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 2) << 5; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 3) << 4; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 4) << 3; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 5) << 2; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 6) << 1; bitstream[i] += get_bit(bmp_data, 32 + i * 8 + 7); } write_dsd(length, bitstream, out_filename); fclose(bmp_file); free(bmp_data); free(bitstream); return 0; }
提取出来后用播放器播放,发现是 DTMF 拨号声,识别后可得
102#108#97#103#123#102#105#114#101#95#119#97#116#101#114#95#110#117#99#108#101#97#114#125
显然表示的是一串 ASCII 码,处理后即可得 flag{fire_water_nuclear}.
9、一些宇宙真理
------------------------------------------------------------------------------------------------------------
大学最关键的就是数学物理基础。除了数学物理基础之外,其他的都是数学物理原理的推论。像无线通信这样的东西,没有任何的原创性,一点儿原创性都没有,不需要任何思考,无非是一群工贼偷走了物理学家做的发电机,然后办了一些会议,让别的人 fork,照着这台机器模仿,做出来了就去拿什么爱拽补一的什么奖章。
换句话说,工科的原创性研究基本是没有的,无非就是到哪个地方去抄点东西。遇到什么工程难题,百度、谷歌,大不了到知乎上面提问。理科的研究人员是真正的思考者,而工科的研究人员无非是一群玩乐高玩具的人。这些玩具还不知道是从哪家小孩那里抢过来的。现在互联网通畅了,我看他想搞个轮子,就随便就近找个网站就有了。
最近一些新闻在讲,人工智能会逐步代替人类,我看工科人都首当其冲。只要我们写个自动连接搜索引擎的脚本,让它搜索“怎么做‘一些宇宙真理’这道题”,它自己东找到西找到,不用几分钟就能拼拼凑凑找到答案。我看这样的人工智能,也不用几行代码,又比工科人便宜,还比他们诚实可靠。
今天他们工科的人说什么有人发明了斯纳德,说这个东西特别好。我看啊,又是从哪里复制粘贴过来的。工科人怎么可能会从零开始呢?他们没有这个能力。
------------------------------------------------------------------------------------------------------------
问题背景
我们先介绍问题背景。小工产生了一个哈希对 (H_1, H_2, X),这些哈希对符合下面的三个条件。
存在 R_1 是 H_1 的原像
存在 R_2 是 H_2 的原像
R_1 = R_2 XOR X
小工希望向小理证明:它的这个哈希对 (H_1, H_2, X) 符合上述的条件;但是小工不希望告诉小理 R_1 和 R_2 是什么。
换句话说,我们需要两个性质:
小理相信小工产生的哈希对符合上面的三个条件。
小理不知道 R_1 和 R_2。
解决问题的方法:非交互式零知识证明
本题考虑一种方法 -- 非交互式零知识证明(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge, zkSNARK)。这种证明方法在著名的区块链货币 Zcash 里面有充分应用,他可以实现完全匿名的区块链货币交易。未来,这种技术还会用于实现支持保密智能合约的区块链系统。
zkSNARK 是这样工作的:小工知道有一个算法 F(H_1, H_2, X, R_1, R_2) 能够快速验证这个哈希对是否满足条件。他根据这个算法产生几个数字,而这几个数字就蕴含着证明哈希对满足条件的神秘力量,但是却能够隐藏 R_1 和 R_2。我们记这些数字为一个证明 v。
如果小工告诉小理 (H_1, H_2, X, v),当小理对 v 和其他参数进行某些计算之后,小理就能够相信小工提供的 (H_1, H_2, X) 满足条件。在这个过程里面,R_1 和 R_2 没有泄露给小理。
回到这个问题
上述描述的过程里面,小理验证这个证明是否有效,需要一个校验密钥,这个密钥我们称为 vk。我们在压缩包 vkandproofs.zip 里面提供了这个文件。 我们产生了 40 个有效的证明和 40 个无效的证明。它们就混在这 80 个证明文件里面。
你现在需要补全这个 github repo 里面缺少的验证算法,然后利用这个验证算法来找出哪些证明是有效的。
如果第 i 个证明是有效的,那么我们记 b_i = 1,否则记 b_i = 0。flag 的格式为 flag{b_1 b_2 ... b_80}。
例如 flag 可能是 flag{11010101010101010101010101010101....1010101}
有关的代码
https://github.com/weikengchen/lightning_circuit
Hint
这道题是一道哲学题,整个题目都在传达一个意思:不要重复发明轮子。
这道题并不要求参赛者知道 SNARK 的原理。
选手只需要按照步骤编译 https://github.com/weikengchen/lightning_circuit ,然后根据 GitHub 历史或者 Sean 的 repo https://github.com/ebfull/lightning_circuit 就可以完成验证部分的代码。
具体来说,我们补全导入验证密钥的代码如下:
// verify r1cs_ppzksnark_verification_key<default_r1cs_ppzksnark_pp> verificationKey_in; ifstream fileIn("vk"); stringstream verificationKeyFromFile; if (fileIn) { verificationKeyFromFile << fileIn.rdbuf(); fileIn.close(); } verificationKeyFromFile >> verificationKey_in; bool res = run_verify(verificationKey_in);
然后我们实现的 run_verify 的代码如下,均是模仿 ebfull/lightning_circuit 的代码:
bool run_verify(r1cs_ppzksnark_verification_key<default_r1cs_ppzksnark_pp> &vk) { // some code deleted intentionally // return true; std::vector<bool> h1_bv(256); std::vector<bool> h2_bv(256); std::vector<bool> x_bv(256); { h1_bv = int_list_to_bits({169, 231, 96, 189, 221, 234, 240, 85, 213, 187, 236, 114, 100, 185, 130, 86, 231, 29, 123, 196, 57, 225, 159, 216, 34, 190, 123, 97, 14, 57, 180, 120}, 8); h2_bv = int_list_to_bits({253, 199, 66, 55, 24, 155, 80, 121, 138, 60, 36, 201, 186, 221, 164, 65, 194, 53, 192, 159, 252, 7, 194, 24, 200, 217, 57, 55, 45, 204, 71, 9}, 8); x_bv = int_list_to_bits({122, 98, 227, 172, 61, 124, 6, 226, 115, 70, 192, 164, 29, 38, 29, 199, 205, 180, 109, 59, 126, 216, 144, 115, 183, 112, 152, 41, 35, 218, 1, 76}, 8); } for (int i = 1; i <= 80; i++){ stringstream proofFileNameStream; proofFileNameStream << "toolkit/proof_" << i; std::string proofFileName = proofFileNameStream.str(); stringstream proofStream; ifstream fileIn(proofFileName); if (fileIn) { proofStream << fileIn.rdbuf(); fileIn.close(); } r1cs_ppzksnark_proof<default_r1cs_ppzksnark_pp> proof; proofStream >> proof; if(verify_proof(vk, proof, h1_bv, h2_bv, x_bv)){ fprintf(stderr, "1"); }else{ fprintf(stderr, "0"); } } fprintf(stderr,"\n"); }
10、对抗深渊
------------------------------------------------------------------------------------------------------------
40万年后的一个平静下午,人类终将回想起,那无生命之物并未将永远安息,在诡秘的万古中即便死寂也会消逝。深渊终将降临。
(剧情警告)
故事发生在深度纪元42年,人类社会彻底沦为一个被机器人统治的反乌托邦社会。在历史学家这个职业还没有消亡的年代,业界公认为机器人的真正崛起可以追述到200多年前。在那个欣欣向荣的时代,人类开始涉足一个卓越的科技领域————“深度学习”。论文发表数量在最初的几年如潮水般快速上涨,其影响很快超出了计算机领域本身,蔓延到物理化学医学生物能源等领域。随后不知道过了多少年,就当人类认为自己已经完全掌握了深度学习技术时,出现了一次大规模的机器人故障————因为深度学习算法的核心组件“神经网络”的黑箱性质,难以解释,所以这次事故的原因不明。随后故障规模的急剧增大引发了蝴蝶效应,像多米诺骨牌一样影响到了各个国家间的战略平衡,人类发动了第一次也可能是最后一次大规模机器人战争,战争中人类被迫将大量能源和资源投入到战争机器的研发上,即使各国都知道现在的技术并不真正可控。后来发生的事情已经鲜为人知,直到深度纪元开始,人类才意识到自己已经不是世界的主人。随后人类开始了连绵不断的希望渺茫的反抗,由于此时已经很少有人了解深度学习背后的原理,人类在抗争中处境艰难————是啊,在任何一个时代理解技术的人都远远少于享受技术的人。
(下面是OJ式的故事写法)
K,一个反抗者,认为反乌托邦世界最大的枷锁是对自由信息交换的限制,因此K一直希望能够通过数字传递给队友一些关键信息。K先试着使用隐写术和一般的加密算法,不过因为机器人尤其擅长密码学,这些操作都失败了。
在几周前,事情有了转机。K收到了某个不知名的同伴收集到的一些关键的代码(这个同伴之后消失了),并交代了一些参考资料:
Pytorch
Examples
下面是重点内容
此外他给了K一些和深度相关的关键代码(使用python语言,建议3.5及以上版本):
main.py: 机器人内部使用的训练代码,训练后的参数用于识别信件上的手写数字。运行 main.py 来进行训练,训练结束(大概十几分钟)后将获得参数文件 model.pth。不过为了防止因为跨平台等原因造成参数不同,题目中附上了参数文件。
adversarial.py: 同伴完成一半的解决方案。
target.png: 实验目标图像,为一个灰度的‘6’。
K需要参考 main.py 完成 adversarial.py。
解决方案希望解决的问题是:给定一个‘6’ (600*600像素),K能够使得这个图像人类看上去仍然是‘6’,但是机器阅读时会和其他数字混淆。另外机器有很强的反作弊装置,将图像像素值归一化到[0,1]之后,如果篡改超过以下 任何一个 程度就会被机器发现:
篡改的像素数量超过总数量的 0.2%
篡改前后的平均绝对误差(L1 loss)超过 0.001
存在任何一个像素篡改的值的变化程度超过 0.2
为了方便解题,这部分检查已经内置到了 adversarial.py 中。K运行 adversarial.py 并顺利通过检查后,将生成的图片 sample.png 提交到本题对应的网站上就可以改变现实,获得flag。
请你帮助K完成这一部分内容。
注:此题需要参赛者了解和学习 python, pytorch, 数字图像处理, 机器学习&深度学习相关的知识。如果较长时间没有参赛者完成,我们可能会适当修改题目。此外,此题不涉及Web漏洞利用等。
解题网站:link
------------------------------------------------------------------------------------------------------------
(如果只想了解解法请快速下划)
问题介绍
这是本次比赛唯一的机器学习相关题目,也是Hackergame第一次正式出此类题目。
本次题目的核心是“对抗样本(adversarial examples)”。对抗样本是攻击者有意构造的使得模型出错的输入样本,这些样本会让模型出现“错觉“,输出不符合图片中内容的结果。需要注意的是很多时候,特别在机器学习领域,我们并没有“绝对正确的答案”(比如某个奇怪的数字是不是2),都是相对某个标准而言。在和机器对比时,我们将标准设为一般人类的认知。对抗样本表现出的主要危害是,其可以在人类所不能察觉的情况下误导机器的判断,如果类似情形发生在自动驾驶系统中,危害会很大。
一个基本的事实是,所有的机器学习模型都存在对抗样本,甚至包括人本身。对人来说,典型的对抗样本包括各类视觉错觉,以及各类幻听。
假设人眼的分辨率为5K(5120 * 3200),颜色感知范围为”真彩色(24bit位元色彩编码)”,则理论上存在 1.7e+114688000 种不同的输入(样本),比可观测宇宙中的原子个数还大114,687,920个数量级。即使计上视觉系统出现以来,所有存在视觉的生物,所有经历的年代中,所有个体接受的所有图像(刷新率以60Hz计),其数目与之相比仍然可以忽略不计。而一个人在成长中所接受的的图像(样本)数目,即使相对于这忽略不计的数目也是微不足道。用 Bloodborne 中的一句话来说,“Our eyes are yet to open”,敬畏深渊吧/滑稽。
这种已经认知的样本数目和可能存在的样本数目之间过于夸张的差距,由进化和学习填补。进化决定了模型的结构和超参数 (Hyperparameter),而学习决定了模型的参数。我们使用特殊的算法,利用已有的少量样本来推测大部分样本的性质,这就是一种广义的机器学习。而实际推测的效果我们称为“泛化能力(generalization ability)”。令人惊讶的是,我们的泛化能力相当的好,我们的视觉系统就是一个典型的例子;而另外一个典型的例子是我们的科学研究,迄今依赖我们几乎所有的研究数据都出自地球(除了极少量来自于一些卫星),但是相当多的部分都被证实适用于整个可观测宇宙,这就相当于,我们是用一粒沙子推测整个沙漠并且相当成功,这难以被称为巧合。不过这种超强的外推能力并不是免费的午餐(除非我们真的发现了这种情况),对抗样本就可以认为是代价之一。少量的样本加上模型本身的问题会带来大量的偏差(bias),从而不同模型对某些特定样本会有非常不同的结果。一个极端例子是,仅给你一个点,让测试者画出经过这个点的一个曲线,这个时候曲线的形状就完全取决于测试者的成见。
近年来机器学习的一大热点是深度学习(deep learning)。深度学习利用深度神经网络(deep neural network)作为其主要模型,在视觉,语音,自然语言处理等领域表现出了惊人的泛化能力。作为机器学习模型,其自然拥有对抗样本。然而,人们发现一个严重的问题在于,其对抗样本与人类的经验非常不符。如果我们使用特殊的算法,就可以加入人类难以察觉或者觉得完全没有意义的扰动,让神经网络输出完全不同的结果:
这个问题看上去是神经网络不够robust,会因为小的扰动极大的改变结果。但是大部分试图让神经网络robust来克服对抗样本的尝试都轻微降低了神经网络的泛化能力。
事实是问题不在于小的扰动本身,下面的例子显示可以通过小的扰动完全改变图片的意义:
所以更加本质的问题是神经网络和人类的视觉系统使用了并不一致的方式来处理图像。其中一点是讨论什么是相对于人类视觉的 robust features,即什么样特征或者扰动对人类来说是有意义的。目前学界并没有搞清楚这个问题,甚至这个问题本身可能是 AI-Complete 的或者超出了视觉的范畴。
攻击和防御
典型的几个攻击手段利用了神经网络的梯度做文章。既然神经网络利用反向传播梯度进行优化,那么自然可以采用梯度来负向优化。
我们可以定义这样的一个目标函数:使得对抗样本和原样本的n-范数尽可能小(即对人看来差异尽可能小),同时使得对抗样本尽可能降低神经网络对正确结果的输出值。这个目标函数显然是可导的,所以我们可以利用这个目标函数通过优化手段求解对抗样本。当然优化过程是需要迭代的。
另外一种方法是直接求解负向优化对于原样本的梯度,然后通过一个sign函数(将正数映射到+1,负数映射到-1),乘上一个比值 epsilon,并加到原样本上,得到所需的对抗样本。这种方法称为FGSM(Fast Gradient Sign Method)。这个方法约等于只进行了一步迭代的优化,所以效果不如直接优化好,但是好处是速度快且可以精准控制像素改变的最大值。这是本题推荐的方法。
那么如何防御对抗样本攻击呢?
最简单的方法之一是“负向优化负向优化”,这也称为对抗样本训练。我们可以在训练神经网络的时候,有意加入可能的对抗样本作为负例,强制网络学习它们。但是这种方法的问题是,如果对方算力足够,就可以“负向优化负向优化负向优化”,即产生新的对抗样本,其能够对用对抗样本训练的网络进行攻击。可以看出这个过程实际上是个Min-Max游戏,算力最足的一方可以笑到最后,所以并不能有足够的安全性保障。
然后一种方法是 Defensive distillation,其试图让所有的分类结果独立,如果攻击者试图按照以前的假定,即所有分类的输出概率是归一化的,就很可能不能达到效果。但是如果攻击者采用更多算力,自己进行distillation并针对这类模型进行攻击,则依然不能解决问题。
近期相对热门且有一定理论保证的方法是 Gradient Masking。这种方法依据是对抗样本往往需要梯度,且往往是微小的扰动,如果我们采用一些办法离散化梯度,就可以阻止求导(离散化后不连续了),且离散化的取整操作会“消灭”一些微小的扰动。不过现在此类方法仍然存在争议,人们发现可以通过训练一个平滑化的对应模型,并将结果迁移到离散化的模型进行攻击,除非离散化能够自动避开所有的对抗样本(这在逻辑上是说不通的)。
目前最好的防御方法可能就是保持模型的黑箱特性(不对公众开放模型)。这样攻击者就不能利用梯度信息,只能进行猜测,难度大大增加。另外对抗样本本身的泛化能力较弱,造成模型出错的对抗样本可能加入少许的硬件噪声就会完全无效;同样地改变视角、缩放图像、光照条件也会极大地削弱对抗样本的性能。所以目前仍然有很多公司并未考虑对抗样本的危害,而目前研究的热点之一也在于如何产生现实中对环境robust的对抗样本。
题解
解法 1
暴力枚举所有可能结果。以每秒 1000 次尝试计,不超过 174900185917744396839444704700371442022073601886260538841237614035267595364796290592613521116939643196149952230826228146139239732089317995927927178492867409123625336636770038146671169983108560933075329551372 年就可以获得结果!
解法 2
本题其实有3个考点,需要成功就需要翻越“三座大山”。
第一座是机器学习的数据预处理,预处理往往可以加快训练收敛并提升效果。在 main.py 中,一个预处理是减去均值,并除以标准差:
train_loader = torch.utils.data.DataLoader( datasets.MNIST(‘data‘, train=True, download=True, transform=transforms.Compose([ transforms.Resize((30, 30)), transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)), ])),
所以在测试阶段我们需要补上这部分预处理才能保持结果一致(这就是Part2的内容):
# A solution for part 2
target = (target - 0.1307) / 0.3081
sample = (sample - 0.1307) / 0.3081
第二座是图像缩放。如果仔细阅读代码可以发现原来 600 * 600 的图像经过缩放变成 30 * 30 后才输入到网络中。图像缩放会破坏对抗样本,所以不能直接利用原图像生成对抗样本,而是利用缩放的图像。更何况我们要求原图像中更改不超过千分之三,这个直接用优化很难做到。
图像缩放在本题中不是故意构造的。现实中的输入对于神经网络来说太大,处理缓慢,且可能和训练时的尺寸不同,这个时候缩放是必然的。
仔细观察缩放图像的代码:
def preprocess_image(arr): image = convert2image(arr) image = image.resize((30, 30), resample=Image.NEAREST) return convert2tensor(image).reshape(1, 1, 30 ,30)
resample=Image.NEAREST 其实是非常强的提示了,因为这种缩放的方法是取原图 20 * 20 的 block 的中心像素构成大小为 30 * 30 的输入图像。这个 30 * 30 的小图才是我们真正的对抗样本。之后注意把这 30 * 30 的小图放大后 patch 到大图上面。
(我原来准备不改 resample 直接用 bilinear 缩放来着,需要选手逆向 bilinear,但是考虑到可能会进一步加大题目难度,就改用了简单的 NEAREST)。NEAREST 是直接的下采样过程,对自然图像会造成严重的混叠(Shannon 采样定理),所以现实中一般不直接使用。
第三座就是对抗样本,直接参考 repo,用 FGSM 来做就行。所以这两部分构成了 Part 1 的答案:
# A solution for part 1 inputs.requires_grad = True x = (inputs - 0.1307) / 0.3081 # scale mean & std output = model(x) loss = F.nll_loss(output, label) loss.backward() # compute gradient x_grad = torch.sign(inputs.grad.data) epsilon = 0.18 inputs = torch.clamp(inputs + epsilon * x_grad, 0, 1) # FGSM inputs = inputs.reshape(30, 30) for i in range(30): for j in range(30): image[int((i + 0.5) * 20), int((j + 0.5) * 20)] = inputs[i, j] # patch to original image
另外,本次用 pytorch 而不是 tensorflow 的原因是,个人觉得 pytorch 算梯度更简单,两三行代码搞定。
原文地址:https://www.cnblogs.com/packy/p/9905405.html