pyc、pyo和pyd
本文最后更新于 294 天前,其中的信息可能已经过时,如有错误可以直接在文章下留言

最近想着复现之前矩阵杯的一道分析 pyd 的题目,难度实在是对我来说有点大,看 Writeup 都费劲,所以想自己写源码再生成一个简单的 pyd 再分析一下,我们这篇文章来学习一下 Python 逆向当中的一些特殊文件

Pyc

pyc 文件的全称是 Python Compiled Bytecode File

关于 Pyc 的结构,我有在 marshal 模块和 pyc 的关系当中学习过了

pyc 与 marshal – Arnold’s Blog (arnold66.top)

当然,我觉得在 Python 逆向当中,学习到这个程度可能已经够了,这里再做一下简单介绍

pyc 文件是由用 Python 编程语言编写的源代码生成的编译输出文件。当使用 Python 解释器运行 py 文件时,它会被转换为字节码以供执行。同时,编译后的字节码也会保存为 pyc 文件,以便以后在适用的情况下从缓存中重用。而无需在每次运行脚本时重新编译源代码。这可能会导致更快的脚本执行时间,尤其是对于大型脚本或模块。

pyc 文件有保护源码的作用,我们看不到它的源码,但可以通过命令运行它。

Pyo

pyo 文件的全称是 Python Optimized Bytecode File

pyc 文件,是 python 编译后的字节码(bytecode)文件。

  pyo 文件,是 python 编译优化后的字节码文件。pyo 文件在大小上,一般小于等于 pyc 文件。如果想得到某个 py 文件的 pyo 文件,可以这样

python -O -m py_compile sample.py

这里注意 O 要大写,没想到生成后还是 pyc 后缀的文件

优化的大小和不优化大小也是一样的,这种优化可能只优化一些特定的代码

那就没啥好说的了,跟 pyc 一样的逆向分析方法。

Pyd

pyd 文件的全称是 Python Dynamic Module

pyd 文件是 Python 中一种特殊的二进制文件,主要用于在 Windows 操作系统上存放 C 或 C++ 编写的 Python 扩展模块。这些扩展模块通过编译成.pyd 文件,可以在 Python 环境中被导入和使用,从而提供比纯 Python 代码更高的执行效率。这种文件类似于 Unix 系统中的.so(共享对象)文件,它们的功能和用法都十分相似。

当然,python 代码也可以打包为 pyd。但是我猜测可能利用 C 或 C++ 编写更多,因为可以达到提高代码执行效率的目的,python 代码打包肯定是没有 C 和 C++ 那么显著的。

我们先看 Python 代码打包成 pyd 的情况

创建一个 setup.py 文件,内容如下

from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("sample.py")
)

然后我 sample.py 里面为了方便逆向分析,内容如下

def ADD(x,y):
x=x+10
y=y+10
sum=x+y
return sum
def SUB(x,y):
x=x-6
y=y-6
res=x-y
return res
def JUGE(x,y):
sum=x+y
if(sum==66):
print("Arnold!!!")
return 0

然后再用以下命令即可生成 pyd

python setup.py build_ext --inplace

就可以成功生成 pyd 文件了

运行命令后会生成一个 bulid 文件夹,里面有两个文件夹,第一个内容就是 pyd,第二个应该是关于 C 语言的,我们不关注这部分,我们看 pyd。

可以看到是可以正常作为 Python 的模块导入并且使用的,我们可以通过 help 来查看模块当中都有哪些函数。

接下来再在 IDA 当中逆向分析 pyd,我们可以在 String 窗口追踪到相应的函数,以 ADD 函数为例

函数有很多的混淆代码,根本难以发现逻辑点,这也是 pyd 为啥难以分析的原因,仔细找,发现了一个相关的 API 函数,很明显是执行加法的运算

再通过这个 API 函数,我们定位 sub_180003610 函数,点进去发现确实是 ADD 函数的大概逻辑,但是这是在我们知道源代码的情况下分析的,倘若不知道源代码,其逆向分析的难度你看一下这些代码就知道并不简单。

SUB 函数的分析过程和 ADD 函数一样

那么我就想修改一下 SUB 或者 ADD 函数的代码,去观察它们的区别来看往后我们逆向分析的时候关键看哪里,我把 SUB 函数和 ADD 函数改成了这样

def ADD(x,y,z):
x=x+10
y=y+8
z=z+4
sum=x+y+z
return sum
def SUB(x,y):
x=x-6
y=y-7
res=x-y
return res

SUB 函数的减去的数不同之后,传入关键函数的参数就多了一个

ADD 函数也一样,并且多用了一个 API 函数来实现三数的相加,这些简单逻辑视乎都只用关注关键函数的 return 返回值的部分。

在修改了代码之后,还发现下面这个 API 函数 PyErr_Format 传入的这个位置的参数,就是我们分析的函数实际需要接收的参数的个数。

我们在 Python 中调用,并且只传入 2 个函数给 ADD 看看,应证我们的猜想是正确的

那么这两个简单函数在 pyd 中的呈现我们就看完了,接下来就看 JUGE 函数

很容易就找到了 JUGE 函数的主要逻辑,第三部分的代码一般只要 Python 中出现 If 的判断语句都会有

这里的 IsTrue 变量就是一个标志,还有 PyObject_RichCompare、PyObject_IsTrue 等 API 函数可能也都是。

但是我有一个疑惑的点在于倘若判断成立,应该会输出”Arnold!!!” 的字符串,但是函数里面找不到这样的操作,我认为打印函数可能是下面这个

因为 PyObject_Call 这个 API 函数全局只调用了一次,但是我 patch 了一下,发现并没有什么影响,然后前面的跳转也试了 patch 一下,还是找不到哪里是打印函数的地方,算了在这里就打个问号先吧。

我还发现引用 PyUnicode_InternFromString 的函数,一般有一些关键字符串变量在里面,如图

这里的字符串比如”Arnold!!!” 如果被修改的话,是会有影响的。

在 pyd 逆向当中,因为 CTF 题目通常要得出 flag,所以一般密文会以数组的形式存在,接下来我们探讨一下数组的形式在 pyd 文件当中的呈现,源代码如下

def ARRAY(x):
data=[11,66,5594,8742,645,31,4654,45,13,5312,151,21564,153,99,66,55,22,33,418]
if (data[x]==66):
print("alright!")
data[x]=66
data[6]=x
return 0

这函数里面有一个长度为 19 的列表,再放到 IDA 中看看,很容易找到关键逻辑

用一个 API 创建了一个列表,后面 v5 偏移每次加 8,加到 144,刚好是 19 个元素,但是这里没有显示列表当中的数据,找到 PyUnicode_InternFromString 函数的地方,也没有列表当中的数据

通过交叉引用可以发现列表当中的数据

这里的数据从小到大排列,有很密集的 PyLong_FromLong 函数,传入函数的参数就有我们列表当中的数据,我们看看如何确定列表数据的实际顺序

如图,左边第一个列表元素的索引为 23,再看右边的索引,就能得到列表的第一个元素是 11,这样我们就能得到列表的全部元素了。

这里还发现 PyObject_SetItem 函数一般用于设置列表索引的值,如下图

用了两次函数其实就相当于上面的 Python 代码

data[x]=66
data[6]=x

目前为止就是我关于 pyd 的所有发现了,等学到了新知识再来补充。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇