0x00 前言

Cython逆向,借机学一下:Cython 二进制库逆向分析全面指南

WP来自2024年羊城杯粤港澳大湾区网络安全大赛WP-Reverse篇(全网唯一chal题解的含金量)

本体对环境有要求,一定要是linux+python3.12的环境

0x01 解

根据给出的main.py,可以知道chal里面有个chal

1
2
3
4
import chal

flag = input("flag: ")
chal.chal(flag)

开启python,导入一下chal库然后打印一下属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Python 3.12.5 | packaged by Anaconda, Inc. | (main, Sep 12 2024, 18:27:27) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import chal
>>> dir(chal)
['__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__test__', 'chal', 'os', 'random']
>>> dir(chal.chal)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_p1', '_p2', '_p3']
>>> chal.chal._p1
<cyfunction chal._p1 at 0x7f40fe598860>
>>> chal.chal._p2
<cyfunction chal._p2 at 0x7f40fe598930>
>>> chal.chal._p3
<cyfunction chal._p3 at 0x7f40fe598a00>
>>> dir(chal.chal('a'))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_p1', '_p2', '_p3', '_tips', '_var1', '_var2', '_var3']
>>> chal.chal('a')._tips
"Don't peek!!!"
>>> chal.chal('a')._var1
'a'
>>> chal.chal('a')._var2
[121, 73, 141, 146, 115, 230, 181, 65, 238, 17, 146, 73, 228, 82, 188, 66, 12, 148, 225, 66, 255, 254, 47, 22, 163, 250, 222, 133, 35, 232, 106, 176]
>>> chal.chal('a')._var3
[12, 243, 133, 147, 7, 36, 29, 49, 226, 211, 156, 56, 142, 78, 254, 12]

第一步可以发现chal实际上是chal模块里的一个类

第二步可以发现类里面有_p1_p2_p3三个函数

第三步初始化以后多了_tips_var1_var2_var3四个变量,其中_var1存储的就是输入,_var2_var3是固定数组

main.py里面实际上是用输入的flag初始化了一个chal.chal类,对flag的校验在类的__init__函数中。而__init__函数透露信息不多,因此归根到底得看伪代码()

C级别的全局变量赋值

字符串类型

在Cython中,使用函数_Pyx_CreateStringTabAndInitStrings进行全局字符串赋值,在函数中,可以看到如下这个样子的函数

1
v8 = _mm_unpacklo_epi64((__m128i)(unsigned __int64)&qword_28A98, (__m128i)(unsigned __int64)"AttributeError");

这个意思就是将"AttributeError"这个字符串放入&qword_28A98这个地址进行存储,后面需要用到这个字符串的时候将直接调这个地址

实际上,该函数中使用的字符串定义结构体是这样的(可以参考这个Cython程序分析_1):

1
2
3
4
5
6
7
8
9
typedef struct {
PyObject **p;
const char *s;
const Py_ssize_t n;
const char* encoding;
const char is_unicode;
const char is_str;
const char intern;
} __Pyx_StringTabEntry;

而字符串是通过__Pyx_StringTabEntry的数组进行初始化的,也就是说当我们在该函数中看到以下伪代码时:

1
2
3
4
5
6
7
8
9
10
11
__m128i v8;
__int64 v9;
__int64 v10;
__int16 v11;
char v12;

v8 = _mm_unpacklo_epi64(&qword_28A98, "AttributeError");
v9 = 15LL; // len
v10 = 0LL;
v11 = 0x100; // 1 and 0 (binary = 0000 0001 0000 0000)
v12 = 1;

就代表这是一个{&qword_28A98, "AttributeError", 15, 0, 1, 0, 1}__Pyx_StringTabEntry,也就是说qword_28A98中将要初始化一个内容是"AttributeError"的字符串对象的地址,在后续调用中,调用到AttributeError字符串的地方都会用&qword_28A98指代。

整数类型

在函数_pyx_pymod_exec_chal中(最后是库名字,只要找__pyx_pymod_exec_即可),第205行左右,可以找到形如这样的内容:

1
2
qword_28DE8 = PyLong_FromLong(0LL);
if ( qword_28DE8 )

__pyx_pymod_exec_test2()函数的前半段都是检查和初始化。这里可以搜索到初始化的Python int型变量(PyLong_FromLong)。

Cython程序分析_2

这个地方就是赋值了一个整数类型,qword_28DE8中将存储一个值为0的整数类型的Python对象。

而在983行左右的位置可以看到这样的内容

1
2
qword_29600 = PyLong_FromString("2654435769", 0LL, 0LL);
if ( qword_29600 )

这是大数的存储方式,大数会用PyLong_FromString函数来初始化,这里qword_29600中将存储一个值为2654435769的整数类型的Python对象,后续用到2654435769的地方将使用qword_29600

内建函数/变量

在函数_pyx_pymod_exec_chal中,第1042行左右,可以找到如下内容(在某些优化下也会直接嵌入 _pyx_pymod_exec_chal):

1
2
3
4
5
6
7
8
9
if ( (int)_Pyx_InitCachedBuiltins() < 0 )
{
v32 = 0LL;
v7 = 0LL;
v33 = 10294;
v35 = 1;
v34 = 1;
goto LABEL_349;
}

此处是初始化内建函数

事实上,在函数_Pyx_InitCachedBuiltins()中可以看到形如以下的内容:

1
2
3
_pyx_builtin_AttributeError = _Pyx_GetBuiltinName(qword_28A98);
if ( !_pyx_builtin_AttributeError )
return 0xFFFFFFFFLL;

qword_28A98就是前面的"AttributeError"字符串,这里是通过名字找到AttributeError对象,并赋值给_pyx_builtin_AttributeError,后续用到AttributeError对象的地方将使用_pyx_builtin_AttributeError

常量

在函数_pyx_pymod_exec_chal中,第1051行左右,可以找到如下内容(在某些优化下也会直接嵌入 _pyx_pymod_exec_chal):

1
2
3
4
5
6
7
8
9
10
inited = _Pyx_InitCachedConstants();
if ( inited < 0 )
{
v32 = 0LL;
v7 = 0LL;
v33 = 10296;
v35 = 1;
v34 = 1;
goto LABEL_349;
}

此处是初始化常量

在函数_Pyx_InitCachedConstants()中可以找到形如以下的内容:

1
2
3
4
>qword_29630 = PyTuple_Pack(2LL, qword_29600, qword_29618); 
>// 我这个地方是只有qword_29600,但是汇编是能看到qword_29618的,怀疑是IDA反编译的锅
>if ( !qword_29630 )
return 0xFFFFFFFFLL;

qword_29600就是前面的值为2654435769的整数类型的Python对象,同理qword_29618就是值为3337565984的整数类型的Python对象。这里将这两个对象打包成了一个长度为2的元组(2654435769, 3337565984),并赋值给qword_29630变量,后续用到这个元组的地方将使用qword_29630

函数声明及定义

声明在_Pyx_InitCachedConstants, called by _pyx_pymod_exec_chal

(在某些优化下也会直接嵌入 _pyx_pymod_exec_chal

定义在_pyx_pymod_exec_chal

即函数的声明和常量在同一个函数中进行初始化,常量上文给出了例子,函数的声明则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
qword_29660 = PyTuple_Pack(7LL, qword_28C98);
if ( !qword_29660 )
return 0xFFFFFFFFLL;
qword_29688 = _Pyx_PyCode_New_constprop_0(
3,
7,
qword_28A80,
qword_28A78,
qword_28A78,
qword_29660,
qword_28A78,
qword_28A78,
qword_28B48,
qword_28C28,
19,
qword_28A80);
if ( !qword_29688 )
return 0xFFFFFFFFLL;

将变量恢复一下符号(转成能看的样子)如下(来自2024年羊城杯粤港澳大湾区网络安全大赛WP-Reverse篇):

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
// *** _Pyx_InitCachedConstants ***
// 元组赋值
v1 = PyTuple_Pack(7LL, self, x1, x2, tmp, low, high, ans);
// 我这边只显示了qword_28C98这个变量,但是汇编中可以看到更多变量(然而没有符号,这边抄的是手动恢复了符号的),恢复符号后的汇编如下:
/*
.text:0000000000003AC0 push cs:ans
.text:0000000000003AC6 mov r9, cs:low
.text:0000000000003ACD xor eax, eax
.text:0000000000003ACF mov edi, 7
.text:0000000000003AD4 mov rsi, cs:self
.text:0000000000003ADB push cs:high
.text:0000000000003AE1 mov r8, cs:tmp
.text:0000000000003AE8 mov rcx, cs:x2
.text:0000000000003AEF mov rdx, cs:x1
.text:0000000000003AF6 call _PyTuple_Pack
.text:0000000000003AFB pop rsi
.text:0000000000003AFC pop rdi
.text:0000000000003AFD mov cs:qword_29660, rax
*/
// 手动恢复符号的方法为:
//(汇编中看到的数-_Pyx_CreateStringTabAndInitStrings中的起始位置)/8 = 索引
// 根据索引找string就行
if ( !v1 )
return 0xFFFFFFFFLL;
// 函数定义
qword_29688 = _Pyx_PyCode_New_constprop_0(
3, 7, qword_28A80, qword_28A78, qword_28A78, v1, qword_28A78,
qword_28A78, chal_py, p1, 19, qword_28A80
);
if ( !qword_29688 )
return 0xFFFFFFFFLL;

_Pyx_PyCode_New_constprop_0用于创建一个PyCodeObject,其参数就是PyCodeObject的各属性,具体可参考各版本cpython源码中对PyCodeObject的定义,这里就是以v1元组为参数+局部变量名(前3个为参数),原Python函数第一行在文件中的第19行(qword_28A78()(空的tuple,见_pyx_pymod_exec_chal的第155行左右),qword_28A80"",无内容不用关注)创建了一个名为_p1的函数PyCodeObject,相当于是函数声明(因为co_code字段是空的,没有指定具体行为)。

PyCodeObject结构体的定义如下:

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
typedef struct {
PyObject_HEAD /* 头部信息, 我们看到真的一切皆对象, 字节码也是个对象 */
int co_argcount; /* 可以通过位置参数传递的参数个数 */
int co_posonlyargcount; /* 只能通过位置参数传递的参数个数, Python3.8新增 */
int co_kwonlyargcount; /* 只能通过关键字参数传递的参数个数 */
int co_nlocals; /* 代码块中局部变量的个数,也包括参数 */
int co_stacksize; /* 执行该段代码块需要的栈空间 */
int co_flags; /* 参数类型标识 */
int co_firstlineno; /* 代码块在对应文件的行号 */
PyObject *co_code; /* 指令集, 也就是字节码, 它是一个bytes对象 */
PyObject *co_consts; /* 常量池, 一个元组,保存代码块中的所有常量。 */
PyObject *co_names; /* 一个元组,保存代码块中引用的其它作用域的变量 list of strings (names used) */
PyObject *co_varnames; /* 一个元组,保存当前作用域中的变量 tuple of strings (local variable names)*/
PyObject *co_freevars; /* 内层函数引用的外层函数的作用域中的变量 tuple of strings (free variable names)*/
PyObject *co_cellvars; /* 外层函数中作用域中被内层函数引用的变量,本质上和co_freevars是一样的 tuple of strings (cell variable names)*/

Py_ssize_t *co_cell2arg; /* 无需关注 */
PyObject *co_filename; /* 代码块所在的文件名 */
PyObject *co_name; /* 代码块的名字,通常是函数名或者类名 */
PyObject *co_lnotab; /* 字节码指令与python源代码的行号之间的对应关系,以PyByteObject的形式存在 */

//剩下的无需关注了
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */
void *co_extra;
unsigned char *co_opcache_map;
_PyOpcache *co_opcache;
int co_opcache_flag;
unsigned char co_opcache_size;
} PyCodeObject;

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PyCode_New(
0, /*int argcount,*/
0, /*int kwonlyargcount,*/
0, /*int nlocals,*/
0, /*int stacksize,*/
0, /*int flags,*/
empty_string, /*PyObject *code,*/
empty_tuple, /*PyObject *consts,*/
empty_tuple, /*PyObject *names,*/
empty_tuple, /*PyObject *varnames,*/
empty_tuple, /*PyObject *freevars,*/
empty_tuple, /*PyObject *cellvars,*/
py_srcfile, /*PyObject *filename,*/
py_funcname, /*PyObject *name,*/
lineno, /*int firstlineno,*/
empty_string /*PyObject *lnotab*/
);

而根据对_Pyx_PyCode_New_constprop_0函数的结果(qword_29688)进行索引,可以在_pyx_pymod_exec_chal中找到

1
v48 = _Pyx_CyFunction_New_constprop_0(&_pyx_mdef_4chal_4chal_3_p1, qword_28B28, qword_28B18, _pyx_mstate_global_static, qword_29688);

对应恢复符号后的结果为:

1
v48 = _Pyx_CyFunction_New_constprop_0(&_pyx_mdef_4chal_4chal_3_p1, chal__p1, chal, _pyx_mstate_global_static, qword_29688);

而在这下面有

1
v49 = (unsigned int)PyObject_SetItem(v7, qword_28C28, v48) >> 31;

同样,对应恢复符号后的结果为

1
v49 = PyObject_SetItem(v7, p1, v48) >> 31; // self._p1 = v559

(不知道为什么我这边解析出的变量名和WP有出入,不过基本上都对的上,这边改成了我的变量名了)

cython中一般使用PyMethodDef进行指定,PyMethodDef结构体源码(Include/methodobject.h)如下:

1
2
3
4
5
6
7
8
9
10
typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);

struct PyMethodDef {
const char *ml_name; /* The name of the built-in function/method */
PyCFunction ml_meth; /* The C function that implements it */
int ml_flags; /* Combination of METH_xxx flags, which mostly
describe the args expected by the C func */
const char *ml_doc; /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

PyMethodDef 是一个 C结构体,用来完成一个映射,也就是便于方法查找,我们把需要被外面调用的方法都记录在这表内。

PyMethodDef 结构体成员说明:

  1. 第一个字段:在 Python 里面使用的方法名;
  2. 第二个字段:C 模块内的函数名(已经实现了的函数名);(IDA中可以由此找到函数体所在的位置)
  3. 第三个字段:方法参数类型,是无参数(METH_NOARGS) , 还是有位置参数(METH_VARARGS), 还是其他等等;
  4. 第四个字段:方法描述,就是通过 help() 或者 doc 可以看到的;

正因为存在这样的一份记录表,Python 才能够寻找到相应的函数

伪代码中的_pyx_mdef_4chal_4chal_3_p1就是一个PyMethodDef

1
2
3
4
5
6
.data:00000000000289C0 __pyx_mdef_4chal_4chal_3_p1 dq offset aChalChalP1+0Ah
.data:00000000000289C0 ; DATA XREF: __pyx_pymod_exec_chal+236B↑o
.data:00000000000289C0 ; "_p1"
.data:00000000000289C8 dq offset __pyx_pw_4chal_4chal_3_p1
.data:00000000000289D0 db 82h
.data:00000000000289D1 db 0

_p1的函数体实际上在__pyx_pw_4chal_4chal_3_p1中。

import

在函数_pyx_pymod_exec_chal

可以找到如下内容:

1
2
3
4
5
6
7
8
9
10
11
v22 = _Pyx_ImportDottedModule_constprop_0(qword_28C70);
v7 = v22;
if ( !v22 )
{
v32 = 0LL;
v2 = 0LL;
v33 = 10315;
v35 = 1;
v34 = 1;
goto LABEL_349;
}

通过手动恢复符号,可以发现qword_28C70对应的字符串为"random",说明其对应的就是python中的import random的操作

Python代码的转译

对象变量赋值

在函数_pyx_pw_4chal_4chal_1__init__中,第358行左右,可以找到如下内容:

1
v22 = PyObject_SetAttr(v19, qword_28D50, v20); // qword_28D50 = "_var1"

直接用了PyObject_SetAttr函数,其实反编译过来就是self._var1 = v20

数组赋值

在函数_pyx_pw_4chal_4chal_1__init__中,第369行左右,可以找到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v23 = (__int64 *)PyList_New(32LL);
if ( !v23 )
{
v107 = 0LL;
v95 = 0LL;
Attr = 0LL;
v126 = 4234LL;
v127 = 7LL;
goto LABEL_211;
}
v24 = qword_291B0; // qword_291B0 = PyLong_FromLong(121LL);
if ( *(_DWORD *)qword_291B0 != -1 )
++*(_DWORD *)qword_291B0;
v25 = (__int64 *)v23[3];
*v25 = v24;
// 重复v24 = qword_291B0;到*v25 = v24;这几步处理对列表赋值
// ...
// 最后来到这
v58 = *(__int64 (__fastcall **)(__int64, __int64, __int64 *))(*(_QWORD *)(v19 + 8) + 152LL);
if ( v58 )
v59 = v58(v19, qword_28D60, v23);
else
v59 = PyObject_SetAttr(v19, qword_28D60, v23); // 重点是这步qword_28D60 = "_var2"

即最后将成型的列表v23赋给self._var2,这里还原后的结果就是self._var2 = v23 = [121, ...]

参数解析

在函数_pyx_pw_4chal_4chal_1__init__可以找到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v15 = *(_QWORD *)(a4 + 16);
v210 = _mm_loadu_si128(a2); // 使用xmm寄存器存储参数
LABEL_207:
if ( v15 > 0 && (int)_Pyx_ParseOptionalKeywords_constprop_0(a4, v6, &v211, &v210, a3, "__init__") < 0 ) // 解析__init__函数的参数,存放的位置为v210
{
v16 = 4153;
goto LABEL_21;
}
v20 = v210.m128i_i64[1]; // 参数1给了v20
v19 = v210.m128i_i64[0]; // 参数0给了v19
LABEL_26:
v21 = *(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v19 + 8) + 152LL);
if ( v21 )
v22 = v21(v19, var1);
else
v22 = PyObject_SetAttr(v19, var1, v20); // 这个就是上面的赋值

这是对__init__函数传入参数的解析,解析的结果赋值给其他变量

函数调用

在进行函数调用的例子之前,需要

在函数_pyx_pw_4chal_4chal_1__init__的第940行左右,是一段如果参数为变量对Python函数的完整调用,没有标注的都是框架代码及错误处理。

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
v159 = *(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v19 + 8) + 144LL);
// v19 = a2->m128i_i64[0]; 分析后应该是self
if ( v159 )
v95 = v159(v19, qword_28C38); // v95 = self._p3其中qword_28C38 = "_p3"
else
v95 = PyObject_GetAttr(v19, qword_28C38); // v95 = self._p3其中qword_28C38 = "_p3"
if ( !v95 )
{
v107 = 0LL;
Attr = 0LL;
v23 = 0LL;
v126 = 4493LL;
v127 = 10LL;
goto LABEL_211;
}
if ( *(_UNKNOWN **)(v95 + 8) == &PyMethod_Type && (v161 = *(_DWORD **)(v95 + 24)) != 0LL )
{
Attr = *(_QWORD **)(v95 + 16); // Attr = v95
if ( *v161 != -1 )
++*v161;
if ( *(_DWORD *)Attr != -1 )
++*(_DWORD *)Attr;
if ( (int)*(_QWORD *)v95 >= 0 )
{
v162 = *(_QWORD *)v95 - 1LL;
*(_QWORD *)v95 = v162;
if ( !v162 )
_Py_Dealloc(v95, v158, v160);
}
v163 = &v211;
v211 = _mm_loadh_ps((const double *)&qword_28AA0); // 这个地方是参数,qword_28AA0 = "Don't hook!!!"
v23 = (__int64 *)_Pyx_PyObject_FastCallDict_constprop_0(Attr, &v211, 2LL);// 函数调用,执行self._p3("Don't hook!!!"),返回值给v23
if ( (int)*(_QWORD *)v161 >= 0 )
{
v165 = *(_QWORD *)v161 - 1LL;
*(_QWORD *)v161 = v165;
if ( !v165 )
_Py_Dealloc(v161, &v211, v164);
}
}
// 不同的调用方式,和上面的if同层
else
{
Attr = (_QWORD *)v95;
v163 = v206;
v211.m128_u64[0] = 0LL;
v211.m128_u64[1] = qword_28AA0; // qword_28AA0 = "Don't hook!!!""
v23 = (__int64 *)_Pyx_PyObject_FastCallDict_constprop_0(v95, v206, 1LL);// 函数调用,执行self._p3("Don't hook!!!"),返回值给v23
}
if ( !v23 )
{
v107 = 0LL;
v95 = 0LL;
v126 = 4513LL;
v127 = 10LL;
goto LABEL_211;
}
if ( (int)*Attr >= 0 )
{
v166 = *Attr - 1LL;
*Attr = v166;
if ( !v166 )
_Py_Dealloc(Attr, v163, v164);
}

如果参数都为常量则简单得多(在函数_pyx_pf_4chal_4chal_6_p3_isra_0中的第5169行左右,反编译这个函数要改IDA的允许反编译的最大大小):

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
v420 = (__m128 *)_Pyx_PyObject_GetAttrStr((__int64)v419, to_bytes); // 重点
BuiltinName = v420;
v421 = (__int64 *)v420;
if ( !v420 )
{
v1296 = 7153;
v436 = v1256;
v437 = 0LL;
v21 = (__int64 *)v1347;
goto LABEL_863;
}
v422 = qword_29648;
v424 = (__m128 *)_Pyx_PyObject_Call_constprop_0(v420, qword_29648); // 重点,qword_29648是元组,存储的信息是(2,'little'),在_Pyx_InitCachedConstants中有初始化
if ( !v424 )
{
v1296 = 7155;
v436 = v1256;
v437 = 0LL;
v21 = (__int64 *)v1347;
goto LABEL_863;
}
if ( (int)*v421 >= 0 )
{
v425 = *v421 - 1;
*v421 = v425;
if ( !v425 )
_Py_Dealloc(v421, v422, v423);
}

等同于Python中的:

1
v422 = v419.to_bytes(2, 'little')

对比

在函数_pyx_pf_4chal_4chal_6_p3_isra_0中的第998行左右可以找到如下内容

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
// v95 = self._var2
v167 = *(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v19 + 8) + 144LL);
if ( v167 )
v95 = v167(v19, var2);
else
v95 = PyObject_GetAttr(v19, var2); // 重点
if ( !v95 )
{
v107 = 0LL;
Attr = 0LL;
v126 = 4527LL;
v127 = 12LL;
goto LABEL_211;
}
/* 上文返回值v23和v2对比,对比常量:
https://github.com/python/cpython/blob/3.12/Include/object.h#L862
#define Py_LT 0 小于
#define Py_LE 1 小于等于
#define Py_EQ 2 等于
#define Py_NE 3 不等于
#define Py_GT 4 大于
#define Py_GE 5 大于等于
*/
Attr = (_QWORD *)PyObject_RichCompare(v23, v95, 2LL); // 重点
if ( !Attr )
{
v128 = v23;
v126 = 4529LL;
v23 = (__int64 *)v95;
v127 = 12LL;
goto LABEL_246;
}
if ( (int)*(_QWORD *)v95 >= 0 )
{
v169 = *(_QWORD *)v95 - 1LL;
*(_QWORD *)v95 = v169;
if ( !v169 )
_Py_Dealloc(v95, v95, v168);
}
// 确定是否IsTrue
LOBYTE(IsTrue) = Attr == (_QWORD *)&Py_TrueStruct;// 重点
if ( Attr == (_QWORD *)&Py_TrueStruct || Attr == (_QWORD *)&Py_FalseStruct || Attr == (_QWORD *)&Py_NoneStruct )
{
IsTrue = (unsigned __int8)IsTrue;
}
else
{
IsTrue = PyObject_IsTrue(Attr);
if ( IsTrue < 0 )
{
v126 = 4531LL;
v127 = 12LL;
v107 = 0LL;
v95 = 0LL;
goto LABEL_211;
}
}
if ( (int)*Attr >= 0 )
{
v171 = *Attr - 1LL;
*Attr = v171;
if ( !v171 )
_Py_Dealloc(Attr, v95, v168);
}
// 如果not IsTrue,即 v23 != self._var2
if ( !IsTrue )

取数组长度

在函数_pyx_pf_4chal_4chal_6_p3_isra_0中的第670行左右可以找到如下内容

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
v94 = *(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v19 + 8) + 144LL);// (v19 + 8) == 'self'
if ( v94 )
v95 = v94(v19, var1);
else
v95 = PyObject_GetAttr(v19, var1);// 重点
if ( !v95 )
{
v107 = 0LL;
v23 = 0LL;
v126 = 4407LL;
v127 = 9LL;
goto LABEL_211;
}
v96 = PyObject_Size(v95); // 重点
if ( v96 == -1 )
{
v23 = (__int64 *)v95;
v126 = 4409LL;
v107 = 0LL;
v100 = 0LL;
v95 = 0LL;
goto LABEL_254;
}
if ( (int)*(_QWORD *)v95 >= 0 )
{
v97 = *(_QWORD *)v95 - 1LL;
*(_QWORD *)v95 = v97;
if ( !v97 )
_Py_Dealloc(v95, v93, v90);
}
v98 = PyLong_FromSsize_t(v96); // 重点
v95 = v98;
if ( !v98 )
{
v107 = 0LL;
v23 = 0LL;
v126 = 4411LL;
v127 = 9LL;
goto LABEL_211;
}
v99 = v98;

这一段实际上等同于Python中的

1
v93 = len(self._var1)

运算

以下函数均在_pyx_pf_4chal_4chal_6_p3_isra_0中可以找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// v100 = 2654435769 - v98
v100 = PyNumber_Subtract(pyx_int_2654435769, v98);
// v107 = v23 + v95
v107 = PyNumber_Add(v23, v95);
// v303 = v302 ^ v93
v303 = (__m128 *)PyNumber_Xor(v302, v93);
// v379 = v1338 + v377
v379 = (__int64 *)PyNumber_Add(v1338, v377);
// InPlace字样,这里等同于v1322 = (v509 &= 4294967295)
v1322 = (__m128 *)PyNumber_InPlaceAnd(v509, pyx_int_4294967295);
// Slice_constprop_0 = v1322 >> 4
Slice_constprop_0 = (__m128 *)_Pyx_PyInt_RshiftObjC_constprop_0(v1322, pyx_int_4, 4LL);
// v569 = v1334 & 3 (有时候会出现0xFFFF,猜测是当成掩码了)
v569 = (__m128 *)_Pyx_PyInt_AndObjC_constprop_0(v1334, pyx_int_3, 3LL, 0LL);
// ItemInt_List_Fast_constprop_0 = v569 + 4
ItemInt_List_Fast_constprop_0 = (__m128 *)_Pyx_PyInt_AddObjC_constprop_0(v569, pyx_int_4, 4LL, 0LL);
//...

HOOK辅助

使用x86_64机器上的Python 3.12导入chal,即可对其函数进行hook。如我们需要hookself._p1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import chal
>>> setattr(chal.chal, "ori_p1", chal.chal.__dict__["_p1"]) # 保存原来的_p1
>>> def hook_p1(self, a, b): # 写hook函数,打印参数和返回值
... print(a, b)
... ret = self.ori_p1(a, b)
... print(ret)
... return ret
...
>>> setattr(chal.chal, "_p1", hook_p1) # 挂上去
>>> chal.chal("a") # 调用后即可看到每次调用self._p1时的参数和返回值,且不影响函数原来的功能
52232 48895
-27712
52883 49898
-25833
64660 29775
-28948
# ... 数据省略

hook出中间数据以后可以帮助我们确认或纠正在静态手工反编译过程中的一些细节,如这里hook出self._p1的中间数据后,如果我们恢复出的源代码中的self._p1的中间数据与此相同,那么可以保证流程走到self._p1时其参数和返回值一定是正确的;反之则说明恢复的源代码有误。

在本题中,hookself._p1可以确认输入数据流的加密是否正确;hookself._p2可以确认随机数是否相同,从而确认随机数种子及使用random模块中函数的次数是否一致;hookself._p3可以拿到被self._p3改变但未被self.__init__改变的self.tips,确认比对的细节。

解题

用上面的这些方法就可以将so文件里面的内容恢复成python源码了

这边建议先根据_Pyx_CreateStringTabAndInitStrings函数将符号变量什么的恢复出来,方便后面分析

同时,可以搜索一下特别长的数组,说不定能搜出来用了什么加密

重点要用于参考的函数有:_pyx_pw_4chal_4chal_1__init__(int以及杂项内容的初始化)、_Pyx_CreateStringTabAndInitStrings(各种字符串的索引)、_Pyx_InitCachedConstants(函数与一些元组的初始化)、_pyx_pf_4chal_4chal_6_p3_isra_0(p3函数的函数体)

其中在_pyx_pf_4chal_4chal_6_p3_isra_0中,需要交叉索引_Pyx_GetItemInt_List_Fast_constprop_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
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
import os
import random

class chal():
def __init__(self, i):
self.var1 = i
self.var2 = [121, 73, 141, 146, 115, 230, 181, 65, 238, 17, 146, 73, 228, 82, 188, 66, 12, 148, 225, 66, 255, 254, 47, 22, 163, 250, 222, 133, 35, 232, 106, 176]
self.var3 = [12, 243, 133, 147, 7, 36, 29, 49, 226, 211, 156, 56, 142, 78, 254, 12]
random.seed(sum([2654435769-len(i)] + [ord(x) for x in str(len(i))]))
if self._p3("Don't Hook!!!") == self.var2:
print("yes")
elif self.tips == 0:
# 这里有个 self.var2 + self.var3 没用上
print("yes")
self.tips = "Don't peek!!!"

def _p1(self,x1,x2):
c = x1 * x2
cl = c & 0xFFFF
ch = (c>>16) & 0xFFFF
if (cl-ch)<0:
ret = cl - ch + 1
else:
ret = cl - ch

return ret

def _p2(self,i):
# sm4 subbytes
l = [214, 144, 233, 254, 204, 225, 61, 183, 22, 182, 20, 194, 40, 251, 44, 5, 43, 103, 154, 118, 42, 190, 4, 195, 170, 68, 19, 38, 73, 134, 6, 153, 156, 66, 80, 244, 145, 239, 152, 122, 51, 84, 11, 67, 237, 207, 172, 98, 228, 179, 28, 169, 201, 8, 232, 149, 128, 223, 148, 250, 117, 143, 63, 166, 71, 7, 167, 252, 243, 115, 23, 186, 131, 89, 60, 25, 230, 133, 79, 168, 104, 107, 129, 178, 113, 100, 218, 139, 248, 235, 15, 75, 112, 86, 157, 53, 30, 36, 14, 94, 99, 88, 209, 162, 37, 34, 124, 59, 1, 33, 120, 135, 212, 0, 70, 87, 159, 211, 39, 82, 76, 54, 2, 231, 160, 196, 200, 158, 234, 191, 138, 210, 64, 199, 56, 181, 163, 247, 242, 206, 249, 97, 21, 161, 224, 174, 93, 164, 155, 52, 26, 85, 173, 147, 50, 48, 245, 140, 177, 227, 29, 246, 226, 46, 130, 102, 202, 96, 192, 41, 35, 171, 13, 83, 78, 111, 213, 219, 55, 69, 222, 253, 142, 47, 3, 255, 106, 114, 109, 108, 91, 81, 141, 27, 175, 146, 187, 221, 188, 127, 17, 217, 92, 65, 31, 16, 90, 216, 10, 193, 49, 136, 165, 205, 123, 189, 45, 116, 208, 18, 184, 229, 180, 176, 137, 105, 151, 74, 12, 150, 119, 126, 101, 185, 241, 9, 197, 110, 198, 132, 24, 240, 125, 236, 58, 220, 77, 32, 121, 238, 95, 62, 215, 203, 57, 72]
ret = int.from_bytes(bytes(map(lambda x: l[x], i.to_bytes(4, 'little'))), 'little')
return ret

def _p3(self):
# 开头的lambda是个pkcs5padding,但是输入整块的数据就不重要了(块长度是16
# s = pkcs5padding(self.var1).encode()
s = self.var1.encode()
L = [173, 7, 131, 63, 141, 180, 193, 156, 21, 198, 65, 218, 13, 216, 148, 105, 165, 96, 250, 121, 168, 23, 94, 49, 79, 120, 101, 211, 167, 240, 75, 136, 43, 70, 115, 203, 220, 34, 160, 188, 222, 61, 169, 117, 95, 134, 174, 167]
tmpl = []
self.tips = 0
for i in range(len(s)):
tmpl.append(L[i%len(L)] ^ s[i])
rslt = b''
for i in range(0, len(tmpl), 8):
longs = tmpl[i:i+8]
randl = []
# IDEA的轮密钥
for _ in range(52):
randl.append(random.getrandbits(16))
ll = []
for j in range(0, len(longs), 2):
ll.append(int.from_bytes(longs[j:j+2]))
assert len(ll) == 4
v1227, v1229, v1230, v1222 = ll
for v1232 in range(8):
v1225 = random.randint(0x9e3779b9, 0xc6ef3720)
# TEA 密钥
rilv1224 = []
for _ in range(8):
rilv1224.append(random.randint(0x56AA3350, 0xa3b1bac6))
# XTEA 密钥
rilv1228 = []
for _ in range(8):
rilv1228.append(random.randint(0x677D9197, 0xb27022dc))
# 半轮魔改IDEA
lv1233 = []
lv1233.append(self._p1(v1227, randl[6 * v1232 + 0]) & 0xFFFF)
lv1233.append((v1229 + randl[6 * v1232 + 1]) & 0xFFFF)
lv1233.append((v1230 + randl[6 * v1232 + 2]) & 0xFFFF)
lv1233.append(self._p1(v1222, randl[6 * v1232 + 3]) & 0xFFFF)
lv395 = []
for j in range(4):
lv395.append(lv1233[j].to_bytes(2, 'big'))
tmps = b''.join(lv395)
lv424 = []
for j in range(0, len(tmps), 4):
lv424.append(int.from_bytes(tmps[j:j+4], 'little'))
assert len(lv424) == 2
# 魔改TEA
l, r = lv424 # v1213, v1215
l += ((r << 4) + self._p2(rilv1224[0])) ^ (r + v1225) ^ ((r >> 5) + self._p2(rilv1224[1]))
l &= 4294967295
r += ((l << 5) + self._p2(rilv1224[2])) ^ (l + v1225) ^ ((l >> 4) + self._p2(rilv1224[3]))
r &= 4294967295
# 魔改XTEA
l += (((r << 5) ^ (r >> 4)) + r) ^ (v1225 + self._p2(rilv1228[4 + (v1225 & 3)]))
l &= 4294967295
r += (((l << 4) ^ (l >> 5)) + l) ^ (v1225 + self._p2(rilv1228[4 + ((v1225 >> 11) & 3)]))
r &= 4294967295
# 奇数轮更改v1225(TEA/XTEA中的sum)
if v1232 & 1 == 1:
v1225 = random.getrandbits(32)
# 魔改TEA
l += (((r << 3) ^ (r >> 6)) + r) ^ (v1225 + self._p2(rilv1228[v1225 & 3]))
l &= 4294967295
r += (((l << 4) ^ (l >> 5)) + l) ^ (v1225 + self._p2(rilv1228[(v1225 >> 11) & 3]))
r &= 4294967295
# 魔改XTEA
l += ((r << 4) + self._p2(rilv1224[4])) ^ (r + v1225) ^ ((r >> 5) + self._p2(rilv1224[5]))
l &= 4294967295
r += ((l << 2) + self._p2(rilv1224[6])) ^ (l + v1225) ^ ((l >> 7) + self._p2(rilv1224[7]))
r &= 4294967295
tmps = b''.join([x.to_bytes(4, 'little') for x in [l, r]])
lv770 = []
for j in range(0, len(tmps), 2):
lv770.append(int.from_bytes(tmps[j:j+2], 'big'))
assert len(lv770) == 4
# 另外半轮魔改IDEA
a, b, c, d = lv770 # v1242, v1241, v1240, v1239
v1196 = self._p1((a ^ c) & 0xFFFF, randl[v1232 * 6 + 4])
v835 = self._p1(((b ^ d) + v1196) & 0xFFFF, randl[v1232 * 6 + 5]) & 0xFFFF
v1198 = (v1196 + v835) & 0xFFFF
v840 = a ^ v835
v841 = d ^ v1198
v842 = b ^ v1198
v91 = c ^ v835
# 奇数轮和偶数轮交接给下一轮的变量不同
if v1232 & 1 != 0:
v1230 = v91
v1229 = v842
else:
v1230 = v842
v1229 = v91
v1222 = v841
v1227 = v840
# 最后半轮魔改IDEA
v1227 = self._p1(v1227 & 0xFFFF, randl[48]) & 0xFFFF
v1229 = (v1229 + randl[49]) & 0xFFFF
v1230 = (v1230 + randl[50]) & 0xFFFF
v1222 = self._p1(v1222 & 0xFFFF, randl[51]) & 0xFFFF
ll = [v1227, v1229, v1230, v1222]
lv918 = []
for j in range(len(ll)):
lv918.append(ll[j].to_bytes(2, 'big'))
tmps = b''.join(lv918)
print(list(tmps))
rslt += tmps
# 真正的比对在这里(
self.tips += sum([tmps[j] ^ random.getrandbits(8) for j in range(len(tmps))])
print(self.tips, list(rslt))
# 没用的返回值,真随机
return [x ^ y for x, y in zip(os.urandom(len(rslt)), rslt)]

if __name__ == '__main__':
c = chal("a"*48) # 测试

其中关键点在于,其对比不是用self._var2self._var3进行对比,而是用当前轮的random.getrandbits(8)进行对比,xor为0表示相同,只要最后self._tips == 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
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
import random

# 假密文!!!
dst = [121, 73, 141, 146, 115, 230, 181, 65, 238, 17, 146, 73, 228, 82, 188, 66, 12, 148, 225, 66, 255, 254, 47, 22, 163, 250, 222, 133, 35, 232, 106, 176]
dst += [12, 243, 133, 147, 7, 36, 29, 49, 226, 211, 156, 56, 142, 78, 254, 12]
random.seed(sum([2654435769-len(dst)] + [ord(x) for x in str(len(dst))]))

r_randl = []
r8_v1225 = []
r8_rilv1224 = []
r8_rilv1228 = []
r8_rand8b = []
for _ in range(0, len(dst), 8):
r_randl.append([random.getrandbits(16) for _ in range(52)])
tmpl = []
for i in range(8):
tmpl.append(random.randint(0x9e3779b9, 0xc6ef3720))
r8_rilv1224.append([random.randint(0x56AA3350, 0xa3b1bac6) for _ in range(8)])
r8_rilv1228.append([random.randint(0x677D9197, 0xb27022dc) for _ in range(8)])
if i & 1 == 1:
tmpl.append(random.getrandbits(32))
r8_v1225 += tmpl[::-1]
r8_rand8b.append([random.getrandbits(8) for _ in range(8)])
v1225_iter = iter(r8_v1225)
# 真密文
dst = sum(r8_rand8b, [])

def _p1(a, b):
c = a * b
cl = c & 0xFFFF
ch = (c >> 16) & 0xFFFF
if cl - ch < 0:
return cl - ch + 1
else:
return cl - ch

def _p2(i):
l = [214, 144, 233, 254, 204, 225, 61, 183, 22, 182, 20, 194, 40, 251, 44, 5, 43, 103, 154, 118, 42, 190, 4, 195, 170, 68, 19, 38, 73, 134, 6, 153, 156, 66, 80, 244, 145, 239, 152, 122, 51, 84, 11, 67, 237, 207, 172, 98, 228, 179, 28, 169, 201, 8, 232, 149, 128, 223, 148, 250, 117, 143, 63, 166, 71, 7, 167, 252, 243, 115, 23, 186, 131, 89, 60, 25, 230, 133, 79, 168, 104, 107, 129, 178, 113, 100, 218, 139, 248, 235, 15, 75, 112, 86, 157, 53, 30, 36, 14, 94, 99, 88, 209, 162, 37, 34, 124, 59, 1, 33, 120, 135, 212, 0, 70, 87, 159, 211, 39, 82, 76, 54, 2, 231, 160, 196, 200, 158, 234, 191, 138, 210, 64, 199, 56, 181, 163, 247, 242, 206, 249, 97, 21, 161, 224, 174, 93, 164, 155, 52, 26, 85, 173, 147, 50, 48, 245, 140, 177, 227, 29, 246, 226, 46, 130, 102, 202, 96, 192, 41, 35, 171, 13, 83, 78, 111, 213, 219, 55, 69, 222, 253, 142, 47, 3, 255, 106, 114, 109, 108, 91, 81, 141, 27, 175, 146, 187, 221, 188, 127, 17, 217, 92, 65, 31, 16, 90, 216, 10, 193, 49, 136, 165, 205, 123, 189, 45, 116, 208, 18, 184, 229, 180, 176, 137, 105, 151, 74, 12, 150, 119, 126, 101, 185, 241, 9, 197, 110, 198, 132, 24, 240, 125, 236, 58, 220, 77, 32, 121, 238, 95, 62, 215, 203, 57, 72]
return int.from_bytes(bytes(map(lambda x: l[x], i.to_bytes(4, 'little'))), 'little')

def p1_rev(ret, b):
for i in range(0x10000):
if ret == (_p1(i, b) & 0xFFFF):
return i

res = b''
for xi in range(0, len(dst), 8):
randl = r_randl[xi//8]
l8 = dst[xi:xi+8]
l4 = [int.from_bytes(bytes(l8[i:i+2]), 'big') for i in range(0, len(l8), 2)]
v1227, v1229, v1230, v1222 = l4
v1227 = p1_rev(v1227, randl[48]) & 0xFFFF
v1229 = (v1229 - randl[49]) & 0xFFFF
v1230 = (v1230 - randl[50]) & 0xFFFF
v1222 = p1_rev(v1222, randl[51]) & 0xFFFF
for i in range(8)[::-1]:
rilv1224 = r8_rilv1224[xi + i]
rilv1228 = r8_rilv1228[xi + i]
v841 = v1222
v840 = v1227
if i & 1 != 0:
v91 = v1230
v842 = v1229
else:
v842 = v1230
v91 = v1229
a_xor_c = v840 ^ v91
b_xor_d = v841 ^ v842
v1196 = _p1(a_xor_c, randl[i * 6 + 4]) & 0xFFFF
v835 = _p1((b_xor_d + v1196) & 0xFFFF, randl[i * 6 + 5]) & 0xFFFF
v1198 = (v1196 + v835) & 0xFFFF
a = v840 ^ v835
b = v842 ^ v1198
c = v91 ^ v835
d = v841 ^ v1198
l4 = [a, b, c, d]
s = b''.join([x.to_bytes(2, 'big') for x in l4])
l, r = [int.from_bytes(s[j:j+4], 'little') for j in range(0, len(s), 4)]
v1225 = next(v1225_iter)
r -= ((l << 2) + _p2(rilv1224[6])) ^ (l + v1225) ^ ((l >> 7) + _p2(rilv1224[7]))
r &= 4294967295
l -= ((r << 4) + _p2(rilv1224[4])) ^ (r + v1225) ^ ((r >> 5) + _p2(rilv1224[5]))
l &= 4294967295
r -= (((l << 4) ^ (l >> 5)) + l) ^ (v1225 + _p2(rilv1228[(v1225 >> 11) & 3]))
r &= 4294967295
l -= (((r << 3) ^ (r >> 6)) + r) ^ (v1225 + _p2(rilv1228[v1225 & 3]))
l &= 4294967295
if i & 1 == 1:
v1225 = next(v1225_iter)
r -= (((l << 4) ^ (l >> 5)) + l) ^ (v1225 + _p2(rilv1228[4 + ((v1225 >> 11) & 3)]))
r &= 4294967295
l -= (((r << 5) ^ (r >> 4)) + r) ^ (v1225 + _p2(rilv1228[4 + (v1225 & 3)]))
l &= 4294967295
r -= ((l << 5) + _p2(rilv1224[2])) ^ (l + v1225) ^ ((l >> 4) + _p2(rilv1224[3]))
r &= 4294967295
l -= ((r << 4) + _p2(rilv1224[0])) ^ (r + v1225) ^ ((r >> 5) + _p2(rilv1224[1]))
l &= 4294967295
s = b''.join([x.to_bytes(4, 'little') for x in [l, r]])
v1227, v1229, v1230, v1222 = [int.from_bytes(s[j:j+2], 'big') for j in range(0, len(s), 2)]
v1227 = p1_rev(v1227, randl[6 * i + 0]) & 0xFFFF
v1229 = (v1229 - randl[6 * i + 1]) & 0xFFFF
v1230 = (v1230 - randl[6 * i + 2]) & 0xFFFF
v1222 = p1_rev(v1222, randl[6 * i + 3]) & 0xFFFF
res += b''.join([x.to_bytes(2, 'big') for x in [v1227, v1229, v1230, v1222]])

L = [173, 7, 131, 63, 141, 180, 193, 156, 21, 198, 65, 218, 13, 216, 148, 105, 165, 96, 250, 121, 168, 23, 94, 49, 79, 120, 101, 211, 167, 240, 75, 136, 43, 70, 115, 203, 220, 34, 160, 188, 222, 61, 169, 117, 95, 134, 174, 167]
flag = []
for i in range(len(res)):
flag.append(res[i] ^ L[i%len(L)])
print(bytes(flag))
# b'DASCTF{c6090fd29eaf2ae1d111289e3f3c0c7a3819dcc1}'