TPCTF Reverse 复现记录

First Post:

Last Update:

好久没有正经写复现了,这次整个人脑子都处于网咖状态,彻彻底底变成肥宅了,得想办法改改,于是开始写复现报告了。考虑到某些需求,这次着重于逆向部分,Pwn 的部分等啥时候有时间和心情了再写吧。

Reverse

funky

程序流程很清晰,输入 flag 然后加密后和密文比对,相同即可。

然后是这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
do
{
v8 = *v7;
v14 = 0LL;
v15 = 0LL;
v16 = 0LL;
v17 = 0LL;
sub_17F0(v6, v8);
*(_QWORD *)(v9 - 32) = v14;
*(_QWORD *)(v9 - 24) = v15;
*(_QWORD *)(v9 - 16) = v16;
*(_QWORD *)(v9 - 8) = v17;
}

v8 每次取输入的一个字节输入 sub_17F0,该函数如下:

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
void __fastcall sub_17F0(unsigned int *a1, char a2)
{
unsigned int v2; // xmm0_4
unsigned int v3; // xmm0_4
unsigned int v4; // xmm0_4
unsigned int v5; // xmm0_4
unsigned int v6; // xmm0_4
unsigned int v7; // xmm0_4
unsigned int v8; // xmm0_4
unsigned int v9; // xmm0_4

v2 = 0x80000000;
if ( (a2 & 1) != 0 )
v2 = 0;
*a1 = v2;
v3 = 0x80000000;
if ( (a2 & 2) != 0 )
v3 = 0;
a1[1] = v3;
v4 = 0x80000000;
if ( (a2 & 4) != 0 )
v4 = 0;
a1[2] = v4;
v5 = 0x80000000;
if ( (a2 & 8) != 0 )
v5 = 0;
a1[3] = v5;
v6 = 0x80000000;
if ( (a2 & 16) != 0 )
v6 = 0;
a1[4] = v6;
v7 = 0x80000000;
if ( (a2 & 32) != 0 )
v7 = 0;
a1[5] = v7;
v8 = 0x80000000;
if ( (a2 & 64) != 0 )
v8 = 0;
a1[6] = v8;
v9 = 0;
if ( (a2 & 128) == 0 )
v9 = 0x80000000;
a1[7] = v9;
}

对 a2 的每个 bit 下判断,让 a1 的对应索引为 0 或 0x80000000,实质上是做了二值化。其中,0x80000000 对应 0bit,0对应1bit。

然后是如下三个函数对输入进行加密:

1
2
3
sub_2EC0();
sub_2340();
sub_3280();

然后最后再从二值化恢复为字节:

1
2
3
4
5
6
7
8
9
10
do
{
v11 = *v5;
v12 = 2
* ((2
* ((2
* ((2 * ((2 * ((2 * ((2 * (*(v5 + 7) >= 0)) | (*(v5 + 6) >= 0))) | (*(v5 + 5) >= 0))) | (*(v5 + 4) >= 0))) | (*(v5 + 3) >= 0))) | (*(v5 + 2) >= 0))) | (*(v5 + 1) >= 0));
v5 += 4;
*v4++ = (v11 >= 0) | v12;
}

所以关键就是那三个加密函数了。

然后就是慢无边际的调试和确认了,先放放,下次一定

nanoPyEnc

复现过程

pyinstxtractor 一把梭先解包出 run.pyc,反编译一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from secret import key, enc
from Crypto.Cipher import AES
from Crypto.Util.number import *
from Crypto.Util.Padding import pad
key = key.encode()
message = input('Enter your message: ').strip()
if not message.startswith('TPCTF{') or message.endswith('}'):
raise AssertionError

def encrypt_message(key = None, message = None):
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(message, AES.block_size))
return ciphertext

encrypted = list(encrypt_message(key, message.encode()))
for x, y in zip(encrypted, enc):
if x != y:
print('Wrong!')

print('Right!')
return None

代码逻辑很清楚,但是解出来的地方没有 secret.pyc ,所以这部分应该是一起被打包编译好了,得去内存里搜索。

用 gdb 调试二进制会发现不能很好的跟踪上,查一下进程会发现有两个:

1
2
tokamei+    3636  3.4  0.0   2960  1920 pts/0    S+   16:30   0:00 ./nanoPyEnc
tokamei+ 3637 4.5 0.2 67708 24040 pts/0 S+ 16:30 0:00 ./nanoPyEnc

这里直接跟第二个,然后搜一下字符串:

1
2
pwndbg> search secret.
[heap] 0x2326f44 0x702e746572636573 ('secret.p')

跟一下内存:

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
pwndbg> tel 0x2326f40-0x100 100
00:00000x2326e40 ◂— 0x7b0900005a0600
01:00080x2326e48 ◂— 0xffffffff00000001
02:00100x2326e50 ◂— 0x1b0
03:00180x2326e58 ◂— 0x110
04:00200x2326e60 —▸ 0x2067376 ◂— 0xc0000
05:00280x2326e68 ◂— 0x901bb59dc67f3254
06:00300x2326e70 ◂— 0xe2
07:00380x2326e78 ◂— 0xffffffffffffffff
08:00400x2326e80 ◂— 0xe3
09:00480x2326e88 ◂— 0x0
0a:00500x2326e90 ◂— 0x400000001000
0b:00580x2326e98 ◂— 0x640000002cf300
0c:00600x2326ea0 ◂— 0x36402640164005a /* 'Z' */
0d:00680x2326ea8 ◂— 0x664056404640164
0e:00700x2326eb0 ◂— 0xa64096408640764
0f:00780x2326eb8 ◂— 0xe640d640c640b64
10:00800x2326ec0 ◂— 0x1064015a10670f64
11:00880x2326ec8 ◂— 0x303210fa11290053 /* 'S' */
12:00900x2326ed0 ◂— 0x38312d35302d3333 ('33-05-18')
13:00980x2326ed8 ◂— 0xd5e933333a33305f
14:00a0│ 0x2326ee0 ◂— 0xe7e9000000
15:00a8│ 0x2326ee8 ◂— 0x9e9000000c9e9
16:00b0│ 0x2326ef0 ◂— 0xe9000000c5e90000
17:00b8│ 0x2326ef8 ◂— 0x51e9000000e9
18:00c0│ 0x2326f00 ◂— 0xdfe90000006fe900
19:00c8│ 0x2326f08 ◂— 0x22e9000000
1a:00d0│ 0x2326f10 ◂— 0x67e9000000a6e9
1b:00d8│ 0x2326f18 ◂— 0xe9000000e1e90000
1c:00e00x2326f20 ◂— 0xb4e9000000af
1d:00e80x2326f28 ◂— 0x656b03da02a94e00
1e:00f0│ 0x2326f30 ◂— 0xa9636e6503da79
1f:00f8│ 0x2326f38 ◂— 0x15720000001572
20:01000x2326f40 ◂— 0x72636573097a0000
21:01080x2326f48 ◂— 0x3c08da79702e7465
22:01100x2326f50 ◂— 0x13e656c75646f6d
23:01180x2326f58 ◂— 0x2f3000000
24:01200x2326f60 ◂— 0x44080000000104

这里有一个 pyc 字节码的特征值 0xe3(不过这个不一定是这个,但一定程度对比一下头文件是可以识别出来的),把这段导出成二进制文件:

1
dump memmory ./test 0x2326e80 0x2326e80+0x100

再手动加个文件头然后反编译一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
key = '2033-05-18_03:33'
enc = [
213,
231,
201,
213,
9,
197,
233,
81,
111,
223,
34,
166,
103,
225,
175,
180]

这个解出来是 flag{test},比较微妙,那么剩下的代码应该是无法被还原的 pyz 文件里了。在内存里用同样的方法不太好定位出目标文件,因为我们根本就不知道哪个文件导致了数据变化,这里看了下 T 神的解才知道,原来 pyz 文件是可以分解出 pyc 字节码的,写个脚本跑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
os.chdir("_PYZ-00.pyz.extracted")
files = os.listdir(".")
os.mkdir("../solvepyc")
print(files)
for file in files:
if(file.endswith("pyc")):
continue
f = open(file,"rb")
data = f.read()
f.close()
f2 = open("../solvepyc/"+file+".pyc","wb")
# 给数据加上 pyc 的文件头
f2.write(bytearray.fromhex("55 0D 0D 0A 00 00 00 00 00 00 00 00 00 00 00 00")+data)
f2.close()

丢出来再跑一下反编译:

1
2
3
4
import os
files = os.listdir("solvepyc")
for file in files:
os.system("pycdc.exe " + "solvepyc/" +file +" > " + "solvepy/"+file+".py")

然后用 vscode 打开目录批量去搜关键字就可以了:

这里对 enc 进行了更新,估摸着是 from Crypto.Util.number import * 的时候触发的。不过还是解不出来。再看看代码中对数据的处理代码:

1
2
3
    def list(s):
        _x = time.time() % 64 < 1
        return (lambda .0 = None: [ _x ^ x for x in .0 ])(s)

代码重载了 list ,这会让每个字节异或上 1 再打包成数字:

总结

主要是几个技巧:

  • pyz 解包是可以得到 pyc 字节码的
  • cpython 打包出来的可执行文件其实还是执行了字节码,如果有一定的信息,在内存中是可以定位到字节码的。

polynomial

比赛的时候连看都没看,发现做出来的人不多就直接没看这个去看 misc 了,要命。赛后才知道原来运算似乎都是单字节映射还是啥的,反正就是能按序加密按序检查,所以理论上对于 n 个字节的输入,n-1 个字符的值是可以确定的,否则不会检查第 n 个字符。那么就可以从第一个字符开始爆破了,看了下 nepnep 的 wp 感觉挺妙的,把 check 的索引作为程序退出时的返回值,然后每次输入 n 个字符检查返回值是否和 n 相同,不相同就换一个,相同就输入 n+1 继续循环,直到 flag 出了为止。

1
2
3
4
5
6
7
def brute(payload):
s = subprocess.Popen("./poly_pin2",stdout=subprocess.PIPE, stdin=subprocess.
s.stdin.write(payload+b"\n")
s.stdin.close()
out = s.stdout.read()
ret = s.wait()
return ret_code

其他就不写了。看了大哥们的 wp 似乎都是些数学问题,就不慢慢逆了。