StarCTF Reverse writeUp

*CTF Reverse writeUp

由于水平问题,这次只做出来三道 RE 题,分别是 ChineseGame、stream、wherekey,其它 RE 题目等我复现官方 WP 后再来写。

题目下载

files.zip

ChineseGame

题目名提示中国游戏,但是我做完这题之后也不知道到底是什么中国游戏23333。

这道题目的 binary 静态链接的 libc 库,没有符号,所以要手动的判断某些库函数。

整体结构

  1. 有一个链表结构的初始化数组A
  2. 输入字符串为只含有01的字符串
  3. 01 序列对应两种操作: sub_404F06、 sub_404DD6,两种操作的对象是A数组
  4. 执行完所有01操作后,数组A中的值必须全部小于100

识别部分库函数

先识别出几个库函数

sub_4060D0: malloc

分析方法

找一下该函数的子函数,发现如下字符串,说明这个函数可能是 malloc,并且参数是一个数字,返回值是指针。

sub_4C7600(): rand

分析方法:

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

一般而言,随机数的种子相同,生成的随机数序列也相同。进过初步分析,该程序用的随机数种子为 0

其它几个函数主要是 C++ string 对象的函数,对于这种,一般可以通过观察调试时的数据确认。

链表结构初始化数组A

main 函数中调用初始化函数 sub_4052B4(v4, 10); 该函数对 v4 链表初始化。

v4 是一个链表,结构如下

1
2
3
4
struct linklist{
Node * head; // 8bytes
int64 num;
}

在 sub_4052B4 可以分析 Node 的结构体定义如下

1
2
3
4
struct Node{
int64 data;
struct Node * next;
}

IDA 中支持定义结构体, 手动添加这两个结构体

注意这是一个64位程序, 指针为64位,即 DQ,而不是32位的 DD.

sub_4052B4(v4, 10); 把第一个参数的类型改成 linklist *, 选中 a1 参数右键

修改之后的效果如下:

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

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

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

其它地方与链表有关的都修复一下结构,逻辑就很清晰了。

搞清楚链表后,把整个链表抽象理解成数组,初始化函数初始的数组值固定

1
2
3
4
5
6
7
8
9
10
184
86
178
116
194
136
187
193
150
122

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1, 3, 1, 2, 1, 5,
FLAG = ''
FLAG2 = ''
def solve(bits, pos, direct):
global FLAG
global FLAG2
bits = list(bits)

if bits[10 - pos] == direct:
return bits

if pos == 1:
bits[9] = direct
FLAG += '1, '
FLAG2 += str(direct)
else:
bits = solve(bits, pos - 1, 1)
for i in range(pos - 2, 0, -1):
bits = solve(bits, i, 0) # 顺序
bits[10 - pos] = direct
FLAG += str(pos)+', '
FLAG2 += str(direct)
return bits

if __name__ == '__main__':
bits = [1, 0, 1, 1, 1, 1, 1, 1, 1, 1]
for i in range(10, 0, -1):
bits = solve(bits, i, 0)
print(bits)
print(FLAG)
print(FLAG2)

wherekey

这道题目直接启动可能会不成功,可以采用先运行再附加的方法调试。

题目结构不是很清晰,可以从找数据流入手,具体思路就是通过调试,找输入数据出现的位置,然后再找输入数据经过的处理代码。

402072 函数就是输入函数, 逻辑很混乱,若通过调试只观察数据的话,发现这段代码仅仅是简单的接收5字节输入,然后把输入的 flag 数据通过 socket 传递给其它地方。

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

v4 的计算如下:

通俗讲,这个函数就是返回列向量, v4 就是下面这个矩阵的第 i 列向量,i从0开始。设该矩阵为A矩阵

1
2
3
4
5
flag{
are_y
ou_su
re_fr
iend}

401E3E 地址上的函数是一个乘法函数( 有简单的花指令)

把上面写的和矩阵乘法联想一下,就能推出如下公式

(flag * A) % 257 == B

1
2
3
4
5
6
B = [
0x38, 0x6D, 0x4B, 0x4B, 0xB9,
0x8A, 0xF9, 0x8A, 0xBB, 0x5C,
0x8A, 0x9A, 0xBA, 0x6B, 0xD2,
0xC6, 0xBB, 0x05, 0x90, 0x56,
0x93, 0xE6, 0x12, 0xBD, 0x4F]

用 sega 解就可以得到 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# sage
A = [[102, 108, 97, 103, 123], [97, 114, 101, 95, 121], [111, 117, 95, 115, 117], [114, 101, 95, 102, 114], [105, 101, 110, 100, 125]]
B = [
[0x38, 0x6D, 0x4B, 0x4B, 0xB9],
[0x8A, 0xF9, 0x8A, 0xBB, 0x5C],
[0x8A, 0x9A, 0xBA, 0x6B, 0xD2],
[0xC6, 0xBB, 0x05, 0x90, 0x56],
[0x93, 0xE6, 0x12, 0xBD, 0x4F]]
A = Matrix(Zmod(257),A)
B = Matrix(Zmod(257),B)
flag = B*A.inverse()
for i in flag:
for j in i:
print(chr(j),end='')

stream

第一次做 rust 逆向,体验还行 233

直接运行 stream 会报错,因为缺少 flag 文件。

flag -> 题目 -> output

已知 output 求输入flag

很容易找到如下代码,左侧数据是输入的文件字节流,右侧是一个字节的密钥。

加密次序不是依次加密的,而是按照如下次序加密(target是输入文件):

1
2
3
4
5
pos_table = []
v0 = 4
for i in range(len(target)):
pos_table.append(v0 % len(target))
v0 += 7

pos_table 是加密下标表。

这道题的重点就是密钥生成算法,密钥生成算法是用的 rust 随机数库,我们把这个算法抽象成 ENC(T), 那么整个程序的逻辑如下

1
2
3
4
5
6
7
8
9
flag = [0] * 32
key_buffer = [0] * len(flag)
k = 4
for i in range(len(flag)):
pos = k % len(flag)
key_buffer[i % 32] = flag[pos]
flag[pos] ^= ENC(key_buffer)[0]
k += 7
print(flag) # 已经知道这里的flag,求输入

密钥生成算法只和 key_buffer 有关,key_buffer 每次都变,如下:

1
2
3
4
5
6
5:   46
5B: 3B
5BI: C8
5BIP: EE
5BIPW: F2
5BIPW3: 95

爆破思路是依次爆破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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *
data = open('output_1','rb').read()



target = data
pos_table = []
v0 = 4
for i in range(len(target)):
pos_table.append(v0 % len(target))
v0 += 7

dfs([0] * len(target), [0] * len(target), 0)


def get_m(keyset):
fd = open('flag','wb')
fd.write(bytearray(keyset))
fd.close()
p = process('./a')
sleep(0.1)
p.close()
return open('output', 'rb').read()[0]

def make_test_key(keyset, i):
test_key = [0] * 32
for k1 in range(i):
test_key[k1 % 32] = keyset[k1]
return test_key

def calc_m(keyset, i, tar):
data1 = bytearray(keyset)
set1 = []
for v in range(32, 127):
data1[i] = v
data2 = make_test_key(data1, i + 1)
m = get_m(data2)
#print("test: ", data1, "m:", m)
if tar ^ m == v:
set1.append(v)
return set1

def dfs(keyset, ans, i):
prefix = ' ' * i
pos = pos_table[i]
k = calc_m(keyset, i, target[pos]) #calc_m 计算keyset第i位值为0-255的所有取值

print(prefix, "Acurrent i:%d pos:%d " % (i, pos))
print(prefix, bytearray(keyset[0:i]))
#print(prefix, bytearray(ans))
print(prefix, "all sets:" , bytearray(k))
for jj in k:
keyset[i] = jj
ans[pos] = jj
print(prefix, "Try %d is %s" % (i, chr(jj)))
if i >= len(target) - 1:
print(prefix, "ans: " , ans)
#raise
dfs(bytearray(keyset), bytearray(ans), i + 1)
print(prefix, "Bcurrent i:%d pos:%d " % (i, pos))
print(prefix, bytearray(keyset[0:i]))


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!