题目

附件下载

题解

一道虚拟机题,程序读入progmem文件然后加载进虚拟机。点进sub_4015BF,这里面映射了一页可以执行的内存,然后把一些指令片段copy过去:

`sub_4015BF`

再调用sub_401444修正404800处的20个数组,每个数组里面都放着一些数字,它们被解析成指向指令片段的指针或者库函数:

`sub_401444`

然后申请内存创建虚拟机对象,它的数据结构分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct instruct {
uint16_t op, opr1, opr2;
};

struct vm {
uint16_t reg[0x10000];
uint16_t pc;
struct {
uint16_t LF: 1; // SF ^ OF
uint16_t ZF: 1; // ZF
uint16_t GF: 1; // !(SF ^ OF | ZF)
uint16_t FF: 1; // set IF function fails
} flags;
void *ptrs[0x10000];
struct instruct prog[0x10000];
uint16_t mem[0x10000];
};

reg是寄存器,一共有65536个,但其实用上的也就不到10个;pc是程序计数器;flag是标志位;ptrs存放虚拟机调用一些函数时生成的指针,虚拟机内部可以操作这些指针并把它们传入别的函数;prog存放指令;mem存放用户数据。把这些导入IDA,再观察主函数:

`main`

vm_exec做了一些初始工作,然后开始执行虚拟机:

`vm_exec`

这个vm_exec简单读出一条指令,然后执行vm_exec_instruction

`vm_exec_instruction`

与一般的虚拟机题不同,这个虚拟机并没有switch-case,它从之前初始化时的20个数组中找到对应的,把它们copy到栈顶,然后直接调用栈顶的函数指针。对这20个数组进行分析,发现每一个数组的开头都是这个函数:

1
2
add rsp, 10h
retn

它修改了rsp的值。而调用这个stub时,栈上放着返回地址和指针数组(第一个是此stub的地址),而执行第一句指令以后,返回地址就变成了第二个地址,而它执行另一个stub,例如:

1
2
inc ecx
retn

这个stub返回时会执行第三个地址,如此循环下去,相当于执行了一整条ROP链,最后一个stub是:

1
2
leave
retn

此时rsp恢复正常,程序准备执行下一条指令。

把相关数据扒下来,用Python把切成片段的stub重组,然后整理成一个程序重新编译。值得注意的是程序使用popcmov来实现分支跳转,这部分需要单独处理:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from capstone import *
import os, re

cs = Cs(CS_ARCH_X86, CS_MODE_64)
cs.syntax = CS_OPT_SYNTAX_ATT

lines = open('vm.txt').read().splitlines() # vm.txt content from IDA assembly window
arr: list[list[int]] = []
current: list[int] = None
for line in lines:
if line.startswith('qword'):
if current is not None:
arr.append(current)
current = []
line = line[line.find('dq ')+3:]
if line.endswith('h'): line = line[:-1]
line = int(line, 16)
current.append(line)
arr.append(current)

stubs = bytes.fromhex('4839D6C3480F4CD9C383F8FFC30FB775E4C34883E204C34889F3C366890477C34883E202C3488984F708000200C34883C801C348C7C000000000C34883C804C36629D8C34883CE08C34883C802C359C30FB71C57C30FB755E8C348C7C600000000C3488B9CD708000200C34883E201C34889DEC34881E6F7FF0000C383FA00C35BC34883C410C3C30FB78700000200C30FB73477C30FB73457C3C9C366F7F3C3488B7DE8C3480F4FD9C36689B702000200C3FFE3C34889C7C30FB7B702000200C366F7E3C34883E208C366898700000200C36689D0C36601D8C30FB70477C36689C6C30FB79702000200C3480F44D9C3488B84F708000200C30FB7845F08001000C30FB78702000200C36689B47708001000C34889C6C34889DAC3')

ret = False
imap = {}
for addr, _, op, opr in cs.disasm_lite(stubs, 0):
if ret:
assert op == 'retq'
ret = False
else:
if op == 'retq':
op = 'nop'
elif opr != '':
op += ' ' + opr
imap[addr] = op
if op != 'nop':
ret = True

for ops in arr:
for i, op in enumerate(ops):
if op >= 0x100000:
ops[i] = 'call %s' % ('tmpfile', 'fclose', 'getc', 'ungetc', 'putc', 'exit')[op - 0x100000]
else:
ops[i] = imap[op]

class InstrState:
def __init__(self, prefix):
self._list = []
self._state = 0 # 0正常 1rbx 2rcx 其他为cmov
self._branch = [None, None]
self._prefix = prefix
self._i = 0

def push(self, instr: str):
if instr == 'addq $0x10, %rsp':
return
elif instr == 'leave':
instr = 'retq'
elif instr == 'movq -0x18(%rbp), %rdi':
instr = 'movq vm(%rip), %rdi'
elif instr == 'movzwl -0x1c(%rbp), %esi':
instr = 'movzwl opr1(%rip), %esi'

match = re.fullmatch('cmov(.*)q %rcx, %rbx', instr)
if instr == 'popq %rbx':
self._state = 1
elif instr == 'popq %rcx':
self._state = 2
elif match:
self._state = match.group(1)
else:
if self._state == 0:
self._list.append(instr)
elif self._state == 1:
self._branch[0] = instr
self._state = 0
elif self._state == 2:
self._branch[1] = instr
self._state = 0
else:
if instr == 'jmpq *%rbx':
l1 = '.%s%d' % (self._prefix, self._i)
l2 = '.%s%d' % (self._prefix, self._i + 1)
self._i += 2
self._list.extend((
'j%s %s' % (self._state, l1),
self._branch[0],
'jmp %s' % l2,
'%s:' % l1,
self._branch[1],
'%s:' % l2,
))
self._state = 0
else:
self._list.append(instr)

def get(self):
return self._list + ['retq']

for i, ops in enumerate(arr):
state = InstrState('L%02d' % i)
for op in ops[:-1]:
state.push(op)
arr[i] = state.get()

#import json
#arr = dict(('op_%02d' % i, c) for i, c in enumerate(arr))
#json.dump(arr, open('dump.json', 'w'), indent=2)
#exit()

dummy = ''
for i, code in enumerate(arr):
dummy += '''\
static void op_%02d(struct vm *vm, uint16_t opr1, uint16_t opr2) __attribute__((naked));
void op_%02d(struct vm *vm, uint16_t opr1, uint16_t opr2) {
__asm__(
%s
);
}

''' % (i, i, ''.join(' "%s\\n"\n' % c for c in code))

dummy = open('template.c').read().replace('// dummy', dummy)
open('dummy.c', 'w').write(dummy)
assert os.system('gcc -g -o dummy dummy.c') == 0
os.unlink('dummy.c')

dummy.c

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

struct instruct {
uint16_t op, opr1, opr2;
};

struct vm {
uint16_t reg[0x10000];
uint16_t pc;
struct {
uint16_t LF: 1; // SF ^ OF
uint16_t ZF: 1; // ZF
uint16_t GF: 1; // !(SF ^ OF | ZF)
uint16_t FF: 1; // set IF function fails
} flags;
void *ptrs[0x10000];
struct instruct prog[0x10000];
uint16_t mem[0x10000];
};

typedef void (*vm_func)(struct vm *, uint16_t, uint16_t);
static struct vm *vm;
static uint16_t opr1 = 0;

// dummy

static vm_func funcs[] = {op_00, op_01, op_02, op_03, op_04, op_05, op_06, op_07, op_08, op_09, op_10, op_11, op_12, op_13, op_14, op_15, op_16, op_17, op_18, op_19};

static const struct instruct prog[] = {
// from file prog
};

static const uint16_t mem[] = {
// from file mem
};

int main() {
vm = (struct vm *)malloc(sizeof (struct vm));
if (vm) {
memcpy(vm->prog, prog, sizeof (prog));
memcpy(vm->mem, mem, sizeof (mem));
vm->pc = 0;
vm->ptrs[0] = stdin;
vm->ptrs[1] = stdout;
vm->ptrs[2] = stderr;
for (;; ++vm->pc) {
struct instruct *instr = &vm->prog[vm->pc];
if (instr->op < 20) {
opr1 = instr->opr1;
funcs[instr->op](vm, instr->opr1, instr->opr2);
}
}
free(vm);
}
return 0;
}

用IDA打开新生成的dummy文件,就知道20条指令分别做了什么了:

`dummy`

但实际上这个文件并不能被成功执行,经过与原程序的对比调试发现是op_16不一致,但无妨继续分析代码。指令的格式是op x, y,二十条指令分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00 add rx, ry
01 sub rx, ry
02 mul rx, ry
03 div rx, ry
04 mod rx, ry
05 mov rx, y
06 ptrs[rx] = tmpfile()
07 fclose(ptrs[rx])
08 rx = getc(ptrs[ry]), set FF
09 ungetc(rx, ptrs[ry])
10 putc(rx, ptrs[ry])
11 exit(rx)
12 cmp rx, ry
13 jmp $+x
14 jb $+x
15 jz $+x
16 ja $+x
17 jf $+x ; jump if previous getc fails.
18 mov [r1], r2
19 mov r1, [r2]

解析并整理,代码如下(ja指令实际上应该是je指令):

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
printf("Initializing Interpreter...\n");
printf("read(0, buf, 50);\n");
printf("Flag?\n");
f0 = tmpfile()
f1 = tmpfile()
f2 = tmpfile()
f3 = tmpfile()

mov r4, 0
mov r7, 1
L117:
mov r5, [r4]
mov r6, 0
cmp r5, r6
ja L124
ungetc(r5, f1)
add r4, r7
jmp L117
L124:

mov r4, 0
ungetc(r4, f3)
L126:
op20 15, 0
r4 = getc(f1)
ungetc(r4, f1)
jf L250
mov r5, 127
cmp r4, r5
ja L157
mov r5, 159
cmp r4, r5
ja L163
mov r5, 218
cmp r4, r5
ja L238
mov r5, 37
cmp r4, r5
ja L189
mov r5, 9
cmp r4, r5
ja L194
mov r5, 101
cmp r4, r5
ja L205
mov r5, 74
cmp r4, r5
ja L212
mov r5, 66
cmp r4, r5
ja L245
L154:
r4 = getc(f1)
ungetc(r4, f0)
jmp L126

L157:
r5 = getc(f3)
jf L161
L159:
ungetc(r5, f2)
jmp L154
L161:
mov r5, 0
jmp L159
L163:
mov r6, 0
r5 = getc(f3)
ungetc(r5, f3)
cmp r5, r6
ja L169
jmp L154

L169:
mov r6, 0
mov r8, 1
mov r9, 0
L172:
r5 = getc(f1)
ungetc(r5, f1)
mov r7, 159
cmp r5, r7
ja L185
L177:
mov r7, 74
cmp r5, r7
ja L187
L180:
cmp r6, r9
ja L154
r5 = getc(f1)
ungetc(r5, f0)
jmp L172
L185:
add r6, r8
jmp L177
L187:
sub r6, r8
jmp L180
L189:
r5 = getc(f3)
mov r6, 1
putc(r5, stdout)
ungetc(r5, f3)
jmp L154

L194:
r5 = getc(f2)
jf L201
L196:
ungetc(r5, f3)
r5 = getc(f2)
jf L203
L199:
ungetc(r5, f3)
jmp L154

L201:
mov r5, 0
jmp L196
L203:
mov r5, 0
jmp L199

L205:
r5 = getc(f3)
mov r6, 2
mov r7, 256
add r5, r6
div r5, r7
ungetc(r5, f3)
jmp L154

L212:
mov r6, 0
r5 = getc(f3)
ungetc(r5, f3)
cmp r5, r6
jz L218
jmp L154

L218:
mov r6, 0
mov r8, 1
mov r9, 0
L221:
r5 = getc(f1)
ungetc(r5, f1)
mov r7, 159
cmp r5, r7
ja L234
L226:
mov r7, 74
cmp r5, r7
ja L236
L229:
cmp r6, r9
ja L154
r5 = getc(f0)
ungetc(r5, f1)
jmp L221

L234:
add r6, r8
jmp L226
L236:
sub r6, r8
jmp L229

L238:
r5 = getc(f3)
mov r6, 1
mov r7, 256
sub r5, r6
div r5, r7
ungetc(r5, f3)
jmp L154

L245:
mov r5, 0
r6 = getc(stdin)
r5 = getc(f3)
ungetc(r6, f3)
jmp L154
L250:
mov r0, 0
halt

手动翻译X_X:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <stdint.h>

static const uint16_t mem[] = {
// from file mem
};

#include <stdio.h>
#include <stdlib.h>

#define cmp(x, y) ((x) == (y))

int main(){
FILE *f0, *f1, *f2, *f3;
uint16_t r4, r5, r6, r7;
printf("Initializing Interpreter...\n");
printf("read(0, buf, 50);\n");
printf("Flag?\n");
f0 = tmpfile();
f1 = tmpfile();
f2 = tmpfile();
f3 = tmpfile();
for (r4 = 0; ; ++r4) {
r5 = mem[r4];
if (cmp(r5, 0)) break;
ungetc(r5, f1);
printf(">%d\n", r5);
}
ungetc(0, f3);
L126:
r4 = getc(f1);
printf("<%d\n", r4);
if (r4 == 0xFFFF) {
// 成功分支
exit(0);
}
ungetc(r4, f1);
if (!cmp(r4, 127)) {
if (!cmp(r4, 159)) {
if (!cmp(r4, 218)) {
if (!cmp(r4, 37)) {
if (!cmp(r4, 9)) {
if (!cmp(r4, 101)) {
if (!cmp(r4, 74)) {
if (!cmp(r4, 66)) {
L154:
r4 = getc(f1);
ungetc(r4, f0);
goto L126;
} else{
// L245
r6 = getc(stdin);
r5 = getc(f3);
ungetc(r6, f3);
goto L154;
}
} else {
// L212
r5 = getc(f3);
ungetc(r5, f3);
if (r5 == 0) {
// L218
r6 = 0;
L221:
r5 = getc(f1);
ungetc(r5, f1);
if (cmp(r5, 159)) {
// L234
r6 += 1;
}
// L226
if (cmp(r7, 74)) {
// L236
r6 -= 1;
}
// L229
if (cmp(r6, 0)) {
goto L154;
}
r5 = getc(f0);
ungetc(r5, f1);
goto L221;
}
goto L154;
}
} else {
// L205
r5 = getc(f3);
r6 = 2;
r5 += r6;
r5 /= 256;
ungetc(r5, f3);
goto L154;
}
} else{
// L194
r5 = getc(f2);
if (r5 == 0xFFFF) {
r5 = 0;
}
// L196
ungetc(r5, f3);
r5 = getc(f2);
if (r5 == 0xFFFF) {
// L203
r5 = 0;
}
// L199
ungetc(r5, f3);
goto L154;
}
} else {
// L189
r5 = getc(f3);
putc(r5, stdout);
ungetc(r5, f3);
goto L154;
}
} else {
// L238
r5 = getc(f3);
r5 -= 1;
r5 /= 256;
ungetc(r5, f3);
goto L154;
}
} else {
// L163
r5 = getc(f3);
ungetc(r5, f3);
if (!cmp(r5, 0)) {
goto L154;
}
// L169
r6 = 0;
L172:
r5 = getc(f1);
ungetc(r5, f1);
if (cmp(r5, 159)) {
// L185
r6 += 1;
}
// L177
if (cmp(r5, 74)) {
// L187
r6 -= 1;
}
// L180
if (cmp(r6, 0)) {
goto L154;
}
r5 = getc(f1);
ungetc(r5, f0);
goto L172;
}
} else {
// L157
r5 = getc(f3);
if (r5 == 0xFFFF) {
r5 = 0;
}
// L159
ungetc(r5, f2);
goto L154;
}
return 0;
}

它也不能被执行,但是经过一段时间的观察,可以看出这是另一个brainfuck虚拟机,mem中的八种数字分别对应八条指令,但其中+指令实际执行了两次加法,>指令向右移动了两次指针,如果把mem解析成标准brainfuck,则对应表如下(注释是符号在文件中出现的次数):

1
2
3
4
5
6
7
8
9
10
imap = {
74: ']', # 207
218: '-', # 8528
159: '[', # 207
37: '.', # 18
101: '++', # 1314
9: '>>', # 412
127: '<', # 759
66: ',', # 1
}

值得注意的是,mem中存放的brainfuck指令流在解析时应该倒过来看,因为程序是用ungetc把指令逐个push到文件f1里去的。解析完如下(数据太多了,50个++50表示):

1
<5+50[-<+<64+>65]+50>+46<2[-<+<7+>8]<[->+<]<7[>,<[-<+>]<-]<6[->+>6+<7]>[-<+>]>7[-<3+>3]<3[->+>2+<3]>2[>[-<3+>3]<-[->+<]<[->+<]>2]<[->3+<3]>9[-<[-<+<5+>6]<[->+<]<5[<+7[<3[->+>+<2]>[-<+>]>2-]>2[-<2+<+>3]<2[->2+<2]>-[-<+>]<2[->2+<2]<2[-]>3]<8[->+>7+<8]>[-<+>]>8[-<2+<+>3]<3[->3+<3]>2[>[-<4+>4]<-[->+<]<[->+<]>2]>[-]<2[->2+<2]>9.<]<2+10.<9-56[>2[-<+>]<+<[-]]<-249[>2[-<+>]<+<[-]]<-57[>2[-<+>]<+<[-]]<-189[>2[-<+>]<+<[-]]<-100[>2[-<+>]<+<[-]]<-114[>2[-<+>]<+<[-]]<-19[>2[-<+>]<+<[-]]<-194[>2[-<+>]<+<[-]]<-150[>2[-<+>]<+<[-]]<-229[>2[-<+>]<+<[-]]<-15[>2[-<+>]<+<[-]]<-95[>2[-<+>]<+<[-]]<-157[>2[-<+>]<+<[-]]<-173[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-220[>2[-<+>]<+<[-]]<-36[>2[-<+>]<+<[-]]<-35[>2[-<+>]<+<[-]]<-119[>2[-<+>]<+<[-]]<-69[>2[-<+>]<+<[-]]<-248[>2[-<+>]<+<[-]]<-43[>2[-<+>]<+<[-]]<-148[>2[-<+>]<+<[-]]<-216[>2[-<+>]<+<[-]]<-239[>2[-<+>]<+<[-]]<-247[>2[-<+>]<+<[-]]<-68[>2[-<+>]<+<[-]]<-177[>2[-<+>]<+<[-]]<-198[>2[-<+>]<+<[-]]<-81[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-231[>2[-<+>]<+<[-]]<-4[>2[-<+>]<+<[-]]<-241[>2[-<+>]<+<[-]]<-74[>2[-<+>]<+<[-]]<-224[>2[-<+>]<+<[-]]<-237[>2[-<+>]<+<[-]]<-179[>2[-<+>]<+<[-]]<-196[>2[-<+>]<+<[-]]<-51[>2[-<+>]<+<[-]]<-192[>2[-<+>]<+<[-]]<-136[>2[-<+>]<+<[-]]<-96[>2[-<+>]<+<[-]]<-28[>2[-<+>]<+<[-]]<-120[>2[-<+>]<+<[-]]<-29[>2[-<+>]<+<[-]]<-208[>2[-<+>]<+<[-]]<-186[>2[-<+>]<+<[-]]<-59[>2[-<+>]<+<[-]]<-179[>2[-<+>]<+<[-]]+>[[-]<[-]+87.[-]+82.[-]+79.[-]+78.[-]+71.[-]+33.[-]+10.[-]>]<[[-]+67.[-]+79.[-]+82.[-]+82.[-]+69.[-]+67.[-]+84.[-]+33.[-]+10.[-]]

从百度可以了解到一些brainfuck的一些经典操作:

1
2
[-] ; clear `*p`
[->+>+<<]>[-<+>]< ; copy `*p` to `*(p+2)`

代码的前几行是对输入的flag进行变换,中间是判断变换后的flag的每一位是否为指定值,后面是输出正确与否的信息。中间的数组如下:

1
[56,249,57,189,100,114,19,194,150,229,15,95,157,173,224,220,36,35,119,69,248,43,148,216,239,247,68,177,198,81,224,231,4,241,74,224,237,179,196,51,192,136,96,28,120,29,208,186,59,179]

brainfuck的代码看似很多,实际加密过程也就只位于前半部分。对前半部分手动逆向,并用解释器验证结果(可以找一个brainfuck的在线解释器)。

逆向的示意结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
p[4] = 46; p[5] = p[6] = 50; p[11] = last(); p[16~65] = input(); p[70] = 50; p = 5;
[-
p[12] = p[6]; p = 12;
p[14] = p[16] * 7 + p[11]; p = 13;
p[13] = p[12] - 1; p[12] = 0; p = 13;
[
p[12] = p[14]; p[14] = p[16] = 0; p = 13;
]
; now is 0 0 0 0 46 49 50 0 0 0 0 49 p[12-61] 0 0 0 0 0 0 0 0 50; p = 62;

p[62] = p[70]; p = 61;
p[63] = p[61]; p = 62;
[
p[65] = p[61]; p[61] = 0; p = 62;
p[62] -= 1; p = 61;
]
]

brainfuck的一个特点是代码与位置高度相关,因此很难翻译回容易理解的代码,上面的代码仅能起到参考作用。这一段大致意思为:先读入50个字符,接着进行50轮操作,每轮操作如下:

1
2
for i, c in enumerate(ipt):
ipt[i] = c * 7 + ipt[i - 1] & 255

然后可以写出如下脚本解密flag(前半部分只是验证动态调试的结果是否与分析结果一致):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

x=bytearray(b'1'*50)
y=[56,249,57,189,100,114,19,194,150,229,15,95,157,173,224,220,36,35,119,69,248,43,148,216,239,247,68,177,198,81,224,231,4,241,74,224,237,179,196,51,192,136,96,28,120,29,208,186,59,179]

for i in range(len(x)):
for i, c in enumerate(x):
c = c * 7 + x[i - 1]
x[i] = c & 255
assert list(x) == [131,249,88,252,65,234,135,143,175,188,75,191,35,85,222,232,91,218,223,67,11,222,18,97,165,32,232,220,121,44,49,77,183,58,216,51,6,71,14,109,254,4,43,237,39,154,134,59,76,243]

from Crypto.Util.number import inverse
i7 = inverse(7, 256)
for i in range(len(y)):
for i, c in enumerate(reversed(y)):
i = len(y) - i - 1
c = (c - y[i - 1]) * i7
y[i] = c & 255
print(bytes(y).decode())

最后结果:

idek{bf=two_zippers=four_stacks=getc_and_ungetc..}