CTF-WP-4
TPCTF2023
nanPyEnc
pyinstxtractor.py
可以轻松解压。
注意:猜测是不同python版本的原因导致不会解压PYZ archive
python3.10
下:
python3.8
下:
1 | # Source Generated with Decompyle++ |
这里解出来的FLAG是错误的。
猜测是导入的变量又被其他导入覆盖了:from Crypto.Util.number import *
在Crypto.Util.number
中末尾有:
1 | if time.time() % 64 < 1: |
解出来的FLAG还是不对。
再看Crypto.Util.number
导入的from Crypto.Util.py3compat import *
在from Crypto.Util.py3compat import *
中有:
1 | def list(s): |
这个条件和Crypto.Util.number
的相同,因此大概是了。
改变了的地方:
进行操作解FLAG
1 | from Crypto.Cipher import AES |
maze
解压可执行文件
pyinstxtractor.py
可以解压。
1 | # Source Generated with Decompyle++ |
逻辑都在maze
中,是一个.so
文件。
用help
看看maze
里有什么:(好像需要同版本的python)
变量名是base64
编码
1 | import maze |
把DATA
打印出来:
1 | >> import maze |
maze.so
Cython逆向中,一般调用函数都会利用类似 PyObject_Call 这样的函数来调用。比如 PyObject_Call 中第一个参数是函数对象,第二和第三个参数是该被调用函数对象的参数,通常以元组和字典的形式传入(位置参数和关键字参数)。
run
直接搜索进入run
函数。
这里可以看出是print
,但内容找不到:
交叉引用_pyx_mstate_global_static
能看到一堆:
用网上师傅的脚本可以把_pyx_mstate_global_static
结构体拆解成成员的变量的组合,然后就可以交叉引用了。
1 | ys = ['PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyTypeObject *', 'PyTypeObject *', 'PyTypeObject *', 'PyTypeObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *', 'PyObject *'] |
执行脚本后,交叉引用得到字符串:
之后往下看,猜测这里是获取solve函数,然后调用,参数应该是输入的字符串:
1 | v25 = (PyObject *)_pyx_dict_version.18832; |
因此,run
的逻辑为:
1 |
|
solve
再看solve
。
这里根据WP,猜测是MazeLang(base64.b64decoder(_pyx_n_s_UJ9mxXxeoS).decode())
的过程:
1 | __pyx_L4_argument_unpacking_done: |
之后有一个获取len(arg)
的过程,看WP才知道,小小的一段真不容易发现:
s1a
通过交叉引用可以找到为__pyx_args
,即函数的参数,因此应该是获取输入的长度是不是33。
1
2
3
4
5
6
7
8
9
10
11
12
13 .text:0000000000025360 ; PyObject *__fastcall _pyx_pw_4maze_3c29sdmU(PyObject *__pyx_self, PyObject *const *__pyx_args, Py_ssize_t __pyx_nargs, PyObject *__pyx_kwds)
.text:0000000000025360 __pyx_pw_4maze_3c29sdmU proc near ; DATA XREF: .data:__pyx_mdef_4maze_3c29sdmU↓o
.text:0000000000025360
.text:0000000000025360 s1= qword ptr -78h
.text:0000000000025360 var_70= qword ptr -70h
.text:0000000000025360 values= qword ptr -60h
.text:0000000000025360 __pyx_pyargnames= qword ptr -58h
.text:0000000000025360 var_40= qword ptr -40h
.text:0000000000025360
.text:0000000000025360 __pyx_self = rdi ; PyObject *
.text:0000000000025360 __pyx_args = rsi ; PyObject *const *
.text:0000000000025360 __pyx_nargs = rdx ; Py_ssize_t
.text:0000000000025360 __pyx_kwds = rcx ; PyObject *
__pyx_self
(存储在寄存器rdi
中):这是函数的第一个参数,通常是表示函数被调用的对象实例(对于类方法)或者是模块对象(对于模块级别的函数)。__pyx_args
(存储在寄存器rsi
中):这是一个指向参数数组的指针,参数的个数由__pyx_nargs
指定。__pyx_nargs
(存储在寄存器rdx
中):这是一个整数,表示传递给函数的参数个数。__pyx_kwds
(存储在寄存器rcx
中):这可能是一个指向关键字参数的指针或一个表示关键字参数的 Python 字典。
开启循环range(33)
,获取单个字符:
看报错应该是用了ord
:
调用了Mazelang.run_till_output
:
进行异或,v45
是ord(input[index])
,v57
是Mazelang.run_till_output
返回值:
获取secret
,后面有v63 = PyObject_GetItem(Attr, v97);
获取单个字符:
最后,进行比较:
v15
是异或的结果,v45
是secret[index]
。
第三个参数 3LL
表示要进行的比较操作为等于比较,对应的宏是 Py_EQ
。
因此,完整逻辑:
1 | def solve(input): |
exp
1 | import maze |
Mazelang
这里看一看Mazelang
,感觉蛮好玩的。
chrwoods/mazelang: Esoteric maze-based programming language (github.com)
地图根据base64可以得到:(上半部分是地图,下半部分是函数)
根据上面的网站以及自己尝试调试了解释器的源码后,大概知道了些:
car
只要原来的方向还能走,就往原来的方向走- 方向初始化为
['U', 'R', 'D', 'L']
- 方向检测时,第一个检测的是原来的方向,最后一个检测是原来的方向的反方向
- 方向初始化为
- 每走一个格,执行该格对应的指令(可能是指令,可能是函数)
- 一个
car
停在**
上时,会发出signal
,别的车可以根据这个signal
执行条件判断函数操作
1 | # ## ## ## ## ## ## |
由以上可以得到maze
的路径:
只画了前三轮,红车的value每轮加一,进入不同行的**
,导致触发signal
的时机不同,从而改变蓝车的轨迹,输出不同的字符。
输出字符依次为:HITPCTF
,不断循环。
可以直接使用解释器的脚本获取输出:
funky
👉2023 11.25 TPCTF reverse wp
👉国际赛TPCTF 2023 Writeup –Polaris
程序计算的逻辑将4字节数的每个bit转为1个4字节数。
0x8000000
是0,0x00000000
是1。
main
的逻辑还是很好理解的,主要是三个加密函数。
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
enc1
enc1
主要是获取每个输入的字节进行运算。
大部分的操作都是赋值操作,运算的操作集中在三个函数:
1 | void __fastcall and_operation(float *a1, float *rsi_, float *rdx_) |
使用条件断点获取函数的输入输出:
1 | *****enc1 while1***** |
输入是fP
,这里只放了前两个输入的加密。
这里每个输入的加密除了获取输入那里就不在其他地方出现了🤮
动调(把输入字符转化的0x80000000
、0x00000000
改为明显的其他数,不然好难调🤮)发现enc1
是按bit处理的。
继续动调,结合上面函数输出输入的过程,获得逻辑:
1 | def get_andbit(num): |
enc2
enc2
中进行了四次循环加密,循环主体如下:
1 | do |
主要的add_operation
函数,很难看懂:
1 | void __fastcall add_operation(float *a1, float *a2, float *a3) |
参考了LaoGong的wp(真神✪ ω ✪),可以猜测这个函数是个模0xFF
加法操作。
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 expr_table('-(x - y) - (y - x)') # xor
expr_table('-(-x - y)') # and
expr_table('-(-(-(x - y) - (y - x)) - n) - (-x - y)') # carry
expr_table('-((-(x - y) - (y - x)) - n) - (n - (-(x - y) - (y - x)))') # add
'''
-(x - y) - (y - x):
(0, 0) -> 0
(0, 1) -> 1
(1, 0) -> 1
(1, 1) -> 0
-(-x - y):
(0, 0) -> 0
(0, 1) -> 0
(1, 0) -> 0
(1, 1) -> 1
-(-(-(x - y) - (y - x)) - n) - (-x - y):
(0, 0) -> 0
(0, 1) -> 0
(1, 0) -> 0
(1, 1) -> 1
-((-(x - y) - (y - x)) - n) - (n - (-(x - y) - (y - x))):
(0, 0) -> 0
(0, 1) -> 1
(1, 0) -> 1
(1, 1) -> 0
'''
经过调试,得到逻辑:
1 | # rax = 0 |
enc3
enc3
就和enc1
差不多了,只是他操作的是两个字节的数。
其他都是一样的,过。🆒
脚本
1 | import struct |
强网杯
babyre
逻辑很简单,一个类似TEA的加密。
1 | __int64 sub_7FF7A24E2050() |
只是key
和result
被TLSCallback
更改了:
脚本:
1 | import struct |
ezre
放在强网先锋的ezre。
混淆给D8秒了,得到代码:
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
.init_array
有反调试,影响main
里的一些操作:
只用关注红框的就好了。
脚本:
变换的base64表在linux环境下执行以下代码就可以得到了。
1 | import base64 |
ezre
放在逆向的ezre。
混淆同样是D8秒了,我的神。
main里面啥也不是,程序开了子进程,猜测是获取输入,在另一个进程里加密。
1 | unsigned __int64 __fastcall sub_39F0(unsigned int a1) |
有密钥和密文,国密SM4在线加密/解密
dotdot
ILSpy
打开(dnspy
看不到数据):
1 | private static void Main(string[] args) |
程序逻辑,获取输入,然后进入AAA
加密比对,然后将License.dat
数据进行EEE
解密,进行反序列化。
先看AAA
的加密:
网上WP说这是AES白盒,真看不出来。
本来想写个z3
看看能不能解的,结果才知道z3
的变量类型不能当数组的index
。
直接给闵神爆破出来了✪ ω ✪:WelcomeToQWB2023
然后进入EEE:RC4
,密钥是WelcomeToQWB2023
Cyberchef
解得:
这里其他的看不懂,但有个FFF
,应该是反序列化会执行到这个函数。
FFF
是个TEA加密,密文、密钥都有,解得:dotN3t_Is_1nt3r3sting
1 | def tea_dec(cipher, key): |
FFF
后面还有一段:v7
是解密后的Lisence.dat
1 | byte[] array3 = MD5.Create().ComputeHash(Program.v7); |
但执行后,啥也不是。
题干提示了fix the lisence
,且程序进行反序列化的时候报错,所以还需要修改lisence
在报错的地方断住,即switch
的default
位置:
1 | internal void Run() |
报错的原因是因为b
为0x00
,而b
的来源是读取文件的,位置在0x294
:
在switch
断住,一个个看报错前的上一次switch
是什么,发现是在ObjectString
里面有读字符串操作:
先读了一个Int32
为0x6
:
再读字符串:
跟进ReadString
:
Read7BitEncodedInt
按解析1Byte的数据,这里获取的数据为0x0,因此返回string.Empty
- 如果
Read7BitEncodedInt
获取的数据不为0x0,在后面有this.m_stream.Read(this.m_charBytes, 0, count);
就是读取字符串的操作。因此,先读取1Byte的长度,再根据长度读取字符串。
1 | [ ] |
以上,得到:
而下一步报错的原因就是,读取下一步操作的opcode
为0x0
,没有定义,就报错了。
因此,可能就是报错的时候读取的不是opcode
,是字符串被Patch
为00了
再根据被Patch的数据之后,再0x2A8
的位置,也有个06 07 00 00 00
,字符串估计就是从0x293~0x2A7
,长度为21。
恰好FFF
解密得到的字符串就为21长度,放进去。
对于后面的06 07 00 00 00
也是对应一个字符串,长度为16,应该就是放AAA
解密的字符串了。
再想想,有点难以理解,但是想到FFF
的参数恰好就是上面的两个字符串,可能是这个反序列化过程,这两个字符串可以作为其FFF
函数的参数了吧。
Patch之后:
再继续FFF
后面的过程就可以得到FLAG了:
1 | v10 = [ |
楚慧杯2023
babyre
解混淆后:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
提示了TEA,就直接脚本解了:
1 | import struct |
ezbase
栈溢出:
1 | __int64 sub_404986() |
改变返回地址到这,不知道为啥0ex40490d
不行,用的0x404911
:
EXP:
1 | from pwn import * |