Python模块注入技术简析

admin 2024年11月13日23:28:56评论10 views字数 4871阅读16分14秒阅读模式
创建: 2023-03-27 22:12
更新: 2023-03-30 15:47
https://scz.617.cn/python/202303272212.txt

有人在推特上提供了一段神奇的Python代码

#
# https://twitter.com/David3141593/status/1640115094255198208
#
unused=b'x50K34'+b'�'*26+b'+(xcaxcc+
xd1PxcfHLxceNMQxc8xc9xcfQxd74�PK12'+
b'�'*6+b'1'+b'�'*9+b'x15'+b'�'*7+b'13'+b'�'*17+
b'__x6dax69n__.x70yx50K56'+b'�'*8+b'9���003���'
i=__import__
i("runpy").run_path(i("py_compile").compile(__file__))

假设将上述代码置入test.py,以Python 3.8及以上版本执行之,输出"hacked lol"。

$ python3 test.py
hacked lol

PoC出场后有两位第一时间揭密

https://twitter.com/AstraKernel/status/1640255265382735873
https://twitter.com/c3rb3ru5d3d53c/status/1640191261435985920

下面是我的分析

PoC执行时,将自身编译成pyc再加载,在此过程中用到了zipimport模块。zipimport模块"意外地"将pyc co_consts中的unused值识别成zip文件,用_read_directory()对其中的__main__.py进行解析,后来又用_get_data()返回解压后的__main__.py字节码。最后由runpy.run_code()执行__main__.py字节码。

David3141593在twitter上提供PoC时,刻意做了些混淆;unused最前面4字节是zip文件的magic number,他故意不以可打印字符显示;unused中部有"__main__.py",他故意不以可打印字符显示。PoC中unused是个畸形zip文件,作者故意的,实际此处可以是任意有效zip文件。最简方案是,将想执行的代码放入__main__.py,再压缩成zip文件,将zip文件的bytes表示赋给unused变量即可。不过作者后来提了issue,讲了不少细节。

https://github.com/python/cpython/issues/103051

zip文件格式参看

https://en.wikipedia.org/wiki/Zip_%28file_format%29

Python 3.10.6的调用栈简介

runpy.run_path(py_compile.compile(__file__))
  importer = pkgutil.get_importer(path_name)                    // runpy.py:279
                                                                // 返回zipimporter
    importer = path_hook(path_item)                             // pkgutil.py:421
                                                                // path_hook等于zipimport.zipimporter
      zipimport.zipimporter.__init__(path_item)                 // 处理test.cpython-310.pyc
        files = zipimport._read_directory(path)                 // zipimport.py:95
                                                                // zipimport._read_directory()从pyc尾部倒着搜索"PK56"
                                                                // 幺蛾子出在zipimport._read_directory()中
  code = runpy._get_main_module_details()                       // runpy.py:302
    runpy._get_module_details("__main__")                       // runpy.py:238
      code = loader.get_code("__main__")                        // runpy.py:157
                                                                // 调用zipimport.zipimporter.get_code()
        code = zipimport._get_module_code(self, fullname)       // zipimport.py:196
          data = zipimport._get_data(self.archive, "__main__")  // zipimport.py:752
                                                                // zipimport._get_data()返回解压后的__main__.py
  runpy.run_code(code,...)                                      // runpy.py:306
    exec(code,...)                                              // runpy.py:86
      __main__.py                                               // test.cpython-310.pyc

我这是用gdb调试Python解释器得到的调用栈回溯,实际情况有些微妙,后面是小侯的研究结论。

cpython-3.10.6Libzipimport.py
cpython-3.10.6Pythonimportlib_zipimport.h
cpython-3.10.6Programs_freeze_importlib.c

_freeze_importlib.c将zipimport.py编译成pyc,再序列化到importlib_zipimport.h,后者内容形如

const unsigned char _Py_M__zipimport[] = {99,0,0,0,...,19,12,15,}

Python源码提供zipimport.py,同时也提供预编译好的importlib_zipimport.h。编译Python源码时,缺省用importlib_zipimport.h,之后修改zipimport.py并不影响解释器。

参看

cpython-3.10.6Makefile.pre.in

编译源码时可用如下命令强制重新生成importlib_zipimport.h

make regen-importlib

这是老版定义frozen module的位置

https://github.com/python/cpython/blob/3.8/Python/frozen.c

static const struct _frozen _PyImport_FrozenModules[] = {
    /* importlib */
    {"_frozen_importlib", _Py_M__importlib_bootstrap,
        (int)sizeof(_Py_M__importlib_bootstrap)},
    {"_frozen_importlib_external", _Py_M__importlib_bootstrap_external,
        (int)sizeof(_Py_M__importlib_bootstrap_external)},
    {"zipimport", _Py_M__zipimport,
        (int)sizeof(_Py_M__zipimport)},
    /* Test module */
    {"__hello__", M___hello__, SIZE},
    /* Test package (negative size indicates package-ness) */
    {"__phello__", M___hello__, -SIZE},
    {"__phello__.spam", M___hello__, SIZE},
    {000/* sentinel */
};

这是新版(3.12.0)定义frozen module的位置

https://github.com/python/cpython/blob/60bdc16b459cf8f7b359c7f87d8ae6c5928147a4/Programs/_bootstrap_python.c#L36

static const struct _frozen bootstrap_modules[] = {
    {"_frozen_importlib", _Py_M__importlib__bootstrap, (int)sizeof(_Py_M__importlib__bootstrap)},
    {"_frozen_importlib_external", _Py_M__importlib__bootstrap_external, (int)sizeof(_Py_M__importlib__bootstrap_external)},
    {"zipimport", _Py_M__zipimport, (int)sizeof(_Py_M__zipimport)},
    {000/* bootstrap sentinel */
};

搞清楚原理后,可以自制这样的PoC,放一个正常zip到unused变量即可,也可以放畸型的,比如hello.py。

unused=b'PK34'+b'�'*26+
b'+(xcaxcc+xd1Pxf7Hxcdxc9xc9Wx08xcf/xcaIQTxd74�PK12'+
b'�'*6+b'1'+b'�'*9+b'x17'+b'�'*7+b'v'+b'�'*17+b'__main__.py'+
b'PK56'+b'�'*8+b'9'+b'�'*3+b'5'+b'�'*3
i=__import__
i("runpy").run_path(i("py_compile").compile(__file__))

$ python3 hello.py
Hello World!

原文始发于微信公众号(青衣十三楼飞花堂):Python模块注入技术简析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月13日23:28:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Python模块注入技术简析https://cn-sec.com/archives/1641197.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息