题目为XCTF2023的“我不是病毒2.0”
die查看,发现是个拟态C++,实际上是个python3.10程序,万幸它没用3.11,遂直接用pyinstxtractor和pycdc跟它爆了。
具体怎么爆可以看fortinet上的教程、一篇有关解码加密pyc的文章,以及Redamancy的博客。第一步肯定还是尝试使用pyinstxtractor拆解exe文件,但当你拆解完成之后,你会发现主函数对一个 意⭐义⭐不⭐明 的pyc进行调用:
import sign
sign.main()
这意味着源代码其实是在解压出的pyc包中的这个自定义pyc模块:
但是怎么解码呢?我们得先明白,这个python程序是如何打包的,以及为什么会出现这样的加密数据:
python程序的打包
(资料参考王小希ww 的博客和琴声悠悠-悠悠琴声 的博客)
目前python程序的打包方式主要是通过PyInstaller或setupTools来实现的。前者是将python文件及其调用模块打包成一个可执行文件(exe/elf),后者则是将其作为python用的软件包打包,并发布到PyPI或者其它软件包管理器中,作为其它python程序的调用库被下载和使用。当然,你也可以选择直接拷贝一份你的运行环境并写一个启动程序,不过,这种方法的稳定性、高效性和安全性就取决于你的代码能力和肝度了(笑
关于pyinstaller打包命令参数如下:
-w 表示希望在生成的.exe程序运行过程中,不要出现cmd黑框,一般用于打包GUI界面
-F:表示希望将所有的程序全部打包在一起,生成的只有一个.exe文件,这样的文件集成度高,但是运行速度慢
(如果不写-F,生成的还有一堆.dll文件,这样的程序里文件很多,但是运行速度比较快)-D:生成一个文件目录包含可执行文件和相关动态链接库和资源文件等
(对于打包结果较大的项目,选用-d生成目录相比单可执行文件的打包方式,执行速度更快,但包含更加多的文件)-p:自定义需要加载的类的路径
-i:自己做的软件可以放上自己的图标,分享一个网站,可以把其他格式图片转成ico格式
需要注意的是,被打包的python文件在import某个包内的其它模块的时候,需要添加调用包的根目录名,或者将被调用模块的根目录打包成一个新包,否则会因为找不到调用模块而报错。
而我们看到的这一个程序,明显是-F指令生成的集成程序。至于为什么使用pyinstxtractor解包后看到的调用包是一堆encrypted的包,是因为pyinstaller还可以使用特定的key参数对代码进行AES加密,比方说:
pyinstaller --key 123 demo.py
但实际上,这个加密有个致命的弱点,那就是存放key的pyimod00_crypto_key
,以及入口函数main.py
并没有加密。这样,我们的目标库和加密key简直就是开盖即食。于是先用pycdc反编译一下key,然后用代码解pyc加密的同时加上一下文件头,最后用pycdc处理一手:
import tinyaes
import zlib
CRYPT_BLOCK_SIZE = 16
# 从pyimod00_crypto_key里把key拽出来
key = bytes('HelloHiHowAreYou', 'utf-8')
inf = open('./PYZ-00.pyz_extracted/sign.pyc.encrypted', 'rb') # encrypted file input
outf = open('sign.pyc', 'wb') # output file
# 代码块就这么长,能AES的不多,先初始化一手
iv = inf.read(CRYPT_BLOCK_SIZE)
cipher = tinyaes.AES(key, iv)
# 解码并解压
plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read()))
# 写入pyc文件头
outf.write(b'\x6f\x0d\x0d\x0a\0\0\0\0\0\0\0\0\0\0\0\0')
# 写入解码过的pyc
outf.write(plaintext)
inf.close()
outf.close()
最后爆出来这一坨:
import hashlib as 沈阳
import base64 as 杭州
import ctypes as 蚌埠
def main():
蚌埠.windll.kernel32.VirtualAlloc.restype = 蚌埠.c_void_p
福建 = input('您的输入:')
天津 = '9K98jTmDKCXlg9E2kepX4nAi8H0DB57IU57ybV37xjrw2zutw+KnxkoYur3IZzi2ep5tDC6jimCJ7fDpgQ5F3fJu4wHA0LVq9FALbjXN6nMy57KrU8DEloh+Cji3ED3eEl5YWAyb8ktBoyoOkL1c9ASWUPBniHmD7RSqWcNkykt/USjhft9+aV930Jl5VjD6qcXyZTfjnY5MH3u22O9NBEXLj3Y9N5VjEgF2cFJ+Tq7jj92iIlEkNvx8Jl+eH5/hipsonKLTnoLGXs4a0tTQX/uXQOTMBbtd70x04w1Pa0fp+vA9tCw+DXvXj0xmX8c5HMybhpPrwQYDonx7xtS+vRIj/OmU7GxkHOOqYdsGmGdTjTAUEBvZtinOxuR7mZ0r9k+c9da0W93TWm5+2LKNR6OJjmILaJn0lq4foYcfD5+JITDsOD6Vg01yLRG1B4A6OxJ7Rr/DBUabSu2fYf1c4sTFvWgfMV8il6QfJiNMGkVLey1cBPSobenMo+TQC1Ql0//9M4P01sOiwuuVKLvTyDEv6dKO//muVL9S2gq/aZUBWkjj/I5rUJ6Mlt4+jsngmuke9plAjw22fUgz+8uSzn40dhKXfBX/BOCnlwWsMGAefAfoz/XAsoVSG2ioLFmlcYe/WBgaUJEoRUSyv73yiEOTVwIK6EPnDlwRgZZHx2toLu8udpEZ0aKGkex5sn7P8Jf9AbD4/EiQU+FdoJSxGorPSZGvrc4='
北京 = 沈阳.md5('云南'.encode('utf-8')).hexdigest()
重庆 = 杭州.b64decode(天津)
河南 = b''
北京_len = len(北京)
广州 = list(range(256))
j = 0
for i in range(256):
j = (j + 广州[i] + ord(北京[i % 北京_len])) % 256
广州[i] = 广州[j]
广州[j] = 广州[i]
山东 = 陕西 = 0
for 河北 in 重庆:
山东 = (山东 + 1) % 256
陕西 = (陕西 + 广州[山东]) % 256
广州[山东] = 广州[陕西]
广州[陕西] = 广州[山东]
河南 += bytes([
河北 ^ 广州[(广州[山东] + 广州[陕西]) % 256]])
四川 = 蚌埠.create_string_buffer(福建.encode())
黑龙江 = 蚌埠.windll.kernel32.VirtualAlloc(蚌埠.c_int(0), 蚌埠.c_int(len(河南)), 蚌埠.c_int(12288), 蚌埠.c_int(64))
蚌埠.windll.kernel32.RtlMoveMemory(蚌埠.c_void_p(黑龙江), (蚌埠.c_ubyte * len(河南)).from_buffer(bytearray(河南)), 蚌埠.c_size_t(len(河南)))
辽宁 = 蚌埠.windll.kernel32.CreateThread(蚌埠.c_int(0), 蚌埠.c_int(0), 蚌埠.c_void_p(黑龙江), 蚌埠.byref(四川), 蚌埠.c_int(0), 蚌埠.pointer(蚌埠.c_int(0)))
蚌埠.windll.kernel32.WaitForSingleObject(蚌埠.c_int(辽宁), 蚌埠.c_int(-1))
if 四川.raw == b'\xdb\x1b\x00Dy\\C\xcc\x90_\xca.\xb0\xb7m\xab\x11\x9b^h\x90\x1bl\x19\x01\x0c\xeduP6\x0c0\x7f\xc5E-L\xb0\xfb\xba\xf6\x9f\x00':
print('是的!你得到了!')
return None
None('不,再尝试更多。 (笑脸符号)')
if __name__ == '__main__':
main()
return None
虽然加了一堆的中文混淆,但是,基本逻辑还是比较清晰的。不过有个坏消息,这又是一道密码题,它先以“云南”为key生成S盒,然后直接跑一个浅显的RC4(详情请见0verWatch 的博客),把一份shellcode解密完再开一个子进程运行。虽然初见看的我两眼一黑,但我有wp(
我们先直接进行一个RC4的解:
from Crypto.Cipher import ARC4
import hashlib
import base64
天津 = b'9K98jTmDKC...
北京 = hashlib.md5('云南'.encode('utf-8')).hexdigest()
北京 = bytes(北京.encode('utf-8'))
重庆 = base64.b64decode(天津)
天津 = ARC4.new(北京)
output = 天津.decrypt(bytes(重庆))
print(list(output))
然后得到的东西像原代码一样,我们之前也见过类似的东西,也就是数据层藏代码然后放到子进程运行,只不过这里是在python上调用的C库。但是接下来的事情直接用C就成,开个虚拟栈帧+子进程把shellcode弄出来(参考SU战队的题解):
#include <stdio.h>
#include <Windows.h>
unsigned char shellcode[] = {
81, 232, 0, 0, 0, 0...
};
int main() {
PVOID p = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(p, shellcode, sizeof(shellcode));
((void(__stdcall*)())(p))();
return 0;
}
然后我们从ida里可以看到它本来的样子……的一部分。虽然你第一遍可能看不明白它为什么会高位截瘫,你甚至找不到一个return,还得自己补一个。
但我们至少得让这里面的东西解出来再说。我们通过刚才强行凑函数的结果可以看到,这玩意其实在对后面那段shellcode进行解密,尤其是在动调一遍后,这一段尤其明显:
等下,这不是解密完了吗?我们直接create function然后f5进去看一眼:
_WORD *sub_1D0162()
{
_WORD *result; // rax
unsigned int v1; // [rsp-1Ch] [rbp-1Ch]
unsigned int v2; // [rsp-14h] [rbp-14h]
unsigned int v3; // [rsp-10h] [rbp-10h]
int i; // [rsp-Ch] [rbp-Ch]
__int64 v5; // [rsp-8h] [rbp-8h]
for ( i = 0; i <= 20; ++i )
{
v2 = 2029;
v3 = *(unsigned __int16 *)(2i64 * i + v5) % 0xD1EFu;
v1 = 1;
while ( v2 )
{
if ( (v2 & 1) != 0 )
v1 = v3 * v1 % 0xD1EF;
v3 = v3 * v3 % 0xD1EF;
v2 >>= 1;
}
result = (_WORD *)(2i64 * i + v5);
*result = v1;
}
return result;
}
好家伙,算法直接看完了。就是把数据两个两个提出来(因为是int16且i*2的提法)于是我们直接数据提出来,写脚本开爆:
·python part
---------------
output = []
str1 = '\xdb\x1b\x00D...
for i in str1:
output.append(ord(i))
print(output)
·C++ part
---------------
#include <iostream>
#define modnum 0xD1EFu
using namespace std;
unsigned char cipher[] = {219, 27, 0, 68...
int main()
{
for(unsigned int i=0;i<=20;++i){
int laztag = 0; // 爆出即停用标记
unsigned int cip = (cipher[i*2] + (cipher[i*2+1]<<8)) % modnum;
for(unsigned int j=0;j<127;j++){
for(unsigned int k=0;k<127;k++){ // 暴力枚举每一个可能字符
unsigned int key = 2029;
unsigned int guess = (j + (k<<8)) % modnum;
unsigned int v1 = 1;
while(key){ // 还原算法
if(key & 1 != 0)
v1 = guess * v1 % modnum;
guess = guess * guess % modnum;
key >>= 1;
}
if(v1 == cip){ // 猜对了
printf("%c%c",j,k);
laztag = 1;
break;
}
}
if(laztag == 1)break; // 爆出即停,节约算力
}
}
}
做个总结: