RCTF 2022 RE wp
今年的题目一言难尽啊……没活可以咬打火机而不是用 rust 来折磨我
题目附件下载
CheckYourKey
JNI_onLoad 将 sub_8965 注册为 ooxx,输入先后经过如下变化
- sub_FB40:标准 AES-128-ECB
- sub_F7DC:标准 base58
- sub_13788:变表 base64
最后与目标值比较,逐个逆回去即可
web_run
通过 js 发现是 wasm 逆向题,wasm 的入口是 _main,且定义了 一些交互接口
先改个后缀
- .1 是 html
- .2 是 wasm
jeb 分析 ez_ca.wasm,还原 _f11 部分符号:
get_input_time 首先接收 20 个字符(fd_read 的实现里会自动加换行,占用一个字符),前 16 个必须满足 %llu/%llu/%llu %llu:%llu
的格式,转成 int 形式后不能是 202211110054:
之后 generate_serial 函数依据输入的时间生成序列号,要求第二次输入必须与计算得到的序列号相同。但是由于 0xA20 这个地址中的内容恒为 0,因此不管输什么时间进去,就算序列号对了,也会输出 right value,But the time is not the time I want to hide
。
观察到代码中判断时间为 202211110054 时会退出执行,不产生序列号。于是猜测题目将 2022/11/11 00:54 这个时间对应的序列号作为 flag,故分析 generate_serial 并自行实现就可以求得 flag 了(也可以 patch wasm)。
1 | def tohex(v: int): |
huowang
有两个迷宫,第一个是用 unicorn 跑的,里面碰壁了就会触发 exit 系统调用(0x3c),走到终点就会触发 write 系统调用(1),这个地图不太好提出来。
而第二个迷宫就是常规迷宫了,注意到二者用到的输入序列相同,所以先把第二个迷宫所有路径求出来,再一个个试就行了。
找个脚本小改一下输出迷宫 2 的所有路径:
1 |
|
将输出的路径存放到 path.txt 中,再用脚本爆破即可:
1 | from subprocess import * |
picStore(re)
动调跟 luaL_loadfilex,发现程序修改了 lua 引擎的 LoadByte, LoadInt, LoadInteger, LoadNumber 函数,对经过这四个函数的每个读出的字节,都会进行如下判断和变换:
按照相同的改法修改 Lua5.3.3 代码并重新编译,替换 luadec 自带的 lua-5.3 项目,反编译失败,只能拿到反汇编代码。因此考虑更换 unluac 工具,在修改了的 lua 交互执行环境中用 string.dump 将明文的 picStore.luac dump 出来,再用 unluac 反编译并进行代码美化。
其中和 re 有关的函数有两个,分别是 check_impl 和 check_func。check_impl 将 30 个 note 中的字符读出拼接并传入 check_func 进行检查:
1 | function check_impl() |
check_func 初始化了一个长度为 256 的 s 盒,对每个字符异或特定值后进行置换,将结果数组传入 check_result_23_impl 进行检查:
1 | function check_func(A0_2) |
check_result_23_impl 在 elf 中实现,其中实际调用 chk_23 来检查加密后的数组:
可以看出就是一连串的等式约束,写个 z3 脚本再解密回去即可:
1 | from z3 import * |
rdefender
有符号的 rustc 程序,其中定义了两个向量数组,记作 vec1 和 vec2,它们的最大元素个数都是 16。
首先将本地 flag 文件内容读取到向量,中作为 vec1 的第一项,再进入一个 while 1 循环,根据接收到的 8 字节指令执行不同的功能(根据指令的第一字节,可以判断要执行哪个功能)。
对于功能 1,其对应的 8 字节结构体为:
1 | struct { |
该功能可以向 flag 所在的 vec1 中插入新向量,向量内容为通过 get_data 函数获得的用户输入及 str 字段内容。
对于功能 3,其对应的 8 字节结构体为:
1 | struct { |
该功能可以向 vec2 中插入新向量,向量内容为通过 get_data 函数获得的用户输入及 check_type 字段内容。
对于功能 2,其对应的 8 字节结构体为:
1 | struct { |
功能 2 是程序的核心部分,其根据指定的 vec1 和 vec2 下标,从二者中分别取出 v1 = vec1[idx1] 及 v2 = vec2[idx2] ,根据 v2.check_type 字段值来决定如何进行 check。
check 方式同样有三种,以下记 v1 和 v2 中的用户输入为 data1 和 data2。
第一种(v2.check_type == 0)伪代码如下:
1 | from functools import reduce |
第二种(v2.check_type == 1)伪代码如下:
1 | def check2(data1: bytes, data2: bytes): |
由第二种功能可以确定 data1 中含有哪些字符,如果 data1 中字符不重复且比较短的话倒是可以结合功能 1 来确定这些字符的排列顺序。
第三种(v2.check_type == 2)实现了一个基于栈的小型 vm,将 data2 作为代码段,data1 作为数据段来运行。其中比较有用的是以下几条:
opcode | 指令长度 | 指令构成 | 伪代码 | 描述 |
---|---|---|---|---|
0 | 2 | [opcode] [operand] | push operand | 压入立即数 |
1 | 2 | [opcode] [operand] | push data1[operand] | 压入 data1 第 operand 项 |
3 | 2 | [opcode] [operand] | push stack[top] ? stack[top - 1] ; ?为 {+, -, *, /, &, |, ^} 中的一个 | 将栈顶两个元素弹出,进行运算后将结果压入 |
5 | 1 | [opcode] | return stack[top] == 0 | 结束虚拟机运行,返回栈顶元素是否为 0 |
因此,可以构造这样一个爆破思路:将 flag 每个字符与猜测的立即数进行减或者异或运算,结束虚拟机运行,再根据服务器响应的返回值来判断猜测是否正确。
代码实现:
1 | from pwn import * |
checkserver
本题基于 netlink 构建了一个 webserver,客户端对应的处理函数是 sub_401D40:
将接收到的 HTTP 请求报文传入 do_response 函数(sub_404FB0),再传入 sub_404A10 进行 HTTP 报文解析。
如果请求体中含有 authcookie 键值对,则调用 sub_404530,并传入解析得到的 authcookie 值:
地址 0x4045E5 处调用加密函数 sub_402040 对输入进行加密,该函数有非常明显的流加密特征,故可通过测试样例动调获取异或流。加密的结果在 0x4046C1 处与目标数组进行比较。
解题脚本:
1 | plain = b'a' * 64 |
RTTT
无符号的 rustc 程序,但是加密比较简单。程序先接收输入,将输入序列构建成一棵树,再对其进行遍历(sub_DBC0),遍历得到的结果只是字节位置发生了变化,可以通过测试样例找到它们的映射关系。
之后做 RC4 流加密(sub_E310),key 是两个数组异或得到的。
解题脚本:
1 | from Crypto.Cipher import ARC4 |