StarCTF Reverse writeUp
*CTF Reverse writeUp
由于水平问题,这次只做出来三道 RE 题,分别是 ChineseGame、stream、wherekey,其它 RE 题目等我复现官方 WP 后再来写。
题目下载
files.zipChineseGame
题目名提示中国游戏,但是我做完这题之后也不知道到底是什么中国游戏23333。
这道题目的 binary 静态链接的 libc 库,没有符号,所以要手动的判断某些库函数。
整体结构
- 有一个链表结构的初始化数组A
- 输入字符串为只含有01的字符串
- 01 序列对应两种操作: sub_404F06、 sub_404DD6,两种操作的对象是A数组
- 执行完所有01操作后,数组A中的值必须全部小于100
识别部分库函数
先识别出几个库函数
sub_4060D0: malloc
分析方法
找一下该函数的子函数,发现如下字符串,说明这个函数可能是 malloc,并且参数是一个数字,返回值是指针。

sub_4C7600(): rand
分析方法:
- sub_4C7600() % 100 + 101 这种调用方式,自然联想到取某个范围内的随机数
- 递归进入分析该函数,发现某个子函数中有随机数方程

一般而言,随机数的种子相同,生成的随机数序列也相同。进过初步分析,该程序用的随机数种子为 0
其它几个函数主要是 C++ string 对象的函数,对于这种,一般可以通过观察调试时的数据确认。
链表结构初始化数组A
main 函数中调用初始化函数 sub_4052B4(v4, 10); 该函数对 v4 链表初始化。
v4 是一个链表,结构如下
1 |
|
在 sub_4052B4 可以分析 Node 的结构体定义如下
1 |
|
IDA 中支持定义结构体, 手动添加这两个结构体

注意这是一个64位程序, 指针为64位,即 DQ,而不是32位的 DD.
sub_4052B4(v4, 10); 把第一个参数的类型改成 linklist *, 选中 a1 参数右键

修改之后的效果如下:

可见 v3 变量是刚分配的 Node 内存,继续修改 V3 的名字和数据类型为 Node *

修改之后并没有太大的直观效果,因为该函数中会调用另外一个函数初始化 Node

继续把 sub_405288 的第一个参数类型修改为 Node *

其它地方与链表有关的都修复一下结构,逻辑就很清晰了。
搞清楚链表后,把整个链表抽象理解成数组,初始化函数初始的数组值固定
1 |
|
01 序列对应两种操作: sub_404F06、 sub_404DD6
sub_404F06(arr, x);
sub_404DD6(arr, x);
arr 就是前面分析的链表, x是 dword_5D5140[i],dword_5D5140 指定了当前操作位置。
意思是依次对 arr 数组倒数第 x 个元素操作,
操作有两种类型:
0、把x元素改成小于100
1、把x元素改成大于100
A操作的条件是: 数组中当前位置的下一个元素必须大于100,下下一个到最后一个元素必须小于100
B操作的条件是: 数组中当前位置的下一个元素必须大于100,下下一个到最后一个元素必须小于100
题目要求输入一个操作序列,使所有元素都小于100
数组中的数字只有两种状态,大于100或小于100,所以可以抽象为 01。
解法
可以先尝试手动选择01操作,使数组中的某些元素变成小于100,
大于100,小于100,可以抽象为1, 0,数组可以抽象为01序列: bits = [1, 0, 1, 1, 1, 1, 1, 1, 1, 1]
手动分析,就发现其实是一个递归结构。
solve 输出设置pos位为direct的操作方法。
设置倒数 pos 位为direct要求如下:
bits[pos - 1] == 1 递归设置这个
bits[pos - 2 … 1] 0 循环递归设置这个
编写递归函数如下:
1 |
|
wherekey
这道题目直接启动可能会不成功,可以采用先运行再附加的方法调试。
题目结构不是很清晰,可以从找数据流入手,具体思路就是通过调试,找输入数据出现的位置,然后再找输入数据经过的处理代码。
402072 函数就是输入函数, 逻辑很混乱,若通过调试只观察数据的话,发现这段代码仅仅是简单的接收5字节输入,然后把输入的 flag 数据通过 socket 传递给其它地方。

输入的 flag 传递到哪里了?经过简单的调试找到了下面这段代码 sub_4022DE 其中 v1 指向输入的字符串,v1的长度为5个字节,是输入的行向量

v4 的计算如下:

通俗讲,这个函数就是返回列向量, v4 就是下面这个矩阵的第 i 列向量,i从0开始。设该矩阵为A矩阵
1 |
|
401E3E 地址上的函数是一个乘法函数( 有简单的花指令)

把上面写的和矩阵乘法联想一下,就能推出如下公式
(flag * A) % 257 == B
1 |
|
用 sega 解就可以得到 flag
1 |
|
stream
第一次做 rust 逆向,体验还行 233
直接运行 stream 会报错,因为缺少 flag 文件。
flag -> 题目 -> output
已知 output 求输入flag
很容易找到如下代码,左侧数据是输入的文件字节流,右侧是一个字节的密钥。

加密次序不是依次加密的,而是按照如下次序加密(target是输入文件):
1 |
|
pos_table 是加密下标表。
这道题的重点就是密钥生成算法,密钥生成算法是用的 rust 随机数库,我们把这个算法抽象成 ENC(T), 那么整个程序的逻辑如下
1 |
|
密钥生成算法只和 key_buffer 有关,key_buffer 每次都变,如下:
1 |
|
爆破思路是依次爆破key_buffer。
例如第一轮的时候,尝试 m = [ENC(‘\x30’) … ENC(‘\x80’) ] 所有取值,并找出满足m[x] ^ x == x 的值作为解。主要解不唯一,因此要DFS遍历所有情况。 确认第一位后,再爆破第二位,依次类推,直到 46 位。
ENC 实际上是 rust 随机数函数,为了方便,直接 patch 源程序使其能实现 ENC的输入输出。
patch 方法如下:

V9 就是keybuffer,使这个第二个参数改为输入文件缓冲区
修改如下代码为赋值语句,可以直接输出 ENC 结果到文件

patch 完成后,运行patch之后的程序,flag 文件相当于 ENC 的参数,output 相当于 ENC 的返回值。
编写爆破脚本如下:
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!