天天看点

中国科学技术大学第五届信息安全大赛(hackergame2018自我总结)2

这一批题都是我不会的,只能把官方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

3、加密算法和解密算法

------------------------------------------------------------------------------------------------------------

提示:本题中使用的 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:5
note: 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 的……其实我也很希望能看到用其他(我)想象不到的方式做出这道题的题解。

没人觉得,「她」所在的文学社似乎不是很文学吗