从Stegosaurus隐写涉及Python模块导入机制详情

 

前言

无事就翻翻大师傅们的博客,然后看到了一篇关于Stegosaurus隐写的,然后延伸到Python的import模块导入等部分知识。 于是开始学习延伸更多。

 

什么是Stegosaurus?

Stegosaurus是一个隐写工具,它是通过将payload隐藏在Python的字节码文件中。而且不会改变源代码的运行行为,也不会改变源代码的大小。
Payload 代码会被分散嵌入到字节码之中,所以类似 strings 这样的代码工具无法查找到实际的 PayloadPythondis 模块会返回源文件的字节码,然后我们就可以使用 Stegosaurus 来嵌入 Payload 了。

 

简单剖析pyc和简析import导入

pyc文件是python的字节码文件,是二进制文件,就是Python源文件在经过解释器编译后生成的字节码文件。现在实验一下。
在linux平台实验。

mkdir m0re && cd m0re/

创建一个py文件,内容

def print_test():
    print('hello,m0re!')

print_test()

记得要给执行权限。

执行一下,成功执行后,再看目录下文件,到那时没发现pyc文件

再创建一个文件

vim m0re.py
#内容如下:
import test

test.print_test()

执行成功后,发现打印了两行,而且出现了一个文件夹__pycache__

在执行python3 m0re.py时,第一条命令是import test先导入test模块,一个模块在被导入时,PVM(python虚拟机)会在后台从模块包里面搜索test这个模块。Python 中所有加载到内存的模块都放在 sys.modules(保存了之前import的类库的缓存)

搜索过程:

1、 在当前目录下搜索这个模块
2、 在环境变量中PYTHONPATH中指定的路径列表依次搜索
3、在Python安装路径中搜索

模块的搜索路径

sys.path返回导入模块时搜索路径集,是一个list列表

In [1]: import sys
In [2]: sys.path
Out[2]: 
['',
 '/usr/lib/python38.zip',
 '/usr/lib/python3.8',
 '/usr/lib/python3.8/lib-dynload',
 '/usr/local/lib/python3.8/dist-packages',
 '/root/tools/gaps',
 '/usr/lib/python3/dist-packages',
 '/usr/lib/python3/dist-packages/IPython/extensions',
 '/root/.ipython']

1、 从上面列出的目录里依次查找要导入的模块文件
2、''表示当前路径
3、 列表中的路径的前后顺序表明了python解释器在搜索模块时的前后顺序。

添加新模块

sys.path.append('/home/python/xxxx')
sys.path.insert(0,'/home/python/xxxx')

注意:使用sys.path.append()sys.path.insert()添加的路径会在退出交互式环境或者IDE后自动消失。

import只能导入模块,不能导入模块中的对象(类、函数、变量等)python不能像Java那样,直接通过import来导入某个对象或者类以及某个函数。

import java.swing.*;

python的方法是这样的

from test import print_test

在解释器中

 

Python模块嵌套导入

然后再来看看嵌套导入

看例子

#m0re.py
from m1re import m3re

class m2re:
    pass
#m1re.py
from m0re import m2re

class m3re:
    pass

importError,导入失败,这样导入是存在问题的。

导入时,先搜索m1re,如果存在,就会获得m1re对应的module对象<module m1re>,从这个module对象的__dict中获取m3re对应的对象。如果不存在就抛出异常。

如果m1re不存在,就会新建一个module对象<module m1re>,此时的module对象的__dict__为空。,执行m1re.py中的表达式,填充<module m1re>__dict__的空白。再从<module m1re>__dict__中获取m3re的对象。如果不存在则抛出异常。

所以这个程序的执行流程为

1.执行m0re.py,先执行from m1re import m3re

没有m1re的模块,所以此刻创建一个新的module,此刻的module为空。然后执行m1re.py填充<module m1re>__dict__的空白。

2.执行m1re.py,先执行from m0re import m2re

同样的,检查缓存中是否存在<module m0re>,因为m0re.py还正在执行,所以缓存中还没有这个module,同样是要创建新的module,然后就是再次执行m0re.py中的代码

3.再次执行m0re.py中的from m1re import m3re,第一步,创建过<module m1re>,所以已经存在了,但是里面还是空的,什么也没有。所以从里面获取不到m3re这个对象,这里就会抛出异常ImportError

将m0re.py修改一下

import m1re

class m2re:
    pass

执行过程中就第三步就是只搜索存在<module m1re>,而不用搜索m3re

执行时就无异常抛出

 

覆盖导入

当导入的模块与标准库中的模块重名,在导入时会发生覆盖导入,因为python解释器会首先在当前文件夹下搜索查找模块名。这里用一段代码来解释。创建random.py写入下面内容。

 #!/usr/bin/python
 import random
 print(random.randint(12,20))

在导入包时,python解释器在当前目录下找到了random,就直接导入。不会进行下面的搜索。我们知道Python的标准库里面的random模块是存在randint方法的。然而,这里运行的话,会报错AttributeError

提示:模块random中没有方法randint
import默认就把本身作为模块导入,那么显然代码中没有randint方法,所以会导致报错;

经过多次测试,并不是所有的冲突都会出现。
比如下面这个例子time.py

import time
print(time.time())

还有math.py

import math

def square_root(number):
    return math.sqrt(number)

print(square_root(1600))

这两个例子在网上查阅资料,是无法运行的,我的可以运行。
不止这两个,为了找个运行不起来的反例,我也是查阅了好多博客。
如果出现错误,解决办法那就是改下文件名,尽量避免命名与python文件内调用的模块重复。

 

绝对导入

无论是相对导入还是绝对导入,都是需要一个参照物的的。而绝对导入的参照物就是项目的根目录。

在一个目录中有下面的结构:

m0re作为根目录,如上图

其中,module2.py中有一个function1函数,dir2__init__.py中有一个class1的类,dir2下的dir3中的module5.py中有一个function3函数。
使用绝对路径导入示例:

from dir1 import module1
from dir1.module2 import function1
from dir2 import class1
from dir2.dir3.module5 import function2

绝对路径要求我们必须从最顶层的文件夹开始,为每个包或每个模块提供出完整详细的导入路径。

现在的python3.x中,绝对导入是默认的导入形式。它的优点是很明显的知道要导入的包或者模块在什么位置。

那么问题来了,如果导入包的话,顺序是什么?

这里重新构造一个目录如下

m0re.pymodule1.pymodule2.py都写入以下内容

def getName():
    pass

__init__.py写入
print('hello,world')

一般的执行过程:

通常情况下,首先从根目录开始导入,所以第一步是写个test.py文件导入模块

import sys
import dir1.m0re
import dir1.dir2
import dir1.dir2.module1 as m1
import dir1.dir3.module2
dir1.m0re.getName()
m1.getName()
dir1.dir3.module2.getName()

执行结果:

先导入m0re,然后sys.module会创建dir1dir1.m0re两个模块,然后m0re.py中的函数就可以调用了。但是dir2中的函数不能调用,因为只是存在dir1这个模块,模块中关于dir2的内容还是空的。

执行import dir1.dir2后就可调用dir2中的函数了,此时的dir2算是被载入内存中了。

as 的作用就是,m1dir1.dir2.module1等效。作用一样。都可以进行调用内存中module1模块中的函数。

然后导入module2,所有的模块dir1,dir1.m0re,dir.m0re.module1,dir1.dir3.module2四个模块就全部导入了,函数都可以调用。

下面的三条语句都可以实现了。

一般都使用绝对导入,而相对导入使用不方便,很容易发生混乱,导致程序报错。

 

回到开头

说了那么多,还是把之前引起思考的那个题目的知识点总结一下。bugkuCTF的题目QAQ

题目wp就不重复了,然后就是那个工具,Stegosaurus在github的项目地址为: https://github.com/AngelKitty/stegosaurus

使用方法学习一下,以便自己以后查看。

python stegosaurus.py -h #查看帮助信息、
usage: stegosaurus.py [-h] [-p PAYLOAD] [-r] [-s] [-v] [-x] [-e EXPLODE]
                      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
  -e EXPLODE, --explode EXPLODE
                        Explode payload into groups of a limited length if necessary
# 参数说明
-h #帮助信息
-p #要隐藏在文件中的payload,payload要用双引号包起
-x #提取出来隐藏在文件中的payload
-s #不覆盖,再生成一个文件,为隐藏信息后的文件。保留原来的文件
-r #查看这个文件,最多成隐藏多少字节的数据
-v #查看隐藏信息的详细过程,使用次数决定详细程度,一般使用两次就够-vv

单独列出,-e EXPLODE,为什么单独列出来这个,因为在github项目中,readme.md中没有这个参数,在本地执行确发现多了这个参数。

本地执行:

于是就开始多次尝试。

 

再次发现问题并解决

这个工具使用的话,按照github上面的使用方式使用,无法运行。报错如下:

root@kali:~/Desktop/m0re/stegosaurus# python3 stegosaurus.py m0re.py -r
Traceback (most recent call last):
  File "stegosaurus.py", line 251, in <module>
    main()
  File "stegosaurus.py", line 217, in main
    header, code = _loadBytecode(carrier, logger)
  File "stegosaurus.py", line 124, in _loadBytecode
    code = marshal.load(f)
ValueError: bad marshal data (unknown type code)

多次尝试发现:只有-x参数可以正常使用。

其他参数无法使用。使用就会报错。猜想可能不是所有的python文件都可以隐藏payload

当然,猜想错误,pypycpyo文件均可隐藏信息。那这个报错怎么解决?

Google一下,发现答案就在项目中。

https://github.com/AngelKitty/stegosaurus/issues/1

看到了作者的回复

应该是python版本的问题,python3.6是没有问题的 ——作者

而作者打包的那个bin文件,是直接调用的python3.6(这个后面会提到,请继续看),我的windows主机是3.7,而我实验用的kali默认是3.8,所以才造成都无法使用的结果。
按照作者大大的办法,排除问题。

 

工具参数

继续学习这个工具的参数

root@kali:~/Desktop/m0re/stegosaurus# ./stegosaurus m0re.py -s --payload "m0re_wuhu~"
Payload embedded in carrier
root@kali:~/Desktop/m0re/stegosaurus# ./stegosaurus __pycache__/m0re.cpython-36-stegosaurus.pyc -x
Extracted payload: m0re_wuhu~
root@kali:~/Desktop/m0re/stegosaurus#

然后看下是怎么隐藏的

root@kali:~/桌面/m0re/stegosaurus# ./stegosaurus m0re.py -s --payload "m0re_wuhu~" -vv
2020-12-10 17:56:40,030 - stegosaurus - DEBUG - Validated args
2020-12-10 17:56:40,032 - stegosaurus - INFO - Compiled m0re.py as __pycache__/m0re.cpython-36.pyc for use as carrier
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - Read header and bytecode from carrier
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - POP_TOP (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - POP_TOP (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - POP_TOP (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - BINARY_SUBTRACT (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - BINARY_MULTIPLY (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - BINARY_ADD (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,032 - stegosaurus - DEBUG - POP_TOP (0)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,033 - stegosaurus - INFO - Found 14 bytes available for payload
Payload embedded in carrier
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - POP_TOP (109)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - POP_TOP (48)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - POP_TOP (114)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (101)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - BINARY_SUBTRACT (95)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (119)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - BINARY_TRUE_DIVIDE (117)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (104)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - BINARY_MULTIPLY (117)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (126)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - BINARY_ADD (0)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - POP_TOP (0)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - RETURN_VALUE (0)
2020-12-10 17:56:40,033 - stegosaurus - DEBUG - Creating new carrier file name for side-by-side install
2020-12-10 17:56:40,035 - stegosaurus - INFO - Wrote carrier file as __pycache__/m0re.cpython-36-stegosaurus.pyc

上面是隐藏前,发现可以插入的地方。然后下面的部分是隐藏后,最后生成的文件是m0re.cpython-36-stegosaurus.pyc,注意这里生成的pyc文件,cpython-36,可以看出是python3.6解释器解释生成的文件。所以上面说的作者打包的bin文件是直接调用python3.6的解释器的。
-e EXPLODE经过多次测试发现,这个参数后面还需要加上参数,并且这个参数是整型的,所以payload可以为

./stegosaurus -s --explode 2 --payload "m0re_wuhu~_6" m0re.py

不是很理解这个参数,理解能力有点欠缺。下面说一下我的试验结果吧。
1.当payload > Carrier can support的长度,无法隐藏,可以使用这个参数,限制一下长度,不同宿主文件,可以隐藏的payload长度不同,因为代码中无用空间的大小不同,测试出来参数代表的长度也不一样。
2.当payload < Carrier can support的长度,无需用到这个参数。直接隐藏即可。

 

总结

学习到很多知识,首先是工具的使用,涉及到Python中import包的几种导入方式的详细过程。写这篇文章花了挺多的时间和心思的,还有许多不足之处,可能是我没理解到的,还请师傅们多多指点。

 

参考文章

https://www.shangmayuan.com/a/78dcc7c2a10046dea693b44d.html
https://www.cnblogs.com/ECJTUACM-873284962/p/10041534.html
https://www.bbsmax.com/A/A7zgYqRo54/

(完)