一次pyarmor解密分析 - 缘之十七

一次pyarmor解密分析 - 缘之十七

多位业内人士认为,笔者分析的样本pyarmor一共有两层加密,第一层加密co_object,第二层加密co_code,在执行的过程中,每一个函数(代码块)一旦被调用,首先会执行函数__pyarmor__解密第一层co_object,然后执行__pyarmor__或__pyarmor__解密第二层,它们负责恢复代码指令,代码块执行完之后,在返回上一级之前,会调用__armor_exit__把代码恢复成加密状态,那么解密的关键就是找到函数__pyarmor__、pyarmor、__pyarmor__并下断分析其解密逻辑,再编写脚本解密还原python代码即可。 科技新闻。

先是获取Frame,这个rax就是获取Frame的函数,因为截图接不上,就i索性直接描述了,这个函数就是帮我们定位加密数据的,后续就是看哪个函数或者哪串代码在解密这个数据

解包工具更推荐ng这个,不用匹配Python版本,而且是exe版本,方便使用

一次背景与起因

通过分析其结构体定义和内存布局如下,在下面这个结构体中可以寻址到他们对应的函数地址

起初直接用还原key算法后调用官方加解密库函数,发现数据跟解密后的数据完全不一样,后不调用py层加解密库函数,调用c层,为此还找了github源码编译了libtomcrypt.dll模块,然后调用里面的C层函数,结果还是有问题,最后经过排除Sub_70A2E650初始化CTR状态是正常的,但是sub_70A2E020里进行了魔改,先说一下函数原型

k1=dword_70B227AA ^ (DWORD)(v6+44);

一次事件经过

关于魔数 (Magic Number):不同 Python 主版本的魔数不同,例如:

然后依旧是dbg动态调试观察数据什么时候被解密,定位解密位置

伪代码如下,经过分析,计算4个值生成16字节key,然后传入Sub_70A3E650初始化AES-CTR状态,然后将CTR状态传入Sub_70A2E020

一次各方回应

我们先来看__pyarmor__这个函数

经过分析,这里列举一些比较重要的,有检测debugger字段的、有设置线程信息的、有异常退出的、最重要的是一个循环异或很多次数据生成全局常量的case,如下图v10==2即为该逻辑

Python 3.7: 0x420d0d0a

一次影响分析

k0=dword_70B227A6 ^ (DWORD)(v6+40);

https://blog.betamao.me/posts/2022/python-pyarmor-crack/

pyc文件结构关系图:

进入这个函数,用dbg和ida同步调试,最终到这个分支,这个就是关键的密钥生成和异或数据位置

第一次:278C26B1790是模块里的.data数据

k2=dword_70B227AE^ (DWORD)(v6+48);

伪代码如下:

随后会发现走到一个这个LABEL_16分支里,这里就是最关键的解密co_object处,看到这些异或是不是很兴奋很激动,异或涉及一般都是密钥组成,哈哈那么我们来慢慢看看这个逻辑

通过分析,在调用Sub_70A26C50之前,做了四次异或操作,每次都是一个DWORD全局常量异或一个*(基地址+偏移),一共4个,其中3个加了一些固定常量值,简化一下密钥逻辑:

通过查阅学习是pyarmor加密,那么我们直接分析加密的5个文件即可,看看能不能逆向分析出来编写解密脚本,当然这里也有可取巧的方式不用解密,使用github上一个开源脚本工具PyArmor-Unpacker直接就可以静态dump出来,小白不会的,我就是哈哈,可以参考这篇帖子

hook脚本用的是dbg内自带脚本功能,代码可能不太好看,如果用Python写脚本的话可以参考这个项目,不过要装VS2015和Python2.7

https://zhuanlan.zhihu.com/p/609709232

这里笔者先是才用临时patch的方法来看看后面逻辑

Python 3.11: 0x6a0d0d0a

赋值常量的函数为如下这个,这个函数里面是一个巨大的switch-case结构,相信大家听到这个词应该就知道这是一个的VM了吧哈哈,单纯自己手动静态动态肯定是不行的,那么把静态反编译的这个函数扔给AI,让AI给我们筛选一下关键逻辑

先断到这个函数

最终获取的常量数据如下

Python 3.9: 0x5d0d0d0a

将我们打包好的加密文件拖进我们逆向分析的大哥——Exeinfo,可以发现64位,Pyinstaller打包,Python310模块

patch几次后,找到了常量的生成的异或逻辑,如下图

后面就是基本功了,慢慢看每个函数的作用,之后获得了Frame,然后来到了解密函数

现在该我们的二哥和三哥出场了,IDA、dbg,启动!

言归正传,继续逆向分析解密机制:

可以看到关键的encrypt等函数,通过查阅官方文档和调试分析,该模块的执行流程如下:

第二层解密:70b227b2,70b22799

另外有兴趣的友友可以看看在这个函数内部,异或数据是通过CTR状态里的密钥流来异或数据的,CTR状态在调用sub_70A2E650初始化时生成了第一串16字节密钥流,这个16字节密钥流就是IV,然后调用sub_70A2E020函数,每次异或8字节,如下图

这里笔者就只用了一下自带的脚本功能来编写脚本,脚本如下,注意用下面这个脚本需要写入一个txt文档让dbg加载

然后在dbg传参时,明文数据往后移了两位,密文输出是正常的,笔者写到这里发现自己写的解密脚本似乎不用进入函数来修改异或往后移动2位,不过就当学习了

异或2次用完16字节密钥流后,随后调用回调函数ptr_3DES函数生成新的密钥流,最后剩余加密数据不足8字节的时候就是逐字节异或的,如下图

armor分支稍微难弄一点,里面有一个魔改,这个弄了有点久,让我们细细看看吧

后面的三个函数j进去之后就是初始化AES-GCM状态、设置IV、调用解密函数,那么根据这个大致就能得出解密脚本编写用AES-GCM解密

笔者这里是3.10版本的

进去之后最终定位到解密关键位置,如下图,

github项目地址:

然后进入一个比较关键的函数Sub_70A0DC10

加载_pytransform.dll——调用init_module初始化并导入各类需要的模块,获取后续需要调用的关键函数(如marshal.loads,marshal.dumps),初始化一些加密算法用于后续校验本地license.lic文件的product.key——校验本地文件中的product.key——调用init_runtime函数,初始化各类函数地址和模块获取

后续有很多这种异或逻辑,都是这种框架,且数值在变化,不可能人工还原算法,索性就hook一下时间校验函数,然后直接F9拿到最终的常量数据

第一层解密:70b227a6

用解包工具解包,这里GitHub上有开源工具

Python 3.8: 0x4d0d0d0a

这些数据的常量基地址如下:

单步步过会发现开启匹配入口头Pyarmor,并校验了头部

经过分析v13内部有一个时间校验,每次异或常量数据的时候都会调用clock获得当前时间来跟下一次作比对,小于1s就进入异常分支。

可能有些不准,具体情况具体分析

通过大致分析(这里包括但不限于IDA和dbg查找exe和相关dll的字符串和导出函数,关于逆向功底方面,笔者目前初出茅庐,就不过多谈前置分析),笔者觉得重要的是样本加载的一个重要模块_pytransform.dll,该模块是具体加解密模块,这个模块的导出函数如下:

在init_runtime函数里会获取6个重要的结构体,"armor","wraparmor","pyarmor","armor_enter","armor_exit","armor_wrap"

Python 3.10: 0x610d0d0a

这里笔者样本里的核心pyc文件有五个,但是反编译出来都被加密了,如下截图

简化伪代码如下,v26是解密代码起始处,v29是解密代码终结处,全局常量70B227B2

在慢长的过程中定位到这个函数

先断到该函数处

成功解包后用反编译工具pycdas和pycdc来反编译,这里Github上有开源工具,下载下来后用Cmake编译生成exe,并自行添加环保变量路径,后续既可以使用了,熟悉了基础指令,后续就可以交给AI,让AI来将pyc文件进行批量反编译核心文件,毕竟解包出来可能很多没有用的文件,只需要反编译核心文件即可

k3=dword_70B227B2^ (DWORD)(v6+52);

有C标注的是动态生成的全局常量,有M标注的是直接的数据

进入之后挨着dbg和ida同步调试分析,获取了一下当前调用栈帧PyFrameObject结构体

Python常见的各种加密解密算法 - 吾爱破解 - 52pojie.cn

声明:本文信息来源于相关渠道或网络,版权归原作者所有。如涉及版权问题请及时与本站联系删除。本文观点仅供参考,不代表本站立场。