好久没有正经写复现了,这次整个人脑子都处于网咖状态,彻彻底底变成肥宅了,得想办法改改,于是开始写复现报告了。考虑到某些需求,这次着重于逆向部分,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; unsigned int v3; unsigned int v4; unsigned int v5; unsigned int v6; unsigned int v7; unsigned int v8; unsigned int v9; 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, encfrom Crypto.Cipher import AESfrom 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 AssertionErrordef 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 :0000 │ 0x2326e40 ◂— 0x7b0900005a0600 01 :0008 │ 0x2326e48 ◂— 0xffffffff00000001 02 :0010 │ 0x2326e50 ◂— 0x1b0 03 :0018 │ 0x2326e58 ◂— 0x110 04 :0020 │ 0x2326e60 —▸ 0x2067376 ◂— 0xc0000 05 :0028 │ 0x2326e68 ◂— 0x901bb59dc67f3254 06 :0030 │ 0x2326e70 ◂— 0xe2 07 :0038 │ 0x2326e78 ◂— 0xffffffffffffffff 08 :0040 │ 0x2326e80 ◂— 0xe3 09 :0048 │ 0x2326e88 ◂— 0x0 0 a:0050 │ 0x2326e90 ◂— 0x400000001000 0b :0058 │ 0x2326e98 ◂— 0x640000002cf300 0 c:0060 │ 0x2326ea0 ◂— 0x36402640164005a 0 d:0068 │ 0x2326ea8 ◂— 0x664056404640164 0 e:0070 │ 0x2326eb0 ◂— 0xa64096408640764 0f :0078 │ 0x2326eb8 ◂— 0xe640d640c640b64 10 :0080 │ 0x2326ec0 ◂— 0x1064015a10670f64 11 :0088 │ 0x2326ec8 ◂— 0x303210fa11290053 12 :0090 │ 0x2326ed0 ◂— 0x38312d35302d3333 ('33-05-18' )13 :0098 │ 0x2326ed8 ◂— 0xd5e933333a33305f 14 :00 a0│ 0x2326ee0 ◂— 0xe7e9000000 15 :00 a8│ 0x2326ee8 ◂— 0x9e9000000c9e9 16 :00b 0│ 0x2326ef0 ◂— 0xe9000000c5e90000 17 :00b 8│ 0x2326ef8 ◂— 0x51e9000000e9 18 :00 c0│ 0x2326f00 ◂— 0xdfe90000006fe900 19 :00 c8│ 0x2326f08 ◂— 0x22e9000000 1 a:00 d0│ 0x2326f10 ◂— 0x67e9000000a6e9 1b :00 d8│ 0x2326f18 ◂— 0xe9000000e1e90000 1 c:00e0 │ 0x2326f20 ◂— 0xb4e9000000af 1 d:00e8 │ 0x2326f28 ◂— 0x656b03da02a94e00 1 e:00f 0│ 0x2326f30 ◂— 0xa9636e6503da79 1f :00f 8│ 0x2326f38 ◂— 0x15720000001572 20 :0100 │ 0x2326f40 ◂— 0x72636573097a0000 21 :0108 │ 0x2326f48 ◂— 0x3c08da79702e7465 22 :0110 │ 0x2326f50 ◂— 0x13e656c75646f6d 23 :0118 │ 0x2326f58 ◂— 0x2f3000000 24 :0120 │ 0x2326f60 ◂— 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" ) 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 打开目录批量去搜关键字就可以了:
+
![[search.png]]
+
这里对 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 再打包成数字:
+
![[decode.png]]
+
总结 主要是几个技巧:
+
+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 似乎都是些数学问题,就不慢慢逆了。
+