国内关于 Stegosaurus
的介绍少之又少,一般只是单纯的工具使用的讲解之类的,并且本人在学习过程中也是遇到了很多的问题,基于此种情况下写下此文,也是为我逝去的青春时光留个念想吧~
Stegosaurus是什么?
在了解 Stegosaurus
是什么之前,我们首先需要弄清楚的一个问题是:什么是隐写术?
隐写术,从字面上来理解,隐是隐藏,所以我们从字面上可以知道,隐写术是一类可以隐藏自己写的一些东西的方法,可能我们所写的这些东西是一些比较重要的信息,不想让别人看到,我们会考虑采取一些办法去隐藏它,比如对所写的文件加解密,用一些特殊的纸张(比如纸张遇到水后,上面的字才会显示出来)之类的。隐写术这种手段在日常生活中用的十分广泛,我相信部分小伙伴们小时候曾经有过写日记的习惯,写完的日记可能不想让爸爸妈妈知道(青春期萌动的内心,咱们都是过来人,都懂这个2333),所以以前常常会买那种上了把锁的那种日记本,这样就不怕自己的小秘密被爸爸妈妈知道啦。
事实上,隐写术是一门关于信息隐藏的技巧与科学,专业一点的讲,就是指的是采取一些不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容的方法。隐写术的英文叫做 Steganography
,根据维基百科的解释,这个英文来源于特里特米乌斯的一本讲述密码学与隐写术的著作 Steganographia
,该书书名源于希腊语,意为“隐秘书写”。(这个不是重点)
所以今天呢,我们要给大家介绍的是隐写术的其中一个分支(也就是其中一种隐写的方法),也就是 Stegosaurus
, Stegosaurus
是一款隐写工具,它允许我们在 Python
字节码文件( pyc
或 pyo
)中嵌入任意 Payload
。由于编码密度较低,因此我们嵌入 Payload
的过程既不会改变源代码的运行行为,也不会改变源文件的文件大小。 Payload
代码会被分散嵌入到字节码之中,所以类似 strings
这样的代码工具无法查找到实际的 Payload
。 Python
的 dis
模块会返回源文件的字节码,然后我们就可以使用 Stegosaurus
来嵌入 Payload
了。
为了方便维护,我将此项目移至 Github
上:https://github.com/AngelKitty/stegosaurus
首先讲到一个工具,不可避免的,我们需要讲解它的用法,我并不会像文档一样工整的把用法罗列在一起,如果需要了解更加细节的部分请参考 Github
上的详细文档,我会拿一些实际的案例去给大家讲解一些常见命令的用法,在后续的文章中,我会大家深入理解 python
反编译的一些东西。
Stegosaurus
仅支持Python3.6
及其以下版本
拿到一个工具,我们一般会看看它的基本用法:
python3 stegosaurus.py -h
$ python3 -m stegosaurus -h usage: stegosaurus.py [-h] [-p PAYLOAD] [-r] [-s] [-v] [-x] carrier positional arguments: carrier Carrier py, pyc or pyo file optional arguments: -h, --help show this help message and exit -p PAYLOAD, --payload PAYLOAD Embed payload in carrier file -r, --report Report max available payload size carrier supports -s, --side-by-side Do not overwrite carrier file, install side by side instead. -v, --verbose Increase verbosity once per use -x, --extract Extract payload from carrier file
我们可以看到有很多参数选项,我们就以一道赛题来讲解部分参数命令吧~
我们此次要讲解的这道题是来自 Bugku
的 QAQ
赛题链接如下:
http://ctf.bugku.com/files/447e4b626f2d2481809b8690613c1613/QAQ http://ctf.bugku.com/files/5c02892cd05a9dcd1c5a34ef22dd9c5e/cipher.txt
首先拿到这道题,用 010Editor
乍一眼看过去,我们可以看到一些特征信息:
可以判断这是个跟 python
有关的东西,通过查阅相关资料可以判断这是个 python
经编译过后的 pyc
文件。这里可能很多小伙伴们可能不理解了,什么是 pyc
文件呢?为什么会生成 pyc
文件? pyc
文件又是何时生成的呢?下面我将一一解答这些问题。
简单来说, pyc
文件就是 Python
的字节码文件,是个二进制文件。我们都知道 Python
是一种全平台的解释性语言,全平台其实就是 Python
文件在经过解释器解释之后(或者称为编译)生成的 pyc
文件可以在多个平台下运行,这样同样也可以隐藏源代码。其实, Python
是完全面向对象的语言, Python
文件在经过解释器解释后生成字节码对象 PyCodeObject
, pyc
文件可以理解为是 PyCodeObject
对象的持久化保存方式。而 pyc
文件只有在文件被当成模块导入时才会生成。也就是说, Python
解释器认为,只有 import
进行的模块才需要被重用。 生成 pyc
文件的好处显而易见,当我们多次运行程序时,不需要重新对该模块进行重新的解释。主文件一般只需要加载一次,不会被其他模块导入,所以一般主文件不会生成 pyc
文件。
我们举个例子来说明这个问题:
为了方便起见,我们事先创建一个test文件夹作为此次实验的测试:
mkdir test && cd test/
假设我们现在有个 test.py
文件,文件内容如下:
def print_test(): print('Hello,Kitty!') print_test()
我们执行以下命令:
python3 test.py
不用说,想必大家都知道打印出的结果是下面这个:
Hello,Kitty!
我们通过下面命令查看下当前文件夹下有哪些文件:
ls -alh
我们可以发现,并没有 pyc
文件生成。
‘我们再去创建一个文件为 import_test.py
文件,文件内容如下:
注:
test.py
和import_test.py
应当放在同一文件夹下
import test test.print_test()
我们执行以下命令:
python3 import_test.py
结果如下:
Hello,Kitty! Hello,Kitty!
诶,为啥会打印出两句相同的话呢?我们再往下看,我们通过下面命令查看下当前文件夹下有哪些文件:
ls -alh
结果如下:
总用量 20K drwxr-xr-x 3 python python 4.0K 11月 5 20:38 . drwxrwxr-x 4 python python 4.0K 11月 5 20:25 .. -rw-r--r-- 1 python python 31 11月 5 20:38 import_test.py drwxr-xr-x 2 python python 4.0K 11月 5 20:38 __pycache__ -rw-r--r-- 1 python python 58 11月 5 20:28 test.py
诶,多了个 __pycache__
文件夹,我们进入文件夹下看看有什么?
cd __pycache__ && ls
我们可以看到生成了一个 test.cpython-36.pyc
。为什么是这样子呢?
我们可以看到,我们在执行 python3 import_test.py
命令的时候,首先开始执行的是 import test
,即导入 test
模块,而一个模块被导入时, PVM(Python Virtual Machine)
会在后台从一系列路径中搜索该模块,其搜索过程如下:
- 在当前目录下搜索该模块
- 在环境变量
PYTHONPATH
中指定的路径列表中依次搜索 - 在
python
安装路径中搜索
事实上, PVM
通过变量 sys.path
中包含的路径来搜索,这个变量里面包含的路径列表就是上面提到的这些路径信息。
模块的搜索路径都放在了 sys.path
列表中,如果缺省的 sys.path
中没有含有自己的模块或包的路径,可以动态的加入 (sys.path.apend)
即可。
事实上, Python
中所有加载到内存的模块都放在 sys.modules
。当 import
一个模块时首先会在这个列表中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用 import
的模块的 Local
名字空间中。如果没有加载则从 sys.path
目录中按照模块名称查找模块文件,模块文件可以是 py
、 pyc
、 pyd
,找到后将模块载入内存,并加入到 sys.modules
中,并将名称导入到当前的 Local
名字空间。
可以看出来,一个模块不会重复载入。多个不同的模块都可以用 import
引入同一个模块到自己的 Local
名字空间,其实背后的 PyModuleObject
对象只有一个。
在这里,我还要说明一个问题,import
只能导入模块,不能导入模块中的对象(类、函数、变量等)。例如像上面这个例子,我在 test.py
里面定义了一个函数 print_test()
,我在另外一个模块文件 import_test.py
不能直接通过 import test.print_test
将 print_test
导入到本模块文件中,只能用 import test
进行导入。如果我想只导入特定的类、函数、变量,用 from test import print_test
即可。
既然说到了 import
导入机制,再提一提嵌套导入和 Package
导入。
import
嵌套导入
嵌套,不难理解,就是一个套着一个。小时候我们都玩过俄罗斯套娃吧,俄罗斯套娃就是一个大娃娃里面套着一个小娃娃,小娃娃里面还有更小的娃娃,而这个嵌套导入也是同一个意思。假如我们现在有一个模块,我们想要导入模块 A
,而模块 A
中有含有其他模块需要导入,比如模块 B
,模块 B
中又含有模块 C
,一直这样延续下去,这种方式我们称之为 import
嵌套导入。
对这种嵌套比较容易理解,我们需要注意的一点就是各个模块的 Local
名字空间是独立的,所以上面的例子,本模块 import A
完了后,本模块只能访问模块 A
,不能访问 B
及其它模块。虽然模块 B
已经加载到内存了,如果要访问,还必须明确在本模块中导入 import B
。
那如果我们有以下嵌套这种情况,我们该怎么处理呢?
比如我们现在有个模块 A
:
# A.py from B import D class C: pass
还有个模块 B
:
# B.py from A import C class D: pass
我们简单分析一下程序,如果程序运行,应该会去从模块B中调用对象D。
我们尝试执行一下 python A.py
:
报 ImportError
的错误,似乎是没有加载到对象 D
,而我们将 from B import D
改成 import B
,我们似乎就能执行成功了。
这是怎么回事呢?这其实是跟 Python
内部 import
的机制是有关的,具体到 from B import D
, Python
内部会分成以下几个步骤:
- 在
sys.modules
中查找符号B
- 如果符号
B
存在,则获得符号B
对应的module
对象<module B>
。从<module B>
的__dict__
中获得符号D
对应的对象,如果D
不存在,则抛出异常 - 如果符号
B
不存在,则创建一个新的module
对象<module B>
,注意,此时module
对象的__dict__
为空。执行B.py
中的表达式,填充<module B>
的__dict__
。从<module B>
的__dict__
中获得D
对应的对象。如果D
不存在,则抛出异常。
所以,这个例子的执行顺序如下:
1、执行 A.py
中的 from B import D
注:由于是执行的
python A.py
,所以在sys.modules
中并没有<module B>
存在,首先为B.py
创建一个module
对象(<module B>
),注意,这时创建的这个module
对象是空的,里边啥也没有,在Python
内部创建了这个module
对象之后,就会解析执行B.py
,其目的是填充<module B>
这个dict
。
2、执行 B.py
中的 from A import C
注:在执行
B.py
的过程中,会碰到这一句,首先检查sys.modules
这个module
缓存中是否已经存在<module A>
了,由于这时缓存还没有缓存<module A>
,所以类似的,Python
内部会为A.py
创建一个module
对象(<module A>
),然后,同样地,执行A.py
中的语句。
3、再次执行 A.py
中的 from B import D
注:这时,由于在第
1
步时,创建的<module B>
对象已经缓存在了sys.modules
中,所以直接就得到了<module B>
,但是,注意,从整个过程来看,我们知道,这时<module B>
还是一个空的对象,里面啥也没有,所以从这个module
中获得符号D
的操作就会抛出异常。如果这里只是import B
,由于B
这个符号在sys.modules
中已经存在,所以是不会抛出异常的。
我们可以从下图很清楚的看到 import
嵌套导入的过程:
Package
导入
包 (Package)
可以看成模块的集合,只要一个文件夹下面有个 __init__.py
文件,那么这个文件夹就可以看做是一个包。包下面的文件夹还可以成为包(子包)。更进一步的讲,多个较小的包可以聚合成一个较大的包。通过包这种结构,我们可以很方便的进行类的管理和维护,也方便了用户的使用。比如 SQLAlchemy
等都是以包的形式发布给用户的。
包和模块其实是很类似的东西,如果查看包的类型: import SQLAlchemy type(SQLAlchemy)
,可以看到其实也是 <type 'module'>
。 import
包的时候查找的路径也是 sys.path
。
包导入的过程和模块的基本一致,只是导入包的时候会执行此包目录下的 __init__.py
,而不是模块里面的语句了。另外,如果只是单纯的导入包,而包的 __init__.py
中又没有明确的其他初始化操作,那么此包下面的模块是不会自动导入的。
假设我们有如下文件结构:
. └── PA ├── __init__.py ├── PB1 │ ├── __init__.py │ └── pb1_m.py ├── PB2 │ ├── __init__.py │ └── pb2_m.py └── wave.py
wave.py
, pb1_m.py
, pb2_m.py
文件中我们均定义了如下函数:
def getName(): pass
__init__.py
文件内容均为空。
我们新建一个 test.py
,内容如下:
import sys import PA.wave #1 import PA.PB1 #2 import PA.PB1.pb1_m as m1 #3 import PA.PB2.pb2_m #4 PA.wave.getName() #5 m1.getName() #6 PA.PB2.pb2_m.getName() #7
我们运行以后,可以看出是成功执行成功了,我们再看看目录结构:
. ├── PA │ ├── __init__.py │ ├── __init__.pyc │ ├── PB1 │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── pb1_m.py │ │ └── pb1_m.pyc │ ├── PB2 │ │ ├── __init__.py │ │ ├── __init__.pyc │ │ ├── pb2_m.py │ │ └── pb2_m.pyc │ ├── wave.py │ └── wave.pyc └── test.py